Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend the SwaptionTrade CSV plugin for bermudan swaptions. #2376

Merged
merged 6 commits into from Nov 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -347,12 +347,18 @@ public final class CsvLoaderColumns {
/** CSV header (Position/Security). */
public static final String SETTLEMENT_TYPE_FIELD = "Settlement Type";
/** CSV header (Position/Security). */
public static final String EXERCISE_STYLE_FIELD = "Exercise Style";
public static final String EXERCISE_DATES_FIELD = "Exercise Dates";
/** CSV header (Position/Security). */
public static final String VERSION_FIELD = "Version";
public static final String EXERCISE_DATES_CNV_FIELD = "Exercise Dates Convention";
/** CSV header (Position/Security). */
public static final String EXERCISE_DATES_CAL_FIELD = "Exercise Dates Calendar";
/** CSV header (Position/Security). */
public static final String EXERCISE_STYLE_FIELD = "Exercise Style";
/** CSV header (Position/Security). */
public static final String EXERCISE_PRICE_FIELD = "Exercise Price";
/** CSV header (Position/Security). */
public static final String VERSION_FIELD = "Version";
/** CSV header (Position/Security). */
public static final String UNDERLYING_CURRENCY_FIELD = "Underlying Currency";
/** CSV header (Position/Security). */
public static final String UNDERLYING_EXPIRY_FIELD = "Underlying Expiry";
Expand Down
Expand Up @@ -6,6 +6,9 @@
package com.opengamma.strata.loader.csv;

import static com.opengamma.strata.collect.Guavate.toImmutableList;
import static com.opengamma.strata.loader.csv.CsvLoaderColumns.EXERCISE_DATES_CAL_FIELD;
import static com.opengamma.strata.loader.csv.CsvLoaderColumns.EXERCISE_DATES_CNV_FIELD;
import static com.opengamma.strata.loader.csv.CsvLoaderColumns.EXERCISE_DATES_FIELD;
import static com.opengamma.strata.loader.csv.CsvLoaderColumns.EXPIRY_DATE_CAL_FIELD;
import static com.opengamma.strata.loader.csv.CsvLoaderColumns.EXPIRY_DATE_CNV_FIELD;
import static com.opengamma.strata.loader.csv.CsvLoaderColumns.EXPIRY_DATE_FIELD;
Expand All @@ -30,11 +33,19 @@
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.opengamma.strata.basics.currency.AdjustablePayment;
import com.opengamma.strata.basics.date.AdjustableDate;
import com.opengamma.strata.basics.date.AdjustableDates;
import com.opengamma.strata.basics.date.BusinessDayAdjustment;
import com.opengamma.strata.basics.date.BusinessDayConvention;
import com.opengamma.strata.basics.date.DaysAdjustment;
import com.opengamma.strata.basics.date.HolidayCalendarId;
import com.opengamma.strata.collect.io.CsvOutput.CsvRowOutputWithHeaders;
import com.opengamma.strata.collect.io.CsvRow;
import com.opengamma.strata.loader.LoaderUtils;
Expand All @@ -48,6 +59,7 @@
import com.opengamma.strata.product.swaption.CashSwaptionSettlementMethod;
import com.opengamma.strata.product.swaption.PhysicalSwaptionSettlement;
import com.opengamma.strata.product.swaption.Swaption;
import com.opengamma.strata.product.swaption.SwaptionExercise;
import com.opengamma.strata.product.swaption.SwaptionSettlement;
import com.opengamma.strata.product.swaption.SwaptionTrade;

Expand Down Expand Up @@ -125,17 +137,19 @@ private static SwaptionTrade parseRow(CsvRow row, TradeInfo info, Swap underlyin
AdjustablePayment premium = CsvLoaderUtils.tryParsePremiumFromDefaultFields(row)
.orElse(AdjustablePayment.of(underlying.getLegs().get(0).getCurrency(), 0d, expiryDate));

Swaption swaption = Swaption.builder()
Swaption.Builder swaption = Swaption.builder()
.longShort(longShort)
.swaptionSettlement(settlement)
.expiryDate(expiryDate)
.expiryTime(expiryTime)
.expiryZone(expiryZone)
.underlying(underlying)
.build();
.underlying(underlying);

parseSwaptionExercise(row).ifPresent(swaption::exerciseInfo);

return SwaptionTrade.builder()
.info(info)
.product(swaption)
.product(swaption.build())
.premium(premium)
.build();
}
Expand All @@ -156,9 +170,8 @@ private static SwaptionSettlement parseSettlement(CsvRow row) {
public Set<String> headers(List<SwaptionTrade> trades) {
LinkedHashSet<String> headers = new LinkedHashSet<>(
FullSwapTradeCsvPlugin.INSTANCE.headers(trades.stream()
.map(t -> t.getProduct().getUnderlying())
.map(swap -> SwapTrade.of(TradeInfo.empty(), swap))
.collect(toImmutableList())));
.map(trade -> SwapTrade.of(TradeInfo.empty(), trade.getProduct().getUnderlying()))
.collect(toImmutableList())));
headers.add(LONG_SHORT_FIELD);
headers.add(PAYOFF_SETTLEMENT_TYPE_FIELD);
headers.add(PAYOFF_SETTLEMENT_DATE_FIELD);
Expand All @@ -176,6 +189,11 @@ public Set<String> headers(List<SwaptionTrade> trades) {
headers.add(PREMIUM_DIRECTION_FIELD);
headers.add(PREMIUM_CURRENCY_FIELD);
headers.add(PREMIUM_AMOUNT_FIELD);
if (trades.stream().anyMatch(trade -> trade.getProduct().getExerciseInfo().isPresent())) {
headers.add(EXERCISE_DATES_FIELD);
headers.add(EXERCISE_DATES_CAL_FIELD);
headers.add(EXERCISE_DATES_CNV_FIELD);
}
return headers;
}

Expand All @@ -194,10 +212,63 @@ public void writeCsv(CsvRowOutputWithHeaders csv, SwaptionTrade trade) {
csv.writeCell(EXPIRY_TIME_FIELD, product.getExpiryTime());
csv.writeCell(EXPIRY_ZONE_FIELD, product.getExpiryZone().getId());
CsvWriterUtils.writePremiumFields(csv, trade.getPremium());

trade.getProduct().getExerciseInfo()
.ifPresent(exercise -> writeSwaptionExercise(csv, exercise, product.getExpiryDate()));

csv.writeNewLine();
variableElements.writeLines(csv);
}

private void writeSwaptionExercise(
CsvRowOutputWithHeaders csv,
SwaptionExercise exercise,
AdjustableDate expiryDate) {

Function<AdjustableDate, String> extractUnadjustedDate = date -> date.getUnadjusted().toString();

List<AdjustableDate> dates = exercise.getDateDefinition().toAdjustableDateList();
if (exercise.isEuropean() && !dates.contains(expiryDate) || exercise.isBermudan()) {
csv.writeCell(EXERCISE_DATES_FIELD, pipeJoined(dates, extractUnadjustedDate));
csv.writeCell(EXERCISE_DATES_CAL_FIELD, exercise.getDateDefinition().getAdjustment().getCalendar());
csv.writeCell(EXERCISE_DATES_CNV_FIELD, exercise.getDateDefinition().getAdjustment().getConvention());
} else if (exercise.isAmerican()) {
//To implement if/when we support American swaptions. A frequency might have to be added.
}
}

private String pipeJoined(List<AdjustableDate> dates, Function<AdjustableDate, String> mapper) {
return dates.stream().map(mapper).collect(Collectors.joining("|"));
}

private static Optional<SwaptionExercise> parseSwaptionExercise(CsvRow row) {

Optional<String> exerciseDatesString = row.findValue(CsvLoaderColumns.EXERCISE_DATES_FIELD);
if (!exerciseDatesString.isPresent()) {
return Optional.empty();
}

Function<String, List<String>> pipeSplitter = s -> Stream.of(s.split("\\|")).collect(Collectors.toList());
List<LocalDate> dates = exerciseDatesString
.map(pipeSplitter)
.orElse(ImmutableList.of())
.stream()
.map(LoaderUtils::parseDate)
.collect(Collectors.toList());

BusinessDayConvention convention = row.getValue(EXERCISE_DATES_CNV_FIELD, LoaderUtils::parseBusinessDayConvention);
HolidayCalendarId calendar = row.getValue(EXERCISE_DATES_CAL_FIELD, HolidayCalendarId::of);
BusinessDayAdjustment bdAdjustment = BusinessDayAdjustment.of(convention, calendar);

SwaptionExercise exercise;
if (dates.size() == 1) {
exercise = SwaptionExercise.ofEuropean(AdjustableDate.of(dates.get(0), bdAdjustment), DaysAdjustment.NONE);
} else {
exercise = SwaptionExercise.ofBermudan(AdjustableDates.of(bdAdjustment, dates), DaysAdjustment.NONE);
}
return Optional.of(exercise);
}

private void writeSettlement(CsvRowOutputWithHeaders csv, Swaption product) {
if (product.getSwaptionSettlement() instanceof CashSwaptionSettlement) {
CashSwaptionSettlement cashSettle = (CashSwaptionSettlement) product.getSwaptionSettlement();
Expand Down
Expand Up @@ -350,7 +350,7 @@ public ValueWithFailures<List<Trade>> parse(
FailureReason.PARSING,
"Trade type not allowed {tradeType}, only these types are supported: {}",
trade.getClass().getName(),
tradeTypes.stream().map(t -> t.getSimpleName()).collect(joining(", "))));
tradeTypes.stream().map(Class::getSimpleName).collect(joining(", "))));
}
}
return ValueWithFailures.of(valid, failures);
Expand Down Expand Up @@ -408,7 +408,6 @@ private <T extends Trade> ValueWithFailures<List<T>> parseFile(CharSource charSo
private <T extends Trade> ValueWithFailures<List<T>> parseFile(CsvIterator csv, Class<T> tradeType) {
List<T> trades = new ArrayList<>();
List<FailureItem> failures = new ArrayList<>();
rows:
for (CsvRow row : csv.asIterable()) {
String typeRaw = row.findField(TRADE_TYPE_FIELD).orElse("");
String typeUpper = typeRaw.toUpperCase(Locale.ENGLISH);
Expand All @@ -420,7 +419,7 @@ private <T extends Trade> ValueWithFailures<List<T>> parseFile(CsvIterator csv,
if (tradeType.isInstance(overrideOpt.get())) {
trades.add(tradeType.cast(overrideOpt.get()));
}
continue rows;
continue;
}
// standard type matching
TradeCsvParserPlugin plugin = PLUGINS.get(typeUpper);
Expand All @@ -430,17 +429,17 @@ private <T extends Trade> ValueWithFailures<List<T>> parseFile(CsvIterator csv,
additionalRows.add(csv.next());
}
plugin.parseTrade(tradeType, row, additionalRows, info, resolver)
.filter(parsed -> tradeType.isInstance(parsed))
.filter(tradeType::isInstance)
.ifPresent(parsed -> trades.add((T) parsed));
continue rows;
continue;
}
// match type using the resolver
Optional<Trade> parsedOpt = resolver.parseOtherTrade(typeUpper, row, info);
if (parsedOpt.isPresent()) {
if (tradeType.isInstance(parsedOpt.get())) {
trades.add(tradeType.cast(parsedOpt.get()));
}
continue rows;
continue;
}
// better error for VARIABLE
if (typeUpper.equals("VARIABLE")) {
Expand Down
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2021 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.strata.loader.csv;

import static org.assertj.core.api.Assertions.assertThat;
import static org.joda.beans.test.BeanAssert.assertBeanEquals;

import java.util.List;

import org.joda.beans.Bean;

import com.google.common.collect.ImmutableList;
import com.google.common.io.CharSource;
import com.opengamma.strata.collect.result.ValueWithFailures;
import com.opengamma.strata.product.Trade;

/**
* Groups CSV test utils methods used in Granite before they are moved to Strata.
*/
public class CsvTestUtils {

@SafeVarargs
public static <T extends Trade & Bean> void checkRoundtrip(
Class<T> type,
List<T> loadedTrades,
T... expectedTrades) {

StringBuilder buf = new StringBuilder(1024);
TradeCsvWriter.standard().write(loadedTrades, buf);
List<CharSource> writtenCsv = ImmutableList.of(CharSource.wrap(buf.toString()));
ValueWithFailures<List<T>> roundtrip = TradeCsvLoader.standard().parse(writtenCsv, type);
assertThat(roundtrip.getFailures().size()).as(roundtrip.getFailures().toString()).isEqualTo(0);
List<T> roundtripTrades = roundtrip.getValue();
assertThat(roundtripTrades).hasSize(expectedTrades.length);
for (int i = 0; i < roundtripTrades.size(); i++) {
assertBeanEquals(expectedTrades[i], roundtripTrades.get(i));
}
}
}
@@ -0,0 +1,115 @@
/*
* Copyright (C) 2021 - 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.USD;
import static com.opengamma.strata.basics.index.IborIndices.USD_LIBOR_3M;
import static com.opengamma.strata.collect.TestHelper.date;
import static com.opengamma.strata.loader.csv.CsvTestUtils.checkRoundtrip;
import static com.opengamma.strata.product.common.BuySell.SELL;
import static com.opengamma.strata.product.swap.type.FixedIborSwapConventions.USD_FIXED_6M_LIBOR_3M;

import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Period;
import java.time.ZoneId;
import java.util.List;

import org.junit.jupiter.api.Test;

import com.google.common.collect.ImmutableList;
import com.opengamma.strata.basics.ReferenceData;
import com.opengamma.strata.basics.currency.CurrencyAmount;
import com.opengamma.strata.basics.currency.Payment;
import com.opengamma.strata.basics.date.AdjustableDate;
import com.opengamma.strata.basics.date.AdjustableDates;
import com.opengamma.strata.basics.date.DaysAdjustment;
import com.opengamma.strata.collect.io.ResourceLocator;
import com.opengamma.strata.collect.result.ValueWithFailures;
import com.opengamma.strata.product.TradeInfo;
import com.opengamma.strata.product.common.LongShort;
import com.opengamma.strata.product.swap.Swap;
import com.opengamma.strata.product.swaption.PhysicalSwaptionSettlement;
import com.opengamma.strata.product.swaption.Swaption;
import com.opengamma.strata.product.swaption.SwaptionExercise;
import com.opengamma.strata.product.swaption.SwaptionTrade;

/**
* Tests for the {@link SwaptionTradeCsvPlugin}
*/
final class SwaptionTradeCsvPluginTest {

private static final ResourceLocator CSV_FILE =
ResourceLocator.of("classpath:com/opengamma/strata/loader/csv/swaption_trades.csv");

private static final SwaptionTradeCsvPlugin PLUGIN = SwaptionTradeCsvPlugin.INSTANCE;

private static final ReferenceData REF_DATA = ReferenceData.standard();
private static final LocalDate VAL_DATE = date(2015, 8, 7);
private static final LocalDate SWAPTION_EXERCISE_DATE = VAL_DATE.plusYears(5);
private static final LocalTime SWAPTION_EXPIRY_TIME = LocalTime.of(17, 0);
private static final ZoneId SWAPTION_EXPIRY_ZONE = ZoneId.of("America/New_York");
private static final LocalDate SWAP_EFFECTIVE_DATE =
USD_LIBOR_3M.calculateEffectiveFromFixing(SWAPTION_EXERCISE_DATE, REF_DATA);
private static final LocalDate SWAP_MATURITY_DATE = SWAP_EFFECTIVE_DATE.plus(Period.ofYears(10));
private static final Swap SWAP_REC = USD_FIXED_6M_LIBOR_3M
.toTrade(VAL_DATE, SWAP_EFFECTIVE_DATE, SWAP_MATURITY_DATE, SELL, 1E6, 0.01).getProduct();

// With no exercise info, the exercise date is the expiry date.
private static final Swaption DEFAULT_EUROPEAN_SWAPTION = Swaption.builder()
.swaptionSettlement(PhysicalSwaptionSettlement.DEFAULT)
.expiryDate(AdjustableDate.of(SWAPTION_EXERCISE_DATE))
.expiryTime(SWAPTION_EXPIRY_TIME)
.expiryZone(SWAPTION_EXPIRY_ZONE)
.longShort(LongShort.LONG)
.underlying(SWAP_REC)
.build();

private static final Swaption EUROPEAN_SWAPTION_WITH_SPECIFIC_EXERCISE = Swaption.builder()
.swaptionSettlement(PhysicalSwaptionSettlement.DEFAULT)
.exerciseInfo(SwaptionExercise.ofEuropean(AdjustableDate.of(date(2018, 6, 14)), DaysAdjustment.NONE))
.expiryDate(AdjustableDate.of(SWAPTION_EXERCISE_DATE))
.expiryTime(SWAPTION_EXPIRY_TIME)
.expiryZone(SWAPTION_EXPIRY_ZONE)
.longShort(LongShort.LONG)
.underlying(SWAP_REC)
.build();

private static final AdjustableDates BERMUDAN_EXERCISE_DATES =
AdjustableDates.of(date(2016, 6, 14), date(2017, 6, 14), date(2018, 6, 14), date(2019, 6, 14));

private static final Swaption BERMUDAN_SWAPTION = Swaption.builder()
.swaptionSettlement(PhysicalSwaptionSettlement.DEFAULT)
.exerciseInfo(SwaptionExercise.ofBermudan(BERMUDAN_EXERCISE_DATES, DaysAdjustment.NONE))
.expiryDate(AdjustableDate.of(SWAPTION_EXERCISE_DATE))
.expiryTime(SWAPTION_EXPIRY_TIME)
.expiryZone(SWAPTION_EXPIRY_ZONE)
.longShort(LongShort.LONG)
.underlying(SWAP_REC)
.build();

private static SwaptionTrade toSwaptionTrade(Swaption swaption) {
return SwaptionTrade.of(TradeInfo.empty(), swaption, Payment.of(CurrencyAmount.of(USD, 0), VAL_DATE));
}

private static final SwaptionTrade DEFAULT_EUROPEAN_SWAPTION_TRADE = toSwaptionTrade(DEFAULT_EUROPEAN_SWAPTION);
private static final SwaptionTrade EUROPEAN_SWAPTION_WITH_SPECIFIC_EXERCISE_TRADE =
toSwaptionTrade(EUROPEAN_SWAPTION_WITH_SPECIFIC_EXERCISE);
private static final SwaptionTrade BERMUDAN_SWAPTION_TRADE = toSwaptionTrade(BERMUDAN_SWAPTION);

@Test
void testSwaptionCsvPlugin() {
ValueWithFailures<List<SwaptionTrade>> trades = TradeCsvLoader.standard()
.parse(ImmutableList.of(CSV_FILE.getCharSource()), SwaptionTrade.class);
checkRoundtrip(
SwaptionTrade.class,
trades.getValue(),
EUROPEAN_SWAPTION_WITH_SPECIFIC_EXERCISE_TRADE,
DEFAULT_EUROPEAN_SWAPTION_TRADE,
BERMUDAN_SWAPTION_TRADE);
}

}