/** * Copyright (C) 2014 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.sesame.marketdata.scenarios; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import com.opengamma.sesame.marketdata.MarketDataEnvironment; import com.opengamma.sesame.marketdata.MarketDataEnvironmentBuilder; import com.opengamma.sesame.marketdata.MarketDataId; import com.opengamma.sesame.marketdata.MarketDataRequirement; import com.opengamma.sesame.marketdata.SingleValueRequirement; import com.opengamma.util.ArgumentChecker; import com.opengamma.util.tuple.Pair; import com.opengamma.util.tuple.Pairs; /** * Contains the perturbations that should be applied to market data during a single calculation cycle in a scenario. * <p> * A scenario definition defines a set of perturbations and filters that are used to run multiple calculation cycles. * This class represents the subset of those perturbations that should be applied in a particular cycle. * <p> * Perturbations are referred to as "input" and "output" perturbations. Input perturbations apply to the data used * when building market data, e.g. the quotes for the nodal points when building a curve. Output perturbations apply * to the market data in the {@link MarketDataEnvironment}, e.g. a shift applied to a calibrated curve. */ public class CyclePerturbations { private final List<SinglePerturbationMapping> _inputMappings; private final List<SinglePerturbationMapping> _outputMappings; /** * Creates a set of perturbations that apply to the market data specified in a set of requirements. * * @param requirements requirements for a set of market data * @param scenario defines how to perturb the market data during the calculation cycle */ public CyclePerturbations(Set<? extends MarketDataRequirement> requirements, SingleScenarioDefinition scenario) { ArgumentChecker.notNull(requirements, "requirements"); ArgumentChecker.notNull(scenario, "scenario"); Pair<List<SinglePerturbationMapping>, List<SinglePerturbationMapping>> pair = partitionMappings(scenario); _inputMappings = pair.getFirst(); _outputMappings = pair.getSecond(); } // TODO Java 8 - replace with a stream and groupingBy or partitioningBy /** * Partitions perturbations from a scenario into a list that applies to input data used to build market data and * a set that applies to the built market data. * * @param scenario scenario containing perturbations that might apply to input or output market data * @return a pair of lists. The first list is the perturbations that apply to input data used to build market * data. The second list applies to the built market data */ private static Pair<List<SinglePerturbationMapping>, List<SinglePerturbationMapping>> partitionMappings( SingleScenarioDefinition scenario) { List<SinglePerturbationMapping> inputs = new ArrayList<>(); List<SinglePerturbationMapping> outputs = new ArrayList<>(); for (SinglePerturbationMapping mapping : scenario.getMappings()) { switch (mapping.getPerturbation().getTargetType()) { case INPUT: inputs.add(mapping); break; case OUTPUT: outputs.add(mapping); break; } } return Pairs.of(inputs, outputs); } // TODO the only difference between this and the method below is the arguments to filter.apply() // TODO Java 8 - combine this with the method below and use a lambda to apply the filter /** * Returns all perturbations that should be applied to a piece of market data. * <p> * Although each piece of data should only be perturbed once, some market data objects are actually composite * objects containing multiple pieces of independent data that can be independently perturbed. For example * multicurve bundles are represented by a single requirement but contain multiple curves that can be * perturbed. * * @param requirement the requirement for a piece of market data * @return the perturbations that should be applied to the market data and the details of the filter matches */ public Collection<FilteredPerturbation> getPerturbations(MarketDataRequirement requirement) { // it is possible for multiple perturbations to apply to values where different parts of the value can // be perturbed independently. e.g. the individual curves inside a curve bundle. // the keys in this map are the match details returned by the filter. the details identify the part of // the value that was matched Map<MatchDetails, FilteredPerturbation> matches = new HashMap<>(); for (SinglePerturbationMapping mapping : _inputMappings) { // this would be more efficient if the mappings were supplied in a multimap keyed by market data type // not worth the complication yet MarketDataFilter filter = mapping.getFilter(); Class<?> filterDataType = filter.getMarketDataType(); Class<?> filterMarketDataIdType = filter.getMarketDataIdType(); Class<?> requirementDataType = requirement.getMarketDataId().getMarketDataType(); MarketDataId marketDataId = requirement.getMarketDataId(); if (filterDataType == requirementDataType && filterMarketDataIdType == marketDataId.getClass()) { // Assumes input metadata can always be derived from the market data ID (i.e. from config). // But isn't that a given anyway because they're inputs so the data is getting built from config? // Would need to duplicate the logic that creates the metadata in every filter acting on the same market // data type. Lots of helper methods or abstract supertypes? Set<? extends MatchDetails> matchDetails = filter.apply(marketDataId); for (MatchDetails match : matchDetails) { if (!matches.containsKey(match)) { matches.put(match, new FilteredPerturbation(mapping.getPerturbation(), match)); } } } } return matches.values(); } /** * Returns a collection of output perturbations that apply to a piece of market data. * * @param requirement the requirement for the market data * @param marketDataValue the market data value * @param mappings a list of mappings to search for perturbations that apply to the data * @return a collection of output perturbations that apply to a piece of market data */ private Collection<FilteredPerturbation> perturbationsForMarketData( MarketDataRequirement requirement, Object marketDataValue, List<SinglePerturbationMapping> mappings) { // it is possible for multiple perturbations to apply to values where different parts of the value can // be perturbed independently. e.g. the individual curves inside a curve bundle. // the keys in this map are the match details returned by the filter. the details identify the part of // the value that was matched Map<MatchDetails, FilteredPerturbation> matches = new HashMap<>(); for (SinglePerturbationMapping mapping : mappings) { MarketDataFilter filter = mapping.getFilter(); Class<?> filterDataType = filter.getMarketDataType(); Class<?> filterMarketDataIdType = filter.getMarketDataIdType(); Class<?> requirementDataType = requirement.getMarketDataId().getMarketDataType(); MarketDataId marketDataId = requirement.getMarketDataId(); if (filterDataType == requirementDataType && filterMarketDataIdType == marketDataId.getClass()) { Set<? extends MatchDetails> matchDetails = filter.apply(marketDataId, marketDataValue); for (MatchDetails match : matchDetails) { if (!matches.containsKey(match)) { matches.put(match, new FilteredPerturbation(mapping.getPerturbation(), match)); } } } } return matches.values(); } /** * Applies the output perturbations to the market data in the market data environment. * <p> * A new market data environment is created containing new market data values with the perturbations applied. * The input environment is unchanged. * * @param marketData a set of market data * @return a new set of market data, derived from the input data with perturbations applied */ @SuppressWarnings("unchecked") public MarketDataEnvironment apply(MarketDataEnvironment marketData) { MarketDataEnvironmentBuilder builder = marketData.toBuilder(); for (Map.Entry<SingleValueRequirement, Object> entry : marketData.getData().entrySet()) { SingleValueRequirement requirement = entry.getKey(); Object data = entry.getValue(); // TODO check the logic here guarantees this is safe Collection<FilteredPerturbation> filteredPerturbations = perturbationsForMarketData(requirement, data, _outputMappings); Object perturbedData = data; for (FilteredPerturbation filteredPerturbation : filteredPerturbations) { perturbedData = filteredPerturbation.apply(perturbedData); } builder.add(requirement, perturbedData); } // TODO apply perturbations to time series values return builder.build(); } }