/**
* Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.strata.calc.marketdata;
import static com.opengamma.strata.collect.Guavate.not;
import static com.opengamma.strata.collect.Guavate.toImmutableMap;
import static com.opengamma.strata.collect.Guavate.toImmutableSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import com.google.common.collect.ImmutableMap;
import com.opengamma.strata.basics.ReferenceData;
import com.opengamma.strata.collect.MapStream;
import com.opengamma.strata.collect.result.Result;
import com.opengamma.strata.collect.tuple.Pair;
import com.opengamma.strata.data.MarketData;
import com.opengamma.strata.data.MarketDataId;
import com.opengamma.strata.data.ObservableId;
import com.opengamma.strata.data.scenario.MarketDataBox;
import com.opengamma.strata.data.scenario.ScenarioMarketData;
/**
* The default market data factory.
* <p>
* This uses two providers, one for observable data and one for time-series.
*/
final class DefaultMarketDataFactory implements MarketDataFactory {
/** Builds observable market data. */
private final ObservableDataProvider observableDataProvider;
/** Provides time-series of observable market data values. */
private final TimeSeriesProvider timeSeriesProvider;
/** Market data functions, keyed by the type of the market data ID they can handle. */
private final Map<Class<? extends MarketDataId<?>>, MarketDataFunction<?, ?>> functions;
//-------------------------------------------------------------------------
/**
* Creates an instance of the factory based on providers of market data and time-series.
* <p>
* The market data functions are used to build the market data.
*
* @param observableDataProvider the provider observable market data
* @param timeSeriesProvider the provider time-series
* @param functions the functions that create the market data
*/
@SuppressWarnings("unchecked")
DefaultMarketDataFactory(
ObservableDataProvider observableDataProvider,
TimeSeriesProvider timeSeriesProvider,
List<MarketDataFunction<?, ?>> functions) {
this.observableDataProvider = observableDataProvider;
this.timeSeriesProvider = timeSeriesProvider;
// Use a HashMap instead of an ImmutableMap.Builder so values can be overwritten.
// If the functions argument includes a missing mapping builder it can overwrite the one inserted below
Map<Class<? extends MarketDataId<?>>, MarketDataFunction<?, ?>> builderMap = new HashMap<>();
functions.stream().forEach(builder -> builderMap.put(builder.getMarketDataIdType(), builder));
this.functions = ImmutableMap.copyOf(builderMap);
}
//-------------------------------------------------------------------------
@Override
public BuiltMarketData create(
MarketDataRequirements requirements,
MarketDataConfig marketDataConfig,
MarketData suppliedData,
ReferenceData refData) {
ScenarioMarketData md = ScenarioMarketData.of(1, suppliedData);
BuiltScenarioMarketData smd = createMultiScenario(requirements, marketDataConfig, md, refData, ScenarioDefinition.empty());
return new BuiltMarketData(smd);
}
@Override
public BuiltScenarioMarketData createMultiScenario(
MarketDataRequirements requirements,
MarketDataConfig marketDataConfig,
MarketData suppliedData,
ReferenceData refData,
ScenarioDefinition scenarioDefinition) {
ScenarioMarketData md = ScenarioMarketData.of(1, suppliedData);
return createMultiScenario(requirements, marketDataConfig, md, refData, scenarioDefinition);
}
@Override
public BuiltScenarioMarketData createMultiScenario(
MarketDataRequirements requirements,
MarketDataConfig marketDataConfig,
ScenarioMarketData suppliedData,
ReferenceData refData,
ScenarioDefinition scenarioDefinition) {
BuiltScenarioMarketDataBuilder dataBuilder = BuiltScenarioMarketData.builder(suppliedData.getValuationDate());
BuiltScenarioMarketData builtData = dataBuilder.build();
// Build a tree of the market data dependencies. The root of the tree represents the calculations.
// The children of the root represent the market data directly used in the calculations. The children
// of those nodes represent the market data required to build that data, and so on
MarketDataNode root = MarketDataNode.buildDependencyTree(requirements, suppliedData, marketDataConfig, functions);
// The leaf nodes of the dependency tree represent market data with no missing requirements for market data.
// This includes:
// * Market data that is already available
// * Observable data whose value can be obtained from a market data provider
// * Market data that can be built from data that is already available
//
// Therefore the market data represented by the leaf nodes can be built immediately.
//
// Market data building proceeds in multiple steps. The operations in each step are:
// 1) Build the market data represented by the leaf nodes of the dependency tree
// 2) Create a copy of the dependency tree without the leaf nodes
// 3) If the root of new dependency tree has children, go to step 1 with the new tree
//
// When the tree has no children it indicates all dependencies have been built and the market data
// needed for the calculations is available.
//
// The result of this method also contains details of the problems for market data can't be built or found.
while (!root.isLeaf()) {
// Effectively final reference to buildData which can be used in a lambda expression
BuiltScenarioMarketData marketData = builtData;
// The leaves of the dependency tree represent market data with no dependencies that can be built immediately
Pair<MarketDataNode, MarketDataRequirements> pair = root.withLeavesRemoved();
// The requirements contained in the leaf nodes
MarketDataRequirements leafRequirements = pair.getSecond();
// Time series of observable data ------------------------------------------------------------
// Build any time series that are required but not available
leafRequirements.getTimeSeries().stream()
.filter(id -> marketData.getTimeSeries(id).isEmpty())
.filter(id -> suppliedData.getTimeSeries(id).isEmpty())
.forEach(id -> dataBuilder.addTimeSeriesResult(id, timeSeriesProvider.provideTimeSeries(id)));
// Copy supplied time series to the scenario data
leafRequirements.getTimeSeries().stream()
.filter(id -> !suppliedData.getTimeSeries(id).isEmpty())
.forEach(id -> dataBuilder.addTimeSeries(id, suppliedData.getTimeSeries(id)));
// Single values of observable data -----------------------------------------------------------
// Filter out IDs for the data that is already available
Set<ObservableId> observableIds = leafRequirements.getObservables().stream()
.filter(not(marketData::containsValue))
.filter(not(suppliedData::containsValue))
.collect(toImmutableSet());
// Observable data is built in bulk so it can be efficiently requested from data provider in one operation
if (!observableIds.isEmpty()) {
Map<ObservableId, Result<Double>> observableResults = observableDataProvider.provideObservableData(observableIds);
MapStream.of(observableResults)
.forEach((id, res) -> addObservableResult(id, res, refData, scenarioDefinition, dataBuilder));
}
// Copy observable data from the supplied data to the builder, applying any matching perturbations
leafRequirements.getObservables().stream()
.filter(suppliedData::containsValue)
.forEach(id -> addValue(id, suppliedData.getValue(id), refData, scenarioDefinition, dataBuilder));
// Non-observable data -----------------------------------------------------------------------
// Filter out IDs for the data that is already available and build the rest
Set<MarketDataId<?>> nonObservableIds = leafRequirements.getNonObservables().stream()
.filter(not(marketData::containsValue))
.filter(not(suppliedData::containsValue))
.collect(toImmutableSet());
Map<MarketDataId<?>, Result<MarketDataBox<?>>> nonObservableResults =
buildNonObservableData(nonObservableIds, marketDataConfig, marketData, refData);
MapStream.of(nonObservableResults)
.forEach((id, result) -> addResult(id, result, refData, scenarioDefinition, dataBuilder));
// Copy supplied data to the scenario data after applying perturbations
leafRequirements.getNonObservables().stream()
.filter(suppliedData::containsValue)
.forEach(id -> addValue(id, suppliedData.getValue(id), refData, scenarioDefinition, dataBuilder));
// --------------------------------------------------------------------------------------------
// Put the data built so far into an object that will be used in the next phase of building data
builtData = dataBuilder.build();
// A copy of the dependency tree not including the leaf nodes
root = pair.getFirst();
}
return builtData;
}
//-------------------------------------------------------------------------
/**
* Builds items of non-observable market data using a market data function.
*
* @param id ID of the market data that should be built
* @param marketDataConfig configuration specifying how the market data should be built
* @param suppliedData existing set of market data that contains any data required to build the values
* @param refData the reference data, used to resolve trades
* @return a result containing the market data or details of why it wasn't built
*/
@SuppressWarnings({"unchecked", "rawtypes"})
private Result<MarketDataBox<?>> buildNonObservableData(
MarketDataId id,
MarketDataConfig marketDataConfig,
BuiltScenarioMarketData suppliedData,
ReferenceData refData) {
// The raw types in this method are an unfortunate necessity. The type parameters on MarketDataBuilder
// are mainly a useful guide for implementors as they constrain the method type signatures.
// In this class a mixture of functions with different types are stored in a map. This loses the type
// parameter information. When the functions are extracted from the map and used it's impossible to
// convince the compiler the operations are safe, although the logic guarantees it.
// This cast removes a spurious warning
Class<? extends MarketDataId<?>> idClass = (Class<? extends MarketDataId<?>>) id.getClass();
MarketDataFunction marketDataFunction = functions.get(idClass);
if (marketDataFunction == null) {
throw new IllegalStateException("No market data function available for market data ID of type " + idClass.getName());
}
return Result.of(() -> marketDataFunction.build(id, marketDataConfig, suppliedData, refData));
}
@SuppressWarnings("unchecked")
private Map<MarketDataId<?>, Result<MarketDataBox<?>>> buildNonObservableData(
Set<? extends MarketDataId<?>> ids,
MarketDataConfig marketDataConfig,
BuiltScenarioMarketData marketData,
ReferenceData refData) {
return ids.stream()
.collect(toImmutableMap(id -> id, id -> buildNonObservableData(id, marketDataConfig, marketData, refData)));
}
/**
* Adds an item of market data to a builder.
* <p>
* If the result is a failure it is added to the list of failures.
* <p>
* If the result is a success it is passed to {@link #addValue} where the scenario definition is
* applied and the data is added to the builder.
*
* @param id ID of the market data value
* @param valueResult a result containing the market data value or details of why it couldn't be built
* @param scenarioDefinition definition of a set of scenarios
* @param builder the value or failure details are added to this builder
*/
private void addResult(
MarketDataId<?> id,
Result<MarketDataBox<?>> valueResult,
ReferenceData refData,
ScenarioDefinition scenarioDefinition,
BuiltScenarioMarketDataBuilder builder) {
if (valueResult.isFailure()) {
builder.addResult(id, valueResult);
} else {
addValue(id, valueResult.getValue(), refData, scenarioDefinition, builder);
}
}
/**
* Adds an item of observable market data to a builder.
* <p>
* If the result is a failure it is added to the list of failures.
* <p>
* If the result is a success it is passed to {@link #addValue} where the scenario definition is
* applied and the data is added to the builder.
*
* @param id ID of the market data value
* @param valueResult a result containing the market data value or details of why it couldn't be built
* @param scenarioDefinition definition of a set of scenarios
* @param builder the value or failure details are added to this builder
*/
private void addObservableResult(
ObservableId id,
Result<Double> valueResult,
ReferenceData refData,
ScenarioDefinition scenarioDefinition,
BuiltScenarioMarketDataBuilder builder) {
if (valueResult.isFailure()) {
builder.addResult(id, Result.failure(valueResult));
} else {
addValue(id, MarketDataBox.ofSingleValue(valueResult.getValue()), refData, scenarioDefinition, builder);
}
}
/**
* Adds an item of market data to a builder.
* <p>
* The mappings from the scenario definition is applied to the value. If any of the mappings match the value
* is perturbed and the perturbed values are added to the market data.
*
* @param id ID of the market data value
* @param value the market data value
* @param scenarioDefinition definition of a set of scenarios
* @param builder the market data is added to this builder
*/
@SuppressWarnings("unchecked")
private void addValue(
MarketDataId<?> id,
MarketDataBox<?> value,
ReferenceData refData,
ScenarioDefinition scenarioDefinition,
BuiltScenarioMarketDataBuilder builder) {
Optional<PerturbationMapping<?>> optionalMapping = scenarioDefinition.getMappings().stream()
.filter(m -> m.matches(id, value, refData))
.findFirst();
if (optionalMapping.isPresent()) {
// This is definitely safe because the filter matched the value and the types of the filter and perturbation
// are compatible
PerturbationMapping<Object> mapping = (PerturbationMapping<Object>) optionalMapping.get();
MarketDataBox<Object> objectValue = ((MarketDataBox<Object>) value);
// Result.of() catches any exceptions thrown by the mapping and wraps them in a failure
Result<MarketDataBox<?>> result = Result.of(() -> mapping.applyPerturbation(objectValue, refData));
builder.addResult(id, result);
} else {
builder.addBox(id, value);
}
}
}