/** * Copyright (C) 2014 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.sesame.marketdata.builders; import java.util.Collection; import java.util.Map; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.threeten.bp.LocalDate; import org.threeten.bp.ZonedDateTime; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.opengamma.core.link.ConfigLink; import com.opengamma.engine.value.ValueRequirement; import com.opengamma.financial.currency.CurrencyMatrix; import com.opengamma.financial.currency.CurrencyMatrixValue; import com.opengamma.financial.currency.CurrencyMatrixValueVisitor; import com.opengamma.financial.currency.CurrencyPair; import com.opengamma.id.ExternalIdBundle; import com.opengamma.sesame.marketdata.FieldName; import com.opengamma.sesame.marketdata.FxRateId; import com.opengamma.sesame.marketdata.MarketDataBundle; import com.opengamma.sesame.marketdata.MarketDataId; import com.opengamma.sesame.marketdata.MarketDataRequirement; import com.opengamma.sesame.marketdata.MarketDataSource; import com.opengamma.sesame.marketdata.MarketDataTime; import com.opengamma.sesame.marketdata.RawId; import com.opengamma.sesame.marketdata.SingleValueRequirement; import com.opengamma.sesame.marketdata.TimeSeriesRequirement; import com.opengamma.sesame.marketdata.scenarios.CyclePerturbations; import com.opengamma.sesame.marketdata.scenarios.FilteredPerturbation; import com.opengamma.timeseries.date.DateEntryIterator; import com.opengamma.timeseries.date.DateTimeSeries; import com.opengamma.timeseries.date.localdate.ImmutableLocalDateDoubleTimeSeries; import com.opengamma.timeseries.date.localdate.LocalDateDoubleTimeSeries; import com.opengamma.timeseries.date.localdate.LocalDateDoubleTimeSeriesBuilder; import com.opengamma.util.ArgumentChecker; import com.opengamma.util.money.Currency; import com.opengamma.util.result.FailureStatus; import com.opengamma.util.result.Function2; import com.opengamma.util.result.Result; import com.opengamma.util.time.LocalDateRange; /** * Creates market data for the FX rates for currency pairs. * <p> * FX rates are requested using {@link FxRateId} instances which specify the currency pair. This builder * hides the tickers used for looking up the rates from a market data provider and derives cross rates * from directly quoted rates. * <p> * The underlying market data tickers and cross rate configuration comes from a {@link CurrencyMatrix}. */ public class FxRateMarketDataBuilder implements MarketDataBuilder { private static final Logger s_logger = LoggerFactory.getLogger(FxRateMarketDataBuilder.class); private final ConfigLink<CurrencyMatrix> _currencyMatrixLink; /** * @param currencyMatrixLink defines how FX rates should be looked up from a market data provider or derived * from other rates */ public FxRateMarketDataBuilder(ConfigLink<CurrencyMatrix> currencyMatrixLink) { _currencyMatrixLink = ArgumentChecker.notNull(currencyMatrixLink, "currencyMatrix"); } @Override public Set<MarketDataRequirement> getSingleValueRequirements(SingleValueRequirement requirement, ZonedDateTime valuationTime, Set<? extends MarketDataRequirement> suppliedData) { FxRateId rateId = (FxRateId) requirement.getMarketDataId(); CurrencyPair currencyPair = rateId.getCurrencyPair(); // If the supplied data already contains the rate then it won't be in the requirements at all. // If the supplied data contains the inverse rate we can use that instead of looking up the market rate FxRateId inverseRateId = FxRateId.of(currencyPair.inverse()); MarketDataRequirement inverseRateRequirement = SingleValueRequirement.of(inverseRateId, requirement.getMarketDataTime()); if (suppliedData.contains(inverseRateRequirement)) { // don't need to return any requirements because the inverse rate is in the supplied data and we can use // that to calculate the rate we need return ImmutableSet.of(); } // TODO Java 8 - replace with a lambda Function2<MarketDataId<?>, MarketDataTime, MarketDataRequirement> requirementBuilder = new Function2<MarketDataId<?>, MarketDataTime, MarketDataRequirement>() { @Override public MarketDataRequirement apply(MarketDataId<?> marketDataId, MarketDataTime time) { return SingleValueRequirement.of(marketDataId, time); } }; Set<MarketDataRequirement> requirements = getRequirements(currencyPair.getBase(), currencyPair.getCounter(), requirement.getMarketDataTime(), requirementBuilder); s_logger.debug("Returning requirements {} for single value requirement {}", requirements, requirement); return requirements; } @Override public Set<MarketDataRequirement> getTimeSeriesRequirements( TimeSeriesRequirement requirement, Map<MarketDataId<?>, DateTimeSeries<LocalDate, ?>> suppliedData) { FxRateId rateId = (FxRateId) requirement.getMarketDataId(); CurrencyPair currencyPair = rateId.getCurrencyPair(); // If the supplied data already contains the rate then it won't be in the requirements at all. // If the supplied data contains the inverse rate we can use that instead of looking up the market rate FxRateId inverseRateId = FxRateId.of(currencyPair.inverse()); MarketDataTime marketDataTime = requirement.getMarketDataTime(); LocalDateRange dateRange = marketDataTime.getDateRange(); DateTimeSeries<LocalDate, ?> timeSeries = suppliedData.get(inverseRateId); if (timeSeries != null && !dateRange.getStartDateInclusive().isBefore(timeSeries.getEarliestTime()) && !dateRange.getEndDateInclusive().isAfter(timeSeries.getLatestTime())) { // don't need to return any requirements because the inverse rate is in the supplied data and we can use // that to calculate the rate we need return ImmutableSet.of(); } // TODO Java 8 - replace with a lambda Function2<MarketDataId<?>, MarketDataTime, MarketDataRequirement> requirementBuilder = new Function2<MarketDataId<?>, MarketDataTime, MarketDataRequirement>() { @Override public MarketDataRequirement apply(MarketDataId<?> marketDataId, MarketDataTime time) { return TimeSeriesRequirement.of(marketDataId, time.getDateRange()); } }; Set<MarketDataRequirement> requirements = getRequirements(currencyPair.getBase(), currencyPair.getCounter(), requirement.getMarketDataTime(), requirementBuilder); s_logger.debug("Returning requirements {} for time series requirement {}", requirements, requirement); return requirements; } @Override public Map<SingleValueRequirement, Result<?>> buildSingleValues(MarketDataBundle marketDataBundle, ZonedDateTime valuationTime, Set<SingleValueRequirement> requirements, MarketDataSource marketDataSource, CyclePerturbations cyclePerturbations) { ImmutableMap.Builder<SingleValueRequirement, Result<?>> resultsBuilder = ImmutableMap.builder(); for (SingleValueRequirement requirement : requirements) { FxRateId rateId = (FxRateId) requirement.getMarketDataId(); CurrencyPair currencyPair = rateId.getCurrencyPair(); // if the supplied data contains the inverse rate we can use that FxRateId inverseRateId = FxRateId.of(currencyPair.inverse()); Result<Double> inverseResult = marketDataBundle.get(inverseRateId, Double.class); if (inverseResult.isSuccess()) { Double inverseRate = inverseResult.getValue(); resultsBuilder.put(requirement, Result.success(1 / inverseRate)); } else { Result<Double> rate = getRate(marketDataBundle, currencyPair.getBase(), currencyPair.getCounter(), cyclePerturbations); resultsBuilder.put(requirement, rate); } } Map<SingleValueRequirement, Result<?>> results = resultsBuilder.build(); s_logger.debug("Returning results {} from buildSingleValues", results); return results; } @Override public Map<TimeSeriesRequirement, Result<? extends DateTimeSeries<LocalDate, ?>>> buildTimeSeries( MarketDataBundle marketDataBundle, Set<TimeSeriesRequirement> requirements, MarketDataSource marketDataSource, CyclePerturbations cyclePerturbations) { ImmutableMap.Builder<TimeSeriesRequirement, Result<? extends DateTimeSeries<LocalDate, ?>>> resultsBuilder = ImmutableMap.builder(); for (TimeSeriesRequirement requirement : requirements) { FxRateId rateId = (FxRateId) requirement.getMarketDataId(); CurrencyPair currencyPair = rateId.getCurrencyPair(); // if the supplied data contains the inverse rate we can use that FxRateId inverseRateId = FxRateId.of(currencyPair.inverse()); LocalDateRange dateRange = requirement.getMarketDataTime().getDateRange(); Result<DateTimeSeries<LocalDate, Double>> inverseResult = marketDataBundle.get(inverseRateId, Double.class, dateRange); if (inverseResult.isSuccess()) { DateTimeSeries<LocalDate, Double> inverseSeries = inverseResult.getValue(); DateTimeSeries<LocalDate, Double> series = reciprocal(inverseSeries); Result<DateTimeSeries<LocalDate, ?>> result = Result.<DateTimeSeries<LocalDate, ?>>success(series); resultsBuilder.put(requirement, result); } else { Result<? extends DateTimeSeries<LocalDate, ?>> seriesResult = getRate(marketDataBundle, currencyPair.getBase(), currencyPair.getCounter(), dateRange); resultsBuilder.put(requirement, seriesResult); } } Map<TimeSeriesRequirement, Result<? extends DateTimeSeries<LocalDate, ?>>> results = resultsBuilder.build(); s_logger.debug("Returning results {} from buildTimeSeries", results); return results; } private DateTimeSeries<LocalDate, Double> reciprocal(DateTimeSeries<LocalDate, Double> series) { if (series instanceof LocalDateDoubleTimeSeries) { return ((LocalDateDoubleTimeSeries) series).reciprocal(); } else { // this shouldn't ever happen LocalDateDoubleTimeSeriesBuilder builder = ImmutableLocalDateDoubleTimeSeries.builder(); DateEntryIterator<LocalDate, Double> itr = series.iterator(); while (itr.hasNext()) { builder.put(itr.nextTimeFast(), 1 / itr.currentValue()); } return builder.build(); } } @Override public Class<? extends MarketDataId> getKeyType() { return FxRateId.class; } // TODO Java 8 - use JDK optional and lambdas for applying the perturbation /** * Returns the FX rate for a pair of currencies, using the currency matrix to determine how to find the data. * An optional perturbation is applied to the rate before returning it. * <p> * If the rate is derived from two other rates (a cross rate) and a perturbation is provided, the result will be * a failure. Applying perturbations to cross rates isn't supported as it introduces inconsistencies in the * set of market data. * * @param marketDataBundle market data for the calculation cycle * @param base base currency of the pair * @param counter counter currency of the pair * @param cyclePerturbations perturbations to apply to the market data in the calculation cycle * @return a result containing the rate */ private Result<Double> getRate( final MarketDataBundle marketDataBundle, final Currency base, final Currency counter, final CyclePerturbations cyclePerturbations) { CurrencyMatrixValueVisitor<Result<Double>> visitor = new CurrencyMatrixValueVisitor<Result<Double>>() { @Override public Result<Double> visitFixed(CurrencyMatrixValue.CurrencyMatrixFixed fixedValue) { return perturbedRate(fixedValue.getFixedValue(), base, counter, cyclePerturbations); } @SuppressWarnings("unchecked") @Override public Result<Double> visitValueRequirement(CurrencyMatrixValue.CurrencyMatrixValueRequirement req) { ValueRequirement valueRequirement = req.getValueRequirement(); ExternalIdBundle id = valueRequirement.getTargetReference().getRequirement().getIdentifiers(); String dataField = valueRequirement.getValueName(); RawId<Double> marketDataId = RawId.of(id, FieldName.of(dataField)); Result<Double> result = marketDataBundle.get(marketDataId, Double.class); if (result.isSuccess()) { Double spotRate = result.getValue(); double rate = req.isReciprocal() ? 1 / spotRate : spotRate; return perturbedRate(rate, base, counter, cyclePerturbations); } else { return Result.failure(result); } } @Override public Result<Double> visitCross(CurrencyMatrixValue.CurrencyMatrixCross cross) { FxRateId rateId = FxRateId.of(base, counter); SingleValueRequirement requirement = SingleValueRequirement.of(rateId); Collection<FilteredPerturbation> perturbations = cyclePerturbations.getPerturbations(requirement); // perturbations aren't allowed for cross rates because they introduce inconsistencies if (!perturbations.isEmpty()) { return Result.failure( FailureStatus.INVALID_INPUT, "Perturbation defined for cross rate {}/{}. Perturbations are not supported for cross rates", base.getCode(), counter.getCode()); } Result<Double> baseCrossRate = getRate(marketDataBundle, base, cross.getCrossCurrency(), cyclePerturbations); Result<Double> crossCounterRate = getRate(marketDataBundle, cross.getCrossCurrency(), counter, cyclePerturbations); // TODO Java 8 - replace with lambda return baseCrossRate.combineWith(crossCounterRate, new Function2<Double, Double, Result<Double>>() { @Override public Result<Double> apply(Double rate1, Double rate2) { return Result.success(rate1 * rate2); } }); } }; CurrencyMatrixValue value = _currencyMatrixLink.resolve().getConversion(base, counter); if (value == null) { return Result.failure(FailureStatus.MISSING_DATA, "No conversion available for {}", CurrencyPair.of(base, counter)); } return value.accept(visitor); } /** * Returns the rate, possibly with a perturbation applied. * * @param rate an FX rate * @param base the base currency of the currency pair * @param counter the counter currency of the currency pair * @param cyclePerturbations the perturbations for the current calculation cycle * @return the rate, possibly with a perturbation applied */ private static Result<Double> perturbedRate( double rate, Currency base, Currency counter, CyclePerturbations cyclePerturbations) { FxRateId rateId = FxRateId.of(base, counter); SingleValueRequirement requirement = SingleValueRequirement.of(rateId); Collection<FilteredPerturbation> perturbations = cyclePerturbations.getPerturbations(requirement); if (perturbations.isEmpty()) { return Result.success(rate); } else { // there is always zero or one perturbations for an FX rate FilteredPerturbation perturbation = perturbations.iterator().next(); return Result.success((Double) perturbation.apply(rate)); } } private Result<DateTimeSeries<LocalDate, Double>> getRate(final MarketDataBundle marketDataBundle, final Currency base, final Currency counter, final LocalDateRange dateRange) { CurrencyMatrixValueVisitor<Result<DateTimeSeries<LocalDate, Double>>> visitor = new CurrencyMatrixValueVisitor<Result<DateTimeSeries<LocalDate, Double>>>() { @Override public Result<DateTimeSeries<LocalDate, Double>> visitFixed(CurrencyMatrixValue.CurrencyMatrixFixed fixedValue) { LocalDateDoubleTimeSeriesBuilder builder = ImmutableLocalDateDoubleTimeSeries.builder(); for (LocalDate localDate : dateRange) { builder.put(localDate, fixedValue.getFixedValue()); } return Result.<DateTimeSeries<LocalDate, Double>>success(builder.build()); } @SuppressWarnings("unchecked") @Override public Result<DateTimeSeries<LocalDate, Double>> visitValueRequirement( CurrencyMatrixValue.CurrencyMatrixValueRequirement req) { ValueRequirement valueRequirement = req.getValueRequirement(); ExternalIdBundle id = valueRequirement.getTargetReference().getRequirement().getIdentifiers(); String dataField = valueRequirement.getValueName(); RawId<Double> marketDataId = RawId.of(id, FieldName.of(dataField)); Result<DateTimeSeries<LocalDate, Double>> result = marketDataBundle.get(marketDataId, Double.class, dateRange); if (result.isSuccess()) { DateTimeSeries<LocalDate, Double> series = result.getValue(); return Result.success(req.isReciprocal() ? reciprocal(series) : series); } else { return Result.failure(result); } } @Override public Result<DateTimeSeries<LocalDate, Double>> visitCross(CurrencyMatrixValue.CurrencyMatrixCross cross) { Result<DateTimeSeries<LocalDate, Double>> baseCrossSeries = getRate(marketDataBundle, base, cross.getCrossCurrency(), dateRange); Result<DateTimeSeries<LocalDate, Double>> crossCounterSeries = getRate(marketDataBundle, cross.getCrossCurrency(), counter, dateRange); // TODO Java 8 - use Result.combineWith and a lambda if (baseCrossSeries.isSuccess() && crossCounterSeries.isSuccess()) { DateTimeSeries<LocalDate, Double> series = multiply(baseCrossSeries.getValue(), crossCounterSeries.getValue()); return Result.success(series); } else { return Result.failure(baseCrossSeries, crossCounterSeries); } } }; CurrencyMatrixValue value = _currencyMatrixLink.resolve().getConversion(base, counter); if (value == null) { return Result.failure(FailureStatus.MISSING_DATA, "No conversion available for {}", CurrencyPair.of(base, counter)); } return value.accept(visitor); } private DateTimeSeries<LocalDate, Double> multiply(DateTimeSeries<LocalDate, Double> series1, DateTimeSeries<LocalDate, Double> series2) { if ((series1 instanceof LocalDateDoubleTimeSeries) && (series2 instanceof LocalDateDoubleTimeSeries)) { return ((LocalDateDoubleTimeSeries) series1).multiply(((LocalDateDoubleTimeSeries) series2)); } else { // this shouldn't happen, the series should be LocalDateDoubleTimeSeries LocalDateDoubleTimeSeriesBuilder builder = ImmutableLocalDateDoubleTimeSeries.builder(); LocalDate start = series1.getEarliestTime().isBefore(series2.getEarliestTime()) ? series1.getEarliestTime() : series2.getEarliestTime(); LocalDate end = series1.getLatestTime().isAfter(series2.getLatestTime()) ? series1.getLatestTime() : series2.getLatestTime(); LocalDateRange dateRange = LocalDateRange.of(start, end, true); for (LocalDate date : dateRange) { Double value1 = series1.getValue(date); Double value2 = series2.getValue(date); if (value1 != null && value2 != null) { builder.put(date, value1 * value2); } } return builder.build(); } } private Set<MarketDataRequirement> getRequirements( final Currency base, final Currency counter, final MarketDataTime marketDataTime, final Function2<MarketDataId<?>, MarketDataTime, MarketDataRequirement> requirementBuilder) { CurrencyMatrixValueVisitor<Set<MarketDataRequirement>> visitor = new CurrencyMatrixValueVisitor<Set<MarketDataRequirement>>() { @Override public Set<MarketDataRequirement> visitFixed(CurrencyMatrixValue.CurrencyMatrixFixed fixedValue) { // if the rate is fixed there's no market data required return ImmutableSet.of(); } @SuppressWarnings("unchecked") @Override public Set<MarketDataRequirement> visitValueRequirement(CurrencyMatrixValue.CurrencyMatrixValueRequirement req) { ValueRequirement valueRequirement = req.getValueRequirement(); ExternalIdBundle id = valueRequirement.getTargetReference().getRequirement().getIdentifiers(); String dataField = valueRequirement.getValueName(); MarketDataId<?> marketDataId = RawId.of(id, FieldName.of(dataField)); return ImmutableSet.of(requirementBuilder.apply(marketDataId, marketDataTime)); } @Override public Set<MarketDataRequirement> visitCross(CurrencyMatrixValue.CurrencyMatrixCross cross) { return ImmutableSet.<MarketDataRequirement>builder() .addAll(getRequirements(base, cross.getCrossCurrency(), marketDataTime, requirementBuilder)) .addAll(getRequirements(cross.getCrossCurrency(), counter, marketDataTime, requirementBuilder)) .build(); } }; CurrencyMatrixValue value = _currencyMatrixLink.resolve().getConversion(base, counter); if (value != null) { return value.accept(visitor); } else { return ImmutableSet.of(); } } }