/** * Copyright (C) 2016 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.strata.pricer.sensitivity; import static java.util.stream.Collectors.toList; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.function.Function; import com.google.common.collect.ImmutableList; import com.opengamma.strata.basics.currency.Currency; import com.opengamma.strata.collect.ArgChecker; import com.opengamma.strata.collect.array.DoubleArray; import com.opengamma.strata.collect.array.DoubleMatrix; import com.opengamma.strata.market.curve.CurveParameterSize; import com.opengamma.strata.market.param.CurrencyParameterSensitivities; import com.opengamma.strata.market.param.CurrencyParameterSensitivity; import com.opengamma.strata.market.param.DatedParameterMetadata; import com.opengamma.strata.market.param.LabelDateParameterMetadata; import com.opengamma.strata.market.param.ParameterMetadata; import com.opengamma.strata.market.param.TenorParameterMetadata; import com.opengamma.strata.math.impl.matrix.CommonsMatrixAlgebra; import com.opengamma.strata.math.impl.matrix.MatrixAlgebra; import com.opengamma.strata.product.ResolvedTrade; /** * Utilities to transform sensitivities. */ public class CurveSensitivityUtils { /** * The matrix algebra used for matrix inversion. */ private static final MatrixAlgebra MATRIX_ALGEBRA = new CommonsMatrixAlgebra(); /** * Construct the inverse Jacobian matrix from the sensitivities of the trades market quotes to the curve parameters. * <p> * All the trades and sensitivities must be in the same currency. The data should be coherent with the * market quote sensitivities passed in an order coherent with the list of curves. * <p> * For each trade describing the market quotes, the sensitivity provided should be the sensitivity of that * market quote to the curve parameters. * * @param curveOrder the order in which the curves should be represented in the jacobian * @param marketQuoteSensitivities the market quotes sensitivity to the curve parameters * @return inverse jacobian matrix, which correspond to the sensitivity of the parameters to the market quotes */ public static DoubleMatrix jacobianFromMarketQuoteSensitivities( List<CurveParameterSize> curveOrder, List<CurrencyParameterSensitivities> marketQuoteSensitivities) { Currency ccy = marketQuoteSensitivities.get(0).getSensitivities().get(0).getCurrency(); DoubleMatrix jacobianMatrix = DoubleMatrix.ofArrayObjects( marketQuoteSensitivities.size(), marketQuoteSensitivities.size(), i -> row(curveOrder, marketQuoteSensitivities.get(i), ccy)); return MATRIX_ALGEBRA.getInverse(jacobianMatrix); } /** * Computes the row corresponding to a trade for the Jacobian matrix. * * @param curveOrder the curve order * @param sensitivities the sensitivities * @param ccy the currency common to all sensitivities * @return */ private static DoubleArray row( List<CurveParameterSize> curveOrder, CurrencyParameterSensitivities parameterSensitivities, Currency ccy) { DoubleArray row = DoubleArray.EMPTY; for (CurveParameterSize curveNameAndSize : curveOrder) { Optional<CurrencyParameterSensitivity> sensitivityOneCurve = parameterSensitivities.findSensitivity(curveNameAndSize.getName(), ccy); if (sensitivityOneCurve.isPresent()) { row = row.concat(sensitivityOneCurve.get().getSensitivity()); } else { row = row.concat(DoubleArray.filled(curveNameAndSize.getParameterCount())); } } return row; } /** * Construct the inverse Jacobian matrix from the trades and a function used to compute the sensitivities of the * market quotes to the curve parameters. * <p> * All the trades must be in the same currency. The trades should be coherent with the curves order. * * @param curveOrder the order in which the curves should be represented in the jacobian * @param trades the list of trades * @param sensitivityFunction the function from a trade to the market quote sensitivity to curve parameters * @return inverse jacobian matrix, which correspond to the sensitivity of the parameters to the market quotes */ public static DoubleMatrix jacobianFromMarketQuoteSensitivities( List<CurveParameterSize> curveOrder, List<ResolvedTrade> trades, Function<ResolvedTrade, CurrencyParameterSensitivities> sensitivityFunction) { List<CurrencyParameterSensitivities> marketQuoteSensitivities = new ArrayList<>(); for (ResolvedTrade t : trades) { marketQuoteSensitivities.add(sensitivityFunction.apply(t)); } return jacobianFromMarketQuoteSensitivities(curveOrder, marketQuoteSensitivities); } /** * Re-buckets a {@link CurrencyParameterSensitivities} to a given set of dates. * <p> * The list of dates must be sorted in chronological order. All sensitivities are re-bucketed to the same date list. * The re-bucketing is done by linear weighting on the number of days, i.e. the sensitivities for dates outside the * extremes are fully bucketed to the extremes and for date between two re-bucketing dates, the weight on the start * date is the number days between end date and the date re-bucketed divided by the number of days between the * start and the end. * The input sensitivity should have a {@link DatedParameterMetadata} for each sensitivity. * * @param sensitivities the input sensitivities * @param targetDates the list of dates for the re-bucketing * @return the sensitivity after the re-bucketing */ public static CurrencyParameterSensitivities linearRebucketing( CurrencyParameterSensitivities sensitivities, List<LocalDate> targetDates) { checkSortedDates(targetDates); int nbBuckets = targetDates.size(); List<ParameterMetadata> pmdTarget = targetDates.stream() .map(date -> LabelDateParameterMetadata.of(date, date.toString())) .collect(toList()); ImmutableList<CurrencyParameterSensitivity> sensitivitiesList = sensitivities.getSensitivities(); List<CurrencyParameterSensitivity> sensitivityTarget = new ArrayList<>(); for (CurrencyParameterSensitivity sensitivity : sensitivitiesList) { double[] rebucketedSensitivityAmounts = new double[nbBuckets]; DoubleArray sensitivityAmounts = sensitivity.getSensitivity(); List<ParameterMetadata> parameterMetadataList = sensitivity.getParameterMetadata(); for (int loopnode = 0; loopnode < sensitivityAmounts.size(); loopnode++) { ParameterMetadata nodeMetadata = parameterMetadataList.get(loopnode); ArgChecker.isTrue(nodeMetadata instanceof DatedParameterMetadata, "re-bucketing requires sensitivity date for node {} which is of type {} while 'DatedParameterMetadata' is expected", nodeMetadata.getLabel(), nodeMetadata.getClass().getName()); DatedParameterMetadata datedParameterMetadata = (DatedParameterMetadata) nodeMetadata; LocalDate nodeDate = datedParameterMetadata.getDate(); rebucketingArray(targetDates, rebucketedSensitivityAmounts, sensitivityAmounts.get(loopnode), nodeDate); } CurrencyParameterSensitivity rebucketedSensitivity = CurrencyParameterSensitivity.of( sensitivity.getMarketDataName(), pmdTarget, sensitivity.getCurrency(), DoubleArray.ofUnsafe(rebucketedSensitivityAmounts)); sensitivityTarget.add(rebucketedSensitivity); } return CurrencyParameterSensitivities.of(sensitivityTarget); } /** * Re-buckets a {@link CurrencyParameterSensitivities} to a given set of dates. * <p> * The list of dates must be sorted in chronological order. All sensitivities are re-bucketed to the same date list. * The re-bucketing is done by linear weighting on the number of days, i.e. the sensitivities for dates outside the * extremes are fully bucketed to the extremes and for date between two re-bucketing dates, the weight on the start * date is the number days between end date and the date re-bucketed divided by the number of days between the * start and the end. The date of the nodes can be directly in the parameter metadata - when the metadata is of the * type {@link DatedParameterMetadata} - or inferred from the sensitivity date and the tenor when the * metadata is of the type {@link TenorParameterMetadata}. Only those types of metadata are accepted. * * @param sensitivities the input sensitivities * @param targetDates the list of dates for the re-bucketing * @param sensitivityDate the date for which the sensitivities are valid * @return the sensitivity after the re-bucketing */ public static CurrencyParameterSensitivities linearRebucketing( CurrencyParameterSensitivities sensitivities, List<LocalDate> targetDates, LocalDate sensitivityDate) { checkSortedDates(targetDates); int nbBuckets = targetDates.size(); List<ParameterMetadata> pmdTarget = targetDates.stream() .map(date -> LabelDateParameterMetadata.of(date, date.toString())) .collect(toList()); ImmutableList<CurrencyParameterSensitivity> sensitivitiesList = sensitivities.getSensitivities(); List<CurrencyParameterSensitivity> sensitivityTarget = new ArrayList<>(); for (CurrencyParameterSensitivity sensitivity : sensitivitiesList) { double[] rebucketedSensitivityAmounts = new double[nbBuckets]; DoubleArray sensitivityAmounts = sensitivity.getSensitivity(); List<ParameterMetadata> parameterMetadataList = sensitivity.getParameterMetadata(); for (int loopnode = 0; loopnode < sensitivityAmounts.size(); loopnode++) { ParameterMetadata nodeMetadata = parameterMetadataList.get(loopnode); ArgChecker.isTrue((nodeMetadata instanceof DatedParameterMetadata) || (nodeMetadata instanceof TenorParameterMetadata), "re-bucketing requires sensitivity date or node for node {} which is of type {}", nodeMetadata.getLabel(), nodeMetadata.getClass().getName()); LocalDate nodeDate; if (nodeMetadata instanceof DatedParameterMetadata) { DatedParameterMetadata datedParameterMetadata = (DatedParameterMetadata) nodeMetadata; nodeDate = datedParameterMetadata.getDate(); } else { TenorParameterMetadata tenorParameterMetadata = (TenorParameterMetadata) nodeMetadata; nodeDate = sensitivityDate.plus(tenorParameterMetadata.getTenor()); } rebucketingArray(targetDates, rebucketedSensitivityAmounts, sensitivityAmounts.get(loopnode), nodeDate); } CurrencyParameterSensitivity rebucketedSensitivity = CurrencyParameterSensitivity.of( sensitivity.getMarketDataName(), pmdTarget, sensitivity.getCurrency(), DoubleArray.ofUnsafe(rebucketedSensitivityAmounts)); sensitivityTarget.add(rebucketedSensitivity); } return CurrencyParameterSensitivities.of(sensitivityTarget); } /** * Re-bucket one sensitivity at a specific date and add it to an existing array. * * @param targetDates the list of dates for the re-bucketing * @param rebucketedSensitivityAmounts the array of sensitivities; the array is modified by the method * @param sensitivityAmount the value of the sensitivity at the given data * @param sensitivityDate the date associated to the amount to re-bucket */ private static void rebucketingArray( List<LocalDate> targetDates, double[] rebucketedSensitivityAmounts, double sensitivityAmount, LocalDate sensitivityDate) { int nbBuckets = targetDates.size(); if (!sensitivityDate.isAfter(targetDates.get(0))) { rebucketedSensitivityAmounts[0] += sensitivityAmount; } else if (!sensitivityDate.isBefore(targetDates.get(nbBuckets - 1))) { rebucketedSensitivityAmounts[nbBuckets - 1] += sensitivityAmount; } else { int indexSensitivityDate = 0; while (sensitivityDate.isAfter(targetDates.get(indexSensitivityDate))) { indexSensitivityDate++; } // 'indexSensitivityDate' contains the index of the node after the sensitivity date long intervalLength = targetDates.get(indexSensitivityDate).toEpochDay() - targetDates.get(indexSensitivityDate - 1).toEpochDay(); double weight = ((double) (targetDates.get(indexSensitivityDate).toEpochDay() - sensitivityDate.toEpochDay())) / intervalLength; rebucketedSensitivityAmounts[indexSensitivityDate - 1] += weight * sensitivityAmount; rebucketedSensitivityAmounts[indexSensitivityDate] += (1.0d - weight) * sensitivityAmount; } } // Check that the dates in the list are sorted in chronological order. private static void checkSortedDates(List<LocalDate> dates) { for (int loopdate = 0; loopdate < dates.size() - 1; loopdate++) { ArgChecker.inOrderNotEqual(dates.get(loopdate), dates.get(loopdate + 1), "first date", "following date"); } } }