/** * Copyright (C) 2016 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.strata.basics.currency; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.stream.Stream; import com.google.common.collect.ImmutableMap; import com.opengamma.strata.collect.ArgChecker; import com.opengamma.strata.collect.MapStream; import com.opengamma.strata.collect.array.DoubleMatrix; /** * A mutable builder class for {@link FxMatrix}. */ public class FxMatrixBuilder { /** * 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), DoubleMatrix.ofUnsafe(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 FxMatrixBuilder 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 FxMatrixBuilder 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 rates -> !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 FxMatrixBuilder addRates(Map<CurrencyPair, Double> rates) { ArgChecker.notNull(rates, "rates"); if (!rates.isEmpty()) { ensureCapacity(rates.keySet().stream() .flatMap(cp -> Stream.of(cp.getBase(), cp.getCounter()))); MapStream.of(rates).forEach((pair, rate) -> addRate(pair, rate)); } return this; } FxMatrixBuilder() { this.currencies = new LinkedHashMap<>(); this.rates = new double[MINIMAL_MATRIX_SIZE][MINIMAL_MATRIX_SIZE]; } FxMatrixBuilder(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)); } FxMatrixBuilder merge(FxMatrixBuilder 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 MapStream.of(other.currencies) .filterKeys(ccy -> !ccy.equals(commonCurrency) && !currencies.containsKey(ccy)) .forEach((ccy, idx) -> addCurrencyPair(commonCurrency, ccy, other.getRate(commonCurrency, ccy))); return this; } 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 = MapStream.of(disjointRates) .filterKeys(pair -> currencies.containsKey(pair.getBase()) || currencies.containsKey(pair.getCounter())) .toMap(); MapStream.of(addable).forEach((pair, rate) -> addNewRate(pair.getBase(), pair.getCounter(), rate)); 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); } //------------------------------------------------------------------------- // copies the array trimming to the specified size private static 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; } }