/** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.strata.pricer.impl.rate.swap; import java.time.LocalDate; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; import com.google.common.collect.ImmutableMap; import com.opengamma.strata.basics.currency.CurrencyAmount; import com.opengamma.strata.basics.currency.Payment; import com.opengamma.strata.basics.index.IborIndex; import com.opengamma.strata.basics.index.IborIndexObservation; import com.opengamma.strata.collect.ArgChecker; import com.opengamma.strata.market.sensitivity.PointSensitivityBuilder; import com.opengamma.strata.pricer.rate.RatesProvider; import com.opengamma.strata.product.common.PayReceive; import com.opengamma.strata.product.rate.FixedRateComputation; import com.opengamma.strata.product.rate.IborRateComputation; import com.opengamma.strata.product.swap.NotionalExchange; import com.opengamma.strata.product.swap.RateAccrualPeriod; import com.opengamma.strata.product.swap.RatePaymentPeriod; import com.opengamma.strata.product.swap.ResolvedSwap; import com.opengamma.strata.product.swap.ResolvedSwapLeg; import com.opengamma.strata.product.swap.SwapLegType; import com.opengamma.strata.product.swap.SwapPaymentPeriod; /** * Computes cash flow equivalent of products. * <p> * Reference: Henrard, M. The Irony in the derivatives discounting Part II: the crisis. Wilmott Journal, 2010, 2, 301-316. */ public final class CashFlowEquivalentCalculator { /** * Computes cash flow equivalent of swap. * <p> * The swap should be a fix-for-Ibor swap without compounding, and its swap legs * should not involve {@code PaymentEvent}. * <p> * The return type is {@code ResolvedSwapLeg} in which individual payments are * represented in terms of {@code NotionalExchange}. * * @param swap the swap product * @param ratesProvider the rates provider * @return the cash flow equivalent */ public static ResolvedSwapLeg cashFlowEquivalentSwap(ResolvedSwap swap, RatesProvider ratesProvider) { validateSwap(swap); ResolvedSwapLeg cfFixed = cashFlowEquivalentFixedLeg(swap.getLegs(SwapLegType.FIXED).get(0), ratesProvider); ResolvedSwapLeg cfIbor = cashFlowEquivalentIborLeg(swap.getLegs(SwapLegType.IBOR).get(0), ratesProvider); ResolvedSwapLeg leg = ResolvedSwapLeg.builder() .paymentEvents( Stream.concat(cfFixed.getPaymentEvents().stream(), cfIbor.getPaymentEvents().stream()).collect(Collectors.toList())) .payReceive(PayReceive.RECEIVE) .type(SwapLegType.OTHER) .build(); return leg; } /** * Computes cash flow equivalent of Ibor leg. * <p> * The return type is {@code ResolvedSwapLeg} in which individual payments are * represented in terms of {@code NotionalExchange}. * * @param iborLeg the Ibor leg * @param ratesProvider the rates provider * @return the cash flow equivalent */ public static ResolvedSwapLeg cashFlowEquivalentIborLeg(ResolvedSwapLeg iborLeg, RatesProvider ratesProvider) { ArgChecker.isTrue(iborLeg.getType().equals(SwapLegType.IBOR), "Leg type should be IBOR"); ArgChecker.isTrue(iborLeg.getPaymentEvents().isEmpty(), "PaymentEvent should be empty"); List<NotionalExchange> paymentEvents = new ArrayList<NotionalExchange>(); for (SwapPaymentPeriod paymentPeriod : iborLeg.getPaymentPeriods()) { ArgChecker.isTrue(paymentPeriod instanceof RatePaymentPeriod, "rate payment should be RatePaymentPeriod"); RatePaymentPeriod ratePaymentPeriod = (RatePaymentPeriod) paymentPeriod; ArgChecker.isTrue(ratePaymentPeriod.getAccrualPeriods().size() == 1, "rate payment should not be compounding"); RateAccrualPeriod rateAccrualPeriod = ratePaymentPeriod.getAccrualPeriods().get(0); CurrencyAmount notional = ratePaymentPeriod.getNotionalAmount(); LocalDate paymentDate = ratePaymentPeriod.getPaymentDate(); IborIndexObservation obs = ((IborRateComputation) rateAccrualPeriod.getRateComputation()).getObservation(); IborIndex index = obs.getIndex(); LocalDate fixingStartDate = obs.getEffectiveDate(); double fixingYearFraction = obs.getYearFraction(); double beta = (1d + fixingYearFraction * ratesProvider.iborIndexRates(index).rate(obs)) * ratesProvider.discountFactor(paymentPeriod.getCurrency(), paymentPeriod.getPaymentDate()) / ratesProvider.discountFactor(paymentPeriod.getCurrency(), fixingStartDate); double ycRatio = rateAccrualPeriod.getYearFraction() / fixingYearFraction; NotionalExchange payStart = NotionalExchange.of(notional.multipliedBy(beta * ycRatio), fixingStartDate); NotionalExchange payEnd = NotionalExchange.of(notional.multipliedBy(-ycRatio), paymentDate); paymentEvents.add(payStart); paymentEvents.add(payEnd); } ResolvedSwapLeg leg = ResolvedSwapLeg.builder() .paymentEvents(paymentEvents) .payReceive(PayReceive.RECEIVE) .type(SwapLegType.OTHER) .build(); return leg; } /** * Computes cash flow equivalent of fixed leg. * <p> * The return type is {@code ResolvedSwapLeg} in which individual payments are * represented in terms of {@code NotionalExchange}. * * @param fixedLeg the fixed leg * @param ratesProvider the rates provider * @return the cash flow equivalent */ public static ResolvedSwapLeg cashFlowEquivalentFixedLeg(ResolvedSwapLeg fixedLeg, RatesProvider ratesProvider) { ArgChecker.isTrue(fixedLeg.getType().equals(SwapLegType.FIXED), "Leg type should be FIXED"); ArgChecker.isTrue(fixedLeg.getPaymentEvents().isEmpty(), "PaymentEvent should be empty"); List<NotionalExchange> paymentEvents = new ArrayList<NotionalExchange>(); for (SwapPaymentPeriod paymentPeriod : fixedLeg.getPaymentPeriods()) { ArgChecker.isTrue(paymentPeriod instanceof RatePaymentPeriod, "rate payment should be RatePaymentPeriod"); RatePaymentPeriod ratePaymentPeriod = (RatePaymentPeriod) paymentPeriod; ArgChecker.isTrue(ratePaymentPeriod.getAccrualPeriods().size() == 1, "rate payment should not be compounding"); RateAccrualPeriod rateAccrualPeriod = ratePaymentPeriod.getAccrualPeriods().get(0); double factor = rateAccrualPeriod.getYearFraction() * ((FixedRateComputation) rateAccrualPeriod.getRateComputation()).getRate(); CurrencyAmount notional = ratePaymentPeriod.getNotionalAmount().multipliedBy(factor); LocalDate paymentDate = ratePaymentPeriod.getPaymentDate(); NotionalExchange pay = NotionalExchange.of(notional, paymentDate); paymentEvents.add(pay); } ResolvedSwapLeg leg = ResolvedSwapLeg.builder() .paymentEvents(paymentEvents) .payReceive(PayReceive.RECEIVE) .type(SwapLegType.OTHER) .build(); return leg; } //------------------------------------------------------------------------- /** * Computes cash flow equivalent and sensitivity of swap. * <p> * The swap should be a fix-for-Ibor swap without compounding, and its swap legs should not involve {@code PaymentEvent}. * <p> * The return type is a map of {@code NotionalExchange} and {@code PointSensitivityBuilder}. * * @param swap the swap product * @param ratesProvider the rates provider * @return the cash flow equivalent and sensitivity */ public static ImmutableMap<Payment, PointSensitivityBuilder> cashFlowEquivalentAndSensitivitySwap( ResolvedSwap swap, RatesProvider ratesProvider) { validateSwap(swap); ImmutableMap<Payment, PointSensitivityBuilder> mapFixed = cashFlowEquivalentAndSensitivityFixedLeg(swap.getLegs(SwapLegType.FIXED).get(0), ratesProvider); ImmutableMap<Payment, PointSensitivityBuilder> mapIbor = cashFlowEquivalentAndSensitivityIborLeg(swap.getLegs(SwapLegType.IBOR).get(0), ratesProvider); return ImmutableMap.<Payment, PointSensitivityBuilder>builder().putAll(mapFixed).putAll(mapIbor).build(); } /** * Computes cash flow equivalent and sensitivity of Ibor leg. * <p> * The return type is a map of {@code NotionalExchange} and {@code PointSensitivityBuilder}. * * @param iborLeg the Ibor leg * @param ratesProvider the rates provider * @return the cash flow equivalent and sensitivity */ public static ImmutableMap<Payment, PointSensitivityBuilder> cashFlowEquivalentAndSensitivityIborLeg( ResolvedSwapLeg iborLeg, RatesProvider ratesProvider) { ArgChecker.isTrue(iborLeg.getType().equals(SwapLegType.IBOR), "Leg type should be IBOR"); ArgChecker.isTrue(iborLeg.getPaymentEvents().isEmpty(), "PaymentEvent should be empty"); Map<Payment, PointSensitivityBuilder> res = new HashMap<Payment, PointSensitivityBuilder>(); for (SwapPaymentPeriod paymentPeriod : iborLeg.getPaymentPeriods()) { ArgChecker.isTrue(paymentPeriod instanceof RatePaymentPeriod, "rate payment should be RatePaymentPeriod"); RatePaymentPeriod ratePaymentPeriod = (RatePaymentPeriod) paymentPeriod; ArgChecker.isTrue(ratePaymentPeriod.getAccrualPeriods().size() == 1, "rate payment should not be compounding"); RateAccrualPeriod rateAccrualPeriod = ratePaymentPeriod.getAccrualPeriods().get(0); CurrencyAmount notional = ratePaymentPeriod.getNotionalAmount(); LocalDate paymentDate = ratePaymentPeriod.getPaymentDate(); IborIndexObservation obs = ((IborRateComputation) rateAccrualPeriod.getRateComputation()).getObservation(); IborIndex index = obs.getIndex(); LocalDate fixingStartDate = obs.getEffectiveDate(); double fixingYearFraction = obs.getYearFraction(); double factorIndex = (1d + fixingYearFraction * ratesProvider.iborIndexRates(index).rate(obs)); double dfPayment = ratesProvider.discountFactor(paymentPeriod.getCurrency(), paymentPeriod.getPaymentDate()); double dfStart = ratesProvider.discountFactor(paymentPeriod.getCurrency(), fixingStartDate); double beta = factorIndex * dfPayment / dfStart; double ycRatio = rateAccrualPeriod.getYearFraction() / fixingYearFraction; Payment payStart = Payment.of(notional.multipliedBy(beta * ycRatio), fixingStartDate); Payment payEnd = Payment.of(notional.multipliedBy(-ycRatio), paymentDate); double factor = ycRatio * notional.getAmount() / dfStart; PointSensitivityBuilder factorIndexSensi = ratesProvider.iborIndexRates(index) .ratePointSensitivity(obs).multipliedBy(fixingYearFraction * dfPayment * factor); PointSensitivityBuilder dfPaymentSensitivity = ratesProvider.discountFactors(paymentPeriod.getCurrency()) .zeroRatePointSensitivity(paymentPeriod.getPaymentDate()).multipliedBy(factorIndex * factor); PointSensitivityBuilder dfStartSensitivity = ratesProvider.discountFactors(paymentPeriod.getCurrency()) .zeroRatePointSensitivity(fixingStartDate).multipliedBy(-factorIndex * dfPayment * factor / dfStart); res.put(payStart, factorIndexSensi.combinedWith(dfPaymentSensitivity).combinedWith(dfStartSensitivity)); res.put(payEnd, PointSensitivityBuilder.none()); } return ImmutableMap.copyOf(res); } /** * Computes cash flow equivalent and sensitivity of fixed leg. * <p> * The return type is a map of {@code NotionalExchange} and {@code PointSensitivityBuilder}. * * @param fixedLeg the fixed leg * @param ratesProvider the rates provider * @return the cash flow equivalent and sensitivity */ public static ImmutableMap<Payment, PointSensitivityBuilder> cashFlowEquivalentAndSensitivityFixedLeg( ResolvedSwapLeg fixedLeg, RatesProvider ratesProvider) { ArgChecker.isTrue(fixedLeg.getType().equals(SwapLegType.FIXED), "Leg type should be FIXED"); ArgChecker.isTrue(fixedLeg.getPaymentEvents().isEmpty(), "PaymentEvent should be empty"); Map<Payment, PointSensitivityBuilder> res = new HashMap<Payment, PointSensitivityBuilder>(); for (SwapPaymentPeriod paymentPeriod : fixedLeg.getPaymentPeriods()) { ArgChecker.isTrue(paymentPeriod instanceof RatePaymentPeriod, "rate payment should be RatePaymentPeriod"); RatePaymentPeriod ratePaymentPeriod = (RatePaymentPeriod) paymentPeriod; ArgChecker.isTrue(ratePaymentPeriod.getAccrualPeriods().size() == 1, "rate payment should not be compounding"); RateAccrualPeriod rateAccrualPeriod = ratePaymentPeriod.getAccrualPeriods().get(0); double factor = rateAccrualPeriod.getYearFraction() * ((FixedRateComputation) rateAccrualPeriod.getRateComputation()).getRate(); CurrencyAmount notional = ratePaymentPeriod.getNotionalAmount().multipliedBy(factor); LocalDate paymentDate = ratePaymentPeriod.getPaymentDate(); Payment pay = Payment.of(notional, paymentDate); res.put(pay, PointSensitivityBuilder.none()); } return ImmutableMap.copyOf(res); } //------------------------------------------------------------------------- private static void validateSwap(ResolvedSwap swap) { ArgChecker.isTrue(swap.getLegs().size() == 2, "swap should have 2 legs"); ArgChecker.isTrue(swap.getLegs(SwapLegType.FIXED).size() == 1, "swap should have unique fixed leg"); ArgChecker.isTrue(swap.getLegs(SwapLegType.IBOR).size() == 1, "swap should have unique Ibor leg"); } }