/** * Copyright (C) 2011 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.strata.basics.currency; import java.io.Serializable; import java.util.Arrays; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.function.BiConsumer; import java.util.stream.Collector; import java.util.stream.Collectors; import java.util.stream.Stream; import org.joda.beans.Bean; import org.joda.beans.BeanBuilder; import org.joda.beans.BeanDefinition; import org.joda.beans.ImmutableBean; import org.joda.beans.JodaBeanUtils; import org.joda.beans.MetaProperty; import org.joda.beans.Property; import org.joda.beans.PropertyDefinition; import org.joda.beans.impl.direct.DirectFieldsBeanBuilder; import org.joda.beans.impl.direct.DirectMetaBean; import org.joda.beans.impl.direct.DirectMetaProperty; import org.joda.beans.impl.direct.DirectMetaPropertyMap; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.opengamma.strata.collect.ArgChecker; import com.opengamma.strata.collect.Messages; import com.opengamma.strata.collect.array.DoubleMatrix; import com.opengamma.strata.collect.tuple.Pair; /** * A matrix of foreign exchange rates. * <p> * This provides a matrix of foreign exchange rates, such that the rate can be queried for any available pair. * For example, if the matrix contains the currencies 'USD', 'EUR' and 'GBP', then six rates can be queried, * 'EUR/USD', 'GBP/USD', 'EUR/GBP' and the three inverse rates. * <p> * This class is immutable and thread-safe. */ @BeanDefinition(builderScope = "private", constructorScope = "package") public final class FxMatrix implements FxRateProvider, ImmutableBean, Serializable { /** * An empty FX matrix containing neither currencies nor rates. */ private static final FxMatrix EMPTY = builder().build(); /** * The map between the currencies and their position within the * {@code rates} array. Generally the position reflects the order * in which the currencies were added, so the first currency added * will be assigned 0, the second 1 etc. * <p> * An ImmutableMap is used so that the currencies are correctly * ordered when the {@link #toString()} method is called. */ @PropertyDefinition(validate = "notNull", get = "") private final ImmutableMap<Currency, Integer> currencies; /** * The matrix with all the exchange rates. Each row represents the * rates required to convert a unit of particular currency to all * other currencies in the matrix. * <p> * If currencies c1 and c2 are assigned indexes i and j respectively * in the {@code currencies} map, then the entry [i][j] is such that * 1 unit of currency c1 is worth {@code rates[i][j]} units of * currency c2. * <p> * If {@code currencies.get(EUR)} = 0 and {@code currencies.get(USD)} = 1, * then the element {@code rates[0][1]} is likely to be around * 1.40 and {@code rates[1][0]} around 0.7142. The rate {@code rates[1][0]} * will be computed from fxRate[0][1] when the object is constructed * by the builder. All the element of the matrix are meaningful and coherent. */ @PropertyDefinition(validate = "notNull") private final DoubleMatrix rates; //------------------------------------------------------------------------- /** * Obtains an empty FX matrix. * <p> * The result contains no currencies or rates. * * @return an empty matrix */ public static FxMatrix empty() { return EMPTY; } /** * Obtains an instance containing a single FX rate. * <p> * This is most likely to be used in testing. * <p> * An invocation of the method with {@code FxMatrix.of(CurrencyPair.of(GBP, USD), 1.6)} * indicates that 1 pound sterling is worth 1.6 US dollars. * The matrix can also be queried for the reverse rate, from USD to GBP. * * @param currencyPair the currency pair to be added * @param rate the FX rate between the base currency of the pair and the * counter currency. The rate indicates the value of one unit of the base * currency in terms of the counter currency. * @return a matrix containing the single FX rate */ public static FxMatrix of(CurrencyPair currencyPair, double rate) { return FxMatrix.of(currencyPair.getBase(), currencyPair.getCounter(), rate); } /** * Obtains an instance containing a single FX rate. * <p> * This is most likely to be used in testing. * <p> * An invocation of the method with {@code FxMatrix.of(GBP, USD, 1.6)} * indicates that 1 pound sterling is worth 1.6 US dollars. * The matrix can also be queried for the reverse rate, from USD to GBP. * * @param ccy1 the first currency of the pair * @param ccy2 the second currency of the pair * @param rate the FX rate between the first currency and the second currency. * The rate indicates the value of one unit of the first currency in terms * of the second currency. * @return a matrix containing the single FX rate */ public static FxMatrix of(Currency ccy1, Currency ccy2, double rate) { return new FxMatrixBuilder().addRate(ccy1, ccy2, rate).build(); } /** * Creates a builder that can be used to build instances of {@code FxMatrix}. * * @return a new builder */ public static FxMatrixBuilder builder() { return new FxMatrixBuilder(); } //------------------------------------------------------------------------- /** * Creates a {@code Collector} that allows a {@code Map.Entry} of currency pair to rate * to be streamed and collected into a new {@code FxMatrix}. * * @return a collector for creating an {@code FxMatrix} from a stream */ public static Collector<? super Map.Entry<CurrencyPair, Double>, FxMatrixBuilder, FxMatrix> entriesToFxMatrix() { return collector((builder, entry) -> builder.addRate(entry.getKey(), entry.getValue())); } /** * Creates a {@code Collector} that allows a collection of pairs each containing * a currency pair and a rate to be streamed and collected into a new {@code FxMatrix}. * * @return a collector for creating an {@code FxMatrix} from a stream */ public static Collector<? super Pair<CurrencyPair, Double>, FxMatrixBuilder, FxMatrix> pairsToFxMatrix() { return collector((builder, pair) -> builder.addRate(pair.getFirst(), pair.getSecond())); } private static <T> Collector<T, FxMatrixBuilder, FxMatrix> collector(BiConsumer<FxMatrixBuilder, T> accumulator) { return Collector.of( FxMatrix::builder, accumulator, FxMatrixBuilder::merge, FxMatrixBuilder::build); } //------------------------------------------------------------------------- /** * Returns the set of currencies held within this matrix. * * @return the currencies in this matrix */ public ImmutableSet<Currency> getCurrencies() { return currencies.keySet(); } //------------------------------------------------------------------------- /** * Gets the FX rate for the specified currency pair. * <p> * The rate returned is the rate from the base currency to the counter currency * as defined by this formula: {@code (1 * baseCurrency = fxRate * counterCurrency)}. * * @param baseCurrency the base currency, to convert from * @param counterCurrency the counter currency, to convert to * @return the FX rate for the currency pair * @throws IllegalArgumentException if no FX rate could be found */ @Override public double fxRate(Currency baseCurrency, Currency counterCurrency) { if (baseCurrency.equals(counterCurrency)) { return 1d; } Integer index1 = currencies.get(baseCurrency); Integer index2 = currencies.get(counterCurrency); if (index1 != null && index2 != null) { return rates.get(index1, index2); } else { throw new IllegalArgumentException(Messages.format( "No FX rate found for {}/{}, matrix only contains rates for {}", baseCurrency, counterCurrency, currencies.keySet())); } } /** * Converts a {@code CurrencyAmount} into an amount in the specified * currency using the rates in this matrix. * * @param amount the {@code CurrencyAmount} to be converted * @param targetCurrency the currency to convert the {@code CurrencyAmount} to * @return the amount converted to the requested currency */ public CurrencyAmount convert(CurrencyAmount amount, Currency targetCurrency) { ArgChecker.notNull(amount, "amount"); ArgChecker.notNull(targetCurrency, "targetCurrency"); // Only do conversion if we need to Currency originalCurrency = amount.getCurrency(); if (originalCurrency.equals(targetCurrency)) { return amount; } return CurrencyAmount.of(targetCurrency, convert(amount.getAmount(), originalCurrency, targetCurrency)); } /** * Converts a {@code MultipleCurrencyAmount} into an amount in the * specified currency using the rates in this matrix. * * @param amount the {@code MultipleCurrencyAmount} to be converted * @param targetCurrency the currency to convert all entries to * @return the total amount in the requested currency */ public CurrencyAmount convert(MultiCurrencyAmount amount, Currency targetCurrency) { ArgChecker.notNull(amount, "amount"); ArgChecker.notNull(targetCurrency, "targetCurrency"); // We could do this using the currency amounts but to // avoid creating extra objects we'll use doubles double total = amount.getAmounts() .stream() .mapToDouble(ca -> convert(ca.getAmount(), ca.getCurrency(), targetCurrency)) .sum(); return CurrencyAmount.of(targetCurrency, total); } //------------------------------------------------------------------------- /** * Merges the entries from the other matrix into this one. * <p> * The other matrix should have at least one currency in common with this one. * The additional currencies from the other matrix are added one by one and * the exchange rate data created is coherent with some data in this matrix. * <p> * Note that if the other matrix has more than one currency in common with * this one, and the rates for pairs of those currencies are different to * the equivalents in this matrix, then the rates between the additional * currencies is this matrix will differ from those in the original. * * @param other the matrix to be merged into this one * @return a new matrix containing the rates from this matrix * plus any rates for additional currencies from the other matrix */ public FxMatrix merge(FxMatrix other) { return toBuilder().merge(other.toBuilder()).build(); } /** * Creates a new builder using the data from this matrix to * create a set of initial entries. * * @return a new builder containing the data from this matrix */ public FxMatrixBuilder toBuilder() { return new FxMatrixBuilder(currencies, rates.toArray()); } @Override public String toString() { return "FxMatrix[" + Joiner.on(", ").join(getCurrencies()) + " : " + Stream.of(rates.toArrayUnsafe()).map(Arrays::toString).collect(Collectors.joining(",")) + "]"; } //------------------------- AUTOGENERATED START ------------------------- ///CLOVER:OFF /** * The meta-bean for {@code FxMatrix}. * @return the meta-bean, not null */ public static FxMatrix.Meta meta() { return FxMatrix.Meta.INSTANCE; } static { JodaBeanUtils.registerMetaBean(FxMatrix.Meta.INSTANCE); } /** * The serialization version id. */ private static final long serialVersionUID = 1L; /** * Creates an instance. * @param currencies the value of the property, not null * @param rates the value of the property, not null */ FxMatrix( Map<Currency, Integer> currencies, DoubleMatrix rates) { JodaBeanUtils.notNull(currencies, "currencies"); JodaBeanUtils.notNull(rates, "rates"); this.currencies = ImmutableMap.copyOf(currencies); this.rates = rates; } @Override public FxMatrix.Meta metaBean() { return FxMatrix.Meta.INSTANCE; } @Override public <R> Property<R> property(String propertyName) { return metaBean().<R>metaProperty(propertyName).createProperty(this); } @Override public Set<String> propertyNames() { return metaBean().metaPropertyMap().keySet(); } //----------------------------------------------------------------------- /** * Gets the matrix with all the exchange rates. Each row represents the * rates required to convert a unit of particular currency to all * other currencies in the matrix. * <p> * If currencies c1 and c2 are assigned indexes i and j respectively * in the {@code currencies} map, then the entry [i][j] is such that * 1 unit of currency c1 is worth {@code rates[i][j]} units of * currency c2. * <p> * If {@code currencies.get(EUR)} = 0 and {@code currencies.get(USD)} = 1, * then the element {@code rates[0][1]} is likely to be around * 1.40 and {@code rates[1][0]} around 0.7142. The rate {@code rates[1][0]} * will be computed from fxRate[0][1] when the object is constructed * by the builder. All the element of the matrix are meaningful and coherent. * @return the value of the property, not null */ public DoubleMatrix getRates() { return rates; } //----------------------------------------------------------------------- @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj.getClass() == this.getClass()) { FxMatrix other = (FxMatrix) obj; return JodaBeanUtils.equal(currencies, other.currencies) && JodaBeanUtils.equal(rates, other.rates); } return false; } @Override public int hashCode() { int hash = getClass().hashCode(); hash = hash * 31 + JodaBeanUtils.hashCode(currencies); hash = hash * 31 + JodaBeanUtils.hashCode(rates); return hash; } //----------------------------------------------------------------------- /** * The meta-bean for {@code FxMatrix}. */ public static final class Meta extends DirectMetaBean { /** * The singleton instance of the meta-bean. */ static final Meta INSTANCE = new Meta(); /** * The meta-property for the {@code currencies} property. */ @SuppressWarnings({"unchecked", "rawtypes" }) private final MetaProperty<ImmutableMap<Currency, Integer>> currencies = DirectMetaProperty.ofImmutable( this, "currencies", FxMatrix.class, (Class) ImmutableMap.class); /** * The meta-property for the {@code rates} property. */ private final MetaProperty<DoubleMatrix> rates = DirectMetaProperty.ofImmutable( this, "rates", FxMatrix.class, DoubleMatrix.class); /** * The meta-properties. */ private final Map<String, MetaProperty<?>> metaPropertyMap$ = new DirectMetaPropertyMap( this, null, "currencies", "rates"); /** * Restricted constructor. */ private Meta() { } @Override protected MetaProperty<?> metaPropertyGet(String propertyName) { switch (propertyName.hashCode()) { case -1089470353: // currencies return currencies; case 108285843: // rates return rates; } return super.metaPropertyGet(propertyName); } @Override public BeanBuilder<? extends FxMatrix> builder() { return new FxMatrix.Builder(); } @Override public Class<? extends FxMatrix> beanType() { return FxMatrix.class; } @Override public Map<String, MetaProperty<?>> metaPropertyMap() { return metaPropertyMap$; } //----------------------------------------------------------------------- /** * The meta-property for the {@code currencies} property. * @return the meta-property, not null */ public MetaProperty<ImmutableMap<Currency, Integer>> currencies() { return currencies; } /** * The meta-property for the {@code rates} property. * @return the meta-property, not null */ public MetaProperty<DoubleMatrix> rates() { return rates; } //----------------------------------------------------------------------- @Override protected Object propertyGet(Bean bean, String propertyName, boolean quiet) { switch (propertyName.hashCode()) { case -1089470353: // currencies return ((FxMatrix) bean).currencies; case 108285843: // rates return ((FxMatrix) bean).getRates(); } return super.propertyGet(bean, propertyName, quiet); } @Override protected void propertySet(Bean bean, String propertyName, Object newValue, boolean quiet) { metaProperty(propertyName); if (quiet) { return; } throw new UnsupportedOperationException("Property cannot be written: " + propertyName); } } //----------------------------------------------------------------------- /** * The bean-builder for {@code FxMatrix}. */ private static final class Builder extends DirectFieldsBeanBuilder<FxMatrix> { private Map<Currency, Integer> currencies = ImmutableMap.of(); private DoubleMatrix rates; /** * Restricted constructor. */ private Builder() { } //----------------------------------------------------------------------- @Override public Object get(String propertyName) { switch (propertyName.hashCode()) { case -1089470353: // currencies return currencies; case 108285843: // rates return rates; default: throw new NoSuchElementException("Unknown property: " + propertyName); } } @SuppressWarnings("unchecked") @Override public Builder set(String propertyName, Object newValue) { switch (propertyName.hashCode()) { case -1089470353: // currencies this.currencies = (Map<Currency, Integer>) newValue; break; case 108285843: // rates this.rates = (DoubleMatrix) newValue; break; default: throw new NoSuchElementException("Unknown property: " + propertyName); } return this; } @Override public Builder set(MetaProperty<?> property, Object value) { super.set(property, value); return this; } @Override public Builder setString(String propertyName, String value) { setString(meta().metaProperty(propertyName), value); return this; } @Override public Builder setString(MetaProperty<?> property, String value) { super.setString(property, value); return this; } @Override public Builder setAll(Map<String, ? extends Object> propertyValueMap) { super.setAll(propertyValueMap); return this; } @Override public FxMatrix build() { return new FxMatrix( currencies, rates); } //----------------------------------------------------------------------- @Override public String toString() { StringBuilder buf = new StringBuilder(96); buf.append("FxMatrix.Builder{"); buf.append("currencies").append('=').append(JodaBeanUtils.toString(currencies)).append(',').append(' '); buf.append("rates").append('=').append(JodaBeanUtils.toString(rates)); buf.append('}'); return buf.toString(); } } ///CLOVER:ON //-------------------------- AUTOGENERATED END -------------------------- }