/** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.strata.basics.currency; import static com.opengamma.strata.basics.BasicProjectAssertions.assertThat; import static com.opengamma.strata.basics.currency.Currency.AUD; import static com.opengamma.strata.basics.currency.Currency.CAD; import static com.opengamma.strata.basics.currency.Currency.CHF; import static com.opengamma.strata.basics.currency.Currency.EUR; import static com.opengamma.strata.basics.currency.Currency.GBP; import static com.opengamma.strata.basics.currency.Currency.JPY; import static com.opengamma.strata.basics.currency.Currency.NZD; import static com.opengamma.strata.basics.currency.Currency.SEK; import static com.opengamma.strata.basics.currency.Currency.USD; import static com.opengamma.strata.basics.currency.FxMatrix.entriesToFxMatrix; import static com.opengamma.strata.basics.currency.FxMatrix.pairsToFxMatrix; import static com.opengamma.strata.collect.TestHelper.assertSerialization; import static com.opengamma.strata.collect.TestHelper.assertThrows; import static com.opengamma.strata.collect.TestHelper.assertThrowsIllegalArg; import static com.opengamma.strata.collect.TestHelper.coverImmutableBean; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.offset; import java.util.LinkedHashMap; import java.util.Map; import org.assertj.core.data.Offset; import org.joda.beans.Bean; import org.joda.beans.ser.JodaBeanSer; import org.testng.annotations.Test; import com.google.common.collect.ImmutableMap; import com.opengamma.strata.collect.tuple.Pair; @Test public class FxMatrixTest { private static final double TOLERANCE = 1e-6; public static final Offset<Double> TOL = offset(TOLERANCE); public void emptyMatrixCanHandleTrivialRate() { FxMatrix matrix = FxMatrix.empty(); assertThat(matrix.getCurrencies()).isEmpty(); assertThat(matrix.fxRate(USD, USD)).isEqualTo(1.0); assertThat(matrix.toString()).isEqualTo("FxMatrix[ : ]"); } public void emptyMatrixCannotDoConversion() { FxMatrix matrix = FxMatrix.builder().build(); assertThat(matrix.getCurrencies()).isEmpty(); assertThrowsIllegalArg(() -> matrix.fxRate(USD, EUR)); } public void singleRateMatrixByOfCurrencyPairFactory() { FxMatrix matrix = FxMatrix.of(CurrencyPair.of(GBP, USD), 1.6); assertThat(matrix.getCurrencies()).containsOnly(GBP, USD); assertThat(matrix.fxRate(GBP, USD)).isEqualTo(1.6); assertThat(matrix.fxRate(USD, GBP)).isEqualTo(0.625); assertThat(matrix.toString()).isEqualTo("FxMatrix[GBP, USD : [1.0, 1.6],[0.625, 1.0]]"); } public void singleRateMatrixByOfCurrenciesFactory() { FxMatrix matrix = FxMatrix.of(GBP, USD, 1.6); assertThat(matrix.getCurrencies()).containsOnly(GBP, USD); assertThat(matrix.fxRate(GBP, USD)).isEqualTo(1.6); assertThat(matrix.fxRate(USD, GBP)).isEqualTo(0.625); } public void singleRateMatrixByBuilder() { FxMatrix matrix = FxMatrix.builder() .addRate(GBP, USD, 1.6) .build(); assertThat(matrix.getCurrencies()).containsOnly(GBP, USD); assertThat(matrix.fxRate(GBP, USD)).isEqualTo(1.6); assertThat(matrix.fxRate(USD, GBP)).isEqualTo(0.625); } public void canAddRateUsingCurrencyPair() { FxMatrix matrix = FxMatrix.builder() .addRate(CurrencyPair.of(GBP, USD), 1.6) .build(); assertThat(matrix.getCurrencies()).containsOnly(GBP, USD); assertThat(matrix.fxRate(GBP, USD)).isEqualTo(1.6); assertThat(matrix.fxRate(USD, GBP)).isEqualTo(0.625); } public void singleRateMatrixCannotDoConversionForUnknownCurrency() { FxMatrix matrix = FxMatrix.builder() .addRate(GBP, USD, 1.6) .build(); assertThat(matrix.getCurrencies()).containsOnly(GBP, USD); assertThrowsIllegalArg(() -> matrix.fxRate(USD, EUR)); } public void matrixCalculatesCrossRates() { FxMatrix matrix = FxMatrix.builder() .addRate(GBP, USD, 1.6) .addRate(EUR, USD, 1.4) .addRate(EUR, CHF, 1.2) .build(); assertThat(matrix.getCurrencies()).containsOnly(GBP, USD, EUR, CHF); assertThat(matrix.fxRate(GBP, USD)).isEqualTo(1.6); assertThat(matrix.fxRate(USD, GBP)).isEqualTo(1 / 1.6); assertThat(matrix.fxRate(EUR, USD)).isEqualTo(1.4); assertThat(matrix.fxRate(USD, EUR)).isEqualTo(1 / 1.4); assertThat(matrix.fxRate(EUR, GBP)).isEqualTo(1.4 / 1.6, TOL); assertThat(matrix.fxRate(GBP, EUR)).isEqualTo(1.6 / 1.4, TOL); assertThat(matrix.fxRate(EUR, CHF)).isEqualTo(1.2); } public void cannotAddEntryWithNoCommonCurrencyAndBuild() { assertThrows( () -> FxMatrix.builder() .addRate(GBP, USD, 1.6) .addRate(CHF, AUD, 1.6) .build(), IllegalStateException.class); } public void canAddEntryWithNoCommonCurrencyIfSuppliedBySubsequentEntries() { FxMatrix.builder() .addRate(GBP, USD, 1.6) .addRate(CHF, AUD, 1.6) // Cannot be added as nothing to tie it to USD or GBP .addRate(EUR, CHF, 1.2) // Again cannot be added .addRate(EUR, USD, 1.4) // Now everything can be tied together .build(); } public void rateCanBeUpdatedInBuilder() { FxMatrix matrix = FxMatrix.builder() .addRate(GBP, USD, 1.5) .addRate(GBP, USD, 1.6) .build(); assertThat(matrix.getCurrencies()).containsOnly(GBP, USD); assertThat(matrix.fxRate(GBP, USD)).isEqualTo(1.6); assertThat(matrix.fxRate(USD, GBP)).isEqualTo(0.625); } public void ratedCanBeUpdatedAndAddedViaBuilder() { FxMatrix matrix1 = FxMatrix.builder() .addRate(GBP, USD, 1.5) .build(); assertThat(matrix1.getCurrencies()).containsOnly(GBP, USD); assertThat(matrix1.fxRate(GBP, USD)).isEqualTo(1.5); FxMatrix matrix2 = matrix1.toBuilder() .addRate(GBP, USD, 1.6) .addRate(EUR, USD, 1.4) .build(); assertThat(matrix2.getCurrencies()).containsOnly(GBP, USD, EUR); assertThat(matrix2.fxRate(GBP, USD)).isEqualTo(1.6); assertThat(matrix2.fxRate(EUR, USD)).isEqualTo(1.4); } public void updatingRateIsNotSymmetric() { /* Expected data as produced from old analytics FxMatrix [USD, GBP, EUR] - { USD {1.0 ,0.666, 0.714283}, GBP {1.5, 1.0, 1.071428}, EUR {1.4, 0.933, 1.0}} [USD, GBP, EUR] - { {1.0, 0.625, 0.66964}, {1.6, 1.0, 1.071428}, {1.49333, 0.9333, 1.0}} [USD, GBP, EUR] - { {1.0, 0.625, 0.71428}, {1.6, 1.0, 1.14285}, {1.4, 0.875, 1.0}} */ FxMatrix matrix1 = FxMatrix.builder() .addRate(GBP, USD, 1.5) .addRate(EUR, USD, 1.4) .build(); FxMatrix matrix2 = matrix1.toBuilder() .addRate(GBP, USD, 1.6) .build(); // Switching the currency order for the update gives a // different matrix and has a different effect on the // the rates FxMatrix matrix3 = matrix1.toBuilder() .addRate(USD, GBP, 1 / 1.6) .build(); assertThat(matrix2).isNotEqualTo(matrix3); assertThat(matrix1.getCurrencies()).hasSize(3); assertThat(matrix1.fxRate(GBP, USD)).isEqualTo(1.5); assertThat(matrix1.fxRate(EUR, USD)).isEqualTo(1.4); assertThat(matrix1.fxRate(EUR, GBP)).isEqualTo(1.4 / 1.5, TOL); // The rate we updated assertThat(matrix2.fxRate(GBP, USD)).isEqualTo(1.6); // Matrix2 update was restating USD wrt GBP so // EUR/USD is affected assertThat(matrix2.fxRate(EUR, USD)).isEqualTo(1.4 * (1.6 / 1.5), TOL); // = 1.49333 // but EUR/GBP is not assertThat(matrix2.fxRate(EUR, GBP)).isEqualTo(1.4 / 1.5, TOL); // = 0.9333 // The rate we updated assertThat(matrix3.fxRate(GBP, USD)).isEqualTo(1.6); // As matrix3 update was restating GBP wrt USD, there is // no effect on EUR/USD assertThat(matrix3.fxRate(EUR, USD)).isEqualTo(1.4); // but there is an effect on EUR/GBP assertThat(matrix3.fxRate(EUR, GBP)).isEqualTo((1.4 / 1.5) * (1.5 / 1.6), TOL); // = 0.875 } public void rateCanBeUpdatedWithDirectionSwitched() { FxMatrix matrix1 = FxMatrix.builder() .addRate(GBP, USD, 1.6) .build(); assertThat(matrix1.getCurrencies()).hasSize(2); assertThat(matrix1.fxRate(GBP, USD)).isEqualTo(1.6); FxMatrix matrix2 = matrix1.toBuilder() .addRate(USD, GBP, 0.625) .build(); assertThat(matrix2.getCurrencies()).hasSize(2); assertThat(matrix2.fxRate(GBP, USD)).isEqualTo(1.6); } public void addSimpleMultipleRates() { // Use linked to force the order of evaluation // want to see that builder recovers when // encountering a currency pair for 2 unknown // currencies but which will appear later LinkedHashMap<CurrencyPair, Double> rates = new LinkedHashMap<>(); rates.put(CurrencyPair.of(GBP, USD), 1.6); rates.put(CurrencyPair.of(EUR, USD), 1.4); FxMatrix matrix = FxMatrix.builder() .addRates(rates) .build(); assertThat(matrix.fxRate(GBP, USD)).isEqualTo(1.6); assertThat(matrix.fxRate(USD, GBP)).isEqualTo(1 / 1.6); assertThat(matrix.fxRate(EUR, USD)).isEqualTo(1.4); assertThat(matrix.fxRate(USD, EUR)).isEqualTo(1 / 1.4); assertThat(matrix.fxRate(EUR, GBP)).isEqualTo(1.4 / 1.6, TOL); assertThat(matrix.fxRate(GBP, EUR)).isEqualTo(1.6 / 1.4, TOL); } public void addMultipleRatesContainingEntryWithNoCommonCurrency() { LinkedHashMap<CurrencyPair, Double> rates = new LinkedHashMap<>(); rates.put(CurrencyPair.of(GBP, USD), 1.6); rates.put(CurrencyPair.of(EUR, USD), 1.4); rates.put(CurrencyPair.of(JPY, CAD), 0.01); // Neither currency linked to one of the others assertThrows( () -> FxMatrix.builder().addRates(rates).build(), IllegalStateException.class); } public void addMultipleRates() { // Use linked map to force the order of evaluation // want to see that builder recovers when // encountering a currency pair for 2 unknown // currencies but which will appear later LinkedHashMap<CurrencyPair, Double> rates = new LinkedHashMap<>(); rates.put(CurrencyPair.of(GBP, USD), 1.6); rates.put(CurrencyPair.of(EUR, USD), 1.4); rates.put(CurrencyPair.of(CHF, AUD), 1.2); // Neither currency seen before rates.put(CurrencyPair.of(SEK, AUD), 0.16); // AUD seen before but not added yet rates.put(CurrencyPair.of(JPY, CAD), 0.01); // Neither currency seen before rates.put(CurrencyPair.of(EUR, CHF), 1.2); rates.put(CurrencyPair.of(JPY, USD), 0.0084); FxMatrix matrix = FxMatrix.builder() .addRates(rates) .build(); assertThat(matrix.fxRate(GBP, USD)).isEqualTo(1.6); assertThat(matrix.fxRate(USD, GBP)).isEqualTo(1 / 1.6); assertThat(matrix.fxRate(EUR, USD)).isEqualTo(1.4); assertThat(matrix.fxRate(USD, EUR)).isEqualTo(1 / 1.4); assertThat(matrix.fxRate(EUR, GBP)).isEqualTo(1.4 / 1.6, TOL); assertThat(matrix.fxRate(GBP, EUR)).isEqualTo(1.6 / 1.4, TOL); assertThat(matrix.fxRate(EUR, CHF)).isEqualTo(1.2); } public void streamEntriesToMatrix() { // If we obtain a stream of rates we can collect to an fx matrix Map<CurrencyPair, Double> rates = ImmutableMap.<CurrencyPair, Double>builder() .put(CurrencyPair.of(GBP, USD), 1.6) .put(CurrencyPair.of(EUR, USD), 1.4) .put(CurrencyPair.of(CHF, AUD), 1.2) // Neither currency seen before .put(CurrencyPair.of(SEK, AUD), 0.1) // AUD seen before but not added yet .put(CurrencyPair.of(JPY, CAD), 0.0) // Neither currency seen before .put(CurrencyPair.of(EUR, CHF), 1.2) .put(CurrencyPair.of(JPY, USD), 0.008) .build(); FxMatrix matrix = rates.entrySet() .stream() .collect(entriesToFxMatrix()); assertThat(matrix.fxRate(GBP, USD)).isEqualTo(1.6); assertThat(matrix.fxRate(EUR, USD)).isEqualTo(1.4); } public void streamPairsToMatrix() { // If we obtain a stream of pairs with rates we can stream them // This could happen if an entry set undergoes a map operation Map<CurrencyPair, Double> rates = ImmutableMap.<CurrencyPair, Double>builder() .put(CurrencyPair.of(GBP, USD), 1.6) .put(CurrencyPair.of(EUR, USD), 1.4) .put(CurrencyPair.of(CHF, AUD), 1.2) // Neither currency seen before .put(CurrencyPair.of(SEK, AUD), 0.1) // AUD seen before but not added yet .put(CurrencyPair.of(JPY, CAD), 0.0) // Neither currency seen before .put(CurrencyPair.of(EUR, CHF), 1.2) .put(CurrencyPair.of(JPY, USD), 0.008) .build(); FxMatrix matrix = rates.entrySet() .stream() .map(e -> Pair.of(e.getKey(), e.getValue() * 1.01)) // Apply some shift .collect(pairsToFxMatrix()); assertThat(matrix.fxRate(GBP, USD)).isEqualTo(1.616); assertThat(matrix.fxRate(EUR, USD)).isEqualTo(1.414); } // By adding more than 8 currencies we force a resizing // operation - ensure it causes no issues public void addMultipleRatesSingle() { FxMatrix matrix = FxMatrix.builder() .addRate(GBP, USD, 1.6) .addRate(EUR, USD, 1.4) .addRate(EUR, CHF, 1.2) .addRate(EUR, CHF, 1.2) .addRate(CHF, AUD, 1.2) .addRate(SEK, AUD, 0.16) .addRate(JPY, USD, 0.0084) .addRate(JPY, CAD, 0.01) .addRate(USD, NZD, 1.3) .build(); assertThat(matrix.fxRate(GBP, USD)).isEqualTo(1.6); assertThat(matrix.fxRate(USD, GBP)).isEqualTo(1 / 1.6); assertThat(matrix.fxRate(EUR, USD)).isEqualTo(1.4, TOL); assertThat(matrix.fxRate(USD, EUR)).isEqualTo(1 / 1.4, TOL); assertThat(matrix.fxRate(EUR, GBP)).isEqualTo(1.4 / 1.6, TOL); assertThat(matrix.fxRate(GBP, EUR)).isEqualTo(1.6 / 1.4, TOL); assertThat(matrix.fxRate(EUR, CHF)).isEqualTo(1.2); } public void convertCurrencyAmount() { FxMatrix matrix = FxMatrix.builder() .addRate(GBP, EUR, 1.4) .addRate(GBP, USD, 1.6) .build(); CurrencyAmount amount = CurrencyAmount.of(GBP, 1600); assertThat(matrix.convert(amount, GBP)).isEqualTo(amount); assertThat(matrix.convert(amount, USD)) .hasCurrency(USD) .hasAmount(2560); assertThat(matrix.convert(amount, EUR)) .hasCurrency(EUR) .hasAmount(2240); } public void convertMultipleCurrencyAmountWithNoEntries() { FxMatrix matrix = FxMatrix.builder() .addRate(GBP, EUR, 1.4) .addRate(GBP, USD, 1.6) .build(); MultiCurrencyAmount amount = MultiCurrencyAmount.of(); assertThat(matrix.convert(amount, GBP)) .hasCurrency(GBP) .hasAmount(0); assertThat(matrix.convert(amount, USD)) .hasCurrency(USD) .hasAmount(0); assertThat(matrix.convert(amount, EUR)) .hasCurrency(EUR) .hasAmount(0); } public void convertMultipleCurrencyAmountWithSingleEntry() { FxMatrix matrix = FxMatrix.builder() .addRate(GBP, EUR, 1.4) .addRate(GBP, USD, 1.6) .build(); MultiCurrencyAmount amount = MultiCurrencyAmount.of(CurrencyAmount.of(GBP, 1600)); assertThat(matrix.convert(amount, GBP)) .hasCurrency(GBP) .hasAmount(1600); assertThat(matrix.convert(amount, USD)) .hasCurrency(USD) .hasAmount(2560); assertThat(matrix.convert(amount, EUR)) .hasCurrency(EUR) .hasAmount(2240); } public void convertMultipleCurrencyAmountWithMultipleEntries() { FxMatrix matrix = FxMatrix.builder() .addRate(GBP, EUR, 1.4) .addRate(GBP, USD, 1.6) .build(); MultiCurrencyAmount amount = MultiCurrencyAmount.of( CurrencyAmount.of(GBP, 1600), CurrencyAmount.of(EUR, 1200), CurrencyAmount.of(USD, 1500)); assertThat(matrix.convert(amount, GBP)) .hasCurrency(GBP) .hasAmount(1600d + (1200 / 1.4) + (1500 / 1.6), TOL); assertThat(matrix.convert(amount, USD)) .hasCurrency(USD) .hasAmount((1600d * 1.6) + ((1200 / 1.4) * 1.6) + 1500); assertThat(matrix.convert(amount, EUR)) .hasCurrency(EUR) .hasAmount((1600d * 1.4) + 1200 + ((1500 / 1.6) * 1.4)); } public void cannotMergeDisjointMatrices() { FxMatrix matrix1 = FxMatrix.builder() .addRate(GBP, USD, 1.6) .addRate(EUR, USD, 1.4) .build(); FxMatrix matrix2 = FxMatrix.builder() .addRate(CHF, AUD, 1.2) .addRate(SEK, AUD, 0.16) .build(); assertThrowsIllegalArg(() -> matrix1.merge(matrix2)); } public void mergeIgnoresDuplicateCurrencies() { FxMatrix matrix1 = FxMatrix.builder() .addRate(GBP, USD, 1.6) .addRate(EUR, USD, 1.4) .addRate(EUR, CHF, 1.2) .build(); FxMatrix matrix2 = FxMatrix.builder() .addRate(GBP, USD, 1.7) .addRate(EUR, USD, 1.5) .addRate(EUR, CHF, 1.3) .build(); FxMatrix result = matrix1.merge(matrix2); assertThat(result).isEqualTo(matrix1); } public void mergeAddsInAdditionalCurrencies() { FxMatrix matrix1 = FxMatrix.builder() .addRate(GBP, USD, 1.6) .addRate(EUR, USD, 1.4) .build(); FxMatrix matrix2 = FxMatrix.builder() .addRate(EUR, CHF, 1.2) .addRate(CHF, AUD, 1.2) .build(); FxMatrix result = matrix1.merge(matrix2); assertThat(result.getCurrencies()).contains(USD, GBP, EUR, CHF, AUD); assertThat(result.fxRate(GBP, USD)).isEqualTo(1.6); assertThat(result.fxRate(GBP, EUR)).isEqualTo(1.6 / 1.4, TOL); assertThat(result.fxRate(EUR, CHF)).isEqualTo(1.2); assertThat(result.fxRate(CHF, AUD)).isEqualTo(1.2); assertThat(result.fxRate(GBP, CHF)).isEqualTo((1.6 / 1.4) * 1.2, TOL); assertThat(result.fxRate(GBP, AUD)).isEqualTo((1.6 / 1.4) * 1.2 * 1.2, TOL); } public void equalsGood() { FxMatrix m1 = FxMatrix.builder() .addRate(GBP, USD, 1.4) .build(); FxMatrix m2 = FxMatrix.builder() .addRate(GBP, USD, 1.39) .build(); FxMatrix m3 = FxMatrix.builder() .addRate(GBP, USD, 1.39) .build(); FxMatrix m4 = FxMatrix.builder() .addRate(GBP, EUR, 1.2) .build(); assertThat(m1.equals(m1)).isTrue(); assertThat(m2.equals(m2)).isTrue(); assertThat(m3.equals(m3)).isTrue(); assertThat(m4.equals(m4)).isTrue(); assertThat(m1.equals(m2)).isFalse(); assertThat(m1.equals(m4)).isFalse(); assertThat(m2.equals(m3)).isTrue(); } public void equalsBad() { FxMatrix test = FxMatrix.builder() .addRate(USD, GBP, 1.4) .build(); assertThat(test.equals("")).isFalse(); assertThat(test.equals(null)).isFalse(); } public void hashCodeCoverage() { FxMatrix m1 = FxMatrix.builder() .addRate(GBP, USD, 1.4) .build(); FxMatrix m2 = FxMatrix.builder() .addRate(GBP, USD, 1.39) .build(); FxMatrix m3 = FxMatrix.builder() .addRate(GBP, USD, 1.39) .build(); assertThat(m1.hashCode()).isNotEqualTo(m2.hashCode()); assertThat(m2.hashCode()).isEqualTo(m3.hashCode()); } //------------------------------------------------------------------------- public void coverage() { coverImmutableBean(FxMatrix.empty()); coverImmutableBean(FxMatrix.builder() .addRate(GBP, USD, 1.6) .addRate(EUR, USD, 1.4) .addRate(EUR, CHF, 1.2) .build()); } public void testSerializeDeserialize() { FxMatrix test1 = FxMatrix.builder() .addRate(GBP, USD, 1.6) .addRate(EUR, USD, 1.4) .addRate(EUR, CHF, 1.2) .build(); FxMatrix test2 = FxMatrix.builder() .addRate(GBP, USD, 1.7) .addRate(EUR, USD, 1.5) .addRate(EUR, CHF, 1.3) .build(); cycleBean(FxMatrix.empty()); cycleBean(test1); cycleBean(test2); assertSerialization(FxMatrix.empty()); assertSerialization(test1); assertSerialization(test2); } private void cycleBean(Bean bean) { JodaBeanSer ser = JodaBeanSer.COMPACT; String result = ser.xmlWriter().write(bean); Bean cycled = ser.xmlReader().read(result); assertThat(cycled).isEqualTo(bean); } }