/** * Copyright (C) 2011 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.basics.currency; import static com.opengamma.collect.Guavate.entriesToImmutableMap; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.Predicate; import java.util.stream.Collector; import java.util.stream.Collectors; import java.util.stream.Stream; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.opengamma.collect.ArgChecker; import com.opengamma.collect.tuple.Pair; /** * Immutable class describing a set of currencies and all the cross rates between them. */ public class FxMatrix { /** * An FX matrix containing neither currencies nor rates. */ public static final FxMatrix EMPTY_FX_MATRIX = 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. */ 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. */ private final double[][] rates; /** * Private constructor. */ private FxMatrix(ImmutableMap<Currency, Integer> currencies, double[][] rates) { this.currencies = currencies; this.rates = rates; } /** * Create a new FxMatrix builder. * * @return a new FxMatrix builder */ public static FxMatrix.Builder builder() { return new FxMatrix.Builder(); } /** * Create 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 FxMatrix.Builder toBuilder() { return new FxMatrix.Builder(currencies, rates); } /** * Return the exchange rate between two currencies. * * @param ccy1 The first currency. * @param ccy2 The second currency. * @return The exchange rate: 1.0 * ccy1 = x * ccy2. */ public double getRate(Currency ccy1, Currency ccy2) { if (ccy1.equals(ccy2)) { return 1; } Integer index1 = currencies.get(ccy1); Integer index2 = currencies.get(ccy2); if (index1 != null && index2 != null) { return rates[index1][index2]; } else { throw new IllegalArgumentException( "No rate found for " + ccy1 + "/" + ccy2 + " - FX matrix only contains rates for: " + currencies.keySet()); } } /** * Convert 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 ccy the currency to convert the {@code CurrencyAmount} to * @return the amount converted to the requested currency */ public CurrencyAmount convert(CurrencyAmount amount, Currency ccy) { ArgChecker.notNull(amount, "amount"); ArgChecker.notNull(ccy, "ccy"); Currency originalCcy = amount.getCurrency(); // Only do conversion if we need to return originalCcy == ccy ? amount : CurrencyAmount.of(ccy, amount.getAmount() * getRate(originalCcy, ccy)); } /** * Convert 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 ccy the currency to convert all entries to * @return the total amount in the requested currency */ public CurrencyAmount convert(MultiCurrencyAmount amount, Currency ccy) { ArgChecker.notNull(amount, "amount"); ArgChecker.notNull(ccy, "ccy"); // 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 -> ca.getAmount() * getRate(ca.getCurrency(), ccy)) .sum(); return CurrencyAmount.of(ccy, total); } /** * Merge the entries from the other matrix into this one. 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(); } /** * Returns an immutable set containing the currencies held within this matrix. * * @return the currencies in this matrix */ public ImmutableSet<Currency> getCurrencies() { return currencies.keySet(); } @Override public String toString() { return getCurrencies() + " - " + Stream.of(rates).map(Arrays::toString).collect(Collectors.joining()); } @Override public int hashCode() { return 31 * currencies.hashCode() + Arrays.deepHashCode(rates); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } FxMatrix other = (FxMatrix) o; return currencies.equals(other.currencies) && Arrays.deepEquals(rates, other.rates); } /** * Creates a {@link Collector} that allows a Map of currency pair -> rates * 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>, Builder, FxMatrix> entriesToFxMatrix() { return collector((builder, entry) -> builder.addRate(entry.getKey(), entry.getValue())); } /** * Creates a {@link 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>, Builder, FxMatrix> pairsToFxMatrix() { return collector((builder, pair) -> builder.addRate(pair.getFirst(), pair.getSecond())); } private static <T> Collector<T, Builder, FxMatrix> collector(BiConsumer<Builder, T> accumulator) { return Collector.of( FxMatrix::builder, accumulator, Builder::merge, Builder::build); } /** * Builder class for FxMatrix. Can be created either by the static * {@link FxMatrix#builder()} or from an existing {@code FxMatrix} * instance by calling {@link FxMatrix#toBuilder()}. */ public static final class Builder { /** * The minimum size of the FX rate matrix. This is intended such * that a number rates of can be added without needing to resize. */ private static final int MINIMAL_MATRIX_SIZE = 8; /** * The currencies held by the builder pointing to their position * in the rates array. An ordered map is used so that it retains * order which means the {@code toString} method of {@code FxMatrix} * is clearer. */ private final LinkedHashMap<Currency, Integer> currencies; /** * A 2 dimensional array holding the rates. Each row of the array holds the * value of 1 unit of Currency (that the row represents) in each of the * alternate currencies. * * The array is square with its order being a power of 2. This means that there * may be empty rows/cols at the bottom/right of the matrix. Leaving this space * means that adding currencies can be done more efficiently as the array only * needs to be resized (via copying) relatively infrequently.. */ private double[][] rates; /** * Rates that have been requested to be added, but which do not * have a currency in common with the currencies already present. * As additional currencies are added, this map will be checked to * see if rates can be handled. * <p> * If this map is not empty by the point that build is called, * an {@link IllegalStateException} will be thrown. */ private final Map<CurrencyPair, Double> disjointRates = new HashMap<>(); /** * Build a new {@code FxMatrix} from the data in the builder. * * @return a new {@code FxMatrix} * @throws IllegalStateException if an attempt was made to add currencies * which have no currency in common with other rates */ public FxMatrix build() { if (!disjointRates.isEmpty()) { throw new IllegalStateException("Received rates with no currencies in common with other: " + disjointRates); } // Trim array down to the correct size - we have to copy the array // anyway to ensure immutability, so we may as well remove any // unused rows return new FxMatrix(ImmutableMap.copyOf(currencies), copyArray(rates, currencies.size())); } /** * Adds a new rate for a currency pair to the builder. See * {@link #addRate(Currency, Currency, double)} for full * explanation. * * @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 the builder updated with the new rate */ public Builder addRate(CurrencyPair currencyPair, double rate) { ArgChecker.notNull(currencyPair, "currencyPair"); return addRate(currencyPair.getBase(), currencyPair.getCounter(), rate); } /** * Add a new pair of currencies to the builder. * <p> * An invocation of the method with {@code builder.addRate(GBP, USD, 1.6)} * indicates that 1 pound sterling is worth 1.6 US dollars. It is * equivalent to: {@code builder.addRate(USD, GBP, 1 / 1.6)} (1 US dollar * is worth 0.625 pounds sterling) for all cases except where the USD/GBP * rates is already in the matrix and so will be updated. * </p> * There are a number of possible outcomes when this method is called: * <ul> * <li> * The builder is currently empty. In this case these currencies * and rates will be added as the initial pair.</li> * <li> * The builder is non-empty and neither of the currencies are * currently in the matrix. In this case the currencies cannot be * immediately added as there is no common currency to allow the * cross rates to be calculated. The currencies and rates are put * into a pending set for later processing, for example after another * pair containing one of the currencies and a currency already in * the matrix is added. If no such event occurs, then an exception * will be thrown when {@link #build()} is called.</li> * <li> * The builder is non-empty and one of the currencies in the pair * is already in the matrix, whilst the other is not. In this case * the pair and rate is added to the matrix and all the cross rates * to the other currencies are calculated. * </li> * <li> * The builder is non-empty and contains both of the currencies in * the pair. In this case the pair is treated as an update to the * rate already in the matrix. The first currency (ccy1) is treated * as the reference currency and the second currency (ccy2) is the * updated currency. All rates involving the updated currency will * be recalculated using the new rate. * <p> * Note that due to one of the rates being treated as a reference, this * operation is not symmetric. That is, the result of * {@code matrix.addRate(USD, EUR, 1.23)} will be different to the * result of {@code matrix.addRate(EUR, USD, 1 / 1.23)} when there * are other currencies present in the builder. * </li> * </ul> * * @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 the builder updated with the new rate */ public Builder addRate(Currency ccy1, Currency ccy2, double rate) { ArgChecker.notNull(ccy1, "ccy1"); ArgChecker.notNull(ccy2, "ccy2"); if (currencies.isEmpty()) { addInitialCurrencyPair(ccy1, ccy2, rate); } else { addCurrencyPair(ccy1, ccy2, rate); } return this; } /** * Adds a collection of new rates for currency pairs to the builder. * Pairs that are already in the builder are treated as updates to the * existing ratese -> !e.getKey().equals(commonCurrency) && !currencies.containsKey(e.getKey()) * * @param rates the currency pairs and rates to be added * @return the builder updated with the new rates */ public Builder addRates(Map<CurrencyPair, Double> rates) { ArgChecker.notNull(rates, "rates"); if (!rates.isEmpty()) { ensureCapacity( rates.keySet() .stream() .flatMap(cp -> Stream.<Currency>of(cp.getBase(), cp.getCounter()))); rates.entrySet() .stream() .forEach(e -> addRate(e.getKey(), e.getValue())); } return this; } private Builder() { this.currencies = new LinkedHashMap<>(); this.rates = new double[MINIMAL_MATRIX_SIZE][MINIMAL_MATRIX_SIZE]; } private Builder(ImmutableMap<Currency, Integer> currencies, double[][] rates) { this.currencies = new LinkedHashMap<>(currencies); // Ensure there is space to add at least one new currency this.rates = copyArray(rates, size(currencies.size() + 1)); } private Builder merge(Builder other) { // Find the common currencies Optional<Currency> common = currencies.keySet() .stream() .filter(other.currencies::containsKey) .findFirst(); Currency commonCurrency = common.orElseThrow(() -> new IllegalArgumentException("There are no currencies in common between " + currencies.keySet() + " and " + other.currencies.keySet())); // Add in all currencies that we don't already have other.currencies.entrySet() .stream() .filter(isNewCurrency(commonCurrency)) .forEach(e -> addCurrencyPair(commonCurrency, e.getKey(), other.getRate(commonCurrency, e.getKey()))); return this; } private Predicate<Map.Entry<Currency, Integer>> isNewCurrency(Currency commonCurrency) { return e -> !e.getKey().equals(commonCurrency) && !currencies.containsKey(e.getKey()); } private double getRate(Currency ccy1, Currency ccy2) { int i = currencies.get(ccy1); int j = currencies.get(ccy2); return rates[i][j]; } private void addCurrencyPair(Currency ccy1, Currency ccy2, double rate) { // Only resize if there's a danger we can't fit a new currency in if (rates.length < currencies.size() + 1) { ensureCapacity(Stream.of(ccy1, ccy2)); } if (!currencies.containsKey(ccy1) && !currencies.containsKey(ccy2)) { // Neither currency present - add to disjoint set disjointRates.put(CurrencyPair.of(ccy1, ccy2), rate); } else if (currencies.containsKey(ccy1) && currencies.containsKey(ccy2)) { // We already have a rate for this currency pair updateRate(ccy1, ccy2, rate); } else { // We have exactly one of the currencies already addNewRate(ccy1, ccy2, rate); // With a new rate added we may be able to handle the disjoint retryDisjoints(); } } private void retryDisjoints() { ensureCapacity( disjointRates.keySet() .stream() .flatMap(cp -> Stream.of(cp.getBase(), cp.getCounter()))); while (true) { int initialSize = disjointRates.size(); ImmutableMap<CurrencyPair, Double> addable = disjointRates.entrySet() .stream() .filter(e -> currencies.containsKey(e.getKey().getBase()) || currencies.containsKey(e.getKey().getCounter())) .collect(entriesToImmutableMap()); addable.entrySet() .stream() .forEach(e -> addNewRate(e.getKey().getBase(), e.getKey().getCounter(), e.getValue())); addable.keySet() .stream() .forEach(disjointRates::remove); if (disjointRates.size() == initialSize) { // No effect so break out break; } } } private void addNewRate(Currency ccy1, Currency ccy2, double rate) { Currency existing = currencies.containsKey(ccy1) ? ccy1 : ccy2; Currency other = existing == ccy1 ? ccy2 : ccy1; double updatedRate = existing == ccy2 ? 1.0 / rate : rate; int indexRef = currencies.get(existing); int indexOther = currencies.size(); currencies.put(other, indexOther); rates[indexOther][indexOther] = 1.0; for (int i = 0; i < indexOther; i++) { double convertedRate = updatedRate * rates[i][indexRef]; rates[i][indexOther] = convertedRate; rates[indexOther][i] = 1.0 / convertedRate; } } // We take the first currency as the reference and the second as // the currency to be updated private void updateRate(Currency ccy1, Currency ccy2, double rate) { int index1 = currencies.get(ccy1); int index2 = currencies.get(ccy2); for (int i = 0; i < currencies.size(); i++) { // Nothing to do - we know and want rates[index2][index2] = 1 if (i != index2) { double convertedRate = rate * rates[i][index1]; rates[i][index2] = convertedRate; rates[index2][i] = 1.0 / convertedRate; } } } private void addInitialCurrencyPair(Currency ccy1, Currency ccy2, double rate) { // No need for capacity check, as initial size is always enough currencies.put(ccy1, 0); currencies.put(ccy2, 1); rates[0][0] = 1.0; rates[0][1] = rate; rates[1][1] = 1.0; rates[1][0] = 1.0 / rate; } private void ensureCapacity(Stream<Currency> potentialCurrencies) { // If adding the currencies would mean we have more // currencies than matrix size, create an expanded array int requiredOrder = (int) Stream.concat(currencies.keySet().stream(), potentialCurrencies) .distinct() .count(); ensureCapacity(requiredOrder); } private void ensureCapacity(int requiredOrder) { if (requiredOrder > rates.length) { rates = copyArray(rates, size(requiredOrder)); } } // size the matrix to either the minimal matrix size, or a power of 2 // sufficient to hold the required currencies private int size(int requiredCapacity) { int lowerPower = Integer.highestOneBit(requiredCapacity); return Math.max(requiredCapacity == lowerPower ? requiredCapacity : lowerPower << 2, MINIMAL_MATRIX_SIZE); } private double[][] copyArray(double[][] rates, int requestedSize) { int order = Math.min(rates.length, requestedSize); double[][] copy = new double[requestedSize][requestedSize]; for (int i = 0; i < order; i++) { System.arraycopy(rates[i], 0, copy[i], 0, order); } return copy; } } }