/**
* Copyright (C) 2013 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.sesame.fxforward;
import static org.threeten.bp.DayOfWeek.SATURDAY;
import static org.threeten.bp.DayOfWeek.SUNDAY;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.threeten.bp.DayOfWeek;
import org.threeten.bp.LocalDate;
import org.threeten.bp.ZoneOffset;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.opengamma.analytics.financial.forex.method.FXMatrix;
import com.opengamma.analytics.financial.provider.sensitivity.multicurve.MultipleCurrencyParameterSensitivity;
import com.opengamma.analytics.math.matrix.DoubleMatrix1D;
import com.opengamma.financial.analytics.TenorLabelledLocalDateDoubleTimeSeriesMatrix1D;
import com.opengamma.financial.analytics.curve.AbstractCurveSpecification;
import com.opengamma.financial.analytics.curve.CurveConstructionConfiguration;
import com.opengamma.financial.analytics.curve.CurveDefinition;
import com.opengamma.financial.analytics.curve.CurveSpecification;
import com.opengamma.financial.analytics.ircurve.strips.CurveNodeWithIdentifier;
import com.opengamma.financial.currency.CurrencyPair;
import com.opengamma.financial.security.fx.FXForwardSecurity;
import com.opengamma.sesame.CurrencyPairsFn;
import com.opengamma.sesame.CurveSpecificationFn;
import com.opengamma.sesame.DiscountingMulticurveBundleResolverFn;
import com.opengamma.sesame.Environment;
import com.opengamma.sesame.FXMatrixFn;
import com.opengamma.sesame.FXReturnSeriesFn;
import com.opengamma.sesame.ImpliedDepositCurveData;
import com.opengamma.sesame.component.StringSet;
import com.opengamma.sesame.marketdata.HistoricalMarketDataFn;
import com.opengamma.timeseries.date.localdate.ImmutableLocalDateDoubleTimeSeries;
import com.opengamma.timeseries.date.localdate.LocalDateDoubleTimeSeries;
import com.opengamma.util.money.Currency;
import com.opengamma.util.money.UnorderedCurrencyPair;
import com.opengamma.util.result.FailureStatus;
import com.opengamma.util.result.Result;
import com.opengamma.util.time.LocalDateRange;
import com.opengamma.util.time.Tenor;
/**
* Calculates yield curve node sensitivity P&L series for
* an FX forward security.
*/
public class DiscountingFXForwardYCNSPnLSeriesFn implements FXForwardYCNSPnLSeriesFn {
private static final Logger s_logger = LoggerFactory.getLogger(DiscountingFXForwardYCNSPnLSeriesFn.class);
private static final ImmutableSet<DayOfWeek> s_weekendDays = ImmutableSet.of(SATURDAY, SUNDAY);
private final FXForwardCalculatorFn _calculatorProvider;
private final CurveDefinition _curveDefinition;
private final Currency _curveCurrency;
private final CurveConstructionConfiguration _curveConfig;
/**
* The requested currency for this P&L series. If not supplied, then the
* output will be in the base currency of the currency pair corresponding
* to the FX Forward's currencies.
*/
private final Optional<Currency> _outputCurrency;
private final FXReturnSeriesFn _fxReturnSeriesProvider;
private final HistoricalMarketDataFn _historicalMarketDataFn;
private final CurveSpecificationFn _curveSpecificationFunction;
private final CurrencyPairsFn _currencyPairsFn;
// todo - this is only a temporary solution to determine the implied deposit curves
private final Set<String> _impliedCurveNames;
private final DiscountingMulticurveBundleResolverFn _bundleResolver;
private final FXMatrixFn _fxMatrixFn;
private final Boolean _useHistoricalSpot;
private final LocalDateRange _dateRange;
@Inject
public DiscountingFXForwardYCNSPnLSeriesFn(FXForwardCalculatorFn calculatorProvider,
CurveDefinition curveDefinition,
Currency curveCurrency,
CurveConstructionConfiguration curveConfig,
Optional<Currency> outputCurrency,
FXReturnSeriesFn fxReturnSeriesProvider,
HistoricalMarketDataFn historicalMarketDataFn,
CurveSpecificationFn curveSpecificationFunction,
CurrencyPairsFn currencyPairsFn,
StringSet impliedCurveNames,
DiscountingMulticurveBundleResolverFn bundleResolver,
FXMatrixFn fxMatrixFn,
Boolean useHistoricalSpot,
LocalDateRange dateRange) {
_calculatorProvider = calculatorProvider;
_curveDefinition = curveDefinition;
_curveCurrency = curveCurrency;
_curveConfig = curveConfig;
_outputCurrency = outputCurrency;
_fxReturnSeriesProvider = fxReturnSeriesProvider;
_historicalMarketDataFn = historicalMarketDataFn;
_curveSpecificationFunction = curveSpecificationFunction;
_currencyPairsFn = currencyPairsFn;
_bundleResolver = bundleResolver;
_impliedCurveNames = impliedCurveNames.getStrings();
_fxMatrixFn = fxMatrixFn;
_useHistoricalSpot = useHistoricalSpot;
_dateRange = dateRange;
}
@Override
public Result<TenorLabelledLocalDateDoubleTimeSeriesMatrix1D> calculateYCNSPnlSeries(Environment env,
FXForwardSecurity security) {
// If this is for an Implied Deposit curve we need to behave differently
// 1. We need to calculate the multicurve bundle for each day that
// we're interested in by moving the valuation date.
// 2. This will have created multiple Implied Deposit curves, we need to
// iterate across them generating a timeseries bundle from the nodal
// values (or do this as we're generating them
// 3. Then use the timeseries bundles as we do for a standard curve
final Currency payCurrency = security.getPayCurrency();
final Currency receiveCurrency = security.getReceiveCurrency();
final UnorderedCurrencyPair pair = UnorderedCurrencyPair.of(payCurrency, receiveCurrency);
final Result<CurrencyPair> cpResult = _currencyPairsFn.getCurrencyPair(pair);
// todo - these should probably be separate classes as there is little commonality in the methods
return _impliedCurveNames.contains(_curveDefinition.getName()) ?
calculateForImpliedCurve(env, security, cpResult) :
calculateForNonImpliedCurve(env, security, cpResult);
}
private Result<TenorLabelledLocalDateDoubleTimeSeriesMatrix1D> calculateForImpliedCurve(Environment env,
FXForwardSecurity security,
Result<CurrencyPair> cpResult) {
// We need the calculator so we can get the block curve sensitivities
Result<FXForwardCalculator> calculatorResult = _calculatorProvider.generateCalculator(env, security);
LocalDate priceSeriesEnd = _dateRange.getEndDateInclusive();
LocalDate priceSeriesStart = _dateRange.getStartDateInclusive().minusWeeks(1);
LocalDateRange priceSeriesRange = LocalDateRange.of(priceSeriesStart, priceSeriesEnd, true);
LocalDateDoubleTimeSeries conversionSeries = generateConversionSeries(env, _curveCurrency, priceSeriesRange);
Result<FXMatrix> fxMatrixResult = getFxMatrix(env);
// Generate our version of an HTS Bundle
ImpliedCurveHtsBundleBuilder builder = new ImpliedCurveHtsBundleBuilder();
// todo - how do we adjust for holidays?
for (LocalDate date = priceSeriesStart; !date.isAfter(priceSeriesEnd); date = date.plusDays(1)) {
// Shifting the date will automatically shift the market data as well
Environment envForDate = env.withValuationTime(date.atStartOfDay(ZoneOffset.UTC));
// build multicurve for the date
Result<ImpliedDepositCurveData> result =
_bundleResolver.extractImpliedDepositCurveData(envForDate, _curveConfig);
// TODO consider how to report failures. either log (as here),
// fail entire calc in all or nothing approach, somewhere in between?
// [SSM-234]
if (result.isSuccess()) {
ImpliedDepositCurveData impliedCurveData = result.getValue();
List<Tenor> tenors = impliedCurveData.getTenors();
List<Double> parRates = impliedCurveData.getParRates();
for (int i = 0; i < tenors.size(); i++) {
builder.add(date, tenors.get(i), parRates.get(i));
}
} else {
//TODO use actual calendars here. [SSM-233]
if (isWorkingDay(date)) {
s_logger.warn("Failed to build curve for date {}. Reason: {}", date, result.getFailureMessage());
}
}
}
TenorLabelledLocalDateDoubleTimeSeriesMatrix1D series = builder.toTimeSeries();
String curveName = _curveDefinition.getName();
if (calculatorResult.isSuccess() && cpResult.isSuccess() && fxMatrixResult.isSuccess()) {
MultipleCurrencyParameterSensitivity bcs = calculatorResult.getValue().generateBlockCurveSensitivities(env);
Map<Currency, DoubleMatrix1D> sensitivities = bcs.getSensitivityByName(curveName);
// TODO this is wrong, don't exit early
if (sensitivities.isEmpty()) {
return Result.failure(FailureStatus.MISSING_DATA, "No sensitivities for curve: {} were found", curveName);
}
Map.Entry<Currency, DoubleMatrix1D> match = sensitivities.entrySet().iterator().next();
DoubleMatrix1D sensitivity = match.getValue();
if (sensitivities.size() > 1) {
s_logger.warn("Curve name: {} is used multiple times - using one for currency: {}", curveName, _curveCurrency);
}
int sensitivitySize = sensitivity.getNumberOfElements();
// TODO curveCurrency and sensitivity are used outside here even if the results aren't successful
// that's big a problem because curveCurrency comes from a Result that depends on market data
// everything above here depends on the calculator result being successful ---------------------------------------
Tenor[] tenors = series.getKeys();
int tenorsSize = tenors.length;
if (sensitivitySize != tenorsSize) {
return Result.failure(FailureStatus.ERROR,
"Unequal number of sensitivities ({}) and curve tenors ({})",
sensitivitySize,
tenorsSize);
}
LocalDateDoubleTimeSeries[] values = new LocalDateDoubleTimeSeries[tenorsSize];
for (int i = 0; i < tenorsSize; i++) {
Series seriesForTenor = builder.getSeriesForTenor(tenors[i]);
LocalDateDoubleTimeSeries ts = trimSeries(ImmutableLocalDateDoubleTimeSeries.of(seriesForTenor._dates,
seriesForTenor._values));
LocalDateDoubleTimeSeries returnSeries = calculateConvertedReturnSeries(env, ts, null);
LocalDateDoubleTimeSeries pnlSeries = returnSeries.multiply(sensitivity.getEntry(i));
if (!conversionIsRequired(_curveCurrency)) {
values[i] = pnlSeries;
continue;
}
//else - do appropriate conversion
if (_useHistoricalSpot) {
values[i] = pnlSeries.multiply(conversionSeries.reciprocal());
} else {
double fxRate = fxMatrixResult.getValue().getFxRate(_curveCurrency, _outputCurrency.get());
values[i] = pnlSeries.multiply(fxRate);
}
}
return Result.success(new TenorLabelledLocalDateDoubleTimeSeriesMatrix1D(tenors, tenors, values));
} else {
return Result.failure(calculatorResult, cpResult);
}
}
private Result<FXMatrix> getFxMatrix(Environment env) {
if (!conversionIsRequired(_curveCurrency)) {
return Result.success(new FXMatrix());
} else {
return _fxMatrixFn.getFXMatrix(env, Sets.newHashSet(_curveCurrency, _outputCurrency.get()));
}
}
private boolean isWorkingDay(LocalDate date) {
//very much a work in progress. should attempt to use
//local calendar data instead. only used to determine
//whether to log error messages atm.
//see [SSM-233] and [SSM-234].
return !s_weekendDays.contains(date.getDayOfWeek());
}
private static class Series {
private final List<LocalDate> _dates = new ArrayList<>();
private final List<Double> _values = new ArrayList<>();
public void add(LocalDate date, Double value) {
_dates.add(date);
_values.add(value);
}
}
private static class ImpliedCurveHtsBundleBuilder {
private final Map<Tenor, Series> _series = new HashMap<>();
public void add(LocalDate date, Tenor tenor, Double aDouble) {
getSeriesForTenor(tenor).add(date, aDouble);
}
private Series getSeriesForTenor(Tenor tenor) {
if (_series.containsKey(tenor)) {
return _series.get(tenor);
} else {
Series series = new Series();
_series.put(tenor, series);
return series;
}
}
public TenorLabelledLocalDateDoubleTimeSeriesMatrix1D toTimeSeries() {
ArrayList<Tenor> sortedTenors = new ArrayList<>(_series.keySet());
Collections.sort(sortedTenors);
int size = sortedTenors.size();
Tenor[] tenors = new Tenor[size];
LocalDateDoubleTimeSeries[] series = new LocalDateDoubleTimeSeries[size];
for (int i = 0; i < size; i++) {
Tenor tenor = sortedTenors.get(i);
tenors[i] = tenor;
Series tenorSeries = _series.get(tenor);
series[i] = ImmutableLocalDateDoubleTimeSeries.of(tenorSeries._dates, tenorSeries._values);
}
return new TenorLabelledLocalDateDoubleTimeSeriesMatrix1D(tenors, series);
}
}
private Result<TenorLabelledLocalDateDoubleTimeSeriesMatrix1D> calculateForNonImpliedCurve(Environment env,
FXForwardSecurity security,
Result<CurrencyPair> cpResult) {
final Result<FXForwardCalculator> calculatorResult = _calculatorProvider.generateCalculator(env, security);
final Result<AbstractCurveSpecification> curveSpecificationResult =
_curveSpecificationFunction.getCurveSpecification(env, _curveDefinition);
LocalDate priceSeriesEnd = _dateRange.getEndDateInclusive();
//take one week off the start date. this ensures that the start of the underlying
//price series will provide at least one business day of data before the required start of
// the return series. the resulting return series is trimmed so that first day of PnL =
//start date.
LocalDate priceSeriesStart = _dateRange.getStartDateInclusive().minusWeeks(1);
LocalDateRange priceSeriesRange = LocalDateRange.of(priceSeriesStart, priceSeriesEnd, true);
if (Result.allSuccessful(calculatorResult, curveSpecificationResult, cpResult)) {
final MultipleCurrencyParameterSensitivity bcs = calculatorResult.getValue().generateBlockCurveSensitivities(env);
final CurveSpecification curveSpecification = (CurveSpecification) curveSpecificationResult.getValue();
// todo - extract common code between this method and calculateForImpliedCurve
final String curveName = _curveDefinition.getName();
final Map<Currency, DoubleMatrix1D> sensitivities = bcs.getSensitivityByName(curveName);
if (sensitivities.isEmpty()) {
return Result.failure(FailureStatus.MISSING_DATA, "No sensitivities for curve: {} were found", curveName);
}
Map.Entry<Currency, DoubleMatrix1D> match = sensitivities.entrySet().iterator().next();
DoubleMatrix1D sensitivity = match.getValue();
Currency curveCurrency = match.getKey();
if (sensitivities.size() > 1) {
s_logger.warn("Curve name: {} is used multiple times - using one for currency: {}", curveName, curveCurrency);
}
final Set<CurveNodeWithIdentifier> nodes = curveSpecification.getNodes();
final int sensitivitiesSize = sensitivity.getNumberOfElements();
final int nodesSize = nodes.size();
if (sensitivitiesSize != nodesSize) {
return Result.failure(FailureStatus.ERROR,
"Unequal number of sensitivities ({}) and curve nodes ({})",
sensitivitiesSize,
nodesSize);
}
LocalDateDoubleTimeSeries conversionSeries = generateConversionSeries(env,
curveCurrency,
priceSeriesRange);
//TODO - [SSM-192] may want to use today's spot rate for conversion here
return calculateSeriesForNodes(env, sensitivity, nodes, conversionSeries, priceSeriesRange);
}
return Result.failure(calculatorResult, curveSpecificationResult, cpResult);
}
private LocalDateDoubleTimeSeries generateConversionSeries(Environment env, Currency curveCurrency, LocalDateRange priceSeriesRange) {
if (conversionIsRequired(curveCurrency)) {
CurrencyPair currencyPair = CurrencyPair.of(curveCurrency, _outputCurrency.get());
Result<LocalDateDoubleTimeSeries> conversionSeriesResult =
_historicalMarketDataFn.getFxRates(env, currencyPair, priceSeriesRange);
if (conversionSeriesResult.isSuccess()) {
return conversionSeriesResult.getValue();
}
// todo handle the case where we got no result
}
return null;
}
private Result<TenorLabelledLocalDateDoubleTimeSeriesMatrix1D> calculateSeriesForNodes(Environment env,
DoubleMatrix1D sensitivities,
Set<CurveNodeWithIdentifier> nodes,
LocalDateDoubleTimeSeries fxConversionSeries,
LocalDateRange priceSeriesRange) {
final int size = sensitivities.getNumberOfElements();
final Tenor[] keys = new Tenor[size];
final Object[] labels = new Object[size];
final LocalDateDoubleTimeSeries[] values = new LocalDateDoubleTimeSeries[size];
int i = 0;
for (final CurveNodeWithIdentifier curveNodeWithId : nodes) {
Result<LocalDateDoubleTimeSeries> timeSeriesResult =
_historicalMarketDataFn.getCurveNodeValues(env, curveNodeWithId, priceSeriesRange);
if (!timeSeriesResult.isSuccess()) {
return Result.failure(timeSeriesResult);
}
LocalDateDoubleTimeSeries ts = trimSeries(timeSeriesResult.getValue());
final LocalDateDoubleTimeSeries returnSeries = calculateConvertedReturnSeries(env, ts, fxConversionSeries);
keys[i] = curveNodeWithId.getCurveNode().getResolvedMaturity();
String curveNodeName = curveNodeWithId.getCurveNode().getName();
labels[i] = curveNodeName != null ? curveNodeName : keys[i];
values[i] = returnSeries.multiply(sensitivities.getEntry(i));
i++;
}
return Result.success(new TenorLabelledLocalDateDoubleTimeSeriesMatrix1D(keys, labels, values));
}
//todo - would be more efficient pass in two lists here rather than the time series.
//this way, the caller wouldn't be forced to build a ts object to trim the time series.
private LocalDateDoubleTimeSeries trimSeries(LocalDateDoubleTimeSeries ts) {
LinkedList<LocalDate> dates = Lists.newLinkedList();
LinkedList<Double> values = Lists.newLinkedList();
for (int j = ts.size() - 1; j >= 0; j--) {
LocalDate date = ts.getTimeAtIndex(j);
Double value = ts.getValueAtIndex(j);
dates.addFirst(date);
values.addFirst(value);
//note - purposely go one date past start series date.
//this means PnL will start the on (or first available date after)
//the start date.
if (date.isBefore(_dateRange.getStartDateInclusive())) {
break;
}
}
return ImmutableLocalDateDoubleTimeSeries.of(dates, values);
}
private LocalDateDoubleTimeSeries calculateConvertedReturnSeries(Environment env,
LocalDateDoubleTimeSeries ts,
LocalDateDoubleTimeSeries conversionSeries) {
LocalDateDoubleTimeSeries series = conversionSeries != null ? ts.multiply(conversionSeries) : ts;
return _fxReturnSeriesProvider.calculateReturnSeries(env, series);
}
private boolean conversionIsRequired(final Currency baseCurrency) {
// No output currency property or it's the same as base means we don't need to convert
return _outputCurrency.or(baseCurrency) != baseCurrency;
}
}