From deee1bcc318c2253053504e9803baf3c24d8fcf6 Mon Sep 17 00:00:00 2001 From: Stephen Colebourne Date: Tue, 22 Aug 2017 19:57:07 +0100 Subject: [PATCH] Add CSV format for trades (#1540) CSV format for trades - FRA, Swap and Term Deposit --- .../examples/data/export/ExportUtils.java | 2 +- .../opengamma/strata/collect/io/CsvRow.java | 8 +- .../strata/loader/csv/FraTradeCsvLoader.java | 159 ++++ .../loader/csv/FullSwapTradeCsvLoader.java | 669 ++++++++++++++ .../strata/loader/csv/SwapTradeCsvLoader.java | 231 +++++ .../loader/csv/TermDepositTradeCsvLoader.java | 156 ++++ .../strata/loader/csv/TradeCsvLoader.java | 457 +++++++++ .../LegalEntityRatesCurvesCsvLoaderTest.java | 2 +- .../loader/csv/RatesCurvesCsvLoaderTest.java | 2 +- .../strata/loader/csv/TradeCsvLoaderTest.java | 865 ++++++++++++++++++ .../opengamma/strata/loader/csv/trades.csv | 16 + .../e2e/SwapCrossCurrencyEnd2EndTest.java | 2 +- 12 files changed, 2561 insertions(+), 8 deletions(-) create mode 100644 modules/loader/src/main/java/com/opengamma/strata/loader/csv/FraTradeCsvLoader.java create mode 100644 modules/loader/src/main/java/com/opengamma/strata/loader/csv/FullSwapTradeCsvLoader.java create mode 100644 modules/loader/src/main/java/com/opengamma/strata/loader/csv/SwapTradeCsvLoader.java create mode 100644 modules/loader/src/main/java/com/opengamma/strata/loader/csv/TermDepositTradeCsvLoader.java create mode 100644 modules/loader/src/main/java/com/opengamma/strata/loader/csv/TradeCsvLoader.java create mode 100644 modules/loader/src/test/java/com/opengamma/strata/loader/csv/TradeCsvLoaderTest.java create mode 100644 modules/loader/src/test/resources/com/opengamma/strata/loader/csv/trades.csv diff --git a/examples/src/main/java/com/opengamma/strata/examples/data/export/ExportUtils.java b/examples/src/main/java/com/opengamma/strata/examples/data/export/ExportUtils.java index 2399b3dc6c..0efa29c9d9 100644 --- a/examples/src/main/java/com/opengamma/strata/examples/data/export/ExportUtils.java +++ b/examples/src/main/java/com/opengamma/strata/examples/data/export/ExportUtils.java @@ -109,7 +109,7 @@ public static void export( public static void export(String string, String fileName) { File file = new File(fileName); Unchecked.wrap(() -> Files.createParentDirs(file)); - Unchecked.wrap(() -> Files.write(string, file, StandardCharsets.UTF_8)); + Unchecked.wrap(() -> Files.asCharSink(file, StandardCharsets.UTF_8).write(string)); } } diff --git a/modules/collect/src/main/java/com/opengamma/strata/collect/io/CsvRow.java b/modules/collect/src/main/java/com/opengamma/strata/collect/io/CsvRow.java index d7e8076b8a..0549b38f6f 100644 --- a/modules/collect/src/main/java/com/opengamma/strata/collect/io/CsvRow.java +++ b/modules/collect/src/main/java/com/opengamma/strata/collect/io/CsvRow.java @@ -131,7 +131,7 @@ public String field(int index) { */ public String getField(String header) { return findField(header) - .orElseThrow(() -> new IllegalArgumentException("Header not found: " + header)); + .orElseThrow(() -> new IllegalArgumentException("Header not found: '" + header + "'")); } /** @@ -159,7 +159,7 @@ public Optional findField(String header) { */ public String getField(Pattern headerPattern) { return findField(headerPattern) - .orElseThrow(() -> new IllegalArgumentException("Header pattern not found: " + headerPattern)); + .orElseThrow(() -> new IllegalArgumentException("Header pattern not found: '" + headerPattern + "'")); } /** @@ -192,7 +192,7 @@ public Optional findField(Pattern headerPattern) { public String getValue(Pattern headerPattern) { String value = getField(headerPattern); if (value.isEmpty()) { - throw new IllegalArgumentException("No value was found for header pattern" + headerPattern); + throw new IllegalArgumentException("No value was found for header pattern: '" + headerPattern + "'"); } else { return value; } @@ -211,7 +211,7 @@ public String getValue(Pattern headerPattern) { public String getValue(String header) { String value = getField(header); if (value.isEmpty()) { - throw new IllegalArgumentException("No value was found for field " + header); + throw new IllegalArgumentException("No value was found for field: '" + header + "'"); } else { return value; } diff --git a/modules/loader/src/main/java/com/opengamma/strata/loader/csv/FraTradeCsvLoader.java b/modules/loader/src/main/java/com/opengamma/strata/loader/csv/FraTradeCsvLoader.java new file mode 100644 index 0000000000..e561f78c57 --- /dev/null +++ b/modules/loader/src/main/java/com/opengamma/strata/loader/csv/FraTradeCsvLoader.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2017 - present by OpenGamma Inc. and the OpenGamma group of companies + * + * Please see distribution for license. + */ +package com.opengamma.strata.loader.csv; + +import static com.opengamma.strata.loader.csv.TradeCsvLoader.BUY_SELL_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.CONVENTION_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.DATE_ADJ_CAL_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.DATE_ADJ_CNV_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.DAY_COUNT_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.END_DATE_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.FIXED_RATE_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.INDEX_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.INTERPOLATED_INDEX_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.NOTIONAL_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.PERIOD_TO_START_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.START_DATE_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.TRADE_DATE_FIELD; + +import java.time.LocalDate; +import java.time.Period; +import java.util.Optional; + +import com.google.common.collect.ImmutableList; +import com.opengamma.strata.basics.ReferenceData; +import com.opengamma.strata.basics.date.BusinessDayAdjustment; +import com.opengamma.strata.basics.date.BusinessDayConvention; +import com.opengamma.strata.basics.date.BusinessDayConventions; +import com.opengamma.strata.basics.date.DayCount; +import com.opengamma.strata.basics.date.HolidayCalendarId; +import com.opengamma.strata.basics.date.Tenor; +import com.opengamma.strata.basics.index.IborIndex; +import com.opengamma.strata.collect.io.CsvRow; +import com.opengamma.strata.product.TradeInfo; +import com.opengamma.strata.product.common.BuySell; +import com.opengamma.strata.product.fra.Fra; +import com.opengamma.strata.product.fra.FraTrade; +import com.opengamma.strata.product.fra.type.FraConvention; + +/** + * Loads FRA trades from CSV files. + */ +final class FraTradeCsvLoader { + + /** + * Parses from the CSV row. + * + * @param row the CSV row + * @param info the trade info + * @param refData the reference data + * @return the loaded trades, all errors are captured in the result + */ + static FraTrade parse(CsvRow row, TradeInfo info, ReferenceData refData) { + BuySell buySell = TradeCsvLoader.parseBuySell(row.getValue(BUY_SELL_FIELD)); + double notional = TradeCsvLoader.parseDouble(row.getValue(NOTIONAL_FIELD)); + double fixedRate = TradeCsvLoader.parseDoublePercent(row.getValue(FIXED_RATE_FIELD)); + Optional conventionOpt = row.findValue(CONVENTION_FIELD).map(s -> FraConvention.of(s)); + Optional periodToStartOpt = row.findValue(PERIOD_TO_START_FIELD).map(s -> Tenor.parse(s).getPeriod()); + Optional startDateOpt = row.findValue(START_DATE_FIELD).map(s -> TradeCsvLoader.parseDate(s)); + Optional endDateOpt = row.findValue(END_DATE_FIELD).map(s -> TradeCsvLoader.parseDate(s)); + Optional indexOpt = row.findValue(INDEX_FIELD).map(s -> IborIndex.of(s)); + Optional interpolatedOpt = row.findValue(INTERPOLATED_INDEX_FIELD).map(s -> IborIndex.of(s)); + Optional dayCountOpt = row.findValue(DAY_COUNT_FIELD).map(s -> DayCount.of(s)); + BusinessDayConvention dateCnv = row.findValue(DATE_ADJ_CNV_FIELD) + .map(s -> BusinessDayConvention.of(s)).orElse(BusinessDayConventions.MODIFIED_FOLLOWING); + Optional dateCalOpt = row.findValue(DATE_ADJ_CAL_FIELD).map(s -> HolidayCalendarId.of(s)); + // not parsing paymentDate, fixingDateOffset, discounting + + // use convention if available + if (conventionOpt.isPresent()) { + if (indexOpt.isPresent() || interpolatedOpt.isPresent() || dayCountOpt.isPresent()) { + throw new IllegalArgumentException( + "Fra trade had invalid combination of fields. When '" + CONVENTION_FIELD + + "' is present these fields must not be present: " + + ImmutableList.of(INDEX_FIELD, INTERPOLATED_INDEX_FIELD, DAY_COUNT_FIELD)); + } + FraConvention convention = conventionOpt.get(); + // explicit dates take precedence over relative ones + if (startDateOpt.isPresent() && endDateOpt.isPresent()) { + if (periodToStartOpt.isPresent()) { + throw new IllegalArgumentException( + "Fra trade had invalid combination of fields. When these fields are found " + + ImmutableList.of(CONVENTION_FIELD, START_DATE_FIELD, END_DATE_FIELD) + + " then these fields must not be present " + + ImmutableList.of(PERIOD_TO_START_FIELD)); + } + LocalDate startDate = startDateOpt.get(); + LocalDate endDate = endDateOpt.get(); + // NOTE: payment date assumed to be the start date + FraTrade trade = convention.toTrade(info, startDate, endDate, startDate, buySell, notional, fixedRate); + return adjustTrade(trade, dateCnv, dateCalOpt); + } + // relative dates + if (periodToStartOpt.isPresent() && info.getTradeDate().isPresent()) { + if (startDateOpt.isPresent() || endDateOpt.isPresent()) { + throw new IllegalArgumentException( + "Fra trade had invalid combination of fields. When these fields are found " + + ImmutableList.of(CONVENTION_FIELD, PERIOD_TO_START_FIELD, TRADE_DATE_FIELD) + + " then these fields must not be present " + + ImmutableList.of(START_DATE_FIELD, END_DATE_FIELD)); + } + LocalDate tradeDate = info.getTradeDate().get(); + Period periodToStart = periodToStartOpt.get(); + FraTrade trade = convention.createTrade(tradeDate, periodToStart, buySell, notional, fixedRate, refData); + trade = trade.toBuilder().info(info).build(); + return adjustTrade(trade, dateCnv, dateCalOpt); + } + + } else if (startDateOpt.isPresent() && endDateOpt.isPresent() && indexOpt.isPresent()) { + LocalDate startDate = startDateOpt.get(); + LocalDate endDate = endDateOpt.get(); + IborIndex index = indexOpt.get(); + Fra.Builder builder = Fra.builder() + .buySell(buySell) + .notional(notional) + .startDate(startDate) + .endDate(endDate) + .fixedRate(fixedRate) + .index(index); + interpolatedOpt.ifPresent(interpolated -> builder.indexInterpolated(interpolated)); + dayCountOpt.ifPresent(dayCount -> builder.dayCount(dayCount)); + return adjustTrade(FraTrade.of(info, builder.build()), dateCnv, dateCalOpt); + } + // no match + throw new IllegalArgumentException( + "Fra trade had invalid combination of fields. These fields are mandatory:" + + ImmutableList.of(BUY_SELL_FIELD, NOTIONAL_FIELD, FIXED_RATE_FIELD) + + " and one of these combinations is mandatory: " + + ImmutableList.of(CONVENTION_FIELD, TRADE_DATE_FIELD, PERIOD_TO_START_FIELD) + + " or " + + ImmutableList.of(CONVENTION_FIELD, START_DATE_FIELD, END_DATE_FIELD) + + " or " + + ImmutableList.of(START_DATE_FIELD, END_DATE_FIELD, INDEX_FIELD)); + } + + // adjust trade based on additional fields specified + private static FraTrade adjustTrade( + FraTrade trade, + BusinessDayConvention dateCnv, + Optional dateCalOpt) { + + if (!dateCalOpt.isPresent()) { + return trade; + } + Fra.Builder builder = trade.getProduct().toBuilder(); + dateCalOpt.ifPresent(cal -> builder.businessDayAdjustment(BusinessDayAdjustment.of(dateCnv, cal))); + return trade.toBuilder() + .product(builder.build()) + .build(); + } + + //------------------------------------------------------------------------- + // Restricted constructor. + private FraTradeCsvLoader() { + } + +} diff --git a/modules/loader/src/main/java/com/opengamma/strata/loader/csv/FullSwapTradeCsvLoader.java b/modules/loader/src/main/java/com/opengamma/strata/loader/csv/FullSwapTradeCsvLoader.java new file mode 100644 index 0000000000..8a2497b8c9 --- /dev/null +++ b/modules/loader/src/main/java/com/opengamma/strata/loader/csv/FullSwapTradeCsvLoader.java @@ -0,0 +1,669 @@ +/* + * Copyright (C) 2017 - present by OpenGamma Inc. and the OpenGamma group of companies + * + * Please see distribution for license. + */ +package com.opengamma.strata.loader.csv; + +import static com.opengamma.strata.loader.csv.TradeCsvLoader.CURRENCY_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.DATE_ADJ_CAL_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.DATE_ADJ_CNV_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.DAY_COUNT_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.END_DATE_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.FIXED_RATE_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.INDEX_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.NOTIONAL_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.START_DATE_FIELD; + +import java.time.LocalDate; +import java.time.Period; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; +import com.opengamma.strata.basics.currency.Currency; +import com.opengamma.strata.basics.currency.CurrencyAmount; +import com.opengamma.strata.basics.date.AdjustableDate; +import com.opengamma.strata.basics.date.BusinessDayAdjustment; +import com.opengamma.strata.basics.date.BusinessDayConvention; +import com.opengamma.strata.basics.date.BusinessDayConventions; +import com.opengamma.strata.basics.date.DayCount; +import com.opengamma.strata.basics.date.DaysAdjustment; +import com.opengamma.strata.basics.date.HolidayCalendarId; +import com.opengamma.strata.basics.date.HolidayCalendarIds; +import com.opengamma.strata.basics.date.Tenor; +import com.opengamma.strata.basics.index.FloatingRateName; +import com.opengamma.strata.basics.index.FxIndex; +import com.opengamma.strata.basics.index.IborIndex; +import com.opengamma.strata.basics.index.OvernightIndex; +import com.opengamma.strata.basics.index.PriceIndex; +import com.opengamma.strata.basics.schedule.Frequency; +import com.opengamma.strata.basics.schedule.PeriodicSchedule; +import com.opengamma.strata.basics.schedule.RollConvention; +import com.opengamma.strata.basics.schedule.StubConvention; +import com.opengamma.strata.basics.value.ValueSchedule; +import com.opengamma.strata.collect.Guavate; +import com.opengamma.strata.collect.io.CsvRow; +import com.opengamma.strata.product.TradeInfo; +import com.opengamma.strata.product.common.PayReceive; +import com.opengamma.strata.product.swap.CompoundingMethod; +import com.opengamma.strata.product.swap.FixedRateCalculation; +import com.opengamma.strata.product.swap.FixedRateStubCalculation; +import com.opengamma.strata.product.swap.FixingRelativeTo; +import com.opengamma.strata.product.swap.FxResetCalculation; +import com.opengamma.strata.product.swap.FxResetFixingRelativeTo; +import com.opengamma.strata.product.swap.IborRateCalculation; +import com.opengamma.strata.product.swap.IborRateResetMethod; +import com.opengamma.strata.product.swap.IborRateStubCalculation; +import com.opengamma.strata.product.swap.InflationRateCalculation; +import com.opengamma.strata.product.swap.NegativeRateMethod; +import com.opengamma.strata.product.swap.NotionalSchedule; +import com.opengamma.strata.product.swap.OvernightAccrualMethod; +import com.opengamma.strata.product.swap.OvernightRateCalculation; +import com.opengamma.strata.product.swap.PaymentRelativeTo; +import com.opengamma.strata.product.swap.PaymentSchedule; +import com.opengamma.strata.product.swap.PriceIndexCalculationMethod; +import com.opengamma.strata.product.swap.RateCalculation; +import com.opengamma.strata.product.swap.RateCalculationSwapLeg; +import com.opengamma.strata.product.swap.ResetSchedule; +import com.opengamma.strata.product.swap.Swap; +import com.opengamma.strata.product.swap.SwapTrade; + +/** + * Loads Swap trades from CSV files. + */ +final class FullSwapTradeCsvLoader { + + // CSV column headers + static final String DIRECTION_FIELD = "Direction"; + private static final String FREQUENCY_FIELD = "Frequency"; + private static final String START_DATE_CNV_FIELD = "Start Date Convention"; + private static final String START_DATE_CAL_FIELD = "Start Date Calendar"; + private static final String END_DATE_CNV_FIELD = "End Date Convention"; + private static final String END_DATE_CAL_FIELD = "End Date Calendar"; + private static final String ROLL_CONVENTION_FIELD = "Roll Convention"; + private static final String STUB_CONVENTION_FIELD = "Stub Convention"; + private static final String FIRST_REGULAR_START_DATE_FIELD = "First Regular Start Date"; + private static final String LAST_REGULAR_END_DATE_FIELD = "Last Regular End Date"; + private static final String OVERRIDE_START_DATE_FIELD = "Override Start Date"; + private static final String OVERRIDE_START_DATE_CNV_FIELD = "Override Start Date Convention"; + private static final String OVERRIDE_START_DATE_CAL_FIELD = "Override Start Date Calendar"; + + private static final String PAYMENT_FREQUENCY_FIELD = "Payment Frequency"; + private static final String PAYMENT_RELATIVE_TO_FIELD = "Payment Relative To"; + private static final String PAYMENT_OFFSET_DAYS_FIELD = "Payment Offset Days"; + private static final String PAYMENT_OFFSET_CAL_FIELD = "Payment Offset Calendar"; + private static final String PAYMENT_OFFSET_ADJ_CNV_FIELD = "Payment Offset Adjustment Convention"; + private static final String PAYMENT_OFFSET_ADJ_CAL_FIELD = "Payment Offset Adjustment Calendar"; + private static final String COMPOUNDING_METHOD_FIELD = "Compounding Method"; + + private static final String NOTIONAL_CURRENCY_FIELD = "Notional Currency"; + private static final String NOTIONAL_INITIAL_EXCHANGE_FIELD = "Notional Initial Exchange"; + private static final String NOTIONAL_INTERMEDIATE_EXCHANGE_FIELD = "Notional Intermediate Exchange"; + private static final String NOTIONAL_FINAL_EXCHANGE_FIELD = "Notional Final Exchange"; + private static final String FX_RESET_INDEX_FIELD = "FX Reset Index"; + private static final String FX_RESET_RELATIVE_TO_FIELD = "FX Reset Relative To"; + private static final String FX_RESET_OFFSET_DAYS_FIELD = "FX Reset Offset Days"; + private static final String FX_RESET_OFFSET_CAL_FIELD = "FX Reset Offset Calendar"; + private static final String FX_RESET_OFFSET_ADJ_CNV_FIELD = "FX Reset Offset Adjustment Convention"; + private static final String FX_RESET_OFFSET_ADJ_CAL_FIELD = "FX Reset Offset Adjustment Calendar"; + + private static final String INITIAL_STUB_RATE_FIELD = "Initial Stub Rate"; + private static final String INITIAL_STUB_AMOUNT_FIELD = "Initial Stub Amount"; + private static final String INITIAL_STUB_INDEX_FIELD = "Initial Stub Index"; + private static final String INITIAL_STUB_INTERPOLATED_INDEX_FIELD = "Initial Stub Interpolated Index"; + private static final String FINAL_STUB_RATE_FIELD = "Final Stub Rate"; + private static final String FINAL_STUB_AMOUNT_FIELD = "Final Stub Amount"; + private static final String FINAL_STUB_INDEX_FIELD = "Final Stub Index"; + private static final String FINAL_STUB_INTERPOLATED_INDEX_FIELD = "Final Stub Interpolated Index"; + private static final String RESET_FREQUENCY_FIELD = "Reset Frequency"; + private static final String RESET_DATE_CNV_FIELD = "Reset Date Convention"; + private static final String RESET_DATE_CAL_FIELD = "Reset Date Calendar"; + private static final String RESET_METHOD_FIELD = "Reset Method"; + private static final String FIXING_RELATIVE_TO_FIELD = "Fixing Relative To"; + private static final String FIXING_OFFSET_DAYS_FIELD = "Fixing Offset Days"; + private static final String FIXING_OFFSET_CAL_FIELD = "Fixing Offset Calendar"; + private static final String FIXING_OFFSET_ADJ_CNV_FIELD = "Fixing Offset Adjustment Convention"; + private static final String FIXING_OFFSET_ADJ_CAL_FIELD = "Fixing Offset Adjustment Calendar"; + private static final String NEGATIVE_RATE_METHOD_FIELD = "Negative Rate Method"; + private static final String FIRST_RATE_FIELD = "First Rate"; + private static final String ACCRUAL_METHOD_FIELD = "Accrual Method"; + private static final String RATE_CUT_OFF_DAYS_FIELD = "Rate Cut Off Days"; + private static final String INFLATION_LAG_FIELD = "Inflation Lag"; + private static final String INFLATION_METHOD_FIELD = "Inflation Method"; + private static final String INFLATION_FIRST_INDEX_VALUE_FIELD = "Inflation First Index Value"; + + private static final String GEARING_FIELD = "Gearing"; + private static final String SPREAD_FIELD = "Spread"; + + /** + * Parses from the CSV row. + * + * @param row the CSV row + * @param info the trade info + * @return the loaded trades, all errors are captured in the result + */ + static SwapTrade parse(CsvRow row, TradeInfo info) { + List legs = new ArrayList<>(); + // parse any number of legs by looking for 'Leg n Pay Receive' + Optional payReceive = Optional.of(getValue(row, "Leg 1 ", DIRECTION_FIELD)); + int i = 1; + while (payReceive.isPresent()) { + legs.add(parseLeg(row, "Leg " + i + " ")); + i++; + payReceive = findValue(row, "Leg " + i + " ", DIRECTION_FIELD); + } + Swap swap = Swap.of(legs); + return SwapTrade.of(info, swap); + } + + // parse a single leg + private static RateCalculationSwapLeg parseLeg(CsvRow row, String leg) { + PayReceive payReceive = TradeCsvLoader.parsePayReceive(getValue(row, leg, DIRECTION_FIELD)); + PeriodicSchedule accrualSch = parseAccrualSchedule(row, leg); + PaymentSchedule paymentSch = parsePaymentSchedule(row, leg, accrualSch.getFrequency()); + NotionalSchedule notionalSch = parseNotionalSchedule(row, leg); + RateCalculation calc = parseRateCalculation( + row, leg, accrualSch.getBusinessDayAdjustment(), accrualSch.getFrequency(), notionalSch.getCurrency()); + return RateCalculationSwapLeg.builder() + .payReceive(payReceive) + .accrualSchedule(accrualSch) + .paymentSchedule(paymentSch) + .notionalSchedule(notionalSch) + .calculation(calc) + .build(); + } + + //------------------------------------------------------------------------- + // accrual schedule + private static PeriodicSchedule parseAccrualSchedule(CsvRow row, String leg) { + PeriodicSchedule.Builder builder = PeriodicSchedule.builder(); + // basics + builder.startDate(TradeCsvLoader.parseDate(getValueWithFallback(row, leg, START_DATE_FIELD))); + builder.endDate(TradeCsvLoader.parseDate(getValueWithFallback(row, leg, END_DATE_FIELD))); + builder.frequency(Frequency.parse(getValue(row, leg, FREQUENCY_FIELD))); + // adjustments + BusinessDayAdjustment dateAdj = parseBusinessDayAdjustment(row, leg, DATE_ADJ_CNV_FIELD, DATE_ADJ_CAL_FIELD) + .orElse(BusinessDayAdjustment.NONE); + Optional startDateAdj = + parseBusinessDayAdjustment(row, leg, START_DATE_CNV_FIELD, START_DATE_CAL_FIELD); + Optional endDateAdj = + parseBusinessDayAdjustment(row, leg, END_DATE_CNV_FIELD, END_DATE_CAL_FIELD); + builder.businessDayAdjustment(dateAdj); + if (startDateAdj.isPresent() && !startDateAdj.get().equals(dateAdj)) { + builder.startDateBusinessDayAdjustment(startDateAdj.get()); + } + if (endDateAdj.isPresent() && !endDateAdj.get().equals(dateAdj)) { + builder.endDateBusinessDayAdjustment(endDateAdj.get()); + } + // optionals + builder.stubConvention(findValueWithFallback(row, leg, STUB_CONVENTION_FIELD) + .map(s -> StubConvention.of(s)) + .orElse(StubConvention.SHORT_INITIAL)); + findValue(row, leg, ROLL_CONVENTION_FIELD) + .map(s -> RollConvention.of(s)) + .ifPresent(v -> builder.rollConvention(v)); + findValue(row, leg, FIRST_REGULAR_START_DATE_FIELD) + .map(s -> TradeCsvLoader.parseDate(s)) + .ifPresent(v -> builder.firstRegularStartDate(v)); + findValue(row, leg, LAST_REGULAR_END_DATE_FIELD) + .map(s -> TradeCsvLoader.parseDate(s)) + .ifPresent(v -> builder.lastRegularEndDate(v)); + parseAdjustableDate( + row, leg, OVERRIDE_START_DATE_FIELD, OVERRIDE_START_DATE_CNV_FIELD, OVERRIDE_START_DATE_CAL_FIELD) + .ifPresent(d -> builder.overrideStartDate(d)); + return builder.build(); + } + + //------------------------------------------------------------------------- + // payment schedule + private static PaymentSchedule parsePaymentSchedule(CsvRow row, String leg, Frequency accrualFrequency) { + PaymentSchedule.Builder builder = PaymentSchedule.builder(); + // basics + builder.paymentFrequency(findValue(row, leg, PAYMENT_FREQUENCY_FIELD) + .map(s -> Frequency.parse(s)) + .orElse(accrualFrequency)); + Optional offsetOpt = parseDaysAdjustment( + row, + leg, + PAYMENT_OFFSET_DAYS_FIELD, + PAYMENT_OFFSET_CAL_FIELD, + PAYMENT_OFFSET_ADJ_CNV_FIELD, + PAYMENT_OFFSET_ADJ_CAL_FIELD); + builder.paymentDateOffset(offsetOpt.orElse(DaysAdjustment.NONE)); + // optionals + findValue(row, leg, PAYMENT_RELATIVE_TO_FIELD) + .map(s -> PaymentRelativeTo.of(s)) + .ifPresent(v -> builder.paymentRelativeTo(v)); + findValue(row, leg, COMPOUNDING_METHOD_FIELD) + .map(s -> CompoundingMethod.of(s)) + .ifPresent(v -> builder.compoundingMethod(v)); + return builder.build(); + } + + //------------------------------------------------------------------------- + // notional schedule + private static NotionalSchedule parseNotionalSchedule(CsvRow row, String leg) { + NotionalSchedule.Builder builder = NotionalSchedule.builder(); + // basics + Currency currency = Currency.of(getValueWithFallback(row, leg, CURRENCY_FIELD)); + builder.currency(currency); + builder.amount(ValueSchedule.of(TradeCsvLoader.parseDouble(getValueWithFallback(row, leg, NOTIONAL_FIELD)))); + // fx reset + Optional fxIndexOpt = findValue(row, leg, FX_RESET_INDEX_FIELD).map(s -> FxIndex.of(s)); + Optional notionalCurrencyOpt = findValue(row, leg, NOTIONAL_CURRENCY_FIELD).map(s -> Currency.of(s)); + Optional fxFixingRelativeToOpt = findValue(row, leg, FX_RESET_RELATIVE_TO_FIELD) + .map(s -> FxResetFixingRelativeTo.of(s)); + Optional fxResetAdjOpt = parseDaysAdjustment( + row, + leg, + FX_RESET_OFFSET_DAYS_FIELD, + FX_RESET_OFFSET_CAL_FIELD, + FX_RESET_OFFSET_ADJ_CNV_FIELD, + FX_RESET_OFFSET_ADJ_CAL_FIELD); + if (fxIndexOpt.isPresent()) { + FxIndex fxIndex = fxIndexOpt.get(); + FxResetCalculation.Builder fxResetBuilder = FxResetCalculation.builder(); + fxResetBuilder.index(fxIndex); + fxResetBuilder.referenceCurrency(notionalCurrencyOpt.orElse(fxIndex.getCurrencyPair().other(currency))); + fxFixingRelativeToOpt.ifPresent(v -> fxResetBuilder.fixingRelativeTo(v)); + fxResetAdjOpt.ifPresent(v -> fxResetBuilder.fixingDateOffset(v)); + builder.fxReset(fxResetBuilder.build()); + } else if (notionalCurrencyOpt.isPresent() || fxFixingRelativeToOpt.isPresent() || fxResetAdjOpt.isPresent()) { + throw new IllegalArgumentException("Swap trade FX Reset must define field '" + leg + FX_RESET_INDEX_FIELD + "'"); + } + // optionals + findValue(row, leg, NOTIONAL_INITIAL_EXCHANGE_FIELD) + .map(s -> TradeCsvLoader.parseBoolean(s)) + .ifPresent(v -> builder.initialExchange(v)); + findValue(row, leg, NOTIONAL_INTERMEDIATE_EXCHANGE_FIELD) + .map(s -> TradeCsvLoader.parseBoolean(s)) + .ifPresent(v -> builder.intermediateExchange(v)); + findValue(row, leg, NOTIONAL_FINAL_EXCHANGE_FIELD) + .map(s -> TradeCsvLoader.parseBoolean(s)) + .ifPresent(v -> builder.finalExchange(v)); + return builder.build(); + } + + //------------------------------------------------------------------------- + // rate calculation + private static RateCalculation parseRateCalculation( + CsvRow row, + String leg, + BusinessDayAdjustment bda, + Frequency accrualFrequency, + Currency currency) { + + Optional fixedRateOpt = findValue(row, leg, FIXED_RATE_FIELD).map(s -> TradeCsvLoader.parseDoublePercent(s)); + Optional indexOpt = findValue(row, leg, INDEX_FIELD); + if (fixedRateOpt.isPresent()) { + if (indexOpt.isPresent()) { + throw new IllegalArgumentException( + "Swap leg must not define both '" + leg + FIXED_RATE_FIELD + "' and '" + leg + INDEX_FIELD + "'"); + } + return parseFixedRateCalculation(row, leg, fixedRateOpt.get(), currency); + } + if (!indexOpt.isPresent()) { + throw new IllegalArgumentException( + "Swap leg must define either '" + leg + FIXED_RATE_FIELD + "' or '" + leg + INDEX_FIELD + "'"); + } + // index might be whole Ibor Index or Floating Rate Name + String indexStr = indexOpt.get(); + Optional frnOpt = FloatingRateName.extendedEnum().find(indexStr); + if (frnOpt.isPresent()) { + FloatingRateName frn = frnOpt.get(); + switch (frn.getType()) { + case IBOR: { + // imply index from accrual frequency + IborIndex ibor = frn.toIborIndex(Tenor.of(accrualFrequency.getPeriod())); + return parseIborRateCalculation(row, leg, ibor, bda, currency); + } + case OVERNIGHT_COMPOUNDED: { + return parseOvernightRateCalculation(row, leg, frn.toOvernightIndex(), OvernightAccrualMethod.COMPOUNDED); + } + case OVERNIGHT_AVERAGED: { + return parseOvernightRateCalculation(row, leg, frn.toOvernightIndex(), OvernightAccrualMethod.AVERAGED); + } + case PRICE: { + return parseInflationRateCalculation(row, leg, frn.toPriceIndex(), currency); + } + default: + throw new IllegalArgumentException("Swap trade index type not known: " + indexStr); + } + } + Optional iborOpt = IborIndex.extendedEnum().find(indexStr); + if (iborOpt.isPresent()) { + return parseIborRateCalculation(row, leg, iborOpt.get(), bda, currency); + } + Optional overnightOpt = OvernightIndex.extendedEnum().find(indexStr); + if (overnightOpt.isPresent()) { + return parseOvernightRateCalculation(row, leg, overnightOpt.get(), OvernightAccrualMethod.COMPOUNDED); + } + Optional priceOpt = PriceIndex.extendedEnum().find(indexStr); + if (priceOpt.isPresent()) { + return parseInflationRateCalculation(row, leg, priceOpt.get(), currency); + } + throw new IllegalArgumentException("Swap trade index not known: " + indexStr); + } + + //------------------------------------------------------------------------- + // fixed rate calculation + private static RateCalculation parseFixedRateCalculation(CsvRow row, String leg, double fixedRate, Currency currency) { + FixedRateCalculation.Builder builder = FixedRateCalculation.builder(); + // basics + builder.dayCount(DayCount.of(getValue(row, leg, DAY_COUNT_FIELD))); + builder.rate(ValueSchedule.of(fixedRate)); + // initial stub + Optional initialStubRateOpt = findValue(row, leg, INITIAL_STUB_RATE_FIELD) + .map(s -> TradeCsvLoader.parseDoublePercent(s)); + Optional initialStubAmountOpt = findValue(row, leg, INITIAL_STUB_AMOUNT_FIELD) + .map(s -> TradeCsvLoader.parseDouble(s)); + if (initialStubRateOpt.isPresent() && initialStubAmountOpt.isPresent()) { + throw new IllegalArgumentException( + "Swap leg must not define both '" + leg + INITIAL_STUB_RATE_FIELD + "' and '" + leg + INITIAL_STUB_AMOUNT_FIELD + "'"); + } + initialStubRateOpt.ifPresent(v -> builder.initialStub( + FixedRateStubCalculation.ofFixedRate(v))); + initialStubAmountOpt.ifPresent(v -> builder.initialStub( + FixedRateStubCalculation.ofKnownAmount(CurrencyAmount.of(currency, v)))); + // final stub + Optional finalStubRateOpt = findValue(row, leg, FINAL_STUB_RATE_FIELD) + .map(s -> TradeCsvLoader.parseDoublePercent(s)); + Optional finalStubAmountOpt = findValue(row, leg, FINAL_STUB_AMOUNT_FIELD) + .map(s -> TradeCsvLoader.parseDouble(s)); + if (finalStubRateOpt.isPresent() && finalStubAmountOpt.isPresent()) { + throw new IllegalArgumentException( + "Swap leg must not define both '" + leg + FINAL_STUB_RATE_FIELD + "' and '" + leg + FINAL_STUB_AMOUNT_FIELD + "'"); + } + finalStubRateOpt.ifPresent(v -> builder.finalStub( + FixedRateStubCalculation.ofFixedRate(v))); + finalStubAmountOpt.ifPresent(v -> builder.finalStub( + FixedRateStubCalculation.ofKnownAmount(CurrencyAmount.of(currency, v)))); + return builder.build(); + } + + //------------------------------------------------------------------------- + // ibor rate calculation + private static RateCalculation parseIborRateCalculation( + CsvRow row, + String leg, + IborIndex iborIndex, + BusinessDayAdjustment bda, + Currency currency) { + + IborRateCalculation.Builder builder = IborRateCalculation.builder(); + // basics + builder.index(iborIndex); + // reset + Optional resetFrequencyOpt = findValue(row, leg, RESET_FREQUENCY_FIELD).map(v -> Frequency.parse(v)); + IborRateResetMethod resetMethod = findValue(row, leg, RESET_METHOD_FIELD) + .map(v -> IborRateResetMethod.of(v)) + .orElse(IborRateResetMethod.WEIGHTED); + BusinessDayAdjustment resetDateAdj = + parseBusinessDayAdjustment(row, leg, RESET_DATE_CNV_FIELD, RESET_DATE_CAL_FIELD).orElse(bda); + resetFrequencyOpt.ifPresent(freq -> builder.resetPeriods(ResetSchedule.builder() + .resetFrequency(freq) + .resetMethod(resetMethod) + .businessDayAdjustment(resetDateAdj) + .build())); + // optionals, no ability to set firstFixingDateOffset + findValue(row, leg, DAY_COUNT_FIELD) + .map(s -> DayCount.of(s)) + .ifPresent(v -> builder.dayCount(v)); + findValue(row, leg, FIXING_RELATIVE_TO_FIELD) + .map(s -> FixingRelativeTo.of(s)) + .ifPresent(v -> builder.fixingRelativeTo(v)); + Optional fixingAdjOpt = parseDaysAdjustment( + row, + leg, + FIXING_OFFSET_DAYS_FIELD, + FIXING_OFFSET_CAL_FIELD, + FIXING_OFFSET_ADJ_CNV_FIELD, + FIXING_OFFSET_ADJ_CAL_FIELD); + fixingAdjOpt.ifPresent(v -> builder.fixingDateOffset(v)); + findValue(row, leg, NEGATIVE_RATE_METHOD_FIELD).map(s -> NegativeRateMethod.of(s)) + .ifPresent(v -> builder.negativeRateMethod(v)); + findValue(row, leg, FIRST_RATE_FIELD) + .map(s -> TradeCsvLoader.parseDoublePercent(s)) + .ifPresent(v -> builder.firstRate(v)); + findValue(row, leg, GEARING_FIELD) + .map(s -> TradeCsvLoader.parseDouble(s)) + .ifPresent(v -> builder.gearing(ValueSchedule.of(v))); + findValue(row, leg, SPREAD_FIELD) + .map(s -> TradeCsvLoader.parseDoublePercent(s)) + .ifPresent(v -> builder.spread(ValueSchedule.of(v))); + // initial stub + Optional initialStub = parseIborStub( + row, + leg, + currency, + builder, + INITIAL_STUB_RATE_FIELD, + INITIAL_STUB_AMOUNT_FIELD, + INITIAL_STUB_INDEX_FIELD, + INITIAL_STUB_INTERPOLATED_INDEX_FIELD); + initialStub.ifPresent(stub -> builder.initialStub(stub)); + // final stub + Optional finalStub = parseIborStub( + row, + leg, + currency, + builder, + FINAL_STUB_RATE_FIELD, + FINAL_STUB_AMOUNT_FIELD, + FINAL_STUB_INDEX_FIELD, + FINAL_STUB_INTERPOLATED_INDEX_FIELD); + finalStub.ifPresent(stub -> builder.finalStub(stub)); + return builder.build(); + } + + // an Ibor stub + private static Optional parseIborStub( + CsvRow row, + String leg, + Currency currency, + IborRateCalculation.Builder builder, + String rateField, + String amountField, + String indexField, + String interpolatedField) { + + Optional stubRateOpt = findValue(row, leg, rateField).map(s -> TradeCsvLoader.parseDoublePercent(s)); + Optional stubAmountOpt = findValue(row, leg, amountField).map(s -> TradeCsvLoader.parseDouble(s)); + Optional stubIndexOpt = findValue(row, leg, indexField).map(s -> IborIndex.of(s)); + Optional stubIndex2Opt = findValue(row, leg, interpolatedField).map(s -> IborIndex.of(s)); + if (stubRateOpt.isPresent() && !stubAmountOpt.isPresent() && !stubIndexOpt.isPresent() && !stubIndex2Opt.isPresent()) { + return Optional.of(IborRateStubCalculation.ofFixedRate(stubRateOpt.get())); + + } else if (!stubRateOpt.isPresent() && stubAmountOpt.isPresent() && !stubIndexOpt.isPresent() && !stubIndex2Opt.isPresent()) { + return Optional.of(IborRateStubCalculation.ofKnownAmount(CurrencyAmount.of(currency, stubAmountOpt.get()))); + + } else if (!stubRateOpt.isPresent() && !stubAmountOpt.isPresent() && stubIndexOpt.isPresent()) { + if (stubIndex2Opt.isPresent()) { + return Optional.of(IborRateStubCalculation.ofIborInterpolatedRate(stubIndexOpt.get(), stubIndex2Opt.get())); + } else { + return Optional.of(IborRateStubCalculation.ofIborRate(stubIndexOpt.get())); + } + } else if (stubRateOpt.isPresent() || stubAmountOpt.isPresent() || + stubIndexOpt.isPresent() || stubIndex2Opt.isPresent()) { + throw new IllegalArgumentException( + "Swap leg must define only one of the following fields " + + ImmutableList.of(leg + rateField, leg + amountField, leg + indexField) + + ", and '" + leg + interpolatedField + "' is only allowed with '" + leg + indexField + "'"); + } + return Optional.empty(); + } + + //------------------------------------------------------------------------- + // overnight rate calculation + private static RateCalculation parseOvernightRateCalculation( + CsvRow row, + String leg, + OvernightIndex overnightIndex, + OvernightAccrualMethod accrualMethod) { + + OvernightRateCalculation.Builder builder = OvernightRateCalculation.builder(); + // basics + builder.index(overnightIndex); + builder.accrualMethod(findValue(row, leg, ACCRUAL_METHOD_FIELD) + .map(s -> OvernightAccrualMethod.of(s)) + .orElse(accrualMethod)); + // optionals + findValue(row, leg, DAY_COUNT_FIELD) + .map(s -> DayCount.of(s)) + .ifPresent(v -> builder.dayCount(v)); + findValue(row, leg, RATE_CUT_OFF_DAYS_FIELD) + .map(s -> Integer.valueOf(s)) + .ifPresent(v -> builder.rateCutOffDays(v)); + findValue(row, leg, NEGATIVE_RATE_METHOD_FIELD).map(s -> NegativeRateMethod.of(s)) + .ifPresent(v -> builder.negativeRateMethod(v)); + findValue(row, leg, GEARING_FIELD) + .map(s -> TradeCsvLoader.parseDouble(s)) + .ifPresent(v -> builder.gearing(ValueSchedule.of(v))); + findValue(row, leg, SPREAD_FIELD) + .map(s -> TradeCsvLoader.parseDoublePercent(s)) + .ifPresent(v -> builder.spread(ValueSchedule.of(v))); + return builder.build(); + } + + //------------------------------------------------------------------------- + // inflation rate calculation + private static RateCalculation parseInflationRateCalculation(CsvRow row, String leg, PriceIndex priceIndex, Currency currency) { + InflationRateCalculation.Builder builder = InflationRateCalculation.builder(); + // basics + builder.index(priceIndex); + builder.lag(parseInflationLag(findValue(row, leg, INFLATION_LAG_FIELD), currency)); + builder.indexCalculationMethod(parseInflationMethod(findValue(row, leg, INFLATION_METHOD_FIELD), currency)); + // optionals + findValue(row, leg, INFLATION_FIRST_INDEX_VALUE_FIELD) + .map(s -> TradeCsvLoader.parseDouble(s)) + .ifPresent(v -> builder.firstIndexValue(v)); + findValue(row, leg, GEARING_FIELD) + .map(s -> TradeCsvLoader.parseDouble(s)) + .ifPresent(v -> builder.gearing(ValueSchedule.of(v))); + return builder.build(); + } + + // parse inflation lag with convention defaults + private static Period parseInflationLag(Optional strOpt, Currency currency) { + if (!strOpt.isPresent()) { + if (Currency.GBP.equals(currency)) { + return Period.ofMonths(2); + } + return Period.ofMonths(3); + } + String str = strOpt.get(); + Integer months = Ints.tryParse(str); + if (months != null) { + return Period.ofMonths(months); + } + return Tenor.parse(str).getPeriod(); + } + + // parse inflation method with convention defaults + private static PriceIndexCalculationMethod parseInflationMethod(Optional strOpt, Currency currency) { + if (!strOpt.isPresent()) { + if (Currency.JPY.equals(currency)) { + return PriceIndexCalculationMethod.INTERPOLATED_JAPAN; + } else if (Currency.USD.equals(currency)) { + return PriceIndexCalculationMethod.INTERPOLATED; + } + return PriceIndexCalculationMethod.MONTHLY; + } + return PriceIndexCalculationMethod.of(strOpt.get()); + } + + //------------------------------------------------------------------------- + // days adjustment, defaulting business day convention + private static Optional parseBusinessDayAdjustment( + CsvRow row, + String leg, + String cnvField, + String calField) { + + BusinessDayConvention dateCnv = findValue(row, leg, cnvField) + .map(s -> BusinessDayConvention.of(s)) + .orElse(BusinessDayConventions.MODIFIED_FOLLOWING); + return findValue(row, leg, calField) + .map(s -> HolidayCalendarId.of(s)) + .map(cal -> BusinessDayAdjustment.of(dateCnv, cal)); + } + + // days adjustment, defaulting calendar and adjustment + private static Optional parseDaysAdjustment( + CsvRow row, + String leg, + String daysField, + String daysCalField, + String cnvField, + String calField) { + + Optional daysOpt = findValue(row, leg, daysField) + .map(s -> new Integer(s)); + HolidayCalendarId cal = findValue(row, leg, daysCalField) + .map(s -> HolidayCalendarId.of(s)) + .orElse(HolidayCalendarIds.NO_HOLIDAYS); + BusinessDayAdjustment bda = parseBusinessDayAdjustment(row, leg, cnvField, calField) + .orElse(BusinessDayAdjustment.NONE); + if (!daysOpt.isPresent()) { + return Optional.empty(); + } + return Optional.of(DaysAdjustment.builder() + .days(daysOpt.get()) + .calendar(cal) + .adjustment(bda) + .build()); + } + + // adjustable date, defaulting business day convention and holiday calendar + private static Optional parseAdjustableDate( + CsvRow row, + String leg, + String dateField, + String cnvField, + String calField) { + + Optional dateOpt = findValue(row, leg, dateField).map(s -> TradeCsvLoader.parseDate(s)); + if (!dateOpt.isPresent()) { + return Optional.empty(); + } + BusinessDayConvention dateCnv = findValue(row, leg, cnvField) + .map(s -> BusinessDayConvention.of(s)) + .orElse(BusinessDayConventions.MODIFIED_FOLLOWING); + HolidayCalendarId cal = findValue(row, leg, calField) + .map(s -> HolidayCalendarId.of(s)) + .orElse(HolidayCalendarIds.NO_HOLIDAYS); + return Optional.of(AdjustableDate.of(dateOpt.get(), BusinessDayAdjustment.of(dateCnv, cal))); + } + + //------------------------------------------------------------------------- + // gets value from CSV + private static String getValue(CsvRow row, String leg, String field) { + return findValue(row, leg, field) + .orElseThrow(() -> new IllegalArgumentException("Swap leg must define field: '" + leg + field + "'")); + } + + // gets value from CSV + private static String getValueWithFallback(CsvRow row, String leg, String field) { + return findValueWithFallback(row, leg, field) + .orElseThrow(() -> new IllegalArgumentException("Swap leg must define field: '" + leg + field + "' or '" + field + "'")); + } + + // finds value from CSV + private static Optional findValue(CsvRow row, String leg, String field) { + return row.findValue(leg + field); + } + + // finds value from CSV + private static Optional findValueWithFallback(CsvRow row, String leg, String field) { + return Guavate.firstNonEmpty(row.findValue(leg + field), row.findValue(field)); + } + + //------------------------------------------------------------------------- + // Restricted constructor. + private FullSwapTradeCsvLoader() { + } + +} diff --git a/modules/loader/src/main/java/com/opengamma/strata/loader/csv/SwapTradeCsvLoader.java b/modules/loader/src/main/java/com/opengamma/strata/loader/csv/SwapTradeCsvLoader.java new file mode 100644 index 0000000000..ee5f09dec6 --- /dev/null +++ b/modules/loader/src/main/java/com/opengamma/strata/loader/csv/SwapTradeCsvLoader.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2017 - present by OpenGamma Inc. and the OpenGamma group of companies + * + * Please see distribution for license. + */ +package com.opengamma.strata.loader.csv; + +import static com.opengamma.strata.loader.csv.FullSwapTradeCsvLoader.DIRECTION_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.BUY_SELL_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.CONVENTION_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.DATE_ADJ_CAL_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.DATE_ADJ_CNV_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.END_DATE_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.FIXED_RATE_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.NOTIONAL_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.PERIOD_TO_START_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.START_DATE_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.TENOR_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.TRADE_DATE_FIELD; + +import java.time.LocalDate; +import java.time.Period; +import java.util.Optional; + +import com.google.common.collect.ImmutableList; +import com.opengamma.strata.basics.ReferenceData; +import com.opengamma.strata.basics.date.BusinessDayAdjustment; +import com.opengamma.strata.basics.date.BusinessDayConvention; +import com.opengamma.strata.basics.date.BusinessDayConventions; +import com.opengamma.strata.basics.date.HolidayCalendarId; +import com.opengamma.strata.basics.date.Tenor; +import com.opengamma.strata.basics.schedule.PeriodicSchedule; +import com.opengamma.strata.basics.schedule.RollConvention; +import com.opengamma.strata.basics.schedule.StubConvention; +import com.opengamma.strata.collect.io.CsvRow; +import com.opengamma.strata.product.TradeInfo; +import com.opengamma.strata.product.common.BuySell; +import com.opengamma.strata.product.swap.RateCalculationSwapLeg; +import com.opengamma.strata.product.swap.SwapLeg; +import com.opengamma.strata.product.swap.SwapTrade; +import com.opengamma.strata.product.swap.type.SingleCurrencySwapConvention; +import com.opengamma.strata.product.swap.type.XCcyIborIborSwapConvention; + +/** + * Loads Swap trades from CSV files. + */ +final class SwapTradeCsvLoader { + + // CSV column headers + private static final String ROLL_CONVENTION_FIELD = "Roll Convention"; + private static final String STUB_CONVENTION_FIELD = "Stub Convention"; + private static final String FIRST_REGULAR_START_DATE_FIELD = "First Regular Start Date"; + private static final String LAST_REGULAR_END_DATE_FIELD = "Last Regular End Date"; + private static final String FX_RATE_FIELD = "FX Rate"; + + /** + * Parses from the CSV row. + * + * @param row the CSV row + * @param info the trade info + * @param refData the reference data + * @return the loaded trades, all errors are captured in the result + */ + static SwapTrade parse(CsvRow row, TradeInfo info, ReferenceData refData) { + Optional conventionOpt = row.findValue(CONVENTION_FIELD); + if (conventionOpt.isPresent()) { + return parseWithConvention(row, info, refData, conventionOpt.get()); + } else { + Optional payReceive = row.findValue("Leg 1 " + DIRECTION_FIELD); + if (payReceive.isPresent()) { + return FullSwapTradeCsvLoader.parse(row, info); + } + throw new IllegalArgumentException( + "Swap trade had invalid combination of fields. Must include either '" + + CONVENTION_FIELD + "' or '" + "Leg 1 " + DIRECTION_FIELD + "'"); + } + } + + // parse a trade based on a convention + static SwapTrade parseWithConvention(CsvRow row, TradeInfo info, ReferenceData refData, String conventionStr) { + BuySell buySell = TradeCsvLoader.parseBuySell(row.getValue(BUY_SELL_FIELD)); + double notional = TradeCsvLoader.parseDouble(row.getValue(NOTIONAL_FIELD)); + double fixedRate = TradeCsvLoader.parseDoublePercent(row.getValue(FIXED_RATE_FIELD)); + Optional periodToStartOpt = row.findValue(PERIOD_TO_START_FIELD).map(s -> Tenor.parse(s).getPeriod()); + Optional tenorOpt = row.findValue(TENOR_FIELD).map(s -> Tenor.parse(s)); + Optional startDateOpt = row.findValue(START_DATE_FIELD).map(s -> TradeCsvLoader.parseDate(s)); + Optional endDateOpt = row.findValue(END_DATE_FIELD).map(s -> TradeCsvLoader.parseDate(s)); + Optional rollCnvOpt = row.findValue(ROLL_CONVENTION_FIELD).map(s -> RollConvention.of(s)); + Optional stubCnvOpt = row.findValue(STUB_CONVENTION_FIELD).map(s -> StubConvention.of(s)); + Optional firstRegStartDateOpt = + row.findValue(FIRST_REGULAR_START_DATE_FIELD).map(s -> TradeCsvLoader.parseDate(s)); + Optional lastRegEndDateOpt = row.findValue(LAST_REGULAR_END_DATE_FIELD).map(s -> TradeCsvLoader.parseDate(s)); + BusinessDayConvention dateCnv = row.findValue(DATE_ADJ_CNV_FIELD) + .map(s -> BusinessDayConvention.of(s)).orElse(BusinessDayConventions.MODIFIED_FOLLOWING); + Optional dateCalOpt = row.findValue(DATE_ADJ_CAL_FIELD).map(s -> HolidayCalendarId.of(s)); + Optional fxRateOpt = row.findValue(FX_RATE_FIELD).map(str -> TradeCsvLoader.parseDouble(str)); + + // explicit dates take precedence over relative ones + if (startDateOpt.isPresent() && endDateOpt.isPresent()) { + if (periodToStartOpt.isPresent() || tenorOpt.isPresent()) { + throw new IllegalArgumentException( + "Swap trade had invalid combination of fields. When these fields are found " + + ImmutableList.of(CONVENTION_FIELD, START_DATE_FIELD, END_DATE_FIELD) + + " then these fields must not be present " + + ImmutableList.of(PERIOD_TO_START_FIELD, TENOR_FIELD)); + } + LocalDate startDate = startDateOpt.get(); + LocalDate endDate = endDateOpt.get(); + SwapTrade trade = createSwap(info, conventionStr, startDate, endDate, buySell, notional, fixedRate, fxRateOpt); + return adjustTrade(trade, rollCnvOpt, stubCnvOpt, firstRegStartDateOpt, lastRegEndDateOpt, dateCnv, dateCalOpt); + } + + // start date + tenor + if (startDateOpt.isPresent() && tenorOpt.isPresent()) { + if (periodToStartOpt.isPresent() || endDateOpt.isPresent()) { + throw new IllegalArgumentException( + "Swap trade had invalid combination of fields. When these fields are found " + + ImmutableList.of(CONVENTION_FIELD, START_DATE_FIELD, TENOR_FIELD) + + " then these fields must not be present " + + ImmutableList.of(PERIOD_TO_START_FIELD, END_DATE_FIELD)); + } + LocalDate startDate = startDateOpt.get(); + Tenor tenor = tenorOpt.get(); + LocalDate endDate = startDate.plus(tenor); + SwapTrade trade = createSwap(info, conventionStr, startDate, endDate, buySell, notional, fixedRate, fxRateOpt); + return adjustTrade(trade, rollCnvOpt, stubCnvOpt, firstRegStartDateOpt, lastRegEndDateOpt, dateCnv, dateCalOpt); + } + + // relative dates + if (periodToStartOpt.isPresent() && tenorOpt.isPresent() && info.getTradeDate().isPresent()) { + if (startDateOpt.isPresent() || endDateOpt.isPresent()) { + throw new IllegalArgumentException( + "Swap trade had invalid combination of fields. When these fields are found " + + ImmutableList.of(CONVENTION_FIELD, PERIOD_TO_START_FIELD, TENOR_FIELD, TRADE_DATE_FIELD) + + " then these fields must not be present " + + ImmutableList.of(START_DATE_FIELD, END_DATE_FIELD)); + } + LocalDate tradeDate = info.getTradeDate().get(); + Period periodToStart = periodToStartOpt.get(); + Tenor tenor = tenorOpt.get(); + if (fxRateOpt.isPresent()) { + XCcyIborIborSwapConvention convention = XCcyIborIborSwapConvention.of(conventionStr); + double notionalFlat = notional * fxRateOpt.get(); + SwapTrade trade = + convention.createTrade(tradeDate, periodToStart, tenor, buySell, notional, notionalFlat, fixedRate, refData); + trade = trade.toBuilder().info(info).build(); + return adjustTrade(trade, rollCnvOpt, stubCnvOpt, firstRegStartDateOpt, lastRegEndDateOpt, dateCnv, dateCalOpt); + } else { + SingleCurrencySwapConvention convention = SingleCurrencySwapConvention.of(conventionStr); + SwapTrade trade = convention.createTrade(tradeDate, periodToStart, tenor, buySell, notional, fixedRate, refData); + trade = trade.toBuilder().info(info).build(); + return adjustTrade(trade, rollCnvOpt, stubCnvOpt, firstRegStartDateOpt, lastRegEndDateOpt, dateCnv, dateCalOpt); + } + } + + // no match + throw new IllegalArgumentException( + "Swap trade had invalid combination of fields. These fields are mandatory:" + + ImmutableList.of(BUY_SELL_FIELD, NOTIONAL_FIELD, FIXED_RATE_FIELD) + + " and one of these combinations is mandatory: " + + ImmutableList.of(CONVENTION_FIELD, TRADE_DATE_FIELD, PERIOD_TO_START_FIELD, TENOR_FIELD) + + " or " + + ImmutableList.of(CONVENTION_FIELD, START_DATE_FIELD, TENOR_FIELD) + + " or " + + ImmutableList.of(CONVENTION_FIELD, START_DATE_FIELD, END_DATE_FIELD)); + } + + // create a swap from known start/end dates + private static SwapTrade createSwap( + TradeInfo info, + String conventionStr, + LocalDate startDate, + LocalDate endDate, + BuySell buySell, + double notional, + double fixedRate, + Optional fxRateOpt) { + + if (fxRateOpt.isPresent()) { + XCcyIborIborSwapConvention convention = XCcyIborIborSwapConvention.of(conventionStr); + double notionalFlat = notional * fxRateOpt.get(); + return convention.toTrade(info, startDate, endDate, buySell, notional, notionalFlat, fixedRate); + } else { + SingleCurrencySwapConvention convention = SingleCurrencySwapConvention.of(conventionStr); + return convention.toTrade(info, startDate, endDate, buySell, notional, fixedRate); + } + } + + // adjust trade based on additional fields specified + private static SwapTrade adjustTrade( + SwapTrade trade, + Optional rollConventionOpt, + Optional stubConventionOpt, + Optional firstRegularStartDateOpt, + Optional lastRegEndDateOpt, + BusinessDayConvention dateCnv, + Optional dateCalOpt) { + + if (!rollConventionOpt.isPresent() && + !stubConventionOpt.isPresent() && + !firstRegularStartDateOpt.isPresent() && + !lastRegEndDateOpt.isPresent() && + !dateCalOpt.isPresent()) { + return trade; + } + ImmutableList.Builder legBuilder = ImmutableList.builder(); + for (SwapLeg leg : trade.getProduct().getLegs()) { + RateCalculationSwapLeg swapLeg = (RateCalculationSwapLeg) leg; + PeriodicSchedule.Builder scheduleBuilder = swapLeg.getAccrualSchedule().toBuilder(); + rollConventionOpt.ifPresent(rc -> scheduleBuilder.rollConvention(rc)); + stubConventionOpt.ifPresent(sc -> scheduleBuilder.stubConvention(sc)); + firstRegularStartDateOpt.ifPresent(date -> scheduleBuilder.firstRegularStartDate(date)); + lastRegEndDateOpt.ifPresent(date -> scheduleBuilder.lastRegularEndDate(date)); + dateCalOpt.ifPresent(cal -> scheduleBuilder.businessDayAdjustment(BusinessDayAdjustment.of(dateCnv, cal))); + legBuilder.add(swapLeg.toBuilder() + .accrualSchedule(scheduleBuilder.build()) + .build()); + } + return trade.toBuilder() + .product(trade.getProduct().toBuilder() + .legs(legBuilder.build()) + .build()) + .build(); + } + + //------------------------------------------------------------------------- + // Restricted constructor. + private SwapTradeCsvLoader() { + } + +} diff --git a/modules/loader/src/main/java/com/opengamma/strata/loader/csv/TermDepositTradeCsvLoader.java b/modules/loader/src/main/java/com/opengamma/strata/loader/csv/TermDepositTradeCsvLoader.java new file mode 100644 index 0000000000..264bb6fe59 --- /dev/null +++ b/modules/loader/src/main/java/com/opengamma/strata/loader/csv/TermDepositTradeCsvLoader.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2017 - present by OpenGamma Inc. and the OpenGamma group of companies + * + * Please see distribution for license. + */ +package com.opengamma.strata.loader.csv; + +import static com.opengamma.strata.loader.csv.TradeCsvLoader.BUY_SELL_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.CONVENTION_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.CURRENCY_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.DATE_ADJ_CAL_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.DATE_ADJ_CNV_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.DAY_COUNT_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.END_DATE_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.FIXED_RATE_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.NOTIONAL_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.START_DATE_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.TENOR_FIELD; +import static com.opengamma.strata.loader.csv.TradeCsvLoader.TRADE_DATE_FIELD; + +import java.time.LocalDate; +import java.time.Period; +import java.util.Optional; + +import com.google.common.collect.ImmutableList; +import com.opengamma.strata.basics.ReferenceData; +import com.opengamma.strata.basics.currency.Currency; +import com.opengamma.strata.basics.date.BusinessDayAdjustment; +import com.opengamma.strata.basics.date.BusinessDayConvention; +import com.opengamma.strata.basics.date.BusinessDayConventions; +import com.opengamma.strata.basics.date.DayCount; +import com.opengamma.strata.basics.date.HolidayCalendarId; +import com.opengamma.strata.basics.date.Tenor; +import com.opengamma.strata.collect.io.CsvRow; +import com.opengamma.strata.product.TradeInfo; +import com.opengamma.strata.product.common.BuySell; +import com.opengamma.strata.product.deposit.TermDeposit; +import com.opengamma.strata.product.deposit.TermDepositTrade; +import com.opengamma.strata.product.deposit.type.TermDepositConvention; + +/** + * Loads TermDeposit trades from CSV files. + */ +final class TermDepositTradeCsvLoader { + + /** + * Parses from the CSV row. + * + * @param row the CSV row + * @param info the trade info + * @param refData the reference data + * @return the loaded trades, all errors are captured in the result + */ + static TermDepositTrade parse(CsvRow row, TradeInfo info, ReferenceData refData) { + BuySell buySell = TradeCsvLoader.parseBuySell(row.getValue(BUY_SELL_FIELD)); + double notional = TradeCsvLoader.parseDouble(row.getValue(NOTIONAL_FIELD)); + double fixedRate = TradeCsvLoader.parseDoublePercent(row.getValue(FIXED_RATE_FIELD)); + Optional conventionOpt = row.findValue(CONVENTION_FIELD).map(s -> TermDepositConvention.of(s)); + Optional tenorOpt = row.findValue(TENOR_FIELD).map(s -> Tenor.parse(s).getPeriod()); + Optional startDateOpt = row.findValue(START_DATE_FIELD).map(s -> TradeCsvLoader.parseDate(s)); + Optional endDateOpt = row.findValue(END_DATE_FIELD).map(s -> TradeCsvLoader.parseDate(s)); + Optional currencyOpt = row.findValue(CURRENCY_FIELD).map(s -> Currency.parse(s)); + Optional dayCountOpt = row.findValue(DAY_COUNT_FIELD).map(s -> DayCount.of(s)); + BusinessDayConvention dateCnv = row.findValue(DATE_ADJ_CNV_FIELD) + .map(s -> BusinessDayConvention.of(s)).orElse(BusinessDayConventions.MODIFIED_FOLLOWING); + Optional dateCalOpt = row.findValue(DATE_ADJ_CAL_FIELD).map(s -> HolidayCalendarId.of(s)); + + // use convention if available + if (conventionOpt.isPresent()) { + if (currencyOpt.isPresent() || dayCountOpt.isPresent()) { + throw new IllegalArgumentException( + "TermDeposit trade had invalid combination of fields. When '" + CONVENTION_FIELD + + "' is present these fields must not be present: " + + ImmutableList.of(CURRENCY_FIELD, DAY_COUNT_FIELD)); + } + TermDepositConvention convention = conventionOpt.get(); + // explicit dates take precedence over relative ones + if (startDateOpt.isPresent() && endDateOpt.isPresent()) { + if (tenorOpt.isPresent()) { + throw new IllegalArgumentException( + "TermDeposit trade had invalid combination of fields. When these fields are found " + + ImmutableList.of(CONVENTION_FIELD, START_DATE_FIELD, END_DATE_FIELD) + + " then these fields must not be present " + + ImmutableList.of(TENOR_FIELD)); + } + LocalDate startDate = startDateOpt.get(); + LocalDate endDate = endDateOpt.get(); + TermDepositTrade trade = convention.toTrade(info, startDate, endDate, buySell, notional, fixedRate); + return adjustTrade(trade, dateCnv, dateCalOpt); + } + // relative dates + if (tenorOpt.isPresent() && info.getTradeDate().isPresent()) { + if (startDateOpt.isPresent() || endDateOpt.isPresent()) { + throw new IllegalArgumentException( + "TermDeposit trade had invalid combination of fields. When these fields are found " + + ImmutableList.of(CONVENTION_FIELD, TENOR_FIELD, TRADE_DATE_FIELD) + + " then these fields must not be present " + + ImmutableList.of(START_DATE_FIELD, END_DATE_FIELD)); + } + LocalDate tradeDate = info.getTradeDate().get(); + Period periodToStart = tenorOpt.get(); + TermDepositTrade trade = convention.createTrade(tradeDate, periodToStart, buySell, notional, fixedRate, refData); + trade = trade.toBuilder().info(info).build(); + return adjustTrade(trade, dateCnv, dateCalOpt); + } + + } else if (startDateOpt.isPresent() && endDateOpt.isPresent() && currencyOpt.isPresent() && dayCountOpt.isPresent()) { + LocalDate startDate = startDateOpt.get(); + LocalDate endDate = endDateOpt.get(); + Currency currency = currencyOpt.get(); + DayCount dayCount = dayCountOpt.get(); + TermDeposit.Builder builder = TermDeposit.builder() + .buySell(buySell) + .currency(currency) + .notional(notional) + .startDate(startDate) + .endDate(endDate) + .dayCount(dayCount) + .rate(fixedRate); + TermDepositTrade trade = TermDepositTrade.of(info, builder.build()); + return adjustTrade(trade, dateCnv, dateCalOpt); + } + // no match + throw new IllegalArgumentException( + "TermDeposit trade had invalid combination of fields. These fields are mandatory:" + + ImmutableList.of(BUY_SELL_FIELD, NOTIONAL_FIELD, FIXED_RATE_FIELD) + + " and one of these combinations is mandatory: " + + ImmutableList.of(CONVENTION_FIELD, TRADE_DATE_FIELD, TENOR_FIELD) + + " or " + + ImmutableList.of(CONVENTION_FIELD, START_DATE_FIELD, END_DATE_FIELD) + + " or " + + ImmutableList.of(START_DATE_FIELD, END_DATE_FIELD, CURRENCY_FIELD, DAY_COUNT_FIELD)); + } + + // adjust trade based on additional fields specified + private static TermDepositTrade adjustTrade( + TermDepositTrade trade, + BusinessDayConvention dateCnv, + Optional dateCalOpt) { + + if (!dateCalOpt.isPresent()) { + return trade; + } + TermDeposit.Builder builder = trade.getProduct().toBuilder(); + dateCalOpt.ifPresent(cal -> builder.businessDayAdjustment(BusinessDayAdjustment.of(dateCnv, cal))); + return trade.toBuilder() + .product(builder.build()) + .build(); + } + + //------------------------------------------------------------------------- + // Restricted constructor. + private TermDepositTradeCsvLoader() { + } + +} diff --git a/modules/loader/src/main/java/com/opengamma/strata/loader/csv/TradeCsvLoader.java b/modules/loader/src/main/java/com/opengamma/strata/loader/csv/TradeCsvLoader.java new file mode 100644 index 0000000000..e140c38908 --- /dev/null +++ b/modules/loader/src/main/java/com/opengamma/strata/loader/csv/TradeCsvLoader.java @@ -0,0 +1,457 @@ +/* + * Copyright (C) 2017 - present by OpenGamma Inc. and the OpenGamma group of companies + * + * Please see distribution for license. + */ +package com.opengamma.strata.loader.csv; + +import static java.util.stream.Collectors.toList; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Locale; + +import com.google.common.collect.ImmutableList; +import com.google.common.io.CharSource; +import com.opengamma.strata.basics.ReferenceData; +import com.opengamma.strata.basics.StandardId; +import com.opengamma.strata.collect.ArgChecker; +import com.opengamma.strata.collect.Guavate; +import com.opengamma.strata.collect.io.CsvIterator; +import com.opengamma.strata.collect.io.CsvRow; +import com.opengamma.strata.collect.io.ResourceLocator; +import com.opengamma.strata.collect.io.UnicodeBom; +import com.opengamma.strata.collect.result.FailureItem; +import com.opengamma.strata.collect.result.FailureReason; +import com.opengamma.strata.collect.result.ValueWithFailures; +import com.opengamma.strata.product.Trade; +import com.opengamma.strata.product.TradeInfo; +import com.opengamma.strata.product.TradeInfoBuilder; +import com.opengamma.strata.product.common.BuySell; +import com.opengamma.strata.product.common.PayReceive; +import com.opengamma.strata.product.deposit.TermDepositTrade; +import com.opengamma.strata.product.deposit.type.TermDepositConventions; +import com.opengamma.strata.product.fra.FraTrade; +import com.opengamma.strata.product.fra.type.FraConventions; +import com.opengamma.strata.product.swap.SwapTrade; +import com.opengamma.strata.product.swap.type.SingleCurrencySwapConvention; + +/** + * Loads trades from CSV files. + *

+ * The trades are expected to be in a CSV format known to Strata. + * The parser is flexible, understanding a number of different ways to define each trade. + * Columns may occur in any order. + * + *

Common

+ *

+ * The following standard columns are supported:
+ *

    + *
  • The 'Type' column is required, and must be the instrument type, + * such as 'Fra' or 'Swap' + *
  • The 'Id Scheme' column is optional, and is the name of the scheme that the trade + * identifier is unique within, such as 'OG-Trade' + *
  • The 'Id' column is optional, and is the identifier of the trade, + * such as 'FRA12345' + *
  • The 'Trade Date' column is optional, and is the date that the trade occurred, + * such as '2017-08-01' + *
  • The 'Trade Time' column is optional, and is the time of day that the trade occurred, + * such as '11:30' + *
  • The 'Trade Zone' column is optional, and is the time-zone that the trade occurred, + * such as 'Europe/London' + *
+ * + *

Fra

+ *

+ * The following columns are supported for 'Fra' trades: + *

    + *
  • 'Buy Sell' - mandatory + *
  • 'Notional' - mandatory + *
  • 'Fixed Rate' - mandatory, percentage + *
  • 'Convention' - see below, see {@link FraConventions} + *
  • 'Period To Start' - see below + *
  • 'Start Date' - see below + *
  • 'End Date' - see below + *
  • 'Index' - see below + *
  • 'Interpolated Index' - see below + *
  • 'Day Count' - see below + *
  • 'Date Convention' - optional + *
  • 'Date Calendar' - optional + *
+ *

+ * Valid combinations to define a FRA are: + *

    + *
  • 'Convention', 'Trade Date', 'Period To Start' + *
  • 'Convention', 'Start Date', 'End Date' + *
  • 'Index', 'Start Date', 'End Date' plus optionally 'Interpolated Index', 'Day Count' + *
+ * + *

Swap

+ *

+ * The following columns are supported for 'Swap' trades: + *

    + *
  • 'Buy Sell' - mandatory + *
  • 'Notional' - mandatory + *
  • 'Fixed Rate' - mandatory, percentage (treated as the spread for some swap types) + *
  • 'Convention' - mandatory, see {@link SingleCurrencySwapConvention} implementations + *
  • 'Period To Start'- see below + *
  • 'Tenor'- see below + *
  • 'Start Date'- see below + *
  • 'End Date'- see below + *
  • 'Roll Convention' - optional + *
  • 'Stub Convention' - optional + *
  • 'First Regular Start Date' - optional + *
  • 'Last Regular End Date' - optional + *
  • 'Date Convention' - optional + *
  • 'Date Calendar' - optional + *
+ *

+ * Valid combinations to define a Swap are: + *

    + *
  • 'Convention', 'Trade Date', 'Period To Start', 'Tenor' + *
  • 'Convention', 'Start Date', 'End Date' + *
  • 'Convention', 'Start Date', 'Tenor' + *
  • Explicitly by defining each leg (not detailed here) + *
+ * + *

Term Deposit

+ *

+ * The following columns are supported for 'TermDeposit' trades: + *

    + *
  • 'Buy Sell' - mandatory + *
  • 'Notional' - mandatory + *
  • 'Fixed Rate' - mandatory, percentage + *
  • 'Convention'- see below, see {@link TermDepositConventions} implementations + *
  • 'Tenor'- see below + *
  • 'Start Date'- see below + *
  • 'End Date'- see below + *
  • 'Currency'- see below + *
  • 'Day Count'- see below + *
  • 'Date Convention' - optional + *
  • 'Date Calendar' - optional + *
+ *

+ * Valid combinations to define a Term Deposit are: + *

    + *
  • 'Convention', 'Trade Date', 'Period To Start' + *
  • 'Convention', 'Start Date', 'End Date' + *
  • 'Start Date', 'End Date', 'Currency', 'Day Count' + *
+ */ +public final class TradeCsvLoader { + + // date formats + private static final DateTimeFormatter DD_MM_YY_SLASH = DateTimeFormatter.ofPattern("dd/MM/yy", Locale.ENGLISH); + private static final DateTimeFormatter DD_MM_YYYY_SLASH = DateTimeFormatter.ofPattern("dd/MM/yyyy", Locale.ENGLISH); + private static final DateTimeFormatter YYYY_MM_DD_SLASH = DateTimeFormatter.ofPattern("yyyy/MM/dd", Locale.ENGLISH); + private static final DateTimeFormatter YYYY_MM_DD_DASH = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.ENGLISH); + private static final DateTimeFormatter YYYYMMDD = DateTimeFormatter.ofPattern("yyyyMMdd", Locale.ENGLISH); + private static final DateTimeFormatter D_MMM_YYYY_DASH = + new DateTimeFormatterBuilder().parseCaseInsensitive().appendPattern("d-MMM-yyyy").toFormatter(Locale.ENGLISH); + private static final DateTimeFormatter D_MMM_YYYY_NODASH = + new DateTimeFormatterBuilder().parseCaseInsensitive().appendPattern("dMMMyyyy").toFormatter(Locale.ENGLISH); + + // shared CSV headers + static final String TRADE_DATE_FIELD = "Trade Date"; + static final String CONVENTION_FIELD = "Convention"; + static final String BUY_SELL_FIELD = "Buy Sell"; + static final String CURRENCY_FIELD = "Currency"; + static final String NOTIONAL_FIELD = "Notional"; + static final String INDEX_FIELD = "Index"; + static final String INTERPOLATED_INDEX_FIELD = "Interpolated Index"; + static final String FIXED_RATE_FIELD = "Fixed Rate"; + static final String PERIOD_TO_START_FIELD = "Period To Start"; + static final String TENOR_FIELD = "Tenor"; + static final String START_DATE_FIELD = "Start Date"; + static final String END_DATE_FIELD = "End Date"; + static final String DATE_ADJ_CNV_FIELD = "Date Convention"; + static final String DATE_ADJ_CAL_FIELD = "Date Calendar"; + static final String DAY_COUNT_FIELD = "Day Count"; + + // CSV column headers + private static final String TYPE_FIELD = "Type"; + private static final String ID_SCHEME_FIELD = "Id Scheme"; + private static final String ID_FIELD = "Id"; + private static final String TRADE_TIME_FIELD = "Trade Time"; + private static final String TRADE_ZONE_FIELD = "Trade Zone"; + + /** + * The reference data. + */ + private final ReferenceData refData; + + //------------------------------------------------------------------------- + /** + * Obtains an instance that uses the standard set of reference data. + * + * @return the loader + */ + public static TradeCsvLoader standard() { + return new TradeCsvLoader(ReferenceData.standard()); + } + + /** + * Obtains an instance that uses the specified set of reference data. + * + * @param refData the reference data + * @return the loader + */ + public static TradeCsvLoader of(ReferenceData refData) { + return new TradeCsvLoader(refData); + } + + //------------------------------------------------------------------------- + /** + * Restricted constructor. + * + * @param refData the reference data + */ + private TradeCsvLoader(ReferenceData refData) { + this.refData = ArgChecker.notNull(refData, "refData"); + } + + //------------------------------------------------------------------------- + /** + * Loads one or more CSV format trade files. + *

+ * CSV files sometimes contain a Unicode Byte Order Mark. + * This method uses {@link UnicodeBom} to interpret it. + * + * @param resources the CSV resources + * @return the loaded trades, trade-level errors are captured in the result + */ + public ValueWithFailures> load(ResourceLocator... resources) { + return load(Arrays.asList(resources)); + } + + /** + * Loads one or more CSV format trade files. + *

+ * CSV files sometimes contain a Unicode Byte Order Mark. + * This method uses {@link UnicodeBom} to interpret it. + * + * @param resources the CSV resources + * @return the loaded trades, all errors are captured in the result + */ + public ValueWithFailures> load(Collection resources) { + Collection charSources = resources.stream() + .map(r -> UnicodeBom.toCharSource(r.getByteSource())) + .collect(toList()); + return parse(charSources); + } + + //------------------------------------------------------------------------- + /** + * Parses one or more CSV format trade files. + *

+ * CSV files sometimes contain a Unicode Byte Order Mark. + * Callers are responsible for handling this, such as by using {@link UnicodeBom}. + * + * @param charSources the CSV character sources + * @return the loaded trades, all errors are captured in the result + */ + public ValueWithFailures> parse(Collection charSources) { + return parse(charSources, Trade.class); + } + + /** + * Parses one or more CSV format trade files. + *

+ * A type is specified to filter the trades. + *

+ * CSV files sometimes contain a Unicode Byte Order Mark. + * Callers are responsible for handling this, such as by using {@link UnicodeBom}. + * + * @param the trade type + * @param charSources the CSV character sources + * @param tradeType the trade type to return + * @return the loaded trades, all errors are captured in the result + */ + public ValueWithFailures> parse(Collection charSources, Class tradeType) { + try { + ValueWithFailures> result = ValueWithFailures.of(ImmutableList.of()); + for (CharSource charSource : charSources) { + ValueWithFailures> singleResult = parseFile(charSource, tradeType); + result = result.combinedWith(singleResult, Guavate::concatToList); + } + return result; + + } catch (RuntimeException ex) { + return ValueWithFailures.of(ImmutableList.of(), FailureItem.of(FailureReason.ERROR, ex)); + } + } + + // loads a single CSV file, filtering by trade type + private ValueWithFailures> parseFile(CharSource charSource, Class tradeType) { + try (CsvIterator csv = CsvIterator.of(charSource, true)) { + if (!csv.headers().contains(TYPE_FIELD)) { + return ValueWithFailures.of(ImmutableList.of(), + FailureItem.of(FailureReason.PARSING, "CSV file does not contain 'Type' header: {}", charSource)); + } + return parseFile(csv, tradeType); + + } catch (RuntimeException ex) { + return ValueWithFailures.of(ImmutableList.of(), + FailureItem.of(FailureReason.PARSING, ex, "CSV file could not be parsed: {}", charSource)); + } + } + + // loads a single CSV file + private ValueWithFailures> parseFile(CsvIterator csv, Class tradeType) { + List trades = new ArrayList<>(); + List failures = new ArrayList<>(); + int line = 2; + for (CsvRow row : (Iterable) () -> csv) { + try { + String typeRaw = row.getField(TYPE_FIELD); + String type = typeRaw.toUpperCase(Locale.ENGLISH); + TradeInfo info = parseTradeInfo(row); + switch (type.toUpperCase(Locale.ENGLISH)) { + case "FRA": + if (tradeType == FraTrade.class || tradeType == Trade.class) { + trades.add(tradeType.cast(FraTradeCsvLoader.parse(row, info, refData))); + } + break; + case "SWAP": + if (tradeType == SwapTrade.class || tradeType == Trade.class) { + trades.add(tradeType.cast(SwapTradeCsvLoader.parse(row, info, refData))); + } + break; + case "TERMDEPOSIT": + case "TERM DEPOSIT": + if (tradeType == TermDepositTrade.class || tradeType == Trade.class) { + trades.add(tradeType.cast(TermDepositTradeCsvLoader.parse(row, info, refData))); + } + break; + default: + failures.add( + FailureItem.of(FailureReason.PARSING, "CSV file trade type '{}' is not known at line {}", typeRaw, line)); + break; + } + } catch (RuntimeException ex) { + failures.add( + FailureItem.of(FailureReason.PARSING, ex, "CSV file trade could not be parsed at line {}: " + ex.getMessage(), line)); + } + line++; + } + return ValueWithFailures.of(trades, failures); + } + + // parse the trade info + private TradeInfo parseTradeInfo(CsvRow row) { + TradeInfoBuilder infoBuilder = TradeInfo.builder(); + String scheme = row.findField(ID_SCHEME_FIELD).orElse("OG-Trade"); + row.findValue(ID_FIELD).ifPresent(id -> infoBuilder.id(StandardId.of(scheme, id))); + row.findValue(TRADE_DATE_FIELD).ifPresent(dateStr -> infoBuilder.tradeDate(parseDate(dateStr))); + row.findValue(TRADE_TIME_FIELD).ifPresent(timeStr -> infoBuilder.tradeTime(LocalTime.parse(timeStr))); + row.findValue(TRADE_ZONE_FIELD).ifPresent(zoneStr -> infoBuilder.zone(ZoneId.of(zoneStr))); + return infoBuilder.build(); + } + + //------------------------------------------------------------------------- + // parses a date + static boolean parseBoolean(String str) { + switch (str.toUpperCase(Locale.ENGLISH)) { + case "TRUE": + case "T": + case "YES": + case "Y": + return true; + case "FALSE": + case "F": + case "NO": + case "N": + return false; + default: + throw new IllegalArgumentException("Unknown boolean value, must 'True' or 'False' but was '" + str + "'"); + } + } + + // parses a double + static Double parseDouble(String s) { + return new BigDecimal(s).doubleValue(); + } + + // parses a double as a percentage + static Double parseDoublePercent(String s) { + return new BigDecimal(s).movePointLeft(2).doubleValue(); + } + + // parses a date + static LocalDate parseDate(String str) { + try { + // yyyy-MM-dd + if (str.length() == 10 && str.charAt(4) == '-' && str.charAt(7) == '-') { + return LocalDate.parse(str, YYYY_MM_DD_DASH); + } + // yyyy/MM/dd + if (str.length() == 10 && str.charAt(4) == '/' && str.charAt(7) == '/') { + return LocalDate.parse(str, YYYY_MM_DD_SLASH); + } + // dd/MM/yy + // dd/MM/yyyy + if (str.length() >= 8 && str.charAt(2) == '/' && str.charAt(5) == '/') { + if (str.length() == 8) { + return LocalDate.parse(str, DD_MM_YY_SLASH); + } else { + return LocalDate.parse(str, DD_MM_YYYY_SLASH); + } + } + // d-MMM-yyyy + if (str.length() >= 10 && str.charAt(str.length() - 5) == '-') { + return LocalDate.parse(str, D_MMM_YYYY_DASH); + } + if (str.length() == 8 && Character.isDigit(str.charAt(2))) { + // yyyyMMdd (and all others) + return LocalDate.parse(str, YYYYMMDD); + } + // dMMMyyyy (and all others) + return LocalDate.parse(str, D_MMM_YYYY_NODASH); + + } catch (DateTimeParseException ex) { + throw new IllegalArgumentException( + "Unknown date format, must be formatted as " + + "yyyy-MM-dd, yyyyMMdd, dd/MM/yyyy, yyyy/MM/dd, 'd-MMM-yyyy' or 'dMMMyyyy' but was: " + str, + ex); + } + } + + // parses buy/sell + static BuySell parseBuySell(String str) { + switch (str.toUpperCase(Locale.ENGLISH)) { + case "BUY": + case "B": + return BuySell.BUY; + case "SELL": + case "S": + return BuySell.SELL; + default: + throw new IllegalArgumentException("Unknown BuySell value, must 'Buy' or 'Sell' but was '" + str + "'"); + } + } + + // parses pay/receive + static PayReceive parsePayReceive(String str) { + switch (str.toUpperCase(Locale.ENGLISH)) { + case "PAY": + case "P": + return PayReceive.PAY; + case "RECEIVE": + case "REC": + case "R": + return PayReceive.RECEIVE; + default: + throw new IllegalArgumentException("Unknown PayReceive value, must 'Pay' or 'Receive' but was '" + str + "'"); + } + } + +} diff --git a/modules/loader/src/test/java/com/opengamma/strata/loader/csv/LegalEntityRatesCurvesCsvLoaderTest.java b/modules/loader/src/test/java/com/opengamma/strata/loader/csv/LegalEntityRatesCurvesCsvLoaderTest.java index af4c266fd1..4423366bbf 100644 --- a/modules/loader/src/test/java/com/opengamma/strata/loader/csv/LegalEntityRatesCurvesCsvLoaderTest.java +++ b/modules/loader/src/test/java/com/opengamma/strata/loader/csv/LegalEntityRatesCurvesCsvLoaderTest.java @@ -164,7 +164,7 @@ public void test_setting_invalid_path() { } @Test(expectedExceptions = IllegalArgumentException.class, - expectedExceptionsMessageRegExp = "Header not found: Curve Name") + expectedExceptionsMessageRegExp = "Header not found: 'Curve Name'") public void test_invalid_settings_missing_column_file() { LegalEntityRatesCurvesCsvLoader.load( ALL_DATES.get(6), diff --git a/modules/loader/src/test/java/com/opengamma/strata/loader/csv/RatesCurvesCsvLoaderTest.java b/modules/loader/src/test/java/com/opengamma/strata/loader/csv/RatesCurvesCsvLoaderTest.java index a715594ff1..822f067c67 100644 --- a/modules/loader/src/test/java/com/opengamma/strata/loader/csv/RatesCurvesCsvLoaderTest.java +++ b/modules/loader/src/test/java/com/opengamma/strata/loader/csv/RatesCurvesCsvLoaderTest.java @@ -86,7 +86,7 @@ public void test_missing_settings_file() { } @Test(expectedExceptions = IllegalArgumentException.class, - expectedExceptionsMessageRegExp = "Header not found: Curve Name") + expectedExceptionsMessageRegExp = "Header not found: 'Curve Name'") public void test_invalid_settings_missing_column_file() { testSettings(SETTINGS_INVALID_MISSING_COLUMN); } diff --git a/modules/loader/src/test/java/com/opengamma/strata/loader/csv/TradeCsvLoaderTest.java b/modules/loader/src/test/java/com/opengamma/strata/loader/csv/TradeCsvLoaderTest.java new file mode 100644 index 0000000000..50f8d9f496 --- /dev/null +++ b/modules/loader/src/test/java/com/opengamma/strata/loader/csv/TradeCsvLoaderTest.java @@ -0,0 +1,865 @@ +/* + * Copyright (C) 2017 - present by OpenGamma Inc. and the OpenGamma group of companies + * + * Please see distribution for license. + */ +package com.opengamma.strata.loader.csv; + +import static com.opengamma.strata.basics.currency.Currency.GBP; +import static com.opengamma.strata.basics.currency.Currency.USD; +import static com.opengamma.strata.basics.date.BusinessDayConventions.FOLLOWING; +import static com.opengamma.strata.basics.date.BusinessDayConventions.MODIFIED_FOLLOWING; +import static com.opengamma.strata.basics.date.BusinessDayConventions.PRECEDING; +import static com.opengamma.strata.basics.date.HolidayCalendarIds.EUTA; +import static com.opengamma.strata.basics.date.HolidayCalendarIds.GBLO; +import static com.opengamma.strata.basics.date.HolidayCalendarIds.USNY; +import static com.opengamma.strata.collect.Guavate.toImmutableList; +import static com.opengamma.strata.collect.TestHelper.coverPrivateConstructor; +import static com.opengamma.strata.collect.TestHelper.date; +import static com.opengamma.strata.product.common.BuySell.BUY; +import static com.opengamma.strata.product.common.BuySell.SELL; +import static com.opengamma.strata.product.common.PayReceive.PAY; +import static com.opengamma.strata.product.common.PayReceive.RECEIVE; +import static org.joda.beans.test.BeanAssert.assertBeanEquals; +import static org.testng.Assert.assertEquals; + +import java.time.LocalTime; +import java.time.Period; +import java.time.ZoneId; +import java.util.List; + +import org.testng.annotations.Test; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.CharSource; +import com.opengamma.strata.basics.ReferenceData; +import com.opengamma.strata.basics.StandardId; +import com.opengamma.strata.basics.currency.CurrencyAmount; +import com.opengamma.strata.basics.date.AdjustableDate; +import com.opengamma.strata.basics.date.BusinessDayAdjustment; +import com.opengamma.strata.basics.date.DayCounts; +import com.opengamma.strata.basics.date.DaysAdjustment; +import com.opengamma.strata.basics.date.Tenor; +import com.opengamma.strata.basics.index.FxIndices; +import com.opengamma.strata.basics.index.IborIndices; +import com.opengamma.strata.basics.index.OvernightIndices; +import com.opengamma.strata.basics.index.PriceIndices; +import com.opengamma.strata.basics.schedule.Frequency; +import com.opengamma.strata.basics.schedule.PeriodicSchedule; +import com.opengamma.strata.basics.schedule.RollConventions; +import com.opengamma.strata.basics.schedule.StubConvention; +import com.opengamma.strata.basics.value.ValueSchedule; +import com.opengamma.strata.collect.io.ResourceLocator; +import com.opengamma.strata.collect.result.FailureItem; +import com.opengamma.strata.collect.result.FailureReason; +import com.opengamma.strata.collect.result.ValueWithFailures; +import com.opengamma.strata.product.Trade; +import com.opengamma.strata.product.TradeInfo; +import com.opengamma.strata.product.deposit.TermDeposit; +import com.opengamma.strata.product.deposit.TermDepositTrade; +import com.opengamma.strata.product.deposit.type.TermDepositConventions; +import com.opengamma.strata.product.fra.Fra; +import com.opengamma.strata.product.fra.FraTrade; +import com.opengamma.strata.product.fra.type.FraConventions; +import com.opengamma.strata.product.swap.CompoundingMethod; +import com.opengamma.strata.product.swap.FixedRateCalculation; +import com.opengamma.strata.product.swap.FixedRateStubCalculation; +import com.opengamma.strata.product.swap.FixingRelativeTo; +import com.opengamma.strata.product.swap.FxResetCalculation; +import com.opengamma.strata.product.swap.FxResetFixingRelativeTo; +import com.opengamma.strata.product.swap.IborRateCalculation; +import com.opengamma.strata.product.swap.IborRateResetMethod; +import com.opengamma.strata.product.swap.IborRateStubCalculation; +import com.opengamma.strata.product.swap.InflationRateCalculation; +import com.opengamma.strata.product.swap.NegativeRateMethod; +import com.opengamma.strata.product.swap.NotionalSchedule; +import com.opengamma.strata.product.swap.OvernightAccrualMethod; +import com.opengamma.strata.product.swap.OvernightRateCalculation; +import com.opengamma.strata.product.swap.PaymentRelativeTo; +import com.opengamma.strata.product.swap.PaymentSchedule; +import com.opengamma.strata.product.swap.PriceIndexCalculationMethod; +import com.opengamma.strata.product.swap.RateCalculationSwapLeg; +import com.opengamma.strata.product.swap.ResetSchedule; +import com.opengamma.strata.product.swap.Swap; +import com.opengamma.strata.product.swap.SwapTrade; +import com.opengamma.strata.product.swap.type.FixedIborSwapConventions; +import com.opengamma.strata.product.swap.type.XCcyIborIborSwapConventions; + +/** + * Test {@link TradeCsvLoader}. + */ +@Test +public class TradeCsvLoaderTest { + + private static final ReferenceData REF_DATA = ReferenceData.standard(); + private static final int NUMBER_SWAPS = 7; + + private static final ResourceLocator FILE = + ResourceLocator.of("classpath:com/opengamma/strata/loader/csv/trades.csv"); + + //------------------------------------------------------------------------- + public void test_load_failures() { + TradeCsvLoader test = TradeCsvLoader.standard(); + ValueWithFailures> trades = test.load(FILE); + + assertEquals(trades.getFailures().size(), 0, trades.getFailures().toString()); + } + + //------------------------------------------------------------------------- + public void test_load_fra() { + TradeCsvLoader test = TradeCsvLoader.standard(); + ValueWithFailures> trades = test.load(FILE); + + List filtered = trades.getValue().stream() + .filter(FraTrade.class::isInstance) + .map(FraTrade.class::cast) + .collect(toImmutableList()); + assertEquals(filtered.size(), 3); + + FraTrade expected1 = FraConventions.of(IborIndices.GBP_LIBOR_3M) + .createTrade(date(2017, 6, 1), Period.ofMonths(2), BUY, 1_000_000, 0.005, REF_DATA) + .toBuilder() + .info(TradeInfo.builder() + .id(StandardId.of("OG", "123401")) + .tradeDate(date(2017, 6, 1)) + .tradeTime(LocalTime.of(11, 5)) + .zone(ZoneId.of("Europe/London")) + .build()) + .build(); + assertBeanEquals(expected1, filtered.get(0)); + + FraTrade expected2 = FraConventions.of(IborIndices.GBP_LIBOR_6M) + .toTrade(date(2017, 6, 1), date(2017, 8, 1), date(2018, 2, 1), date(2017, 8, 1), SELL, 1_000_000, 0.007) + .toBuilder() + .info(TradeInfo.builder() + .id(StandardId.of("OG", "123402")) + .tradeDate(date(2017, 6, 1)) + .tradeTime(LocalTime.of(12, 35)) + .zone(ZoneId.of("Europe/London")) + .build()) + .build(); + assertBeanEquals(expected2, filtered.get(1)); + + FraTrade expected3 = FraTrade.builder() + .info(TradeInfo.builder() + .id(StandardId.of("OG", "123403")) + .tradeDate(date(2017, 6, 1)) + .build()) + .product(Fra.builder() + .buySell(SELL) + .startDate(date(2017, 8, 1)) + .endDate(date(2018, 1, 15)) + .notional(1_000_000) + .fixedRate(0.0055) + .index(IborIndices.GBP_LIBOR_3M) + .indexInterpolated(IborIndices.GBP_LIBOR_6M) + .dayCount(DayCounts.ACT_360) + .build()) + .build(); + assertBeanEquals(expected3, filtered.get(2)); + } + + //------------------------------------------------------------------------- + public void test_load_swap() { + TradeCsvLoader test = TradeCsvLoader.standard(); + ValueWithFailures> trades = test.load(FILE); + + List filtered = trades.getValue().stream() + .filter(SwapTrade.class::isInstance) + .map(SwapTrade.class::cast) + .collect(toImmutableList()); + assertEquals(filtered.size(), NUMBER_SWAPS); + + SwapTrade expected1 = FixedIborSwapConventions.GBP_FIXED_1Y_LIBOR_3M + .createTrade(date(2017, 6, 1), Period.ofMonths(1), Tenor.ofYears(5), BUY, 2_000_000, 0.004, REF_DATA) + .toBuilder() + .info(TradeInfo.builder() + .id(StandardId.of("OG", "123411")) + .tradeDate(date(2017, 6, 1)) + .build()) + .build(); + assertBeanEquals(expected1, filtered.get(0)); + + SwapTrade expected2 = FixedIborSwapConventions.GBP_FIXED_6M_LIBOR_6M + .toTrade(date(2017, 6, 1), date(2017, 8, 1), date(2022, 8, 1), BUY, 3_100_000, -0.0001) + .toBuilder() + .info(TradeInfo.builder() + .id(StandardId.of("OG", "123412")) + .tradeDate(date(2017, 6, 1)) + .build()) + .build(); + assertBeanEquals(expected2, filtered.get(1)); + + Swap expectedSwap3 = Swap.builder() + .legs( + RateCalculationSwapLeg.builder() + .payReceive(PAY) + .accrualSchedule(PeriodicSchedule.builder() + .startDate(date(2017, 8, 1)) + .endDate(date(2022, 9, 1)) + .frequency(Frequency.P6M) + .businessDayAdjustment(BusinessDayAdjustment.of(MODIFIED_FOLLOWING, GBLO)) + .stubConvention(StubConvention.LONG_FINAL) + .build()) + .paymentSchedule(PaymentSchedule.builder() + .paymentFrequency(Frequency.P6M) + .paymentDateOffset(DaysAdjustment.NONE) + .build()) + .notionalSchedule(NotionalSchedule.of(GBP, 4_000_000)) + .calculation(FixedRateCalculation.of(0.005, DayCounts.ACT_365F)) + .build(), + RateCalculationSwapLeg.builder() + .payReceive(RECEIVE) + .accrualSchedule(PeriodicSchedule.builder() + .startDate(date(2017, 8, 1)) + .endDate(date(2022, 9, 1)) + .frequency(Frequency.P6M) + .businessDayAdjustment(BusinessDayAdjustment.of(MODIFIED_FOLLOWING, GBLO)) + .stubConvention(StubConvention.LONG_FINAL) + .build()) + .paymentSchedule(PaymentSchedule.builder() + .paymentFrequency(Frequency.P6M) + .paymentDateOffset(DaysAdjustment.NONE) + .build()) + .notionalSchedule(NotionalSchedule.of(GBP, 4_000_000)) + .calculation(IborRateCalculation.of(IborIndices.GBP_LIBOR_6M)) + .build()) + .build(); + SwapTrade expected3 = SwapTrade.builder() + .info(TradeInfo.builder() + .id(StandardId.of("OG", "123413")) + .tradeDate(date(2017, 6, 1)) + .build()) + .product(expectedSwap3) + .build(); + assertBeanEquals(expected3, filtered.get(2)); + + SwapTrade expected4 = XCcyIborIborSwapConventions.GBP_LIBOR_3M_USD_LIBOR_3M + .createTrade(date(2017, 6, 1), Period.ofMonths(1), Tenor.TENOR_3Y, BUY, 2_000_000, 2_500_000, 0.006, REF_DATA) + .toBuilder() + .info(TradeInfo.builder() + .id(StandardId.of("OG", "123414")) + .tradeDate(date(2017, 6, 1)) + .build()) + .build(); + assertBeanEquals(expected4, filtered.get(3)); + } + + public void test_load_swap_full5() { + TradeCsvLoader test = TradeCsvLoader.standard(); + ValueWithFailures> result = test.parse(ImmutableList.of(FILE.getCharSource()), SwapTrade.class); + assertEquals(result.getFailures().size(), 0); + assertEquals(result.getValue().size(), NUMBER_SWAPS); + + Swap expectedSwap = Swap.builder() + .legs( + RateCalculationSwapLeg.builder() + .payReceive(PAY) + .accrualSchedule(PeriodicSchedule.builder() + .startDate(date(2017, 8, 1)) + .endDate(date(2022, 8, 1)) + .frequency(Frequency.P3M) + .businessDayAdjustment(BusinessDayAdjustment.NONE) + .stubConvention(StubConvention.SHORT_FINAL) + .build()) + .paymentSchedule(PaymentSchedule.builder() + .paymentFrequency(Frequency.P3M) + .paymentDateOffset(DaysAdjustment.NONE) + .build()) + .notionalSchedule(NotionalSchedule.of(GBP, 2_500_000)) + .calculation(FixedRateCalculation.of(0.011, DayCounts.THIRTY_360_ISDA)) + .build(), + RateCalculationSwapLeg.builder() + .payReceive(RECEIVE) + .accrualSchedule(PeriodicSchedule.builder() + .startDate(date(2017, 8, 1)) + .endDate(date(2022, 8, 1)) + .frequency(Frequency.P3M) + .businessDayAdjustment(BusinessDayAdjustment.NONE) + .stubConvention(StubConvention.SHORT_FINAL) + .build()) + .paymentSchedule(PaymentSchedule.builder() + .paymentFrequency(Frequency.P3M) + .paymentDateOffset(DaysAdjustment.NONE) + .build()) + .notionalSchedule(NotionalSchedule.of(GBP, 2_500_000)) + .calculation(IborRateCalculation.of(IborIndices.GBP_LIBOR_3M)) + .build()) + .build(); + SwapTrade expected = SwapTrade.builder() + .info(TradeInfo.builder() + .id(StandardId.of("OG", "123415")) + .tradeDate(date(2017, 6, 1)) + .build()) + .product(expectedSwap) + .build(); + assertBeanEquals(expected, result.getValue().get(4)); + } + + public void test_load_swap_full6() { + TradeCsvLoader test = TradeCsvLoader.standard(); + ValueWithFailures> result = test.parse(ImmutableList.of(FILE.getCharSource()), SwapTrade.class); + assertEquals(result.getFailures().size(), 0); + assertEquals(result.getValue().size(), NUMBER_SWAPS); + + Swap expectedSwap = Swap.builder() + .legs( + RateCalculationSwapLeg.builder() + .payReceive(PAY) + .accrualSchedule(PeriodicSchedule.builder() + .startDate(date(2017, 8, 1)) + .endDate(date(2022, 8, 8)) + .frequency(Frequency.P3M) + .businessDayAdjustment(BusinessDayAdjustment.of(FOLLOWING, GBLO.combinedWith(EUTA))) + .stubConvention(StubConvention.LONG_INITIAL) + .rollConvention(RollConventions.DAY_8) + .build()) + .paymentSchedule(PaymentSchedule.builder() + .paymentFrequency(Frequency.P3M) + .paymentDateOffset(DaysAdjustment.NONE) + .build()) + .notionalSchedule(NotionalSchedule.of(GBP, 1_200_000)) + .calculation(FixedRateCalculation.of(0.012, DayCounts.THIRTY_360_ISDA)) + .build(), + RateCalculationSwapLeg.builder() + .payReceive(RECEIVE) + .accrualSchedule(PeriodicSchedule.builder() + .startDate(date(2017, 8, 8)) + .endDate(date(2022, 8, 8)) + .frequency(Frequency.P3M) + .businessDayAdjustment(BusinessDayAdjustment.NONE) + .stubConvention(StubConvention.SHORT_INITIAL) + .build()) + .paymentSchedule(PaymentSchedule.builder() + .paymentFrequency(Frequency.P3M) + .paymentDateOffset(DaysAdjustment.NONE) + .build()) + .notionalSchedule(NotionalSchedule.of(GBP, 1_200_000)) + .calculation(IborRateCalculation.of(IborIndices.GBP_LIBOR_3M)) + .build()) + .build(); + SwapTrade expected = SwapTrade.builder() + .info(TradeInfo.builder() + .id(StandardId.of("OG", "123416")) + .tradeDate(date(2017, 6, 1)) + .build()) + .product(expectedSwap) + .build(); + assertBeanEquals(expected, result.getValue().get(5)); + } + + public void test_load_swap_full7() { + TradeCsvLoader test = TradeCsvLoader.standard(); + ValueWithFailures> result = test.parse(ImmutableList.of(FILE.getCharSource()), SwapTrade.class); + assertEquals(result.getFailures().size(), 0); + assertEquals(result.getValue().size(), NUMBER_SWAPS); + + Swap expectedSwap = Swap.builder() + .legs( + RateCalculationSwapLeg.builder() + .payReceive(PAY) + .accrualSchedule(PeriodicSchedule.builder() + .startDate(date(2017, 8, 8)) + .endDate(date(2022, 8, 8)) + .frequency(Frequency.P3M) + .businessDayAdjustment(BusinessDayAdjustment.of(PRECEDING, GBLO.combinedWith(USNY))) + .stubConvention(StubConvention.SHORT_INITIAL) + .build()) + .paymentSchedule(PaymentSchedule.builder() + .paymentFrequency(Frequency.P3M) + .paymentDateOffset(DaysAdjustment.NONE) + .build()) + .notionalSchedule(NotionalSchedule.of(GBP, 1_500_000)) + .calculation(FixedRateCalculation.of(0.013, DayCounts.ACT_365F)) + .build(), + RateCalculationSwapLeg.builder() + .payReceive(RECEIVE) + .accrualSchedule(PeriodicSchedule.builder() + .startDate(date(2017, 8, 8)) + .endDate(date(2022, 8, 8)) + .frequency(Frequency.P6M) + .businessDayAdjustment(BusinessDayAdjustment.NONE) + .stubConvention(StubConvention.SHORT_INITIAL) + .build()) + .paymentSchedule(PaymentSchedule.builder() + .paymentFrequency(Frequency.P6M) + .paymentDateOffset(DaysAdjustment.NONE) + .build()) + .notionalSchedule(NotionalSchedule.of(GBP, 1_500_000)) + .calculation(OvernightRateCalculation.of(OvernightIndices.GBP_SONIA)) + .build()) + .build(); + SwapTrade expected = SwapTrade.builder() + .info(TradeInfo.builder() + .id(StandardId.of("OG", "123417")) + .tradeDate(date(2017, 6, 1)) + .build()) + .product(expectedSwap) + .build(); + assertBeanEquals(expected, result.getValue().get(6)); + } + + public void test_load_swap_all() { + ImmutableMap csvMap = ImmutableMap.builder() + .put("Type", "Swap") + .put("Id Scheme", "OG") + .put("Id", "1234") + .put("Trade Date", "20170101") + .put("Trade Time", "12:30") + .put("Trade Zone", "Europe/Paris") + + .put("Leg 1 Direction", "Pay") + .put("Leg 1 Start Date", "2-May-2017") + .put("Leg 1 End Date", "22May2022") + .put("Leg 1 First Regular Start Date", "10/05/17") + .put("Leg 1 Last Regular End Date", "2022-05-10") + .put("Leg 1 Start Date Convention", "NoAdjust") + .put("Leg 1 Start Date Calendar", "NoHolidays") + .put("Leg 1 Date Convention", "Following") + .put("Leg 1 Date Calendar", "GBLO") + .put("Leg 1 End Date Convention", "NoAdjust") + .put("Leg 1 End Date Calendar", "NoHolidays") + .put("Leg 1 Roll Convention", "Day10") + .put("Leg 1 Stub Convention", "Both") + .put("Leg 1 Frequency", "12M") + .put("Leg 1 Override Start Date", "2017/04/01") + .put("Leg 1 Override Start Date Convention", "Following") + .put("Leg 1 Override Start Date Calendar", "USNY") + .put("Leg 1 Payment Frequency", "P12M") + .put("Leg 1 Payment Offset Days", "3") + .put("Leg 1 Payment Offset Calendar", "GBLO") + .put("Leg 1 Payment Offset Adjustment Convention", "Following") + .put("Leg 1 Payment Offset Adjustment Calendar", "USNY") + .put("Leg 1 Payment Relative To", "PeriodStart") + .put("Leg 1 Compounding Method", "Flat") + .put("Leg 1 Currency", "GBP") + .put("Leg 1 Notional Currency", "USD") + .put("Leg 1 Notional", "1000000") + .put("Leg 1 FX Reset Index", "GBP/USD-WM") + .put("Leg 1 FX Reset Relative To", "PeriodEnd") + .put("Leg 1 FX Reset Offset Days", "2") + .put("Leg 1 FX Reset Offset Calendar", "GBLO") + .put("Leg 1 FX Reset Offset Adjustment Convention", "Following") + .put("Leg 1 FX Reset Offset Adjustment Calendar", "USNY") + .put("Leg 1 Notional Initial Exchange", "true") + .put("Leg 1 Notional Intermediate Exchange", "true") + .put("Leg 1 Notional Final Exchange", "true") + .put("Leg 1 Day Count", "Act/365F") + .put("Leg 1 Fixed Rate", "1.1") + .put("Leg 1 Initial Stub Rate", "0.6") + .put("Leg 1 Final Stub Rate", "0.7") + + .put("Leg 2 Direction", "Pay") + .put("Leg 2 Start Date", "2017-05-02") + .put("Leg 2 End Date", "2022-05-22") + .put("Leg 2 Frequency", "12M") + .put("Leg 2 Currency", "GBP") + .put("Leg 2 Notional", "1000000") + .put("Leg 2 Day Count", "Act/365F") + .put("Leg 2 Fixed Rate", "1.1") + .put("Leg 2 Initial Stub Amount", "4000") + .put("Leg 2 Final Stub Amount", "5000") + + .put("Leg 3 Direction", "Pay") + .put("Leg 3 Start Date", "2017-05-02") + .put("Leg 3 End Date", "2022-05-22") + .put("Leg 3 Frequency", "12M") + .put("Leg 3 Currency", "GBP") + .put("Leg 3 Notional", "1000000") + .put("Leg 3 Day Count", "Act/360") + .put("Leg 3 Index", "GBP-LIBOR-6M") + .put("Leg 3 Reset Frequency", "3M") + .put("Leg 3 Reset Method", "Weighted") + .put("Leg 3 Reset Date Convention", "Following") + .put("Leg 3 Reset Date Calendar", "GBLO+USNY") + .put("Leg 3 Fixing Relative To", "PeriodEnd") + .put("Leg 3 Fixing Offset Days", "3") + .put("Leg 3 Fixing Offset Calendar", "GBLO") + .put("Leg 3 Fixing Offset Adjustment Convention", "Following") + .put("Leg 3 Fixing Offset Adjustment Calendar", "USNY") + .put("Leg 3 Negative Rate Method", "NotNegative") + .put("Leg 3 First Rate", "0.5") + .put("Leg 3 Gearing", "2") + .put("Leg 3 Spread", "3") + .put("Leg 3 Initial Stub Rate", "0.6") + .put("Leg 3 Final Stub Rate", "0.7") + + .put("Leg 4 Direction", "Pay") + .put("Leg 4 Start Date", "2017-05-02") + .put("Leg 4 End Date", "2022-05-22") + .put("Leg 4 Frequency", "12M") + .put("Leg 4 Currency", "GBP") + .put("Leg 4 Notional", "1000000") + .put("Leg 4 Index", "GBP-LIBOR-6M") + .put("Leg 4 Initial Stub Amount", "4000") + .put("Leg 4 Final Stub Amount", "5000") + + .put("Leg 5 Direction", "Pay") + .put("Leg 5 Start Date", "2017-05-02") + .put("Leg 5 End Date", "2022-05-22") + .put("Leg 5 Frequency", "6M") + .put("Leg 5 Currency", "GBP") + .put("Leg 5 Notional", "1000000") + .put("Leg 5 Index", "GBP-LIBOR-6M") + .put("Leg 5 Initial Stub Index", "GBP-LIBOR-3M") + .put("Leg 5 Initial Stub Interpolated Index", "GBP-LIBOR-6M") + .put("Leg 5 Final Stub Index", "GBP-LIBOR-3M") + .put("Leg 5 Final Stub Interpolated Index", "GBP-LIBOR-6M") + + .put("Leg 6 Direction", "Pay") + .put("Leg 6 Start Date", "2017-05-02") + .put("Leg 6 End Date", "2022-05-22") + .put("Leg 6 Frequency", "6M") + .put("Leg 6 Currency", "GBP") + .put("Leg 6 Notional", "1000000") + .put("Leg 6 Day Count", "Act/360") + .put("Leg 6 Index", "GBP-SONIA") + .put("Leg 6 Accrual Method", "Averaged") + .put("Leg 6 Rate Cut Off Days", "3") + .put("Leg 6 Negative Rate Method", "NotNegative") + .put("Leg 6 Gearing", "2") + .put("Leg 6 Spread", "3") + + .put("Leg 7 Direction", "Pay") + .put("Leg 7 Start Date", "2017-05-02") + .put("Leg 7 End Date", "2022-05-22") + .put("Leg 7 Frequency", "6M") + .put("Leg 7 Currency", "GBP") + .put("Leg 7 Notional", "1000000") + .put("Leg 7 Day Count", "Act/360") + .put("Leg 7 Index", "GB-RPI") + .put("Leg 7 Inflation Lag", "2") + .put("Leg 7 Inflation Method", "Interpolated") + .put("Leg 7 Inflation First Index Value", "121") + .put("Leg 7 Gearing", "2") + .build(); + String csv = Joiner.on(',').join(csvMap.keySet()) + "\n" + Joiner.on(',').join(csvMap.values()); + + TradeCsvLoader test = TradeCsvLoader.standard(); + ValueWithFailures> result = test.parse(ImmutableList.of(CharSource.wrap(csv)), SwapTrade.class); + assertEquals(result.getFailures().size(), 0, result.getFailures().toString()); + assertEquals(result.getValue().size(), 1); + + Swap expectedSwap = Swap.builder() + .legs( + RateCalculationSwapLeg.builder() // Fixed fixed stub + .payReceive(PAY) + .accrualSchedule(PeriodicSchedule.builder() + .startDate(date(2017, 5, 2)) + .endDate(date(2022, 5, 22)) + .firstRegularStartDate(date(2017, 5, 10)) + .lastRegularEndDate(date(2022, 5, 10)) + .overrideStartDate(AdjustableDate.of(date(2017, 4, 1), BusinessDayAdjustment.of(FOLLOWING, USNY))) + .frequency(Frequency.P12M) + .businessDayAdjustment(BusinessDayAdjustment.of(FOLLOWING, GBLO)) + .startDateBusinessDayAdjustment(BusinessDayAdjustment.NONE) + .endDateBusinessDayAdjustment(BusinessDayAdjustment.NONE) + .rollConvention(RollConventions.DAY_10) + .stubConvention(StubConvention.BOTH) + .build()) + .paymentSchedule(PaymentSchedule.builder() + .paymentFrequency(Frequency.P12M) + .paymentDateOffset(DaysAdjustment.ofBusinessDays(3, GBLO, BusinessDayAdjustment.of(FOLLOWING, USNY))) + .paymentRelativeTo(PaymentRelativeTo.PERIOD_START) + .compoundingMethod(CompoundingMethod.FLAT) + .build()) + .notionalSchedule(NotionalSchedule.builder() + .currency(GBP) + .amount(ValueSchedule.of(1_000_000)) + .fxReset(FxResetCalculation.builder() + .referenceCurrency(USD) + .index(FxIndices.GBP_USD_WM) + .fixingRelativeTo(FxResetFixingRelativeTo.PERIOD_END) + .fixingDateOffset(DaysAdjustment.ofBusinessDays(2, GBLO, BusinessDayAdjustment.of(FOLLOWING, USNY))) + .build()) + .initialExchange(true) + .intermediateExchange(true) + .finalExchange(true) + .build()) + .calculation(FixedRateCalculation.builder() + .dayCount(DayCounts.ACT_365F) + .rate(ValueSchedule.of(0.011)) + .initialStub(FixedRateStubCalculation.ofFixedRate(0.006)) + .finalStub(FixedRateStubCalculation.ofFixedRate(0.007)) + .build()) + .build(), + RateCalculationSwapLeg.builder() // Fixed known amount stub + .payReceive(PAY) + .accrualSchedule(PeriodicSchedule.builder() + .startDate(date(2017, 5, 2)) + .endDate(date(2022, 5, 22)) + .frequency(Frequency.P12M) + .businessDayAdjustment(BusinessDayAdjustment.NONE) + .stubConvention(StubConvention.SHORT_INITIAL) + .build()) + .paymentSchedule(PaymentSchedule.builder() + .paymentFrequency(Frequency.P12M) + .paymentDateOffset(DaysAdjustment.NONE) + .build()) + .notionalSchedule(NotionalSchedule.of(GBP, 1_000_000)) + .calculation(FixedRateCalculation.builder() + .dayCount(DayCounts.ACT_365F) + .rate(ValueSchedule.of(0.011)) + .initialStub(FixedRateStubCalculation.ofKnownAmount(CurrencyAmount.of(GBP, 4000))) + .finalStub(FixedRateStubCalculation.ofKnownAmount(CurrencyAmount.of(GBP, 5000))) + .build()) + .build(), + RateCalculationSwapLeg.builder() // Ibor fixed rate stub + .payReceive(PAY) + .accrualSchedule(PeriodicSchedule.builder() + .startDate(date(2017, 5, 2)) + .endDate(date(2022, 5, 22)) + .frequency(Frequency.P12M) + .businessDayAdjustment(BusinessDayAdjustment.NONE) + .stubConvention(StubConvention.SHORT_INITIAL) + .build()) + .paymentSchedule(PaymentSchedule.builder() + .paymentFrequency(Frequency.P12M) + .paymentDateOffset(DaysAdjustment.NONE) + .build()) + .notionalSchedule(NotionalSchedule.of(GBP, 1_000_000)) + .calculation(IborRateCalculation.builder() + .dayCount(DayCounts.ACT_360) + .index(IborIndices.GBP_LIBOR_6M) + .resetPeriods(ResetSchedule.builder() + .resetFrequency(Frequency.P3M) + .resetMethod(IborRateResetMethod.WEIGHTED) + .businessDayAdjustment(BusinessDayAdjustment.of(FOLLOWING, GBLO.combinedWith(USNY))) + .build()) + .fixingRelativeTo(FixingRelativeTo.PERIOD_END) + .fixingDateOffset(DaysAdjustment.ofBusinessDays(3, GBLO, BusinessDayAdjustment.of(FOLLOWING, USNY))) + .negativeRateMethod(NegativeRateMethod.NOT_NEGATIVE) + .firstRate(0.005) + .gearing(ValueSchedule.of(2)) + .spread(ValueSchedule.of(0.03)) + .initialStub(IborRateStubCalculation.ofFixedRate(0.006)) + .finalStub(IborRateStubCalculation.ofFixedRate(0.007)) + .build()) + .build(), + RateCalculationSwapLeg.builder() // Ibor known amount stub + .payReceive(PAY) + .accrualSchedule(PeriodicSchedule.builder() + .startDate(date(2017, 5, 2)) + .endDate(date(2022, 5, 22)) + .frequency(Frequency.P12M) + .businessDayAdjustment(BusinessDayAdjustment.NONE) + .stubConvention(StubConvention.SHORT_INITIAL) + .build()) + .paymentSchedule(PaymentSchedule.builder() + .paymentFrequency(Frequency.P12M) + .paymentDateOffset(DaysAdjustment.NONE) + .build()) + .notionalSchedule(NotionalSchedule.of(GBP, 1_000_000)) + .calculation(IborRateCalculation.builder() + .dayCount(DayCounts.ACT_365F) + .index(IborIndices.GBP_LIBOR_6M) + .initialStub(IborRateStubCalculation.ofKnownAmount(CurrencyAmount.of(GBP, 4000))) + .finalStub(IborRateStubCalculation.ofKnownAmount(CurrencyAmount.of(GBP, 5000))) + .build()) + .build(), + RateCalculationSwapLeg.builder() // Ibor interpolated stub + .payReceive(PAY) + .accrualSchedule(PeriodicSchedule.builder() + .startDate(date(2017, 5, 2)) + .endDate(date(2022, 5, 22)) + .frequency(Frequency.P6M) + .businessDayAdjustment(BusinessDayAdjustment.NONE) + .stubConvention(StubConvention.SHORT_INITIAL) + .build()) + .paymentSchedule(PaymentSchedule.builder() + .paymentFrequency(Frequency.P6M) + .paymentDateOffset(DaysAdjustment.NONE) + .build()) + .notionalSchedule(NotionalSchedule.of(GBP, 1_000_000)) + .calculation(IborRateCalculation.builder() + .dayCount(DayCounts.ACT_365F) + .index(IborIndices.GBP_LIBOR_6M) + .initialStub( + IborRateStubCalculation.ofIborInterpolatedRate(IborIndices.GBP_LIBOR_3M, IborIndices.GBP_LIBOR_6M)) + .finalStub( + IborRateStubCalculation.ofIborInterpolatedRate(IborIndices.GBP_LIBOR_3M, IborIndices.GBP_LIBOR_6M)) + .build()) + .build(), + RateCalculationSwapLeg.builder() // overnight + .payReceive(PAY) + .accrualSchedule(PeriodicSchedule.builder() + .startDate(date(2017, 5, 2)) + .endDate(date(2022, 5, 22)) + .frequency(Frequency.P6M) + .businessDayAdjustment(BusinessDayAdjustment.NONE) + .stubConvention(StubConvention.SHORT_INITIAL) + .build()) + .paymentSchedule(PaymentSchedule.builder() + .paymentFrequency(Frequency.P6M) + .paymentDateOffset(DaysAdjustment.NONE) + .build()) + .notionalSchedule(NotionalSchedule.of(GBP, 1_000_000)) + .calculation(OvernightRateCalculation.builder() + .dayCount(DayCounts.ACT_360) + .index(OvernightIndices.GBP_SONIA) + .accrualMethod(OvernightAccrualMethod.AVERAGED) + .rateCutOffDays(3) + .negativeRateMethod(NegativeRateMethod.NOT_NEGATIVE) + .gearing(ValueSchedule.of(2)) + .spread(ValueSchedule.of(0.03)) + .build()) + .build(), + RateCalculationSwapLeg.builder() // inflation + .payReceive(PAY) + .accrualSchedule(PeriodicSchedule.builder() + .startDate(date(2017, 5, 2)) + .endDate(date(2022, 5, 22)) + .frequency(Frequency.P6M) + .businessDayAdjustment(BusinessDayAdjustment.NONE) + .stubConvention(StubConvention.SHORT_INITIAL) + .build()) + .paymentSchedule(PaymentSchedule.builder() + .paymentFrequency(Frequency.P6M) + .paymentDateOffset(DaysAdjustment.NONE) + .build()) + .notionalSchedule(NotionalSchedule.of(GBP, 1_000_000)) + .calculation(InflationRateCalculation.builder() + .index(PriceIndices.GB_RPI) + .lag(Period.ofMonths(2)) + .indexCalculationMethod(PriceIndexCalculationMethod.INTERPOLATED) + .firstIndexValue(121d) + .gearing(ValueSchedule.of(2)) + .build()) + .build()) + .build(); + SwapTrade expected = SwapTrade.builder() + .info(TradeInfo.builder() + .id(StandardId.of("OG", "1234")) + .tradeDate(date(2017, 1, 1)) + .tradeTime(LocalTime.of(12, 30)) + .zone(ZoneId.of("Europe/Paris")) + .build()) + .product(expectedSwap) + .build(); + assertBeanEquals(expected, result.getValue().get(0)); + } + + //------------------------------------------------------------------------- + public void test_load_termDeposit() { + TradeCsvLoader test = TradeCsvLoader.standard(); + ValueWithFailures> trades = test.load(FILE); + + List filtered = trades.getValue().stream() + .filter(TermDepositTrade.class::isInstance) + .map(TermDepositTrade.class::cast) + .collect(toImmutableList()); + assertEquals(filtered.size(), 3); + + TermDepositTrade expected1 = TermDepositConventions.GBP_SHORT_DEPOSIT_T0 + .createTrade(date(2017, 6, 1), Period.ofWeeks(2), SELL, 400_000, 0.002, REF_DATA) + .toBuilder() + .info(TradeInfo.builder() + .id(StandardId.of("OG", "123421")) + .tradeDate(date(2017, 6, 1)) + .build()) + .build(); + assertBeanEquals(expected1, filtered.get(0)); + + TermDepositTrade expected2 = TermDepositConventions.GBP_SHORT_DEPOSIT_T0 + .toTrade(date(2017, 6, 1), date(2017, 6, 1), date(2017, 6, 15), SELL, 500_000, 0.0022) + .toBuilder() + .info(TradeInfo.builder() + .id(StandardId.of("OG", "123422")) + .tradeDate(date(2017, 6, 1)) + .build()) + .build(); + assertBeanEquals(expected2, filtered.get(1)); + + TermDepositTrade expected3 = TermDepositTrade.builder() + .info(TradeInfo.builder() + .id(StandardId.of("OG", "123423")) + .tradeDate(date(2017, 6, 1)) + .build()) + .product(TermDeposit.builder() + .buySell(BUY) + .currency(GBP) + .notional(600_000) + .startDate(date(2017, 6, 1)) + .endDate(date(2017, 6, 22)) + .businessDayAdjustment(BusinessDayAdjustment.of(MODIFIED_FOLLOWING, GBLO)) + .dayCount(DayCounts.ACT_365F) + .rate(0.0023) + .build()) + .build(); + assertBeanEquals(expected3, filtered.get(2)); + } + + //------------------------------------------------------------------------- + public void test_load_invalidNoHeader() { + TradeCsvLoader test = TradeCsvLoader.standard(); + ValueWithFailures> trades = test.parse(ImmutableList.of(CharSource.wrap(""))); + + assertEquals(trades.getFailures().size(), 1); + FailureItem failure = trades.getFailures().get(0); + assertEquals(failure.getReason(), FailureReason.PARSING); + assertEquals(failure.getMessage().contains("CSV file could not be parsed"), true); + } + + public void test_load_invalidNoType() { + TradeCsvLoader test = TradeCsvLoader.standard(); + ValueWithFailures> trades = test.parse(ImmutableList.of(CharSource.wrap("Id"))); + + assertEquals(trades.getFailures().size(), 1); + FailureItem failure = trades.getFailures().get(0); + assertEquals(failure.getReason(), FailureReason.PARSING); + assertEquals(failure.getMessage().contains("CSV file does not contain 'Type' header"), true); + } + + public void test_load_invalidUnknownType() { + TradeCsvLoader test = TradeCsvLoader.standard(); + ValueWithFailures> trades = test.parse(ImmutableList.of(CharSource.wrap("Type\nFoo"))); + + assertEquals(trades.getFailures().size(), 1); + FailureItem failure = trades.getFailures().get(0); + assertEquals(failure.getReason(), FailureReason.PARSING); + assertEquals(failure.getMessage(), "CSV file trade type 'Foo' is not known at line 2"); + } + + public void test_load_invalidFra() { + TradeCsvLoader test = TradeCsvLoader.standard(); + ValueWithFailures> trades = test.parse(ImmutableList.of(CharSource.wrap("Type,Buy Sell\nFra,Buy"))); + + assertEquals(trades.getFailures().size(), 1); + FailureItem failure = trades.getFailures().get(0); + assertEquals(failure.getReason(), FailureReason.PARSING); + assertEquals(failure.getMessage(), "CSV file trade could not be parsed at line 2: Header not found: 'Notional'"); + } + + public void test_load_invalidSwap() { + TradeCsvLoader test = TradeCsvLoader.standard(); + ValueWithFailures> trades = test.parse(ImmutableList.of(CharSource.wrap("Type,Buy Sell\nSwap,Buy"))); + + assertEquals(trades.getFailures().size(), 1); + FailureItem failure = trades.getFailures().get(0); + assertEquals(failure.getReason(), FailureReason.PARSING); + assertEquals(failure.getMessage(), + "CSV file trade could not be parsed at line 2: Swap trade had invalid combination of fields. " + + "Must include either 'Convention' or '" + "Leg 1 Direction'"); + } + + public void test_load_invalidTermDeposit() { + TradeCsvLoader test = TradeCsvLoader.standard(); + ValueWithFailures> trades = test.parse(ImmutableList.of(CharSource.wrap("Type,Buy Sell\nTermDeposit,Buy"))); + + assertEquals(trades.getFailures().size(), 1); + FailureItem failure = trades.getFailures().get(0); + assertEquals(failure.getReason(), FailureReason.PARSING); + assertEquals(failure.getMessage(), "CSV file trade could not be parsed at line 2: Header not found: 'Notional'"); + } + + //------------------------------------------------------------------------- + public void coverage() { + coverPrivateConstructor(FraTradeCsvLoader.class); + coverPrivateConstructor(SwapTradeCsvLoader.class); + coverPrivateConstructor(TermDepositTradeCsvLoader.class); + coverPrivateConstructor(FullSwapTradeCsvLoader.class); + } + +} diff --git a/modules/loader/src/test/resources/com/opengamma/strata/loader/csv/trades.csv b/modules/loader/src/test/resources/com/opengamma/strata/loader/csv/trades.csv new file mode 100644 index 0000000000..27e861cbf5 --- /dev/null +++ b/modules/loader/src/test/resources/com/opengamma/strata/loader/csv/trades.csv @@ -0,0 +1,16 @@ +Type,Id Scheme,Id,Trade Date,Trade Time,Trade Zone,Convention,Buy Sell,Period To Start,Tenor,Index,Interpolated index,Fixed Rate,FX Rate,Day Count,Currency,Notional,Start Date,End Date,Date Convention,Date Calendar,Stub Convention,Leg 1 Direction,Leg 1 Start Date,Leg 1 End Date,Leg 1 Stub Convention,Leg 1 Roll Convention,Leg 1 Frequency,Leg 1 Currency,Leg 1 Notional,Leg 1 Fixed Rate,Leg 1 Day Count,Leg 1 Date Convention,Leg 1 Date Calendar,Leg 2 Direction,Leg 2 Start Date,Leg 2 End Date,Leg 2 Frequency,Leg 2 Currency,Leg 2 Notional,Leg 2 Index +Fra,OG,123401,01/06/2017,11:05,Europe/London,GBP-LIBOR-3M,Buy,P2M,,,,0.5,,,,1000000,,,,,,,,,,,,,,,,,,,,,,,, +Fra,OG,123402,01/06/2017,12:35,Europe/London,GBP-LIBOR-6M,Sell,,,,,0.7,,,,1000000,01/08/2017,01/02/2018,,,,,,,,,,,,,,,,,,,,,, +Fra,OG,123403,01/06/2017,,,,Sell,,,GBP-LIBOR-3M,GBP-LIBOR-6M,0.55,,Act/360,,1000000,01/08/2017,15/01/2018,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +Swap,OG,123411,01/06/2017,,,GBP-FIXED-1Y-LIBOR-3M,Buy,P1M,P5Y,,,0.4,,,,2000000,,,,,,,,,,,,,,,,,,,,,,,, +Swap,OG,123412,01/06/2017,,,GBP-FIXED-6M-LIBOR-6M,BUY,,,,,-0.01,,,,3100000,01/08/2017,01/08/2022,,,,,,,,,,,,,,,,,,,,,, +Swap,OG,123413,01/06/2017,,,GBP-FIXED-6M-LIBOR-6M,Buy,,,,,0.5,,,,4000000,01/08/2017,01/09/2022,,,LongFinal,,,,,,,,,,,,,,,,,,, +Swap,OG,123414,01/06/2017,,,GBP-LIBOR-3M-USD-LIBOR-3M,Buy,,P3Y,,,0.6,1.25,,,2000000,05/07/2017,,,,,,,,,,,,,,,,,,,,,,, +Swap,OG,123415,01/06/2017,,,,,,,,,,,,GBP,2500000,01/08/2017,01/08/2022,,,ShortFinal,Pay,,,,,3M,,,1.1,30/360 ISDA,,,Receive,,,3M,,,GBP-LIBOR-3M +Swap,OG,123416,01/06/2017,,,,,,,,,,,,,,,,,,,Pay,01/08/2017,08/08/2022,LongInitial,Day8,3M,GBP,1200000,1.2,30/360 ISDA,Following,GBLO+EUTA,Receive,08/08/2017,08/08/2022,3M,GBP,1200000,GBP-LIBOR-BBA +Swap,OG,123417,01/06/2017,,,,,,,,,,,,,,,,,,,Pay,08/08/2017,08/08/2022,,,3M,GBP,1500000,1.3,Act/365F,Preceding,GBLO+USNY,Receive,08/08/2017,08/08/2022,6M,GBP,1500000,GBP-SONIA +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +TermDeposit,OG,123421,01/06/2017,,,GBP-ShortDeposit-T0,Sell,,P2W,,,0.2,,,,400000,,,,,,,,,,,,,,,,,,,,,,,, +TermDeposit,OG,123422,01/06/2017,,,GBP-ShortDeposit-T0,Sell,,,,,0.22,,,,500000,01/06/2017,15/06/2017,,,,,,,,,,,,,,,,,,,,,, +TermDeposit,OG,123423,01/06/2017,,,,Buy,,,,,0.23,,Act/365F,GBP,600000,01/06/2017,22/06/2017,ModifiedFollowing,GBLO,,,,,,,,,,,,,,,,,,,, diff --git a/modules/pricer/src/test/java/com/opengamma/strata/pricer/swap/e2e/SwapCrossCurrencyEnd2EndTest.java b/modules/pricer/src/test/java/com/opengamma/strata/pricer/swap/e2e/SwapCrossCurrencyEnd2EndTest.java index 7e86082845..bd18b1f719 100644 --- a/modules/pricer/src/test/java/com/opengamma/strata/pricer/swap/e2e/SwapCrossCurrencyEnd2EndTest.java +++ b/modules/pricer/src/test/java/com/opengamma/strata/pricer/swap/e2e/SwapCrossCurrencyEnd2EndTest.java @@ -229,7 +229,7 @@ private void assertFixedNotionalPaymentEvent(ExplainMap paymentEvent, CurrencyAm assertEquals(paymentEvent.get(ExplainKey.TRADE_NOTIONAL), Optional.of(expectedNotional)); assertEquals(paymentEvent.get(ExplainKey.FORECAST_VALUE), Optional.of(expectedNotional)); - double firstDiscountFactor = paymentEvent.get(ExplainKey.ACCRUAL_DAY_COUNT.DISCOUNT_FACTOR).get(); + double firstDiscountFactor = paymentEvent.get(ExplainKey.DISCOUNT_FACTOR).get(); //Fixed notional, so PV is notional * DCF CurrencyAmount expectedPv = expectedNotional.multipliedBy(firstDiscountFactor);