/** * Copyright (C) 2014 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.integration.marketdata.manipulator.dsl; import java.io.IOException; import java.io.InputStream; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.opengamma.OpenGammaRuntimeException; import groovy.lang.Closure; import groovy.lang.GroovyObjectSupport; import groovy.lang.Script; /** * TODO enforce ordering ordering: view, shocks, scenarios */ @SuppressWarnings("unused") // it is used reflectively by Groovy public abstract class StandAloneScenarioScript extends Script { private final ViewDelegate _viewDelegate = new ViewDelegate(); private final List<Map<String, Object>> _scenarioParamList = Lists.newArrayList(); private final Simulation _simulation = new Simulation("todo - what name for the simulation? does it matter?"); /** Scenario parameters keyed by scenario name. The parameters are parameter values keyed by parameter name. */ private final Map<String, Map<String, Object>> _scenarioParameters = Maps.newHashMap(); public StandAloneScenarioScript() { InputStream scriptStream = SimulationScript.class.getResourceAsStream("InitializeScript.groovy"); try { evaluate(IOUtils.toString(scriptStream)); } catch (IOException e) { throw new OpenGammaRuntimeException("Failed to initialize DSL script", e); } } public void view(Closure<?> body) { body.setDelegate(_viewDelegate); body.call(); } public void shockGrid(Closure<?> body) { ShocksDelegate delegate = new ShocksDelegate(); body.setDelegate(delegate); body.setResolveStrategy(Closure.DELEGATE_FIRST); body.call(); _scenarioParamList.addAll(delegate.cartesianProduct()); } public void shockList(Closure<?> body) { ShocksDelegate delegate = new ShocksDelegate(); body.setDelegate(delegate); body.setResolveStrategy(Closure.DELEGATE_FIRST); body.call(); _scenarioParamList.addAll(delegate.list()); } public void scenarios(Closure<?> body) { for (Map<String, Object> params : _scenarioParamList) { List<String> varNamesAndValues = Lists.newArrayListWithCapacity(params.size()); for (Map.Entry<String, Object> entry : params.entrySet()) { String varName = entry.getKey(); Object varValue = entry.getValue(); getBinding().setVariable(varName, varValue); String varStr = (varValue instanceof String) ? "'" + varValue + "'" : varValue.toString(); varNamesAndValues.add(varName + "=" + varStr); } String scenarioName = StringUtils.join(varNamesAndValues, " "); Scenario scenario = _simulation.scenario(scenarioName); _scenarioParameters.put(scenarioName, params); body.setDelegate(new ScenarioDelegate(scenario)); body.call(); } } /** visible for testing */ /* package */ ViewDelegate getViewDelegate() { return _viewDelegate; } /* package */ List<Map<String, Object>> getScenarioParameterList() { return Collections.unmodifiableList(_scenarioParamList); } /* package */ Map<String, Object> getScenarioParameters(String scenarioName) { Map<String, Object> parameters = _scenarioParameters.get(scenarioName); if (parameters == null) { throw new IllegalArgumentException("No scenario found named " + scenarioName); } return parameters; } /* package */ Simulation getSimulation() { return _simulation; } } // TODO abstract delegate that catches unexpected method and property calls and logs them // TODO extend the abstract delegate // TODO should this contain the logic to validate and convert the fields? or should it just be a dumb container? @SuppressWarnings("unused") /* package */ class ViewDelegate { //private final DateTimeFormatter _dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); private String _name; private String _server; private final MarketDataDelegate _marketDataDelegate = new MarketDataDelegate(); /* package */ void name(String name) { _name = name; } /* package */ void server(String server) { _server = server; } /* package */ void marketData(Closure<?> body) { body.setDelegate(_marketDataDelegate); body.call(); } /* package */ String getName() { return _name; } /* package */ String getServer() { return _server; } /** Visible for testing */ /* package */ MarketDataDelegate getMarketDataDelegate() { return _marketDataDelegate; } } @SuppressWarnings("unused") /* package */ class MarketDataDelegate { private final List<MarketDataSpec> _specifications = Lists.newArrayList(); /* package */ void live(String dataSource) { _specifications.add(new MarketDataSpec(MarketDataType.LIVE, dataSource)); } /* package */ void snapshot(String snapshotName) { _specifications.add(new MarketDataSpec(MarketDataType.SNAPSHOT, snapshotName)); } /* package */ void fixedHistorical(String date) { _specifications.add(new MarketDataSpec(MarketDataType.FIXED_HISTORICAL, date)); } /* latestHistorical doesn't have arguments so Groovy views it as a property get */ /* package */ Object getLatestHistorical() { _specifications.add(new MarketDataSpec(MarketDataType.LATEST_HISTORICAL, null)); return null; } /* package */ List<MarketDataSpec> getSpecifications() { return _specifications; } /* package */ enum MarketDataType { LIVE, SNAPSHOT, FIXED_HISTORICAL, LATEST_HISTORICAL } /* package */ static class MarketDataSpec { private final MarketDataType _type; private final String _spec; /* package */ MarketDataSpec(MarketDataType type, String spec) { _type = type; _spec = spec; } /* package */ MarketDataType getType() { return _type; } /* package */ String getSpec() { return _spec; } @Override public int hashCode() { return Objects.hash(_type, _spec); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } final MarketDataSpec other = (MarketDataSpec) obj; return Objects.equals(this._type, other._type) && Objects.equals(this._spec, other._spec); } } } /* package */ class ShocksDelegate extends GroovyObjectSupport { private static final Logger s_logger = LoggerFactory.getLogger(ShocksDelegate.class); /** * Shock variables. Keys are the variable names, values must be lists of values. It's a linked map so the declaration * order in the script is reflected in the way the cartesian product is generated. */ private final Map<String, List<?>> _vars = Maps.newLinkedHashMap(); @Override public void setProperty(String property, Object newValue) { if (!(newValue instanceof List)) { s_logger.warn("Shocks must be a list, type=" + newValue.getClass().getName() + ", value=" + newValue); return; } if (((List) newValue).size() == 0) { s_logger.warn("Shocks must have at least one value"); return; } _vars.put(property, (List<?>) newValue); } /* package */ List<Map<String, Object>> cartesianProduct() { if (_vars.isEmpty()) { return Collections.emptyList(); } if (_vars.size() != 2) { throw new IllegalArgumentException("There must be 2 sets of shocks for shockGrid. For 1 set, use shockList."); } Iterator<Map.Entry<String, List<?>>> itr = _vars.entrySet().iterator(); Map.Entry<String, List<?>> outer = itr.next(); String outerVarName = outer.getKey(); List<?> outerVarValues = outer.getValue(); Map.Entry<String, List<?>> inner = itr.next(); String innerVarName = inner.getKey(); List<?> innerVarValues = inner.getValue(); // list of parameters, one map for each scenario List<Map<String, Object>> params = Lists.newArrayListWithCapacity(outerVarValues.size() * innerVarValues.size()); for (Object outerVarValue : outerVarValues) { for (Object innerVarValue : innerVarValues) { // use a linked map so the var names appear in insertion order - this makes for predictable scenario names Map<String, Object> paramMap = Maps.newLinkedHashMap(); paramMap.put(outerVarName, outerVarValue); paramMap.put(innerVarName, innerVarValue); params.add(paramMap); } } return params; } /* package */ List<Map<String, Object>> list() { // list of parameters, one map for each scenario List<Map<String, Object>> params = Lists.newArrayList(); // calculate number of scenarios and make sure all shock lists have the right number of values int nScenarios = 0; for (List<?> varValues : _vars.values()) { if (nScenarios == 0) { nScenarios = varValues.size(); } else if (nScenarios != varValues.size()) { throw new IllegalArgumentException("All scenario parameters must be lists of the same length"); } } // create a map for each scenario and populate it with a value from each shock list for (int i = 0; i < nScenarios; i++) { // use a linked map so the var names appear in insertion order - this makes for predictable scenario names Map<String, Object> map = Maps.newLinkedHashMap(); for (Map.Entry<String, List<?>> entry : _vars.entrySet()) { String varName = entry.getKey(); List<?> varValues = entry.getValue(); map.put(varName, varValues.get(i)); } params.add(map); } return params; } } /** * Delegate for the closure passed to the {@code scenarios} block in the script. */ /* package */ class ScenarioDelegate { private final Scenario _scenario; /* package */ ScenarioDelegate(Scenario scenario) { _scenario = scenario; } public void valuationTime(String valuationTime) { _scenario.valuationTime(valuationTime); } public void calculationConfigurations(String... configNames) { _scenario.calculationConfigurations(configNames); } /** * Defines a method in the DSL that takes a closure which defines how to select and transform a curve. * @param body The block that defines the selection and transformation */ public void curve(Closure<?> body) { DslYieldCurveSelectorBuilder selector = new DslYieldCurveSelectorBuilder(_scenario); body.setDelegate(selector); body.setResolveStrategy(Closure.DELEGATE_FIRST); body.call(); } /** * Defines a method in the DSL that takes a closure which defines how to select and transform a curve. * @param body The block that defines the selection and transformation */ public void curveData(Closure<?> body) { DslYieldCurveDataSelectorBuilder selector = new DslYieldCurveDataSelectorBuilder(_scenario); body.setDelegate(selector); body.setResolveStrategy(Closure.DELEGATE_FIRST); body.call(); } /** * Defines a method in the DSL that takes a closure which defines how to select and transform a market data point. * @param body The block that defines the selection and transformation */ public void marketData(Closure<?> body) { DslPointSelectorBuilder selector = new DslPointSelectorBuilder(_scenario); body.setDelegate(selector); body.setResolveStrategy(Closure.DELEGATE_FIRST); body.call(); } /** * Defines a method in the DSL that takes a closure which defines how to select and transform a volatility surface. * @param body The block that defines the selection and transformation */ public void surface(Closure<?> body) { DslVolatilitySurfaceSelectorBuilder selector = new DslVolatilitySurfaceSelectorBuilder(_scenario); body.setDelegate(selector); body.setResolveStrategy(Closure.DELEGATE_FIRST); body.call(); } /** * Defines a method in the DSL that takes a closure which defines how to select and transform spot rates. * @param body The block that defines the selection and transformation */ public void spotRate(Closure<?> body) { DslSpotRateSelectorBuilder builder = new DslSpotRateSelectorBuilder(_scenario); body.setDelegate(builder); body.setResolveStrategy(Closure.DELEGATE_FIRST); body.call(); } }