/** * Copyright (C) 2016 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.strata.examples.finance; import static com.opengamma.strata.measure.StandardComponents.marketDataFactory; import static java.util.stream.Collectors.toList; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.time.LocalDate; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Random; import com.google.common.collect.ImmutableList; import com.google.common.io.CharSource; import com.opengamma.strata.basics.ReferenceData; import com.opengamma.strata.basics.currency.Currency; import com.opengamma.strata.calc.CalculationRules; import com.opengamma.strata.calc.CalculationRunner; import com.opengamma.strata.calc.Column; import com.opengamma.strata.calc.Results; import com.opengamma.strata.calc.marketdata.MarketDataConfig; import com.opengamma.strata.calc.marketdata.MarketDataFilter; import com.opengamma.strata.calc.marketdata.MarketDataRequirements; import com.opengamma.strata.calc.marketdata.PerturbationMapping; import com.opengamma.strata.calc.marketdata.ScenarioDefinition; import com.opengamma.strata.calc.runner.CalculationFunctions; import com.opengamma.strata.collect.Messages; import com.opengamma.strata.collect.array.DoubleArray; import com.opengamma.strata.collect.array.DoubleMatrix; import com.opengamma.strata.collect.io.ResourceLocator; import com.opengamma.strata.data.ImmutableMarketData; import com.opengamma.strata.data.ImmutableMarketDataBuilder; import com.opengamma.strata.data.MarketData; import com.opengamma.strata.data.scenario.CurrencyScenarioArray; import com.opengamma.strata.data.scenario.MarketDataBox; import com.opengamma.strata.data.scenario.ScenarioMarketData; import com.opengamma.strata.data.scenario.ScenarioPerturbation; import com.opengamma.strata.examples.marketdata.credit.markit.MarkitRedCode; import com.opengamma.strata.examples.marketdata.credit.markit.MarkitSingleNameCreditCurveDataParser; import com.opengamma.strata.examples.marketdata.credit.markit.MarkitYieldCurveDataParser; import com.opengamma.strata.math.impl.statistics.descriptive.SampleInterpolationQuantileMethod; import com.opengamma.strata.measure.Measures; import com.opengamma.strata.measure.StandardComponents; import com.opengamma.strata.pricer.credit.IsdaCreditCurveInputs; import com.opengamma.strata.pricer.credit.IsdaSingleNameCreditCurveInputsId; import com.opengamma.strata.pricer.credit.IsdaYieldCurveInputs; import com.opengamma.strata.pricer.credit.IsdaYieldCurveInputsId; import com.opengamma.strata.product.Trade; import com.opengamma.strata.product.common.BuySell; import com.opengamma.strata.product.credit.CdsTrade; import com.opengamma.strata.product.credit.RestructuringClause; import com.opengamma.strata.product.credit.SeniorityLevel; import com.opengamma.strata.product.credit.SingleNameReferenceInformation; import com.opengamma.strata.product.credit.type.CdsConventions; /** * Example to illustrate using the calculation API to run a set of scenarios on a single name CDS. * <p> * In this example we load the interest rate curve and the credit curve required to price the CDS using the built-in * curve file loaders. * <p> * We then define scenarios in which each point on the credit curve is randomly perturbed, illustrating a very simple * Monte Carlo approach. It would be equally possible to use other sources of data, such as historical credit spreads, * to generate the perturbations instead. * <p> * The calculation API is used to obtain the present value under each scenario, producing a vector of results. * We then transform this into a vector of P&Ls, which we use to calculate VaR. */ public class CdsScenarioExample { private static final String MARKET_DATA_RESOURCE_ROOT = "example-marketdata"; public static void main(String[] args) { // set up calculation runner component, which needs life-cycle management // a typical application might use dependency injection to obtain the instance try (CalculationRunner runner = CalculationRunner.ofMultiThreaded()) { calculate(runner); } } // loads the trade and market data, and performs the calculations private static void calculate(CalculationRunner runner) { // the trade to price CdsTrade cds = createSingleNameCds(); List<Trade> trades = ImmutableList.of(cds); // the columns, specifying the measures to be calculated List<Column> columns = ImmutableList.of( Column.of(Measures.PRESENT_VALUE)); // build the set of market data for the base scenario on the valuation date // this is the snapshot which will be perturbed in the scenarios MarketData baseMarketData = buildBaseMarketData(); // build scenarios containing credit curve shifts ScenarioDefinition scenarios = buildScenarios(baseMarketData); // use the standard rules defining how to calculate the measures we are requesting CalculationFunctions functions = StandardComponents.calculationFunctions(); CalculationRules rules = CalculationRules.of(functions); // use the built-in reference data, which includes some holiday calendars ReferenceData refData = ReferenceData.standard(); // now combine the base market data with the scenario definition to create the full set of scenario market data MarketDataRequirements reqs = MarketDataRequirements.of(rules, trades, columns, refData); ScenarioMarketData scenarioMarketData = marketDataFactory().createMultiScenario(reqs, MarketDataConfig.empty(), baseMarketData, refData, scenarios); // calculate the results Results results = runner.calculateMultiScenario(rules, trades, columns, scenarioMarketData, refData); // the results contain the one measure requested (Present Value) for each scenario // the first scenario is the base CurrencyScenarioArray pvVector = (CurrencyScenarioArray) results.get(0, 0).getValue(); outputCurrencyValues("PVs", pvVector); // transform the present values into P&Ls, sorted from greatest loss to greatest profit CurrencyScenarioArray pnlVector = getSortedPnls(pvVector); outputCurrencyValues("Scenario PnLs", pnlVector); // use a built-in utility to calculate VaR // since the P&Ls are sorted starting with the greatest loss, the 95% greatest loss occurs at the 5% position double var95 = SampleInterpolationQuantileMethod.DEFAULT.quantileFromSorted(0.05, pnlVector.getAmounts().getValues()); System.out.println(Messages.format("95% VaR: {}", var95)); } //------------------------------------------------------------------------- // builds the set of market data representing the base scenario private static MarketData buildBaseMarketData() { // initialise the market data builder for the valuation date LocalDate valuationDate = LocalDate.of(2014, 10, 16); ImmutableMarketDataBuilder baseMarketDataBuilder = ImmutableMarketData.builder(valuationDate); // add yield curves loadBaseYieldCurves(baseMarketDataBuilder); // add credit curves loadBaseSingleNameCreditCurves(baseMarketDataBuilder); // build a single market data snapshot for the valuation date return baseMarketDataBuilder.build(); } // loads the base yield curves from a fixed resource private static void loadBaseYieldCurves(ImmutableMarketDataBuilder builder) { ResourceLocator resource = ResourceLocator.ofClasspath(MARKET_DATA_RESOURCE_ROOT + "/credit/2014-10-16/cds.yieldCurves.csv"); CharSource inputSource = resource.getCharSource(); // use the built-in markit yield curve parser Map<IsdaYieldCurveInputsId, IsdaYieldCurveInputs> yieldCurves = MarkitYieldCurveDataParser.parse(inputSource); builder.addValueMap(yieldCurves); } // loads the base single name credit curves from a fixed resource private static void loadBaseSingleNameCreditCurves(ImmutableMarketDataBuilder builder) { ResourceLocator curvesResource = ResourceLocator.ofClasspath(MARKET_DATA_RESOURCE_ROOT + "/credit/2014-10-16/singleName.creditCurves.csv"); CharSource curvesInputSource = curvesResource.getCharSource(); ResourceLocator staticDataResource = ResourceLocator.ofClasspath(MARKET_DATA_RESOURCE_ROOT + "/credit/2014-10-16/singleName.staticData.csv"); CharSource staticDataInputSource = staticDataResource.getCharSource(); // use the built-in markit credit curve parser MarkitSingleNameCreditCurveDataParser.parse(builder, curvesInputSource, staticDataInputSource); } //----------------------------------------------------------------------- // create a single name CDS with 100 bps coupon private static CdsTrade createSingleNameCds() { return CdsConventions.USD_NORTH_AMERICAN .toTrade( LocalDate.of(2014, 9, 22), LocalDate.of(2019, 12, 20), BuySell.BUY, 100_000_000d, 0.0100, SingleNameReferenceInformation.of( MarkitRedCode.id("COMP01"), SeniorityLevel.SENIOR_UNSECURED_FOREIGN, Currency.USD, RestructuringClause.NO_RESTRUCTURING_2014), 3_694_117.72d, LocalDate.of(2014, 10, 21)); } //----------------------------------------------------------------------- // build the scenarios to use to perturb the base market data private static ScenarioDefinition buildScenarios(MarketData baseMarketData) { // build perturbations for each single name credit curve in the base market data set // here we are only interested in one credit curve, but this shows how we might deal with a larger set // each perturbation mapping represents the shifts to apply to one item of market data (here, a curve) in all scenarios List<PerturbationMapping<?>> perturbations = baseMarketData.getIds().stream() .filter(id -> id instanceof IsdaSingleNameCreditCurveInputsId) .map(IsdaSingleNameCreditCurveInputsId.class::cast) .map(id -> PerturbationMapping.of( IsdaCreditCurveInputs.class, IsdaCreditCurveFilter.of(id), buildScenarioShifts(baseMarketData.getValue(id)))) .collect(toList()); // we could add perturbations to other pieces of market data by continuing to add to the perturbations list // each perturbation mapping must contain shifts for the same number of scenarios // together the perturbations to the items of market data define the complete set of scenarios return ScenarioDefinition.ofMappings(perturbations); } // build the shifts to apply to a given curve in each scenario private static IsdaCreditCurveShifts buildScenarioShifts(IsdaCreditCurveInputs baseCurve) { // create a shift matrix - one row per scenario, each column representing a nodal point on the curve int scenarioCount = 100; int curveNodes = baseCurve.getNumberOfPoints(); double[][] relativeShifts = new double[scenarioCount + 1][curveNodes]; // include a base scenario with no shifts (i.e. multiply by 1) for (int nodeIndex = 0; nodeIndex < curveNodes; nodeIndex++) { relativeShifts[0][nodeIndex] = 1; } // for the remaining scenarios, create random relative shifts between 0.95 and 1.05 for illustration // here we could instead calculate the shifts from historical data, perhaps applying a weighting such as EWMA Random r = new Random(); for (int scenarioIndex = 1; scenarioIndex <= scenarioCount; scenarioIndex++) { for (int nodeIndex = 0; nodeIndex < curveNodes; nodeIndex++) { relativeShifts[scenarioIndex][nodeIndex] = 1 + ((r.nextDouble() - 0.5) * 0.1); } } // return the shifts object which will be applied to the base market data DoubleMatrix shiftMatrix = DoubleMatrix.copyOf(relativeShifts); return new IsdaCreditCurveShifts(shiftMatrix); } //------------------------------------------------------------------------- private static CurrencyScenarioArray getSortedPnls(CurrencyScenarioArray pvVector) { double[] scenarioPnls = new double[pvVector.getScenarioCount() - 1]; // the base PV was calculated in the first scenario where no shifts were applied double basePv = pvVector.get(0).getAmount(); // for the remaining scenarios, work out the scenario P&L by subtracting the base PV for (int i = 1; i < pvVector.getScenarioCount(); i++) { double scenarioPv = pvVector.get(i).getAmount(); double pnl = scenarioPv - basePv; scenarioPnls[i - 1] = pnl; } // sort the P&Ls, so we have the highest loss to the highest profit Arrays.sort(scenarioPnls); return CurrencyScenarioArray.of(pvVector.getCurrency(), DoubleArray.ofUnsafe(scenarioPnls)); } //------------------------------------------------------------------------- private static void outputCurrencyValues(String title, CurrencyScenarioArray currencyValues) { NumberFormat numberFormat = new DecimalFormat("0.00", new DecimalFormatSymbols(Locale.ENGLISH)); System.out.println(Messages.format("{} ({}):", title, currencyValues.getCurrency())); for (int i = 0; i < currencyValues.getScenarioCount(); i++) { double scenarioValue = currencyValues.get(i).getAmount(); System.out.println(numberFormat.format(scenarioValue)); } System.out.println(); } //------------------------------------------------------------------------- // implements shifts to a credit curve across all scenarios // this custom implementation of ScenarioPerturbation is necessary for CDS where non-standard market data types are used // for other asset clases, the built-in CurvePointShifts may be used private static class IsdaCreditCurveShifts implements ScenarioPerturbation<IsdaCreditCurveInputs> { private final DoubleMatrix shifts; public IsdaCreditCurveShifts(DoubleMatrix shifts) { this.shifts = shifts; } @Override public MarketDataBox<IsdaCreditCurveInputs> applyTo(MarketDataBox<IsdaCreditCurveInputs> marketData, ReferenceData refData) { return marketData.mapWithIndex(shifts.rowCount(), (curve, scenarioIndex) -> applyShifts(scenarioIndex, curve)); } @Override public int getScenarioCount() { return shifts.rowCount(); } private IsdaCreditCurveInputs applyShifts(int scenarioIndex, IsdaCreditCurveInputs curve) { // return the manipulated curve return IsdaCreditCurveInputs.of( curve.getName(), curve.getCreditCurvePoints(), curve.getEndDatePoints(), getShiftedParRates(curve.getParRates(), shifts.rowArray(scenarioIndex)), curve.getCdsConvention(), curve.getScalingFactor()); } private double[] getShiftedParRates(double[] parRates, double[] shifts) { double[] shiftedParRates = new double[parRates.length]; for (int i = 0; i < shiftedParRates.length; i++) { // multiply the par rates from the base curve by the relative shifts generated for this scenario shiftedParRates[i] = parRates[i] * shifts[i]; } return shiftedParRates; } } // implements a filter which selects the credit curve to perturb based on matching a given ID // this custom implementation of MarketDataFilter is necessary for CDS where non-standard market data types are used // for other asset classes, the built-in CurveNameFilter or AllCurvesFilter may be used private static final class IsdaCreditCurveFilter implements MarketDataFilter<IsdaCreditCurveInputs, IsdaSingleNameCreditCurveInputsId> { private final IsdaSingleNameCreditCurveInputsId id; public static IsdaCreditCurveFilter of(IsdaSingleNameCreditCurveInputsId id) { return new IsdaCreditCurveFilter(id); } private IsdaCreditCurveFilter(IsdaSingleNameCreditCurveInputsId id) { this.id = id; } @Override public Class<?> getMarketDataIdType() { return IsdaSingleNameCreditCurveInputsId.class; } @Override public boolean matches( IsdaSingleNameCreditCurveInputsId marketDataId, MarketDataBox<IsdaCreditCurveInputs> marketData, ReferenceData refData) { return this.id.equals(marketDataId); } } }