/** * 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.toImmutableList; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.joining; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.stream.IntStream; import org.joda.beans.Bean; import org.joda.beans.BeanDefinition; import org.joda.beans.ImmutableBean; import org.joda.beans.ImmutableValidator; import org.joda.beans.JodaBeanUtils; import org.joda.beans.MetaProperty; import org.joda.beans.Property; import org.joda.beans.PropertyDefinition; import org.joda.beans.impl.direct.DirectFieldsBeanBuilder; import org.joda.beans.impl.direct.DirectMetaBean; import org.joda.beans.impl.direct.DirectMetaProperty; import org.joda.beans.impl.direct.DirectMetaPropertyMap; import com.google.common.collect.ImmutableList; import com.opengamma.strata.collect.ArgChecker; import com.opengamma.strata.collect.MapStream; /** * A scenario definition defines how to create multiple sets of market data for running calculations over * a set of scenarios. The scenario data is created by applying perturbations to a set of base market data. * A different set of perturbations is used for each scenario. * <p> * Each scenario definition contains market data filters and perturbations. Filters * are used to choose items of market data that are shocked in the scenario, and the perturbations * define those shocks. * <p> * Perturbations are applied in the order they are defined in scenario. An item of market data * can only be perturbed once, so if multiple mappings apply to it, only the first will be used. */ @BeanDefinition public final class ScenarioDefinition implements ImmutableBean { /** The market data filters and perturbations that define the scenarios. */ @PropertyDefinition(validate = "notNull", builderType = "List<? extends PerturbationMapping<?>>") private final ImmutableList<PerturbationMapping<?>> mappings; /** The names of the scenarios. */ @PropertyDefinition(validate = "notNull") private final ImmutableList<String> scenarioNames; /** An empty scenario definition. */ private static final ScenarioDefinition EMPTY = ScenarioDefinition.builder().build(); /** * Returns an empty scenario definition. * * @return an empty scenario definition */ public static ScenarioDefinition empty() { return EMPTY; } /** * Returns a scenario definition containing the perturbations in {@code mappings}. * <p> * Each mapping must contain the same number of perturbations. The definition will contain the * same number of scenarios as the number of perturbations in each mapping. * <p> * The first scenario contains the first perturbation from each mapping, the second scenario contains * the second perturbation from each mapping, and so on. * <p> * Given three mappings, A, B and C, each containing two perturbations, 1 and 2, there will be two * scenarios generated: * <pre> * | | A | B | C | * |------------|------|------|------| * | Scenario 1 | A[1] | B[1] | C[1] | * | Scenario 2 | A[2] | B[2] | C[2] | * </pre> * For example, consider the following perturbation mappings: * <ul> * <li>Filter: USD Curves, Shocks: [-10bp, 0, +10bp]</li> * <li>Filter: EUR/USD Rate, Shocks: [+5%, 0, -5%]</li> * </ul> * The scenario definition would contain the following three scenarios: * <pre> * | | USD Curves | EUR/USD Rate | * |------------|------------|--------------| * | Scenario 1 | -10bp | +5% | * | Scenario 2 | 0 | 0 | * | Scenario 3 | +10bp | -5% | * </pre> * * @param mapping the filters and perturbations that define the scenario. Each mapping must contain the same * number of perturbations * @return a scenario definition containing the perturbations in the mappings */ public static ScenarioDefinition ofMappings(List<? extends PerturbationMapping<?>> mapping) { ArgChecker.notEmpty(mapping, "mappings"); int numScenarios = countScenarios(mapping, false); for (int i = 1; i < mapping.size(); i++) { if (mapping.get(i).getScenarioCount() != numScenarios) { throw new IllegalArgumentException( "All mappings must have the same number of perturbations. First mapping" + " has " + numScenarios + " perturbations, mapping " + i + " has " + mapping.get(i).getScenarioCount()); } } return new ScenarioDefinition(mapping, generateNames(numScenarios)); } /** * Returns a scenario definition containing the perturbations in {@code mappings}. * <p> * Each mapping must contain the same number of perturbations. The definition will contain the * same number of scenarios as the number of perturbations in each mapping. * <p> * The first scenario contains the first perturbation from each mapping, the second scenario contains * the second perturbation from each mapping, and so on. * <p> * Given three mappings, A, B and C, each containing two perturbations, 1 and 2, there will be two * scenarios generated: * <pre> * | | A | B | C | * |------------|------|------|------| * | Scenario 1 | A[1] | B[1] | C[1] | * | Scenario 2 | A[2] | B[2] | C[2] | * </pre> * For example, consider the following perturbation mappings: * <ul> * <li>Filter: USD Curves, Shocks: [-10bp, 0, +10bp]</li> * <li>Filter: EUR/USD Rate, Shocks: [+5%, 0, -5%]</li> * </ul> * The scenario definition would contain the following three scenarios: * <pre> * | | USD Curves | EUR/USD Rate | * |------------|------------|--------------| * | Scenario 1 | -10bp | +5% | * | Scenario 2 | 0 | 0 | * | Scenario 3 | +10bp | -5% | * </pre> * * @param mappings the filters and perturbations that define the scenario. Each mapping must contain the same * number of perturbations * @return a scenario definition containing the perturbations in the mappings */ public static ScenarioDefinition ofMappings(PerturbationMapping<?>... mappings) { return ofMappings(Arrays.asList(mappings)); } /** * Returns a scenario definition containing the perturbations in {@code mappings}. * <p> * Each mapping must contain the same number of perturbations. The definition will contain the * same number of scenarios as the number of perturbations in each mapping. * <p> * The first scenario contains the first perturbation from each mapping, the second scenario contains * the second perturbation from each mapping, and so on. * <p> * The set of scenario names must contain the same number of elements as the mappings. * <p> * Given three mappings, A, B and C, each containing two perturbations, 1 and 2, there will be two * scenarios generated: * <pre> * | | A | B | C | * |------------|------|------|------| * | Scenario 1 | A[1] | B[1] | C[1] | * | Scenario 2 | A[2] | B[2] | C[2] | * </pre> * For example, consider the following perturbation mappings: * <ul> * <li>Filter: USD Curves, Shocks: [-10bp, 0, +10bp]</li> * <li>Filter: EUR/USD Rate, Shocks: [+5%, 0, -5%]</li> * </ul> * The scenario definition would contain the following three scenarios: * <pre> * | | USD Curves | EUR/USD Rate | * |------------|------------|--------------| * | Scenario 1 | -10bp | +5% | * | Scenario 2 | 0 | 0 | * | Scenario 3 | +10bp | -5% | * * @param mappings the filters and perturbations that define the scenario. Each mapping must contain the same * number of perturbations * @param scenarioNames the names of the scenarios. This must be the same size as the list of perturbations * in each mapping and the names must be unique * @return a scenario definition containing the perturbations in the mappings * @throws IllegalArgumentException if there are any duplicate scenario names */ public static ScenarioDefinition ofMappings( List<? extends PerturbationMapping<?>> mappings, List<String> scenarioNames) { ArgChecker.notNull(scenarioNames, "scenarioNames"); int numScenarios = scenarioNames.size(); for (int i = 0; i < mappings.size(); i++) { if (mappings.get(i).getScenarioCount() != numScenarios) { throw new IllegalArgumentException( "Each mapping must contain the same number of scenarios as the definition. There are " + numScenarios + " scenarios in the definition, mapping " + i + " has " + mappings.get(i).getScenarioCount() + " scenarios."); } } return new ScenarioDefinition(mappings, scenarioNames); } /** * Counts the number of scenarios implied by the mappings and the {@code allCombinations} flag. * * @param mappings the mappings that make up the scenarios * @param allCombinations whether the scenarios are generated by taking all combinations of perturbations * formed by taking one from each mapping * @return the number of scenarios */ private static int countScenarios(List<? extends PerturbationMapping<?>> mappings, boolean allCombinations) { ArgChecker.notEmpty(mappings, "mappings"); if (allCombinations) { return mappings.stream() .mapToInt(PerturbationMapping::getScenarioCount) .reduce(1, (s1, s2) -> s1 * s2); } else { return mappings.get(0).getScenarioCount(); } } /** * Returns a list created by repeating the items in the input list multiple times, with each item repeated * in groups. * <p> * For example, given a list [1, 2, 3, 4], total count 12, group size 3 the result is * [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]. * <p> * This is used when creating scenarios from every possible combination of a set of perturbations. * * @param inputs an input list whose elements are repeated in the output * @param totalCount the number of elements in the output list * @param groupSize the number of times each element should be repeated in each group * @param <T> the type of the elements * @return a list created by repeating the elements of the input list */ static <T> List<T> repeatItems(List<T> inputs, int totalCount, int groupSize) { ImmutableList.Builder<T> builder = ImmutableList.builder(); for (int i = 0; i < (totalCount / groupSize / inputs.size()); i++) { for (T input : inputs) { builder.addAll(Collections.nCopies(groupSize, input)); } } return builder.build(); } /** * Generates simple names for the scenarios of the form 'Scenario 1' etc. */ private static ImmutableList<String> generateNames(int numScenarios) { return IntStream.range(1, numScenarios + 1) .mapToObj(i -> "Scenario " + i) .collect(toImmutableList()); } // validates that there are no duplicate scenario names @ImmutableValidator private void validate() { Map<String, List<String>> nameMap = scenarioNames.stream().collect(groupingBy(name -> name)); List<String> duplicateNames = MapStream.of(nameMap) .filterValues(names -> names.size() > 1) .map((name, names) -> name) .collect(toImmutableList()); if (!duplicateNames.isEmpty()) { String duplicates = duplicateNames.stream().collect(joining(", ")); throw new IllegalArgumentException("Scenario names must be unique but duplicates were found: " + duplicates); } } /** * Returns the number of scenarios. * * @return the number of scenarios */ public int getScenarioCount() { return scenarioNames.size(); } //------------------------- AUTOGENERATED START ------------------------- ///CLOVER:OFF /** * The meta-bean for {@code ScenarioDefinition}. * @return the meta-bean, not null */ public static ScenarioDefinition.Meta meta() { return ScenarioDefinition.Meta.INSTANCE; } static { JodaBeanUtils.registerMetaBean(ScenarioDefinition.Meta.INSTANCE); } /** * Returns a builder used to create an instance of the bean. * @return the builder, not null */ public static ScenarioDefinition.Builder builder() { return new ScenarioDefinition.Builder(); } private ScenarioDefinition( List<? extends PerturbationMapping<?>> mappings, List<String> scenarioNames) { JodaBeanUtils.notNull(mappings, "mappings"); JodaBeanUtils.notNull(scenarioNames, "scenarioNames"); this.mappings = ImmutableList.copyOf(mappings); this.scenarioNames = ImmutableList.copyOf(scenarioNames); validate(); } @Override public ScenarioDefinition.Meta metaBean() { return ScenarioDefinition.Meta.INSTANCE; } @Override public <R> Property<R> property(String propertyName) { return metaBean().<R>metaProperty(propertyName).createProperty(this); } @Override public Set<String> propertyNames() { return metaBean().metaPropertyMap().keySet(); } //----------------------------------------------------------------------- /** * Gets the market data filters and perturbations that define the scenarios. * @return the value of the property, not null */ public ImmutableList<PerturbationMapping<?>> getMappings() { return mappings; } //----------------------------------------------------------------------- /** * Gets the names of the scenarios. * @return the value of the property, not null */ public ImmutableList<String> getScenarioNames() { return scenarioNames; } //----------------------------------------------------------------------- /** * Returns a builder that allows this bean to be mutated. * @return the mutable builder, not null */ public Builder toBuilder() { return new Builder(this); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj.getClass() == this.getClass()) { ScenarioDefinition other = (ScenarioDefinition) obj; return JodaBeanUtils.equal(mappings, other.mappings) && JodaBeanUtils.equal(scenarioNames, other.scenarioNames); } return false; } @Override public int hashCode() { int hash = getClass().hashCode(); hash = hash * 31 + JodaBeanUtils.hashCode(mappings); hash = hash * 31 + JodaBeanUtils.hashCode(scenarioNames); return hash; } @Override public String toString() { StringBuilder buf = new StringBuilder(96); buf.append("ScenarioDefinition{"); buf.append("mappings").append('=').append(mappings).append(',').append(' '); buf.append("scenarioNames").append('=').append(JodaBeanUtils.toString(scenarioNames)); buf.append('}'); return buf.toString(); } //----------------------------------------------------------------------- /** * The meta-bean for {@code ScenarioDefinition}. */ public static final class Meta extends DirectMetaBean { /** * The singleton instance of the meta-bean. */ static final Meta INSTANCE = new Meta(); /** * The meta-property for the {@code mappings} property. */ @SuppressWarnings({"unchecked", "rawtypes" }) private final MetaProperty<ImmutableList<PerturbationMapping<?>>> mappings = DirectMetaProperty.ofImmutable( this, "mappings", ScenarioDefinition.class, (Class) ImmutableList.class); /** * The meta-property for the {@code scenarioNames} property. */ @SuppressWarnings({"unchecked", "rawtypes" }) private final MetaProperty<ImmutableList<String>> scenarioNames = DirectMetaProperty.ofImmutable( this, "scenarioNames", ScenarioDefinition.class, (Class) ImmutableList.class); /** * The meta-properties. */ private final Map<String, MetaProperty<?>> metaPropertyMap$ = new DirectMetaPropertyMap( this, null, "mappings", "scenarioNames"); /** * Restricted constructor. */ private Meta() { } @Override protected MetaProperty<?> metaPropertyGet(String propertyName) { switch (propertyName.hashCode()) { case 194445669: // mappings return mappings; case -1193464424: // scenarioNames return scenarioNames; } return super.metaPropertyGet(propertyName); } @Override public ScenarioDefinition.Builder builder() { return new ScenarioDefinition.Builder(); } @Override public Class<? extends ScenarioDefinition> beanType() { return ScenarioDefinition.class; } @Override public Map<String, MetaProperty<?>> metaPropertyMap() { return metaPropertyMap$; } //----------------------------------------------------------------------- /** * The meta-property for the {@code mappings} property. * @return the meta-property, not null */ public MetaProperty<ImmutableList<PerturbationMapping<?>>> mappings() { return mappings; } /** * The meta-property for the {@code scenarioNames} property. * @return the meta-property, not null */ public MetaProperty<ImmutableList<String>> scenarioNames() { return scenarioNames; } //----------------------------------------------------------------------- @Override protected Object propertyGet(Bean bean, String propertyName, boolean quiet) { switch (propertyName.hashCode()) { case 194445669: // mappings return ((ScenarioDefinition) bean).getMappings(); case -1193464424: // scenarioNames return ((ScenarioDefinition) bean).getScenarioNames(); } return super.propertyGet(bean, propertyName, quiet); } @Override protected void propertySet(Bean bean, String propertyName, Object newValue, boolean quiet) { metaProperty(propertyName); if (quiet) { return; } throw new UnsupportedOperationException("Property cannot be written: " + propertyName); } } //----------------------------------------------------------------------- /** * The bean-builder for {@code ScenarioDefinition}. */ public static final class Builder extends DirectFieldsBeanBuilder<ScenarioDefinition> { private List<? extends PerturbationMapping<?>> mappings = ImmutableList.of(); private List<String> scenarioNames = ImmutableList.of(); /** * Restricted constructor. */ private Builder() { } /** * Restricted copy constructor. * @param beanToCopy the bean to copy from, not null */ private Builder(ScenarioDefinition beanToCopy) { this.mappings = beanToCopy.getMappings(); this.scenarioNames = beanToCopy.getScenarioNames(); } //----------------------------------------------------------------------- @Override public Object get(String propertyName) { switch (propertyName.hashCode()) { case 194445669: // mappings return mappings; case -1193464424: // scenarioNames return scenarioNames; default: throw new NoSuchElementException("Unknown property: " + propertyName); } } @SuppressWarnings("unchecked") @Override public Builder set(String propertyName, Object newValue) { switch (propertyName.hashCode()) { case 194445669: // mappings this.mappings = (List<? extends PerturbationMapping<?>>) newValue; break; case -1193464424: // scenarioNames this.scenarioNames = (List<String>) newValue; break; default: throw new NoSuchElementException("Unknown property: " + propertyName); } return this; } @Override public Builder set(MetaProperty<?> property, Object value) { super.set(property, value); return this; } @Override public Builder setString(String propertyName, String value) { setString(meta().metaProperty(propertyName), value); return this; } @Override public Builder setString(MetaProperty<?> property, String value) { super.setString(property, value); return this; } @Override public Builder setAll(Map<String, ? extends Object> propertyValueMap) { super.setAll(propertyValueMap); return this; } @Override public ScenarioDefinition build() { return new ScenarioDefinition( mappings, scenarioNames); } //----------------------------------------------------------------------- /** * Sets the market data filters and perturbations that define the scenarios. * @param mappings the new value, not null * @return this, for chaining, not null */ public Builder mappings(List<? extends PerturbationMapping<?>> mappings) { JodaBeanUtils.notNull(mappings, "mappings"); this.mappings = mappings; return this; } /** * Sets the {@code mappings} property in the builder * from an array of objects. * @param mappings the new value, not null * @return this, for chaining, not null */ public Builder mappings(PerturbationMapping<?>... mappings) { return mappings(ImmutableList.copyOf(mappings)); } /** * Sets the names of the scenarios. * @param scenarioNames the new value, not null * @return this, for chaining, not null */ public Builder scenarioNames(List<String> scenarioNames) { JodaBeanUtils.notNull(scenarioNames, "scenarioNames"); this.scenarioNames = scenarioNames; return this; } /** * Sets the {@code scenarioNames} property in the builder * from an array of objects. * @param scenarioNames the new value, not null * @return this, for chaining, not null */ public Builder scenarioNames(String... scenarioNames) { return scenarioNames(ImmutableList.copyOf(scenarioNames)); } //----------------------------------------------------------------------- @Override public String toString() { StringBuilder buf = new StringBuilder(96); buf.append("ScenarioDefinition.Builder{"); buf.append("mappings").append('=').append(JodaBeanUtils.toString(mappings)).append(',').append(' '); buf.append("scenarioNames").append('=').append(JodaBeanUtils.toString(scenarioNames)); buf.append('}'); return buf.toString(); } } ///CLOVER:ON //-------------------------- AUTOGENERATED END -------------------------- }