/**
* Copyright (C) 2014 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.strata.pricer.swap;
import static com.opengamma.strata.basics.currency.MultiCurrencyAmount.toMultiCurrencyAmount;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.ToDoubleBiFunction;
import com.google.common.collect.ImmutableList;
import com.opengamma.strata.basics.currency.Currency;
import com.opengamma.strata.basics.currency.CurrencyAmount;
import com.opengamma.strata.basics.currency.MultiCurrencyAmount;
import com.opengamma.strata.collect.ArgChecker;
import com.opengamma.strata.collect.tuple.Triple;
import com.opengamma.strata.market.amount.CashFlows;
import com.opengamma.strata.market.explain.ExplainKey;
import com.opengamma.strata.market.explain.ExplainMap;
import com.opengamma.strata.market.explain.ExplainMapBuilder;
import com.opengamma.strata.market.sensitivity.PointSensitivityBuilder;
import com.opengamma.strata.pricer.rate.RatesProvider;
import com.opengamma.strata.product.rate.FixedRateComputation;
import com.opengamma.strata.product.swap.CompoundingMethod;
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;
/**
* Pricer for for rate swap products.
* <p>
* This function provides the ability to price a {@link ResolvedSwap}.
* The product is priced by pricing each leg.
*/
public class DiscountingSwapProductPricer {
/**
* Default implementation.
*/
public static final DiscountingSwapProductPricer DEFAULT = new DiscountingSwapProductPricer(
DiscountingSwapLegPricer.DEFAULT);
/**
* Pricer for {@link ResolvedSwapLeg}.
*/
private final DiscountingSwapLegPricer legPricer;
/**
* Creates an instance.
*
* @param legPricer the pricer for {@link ResolvedSwapLeg}
*/
public DiscountingSwapProductPricer(
DiscountingSwapLegPricer legPricer) {
this.legPricer = ArgChecker.notNull(legPricer, "legPricer");
}
//-------------------------------------------------------------------------
/**
* Gets the underlying leg pricer.
*
* @return the leg pricer
*/
public DiscountingSwapLegPricer getLegPricer() {
return legPricer;
}
//-------------------------------------------------------------------------
/**
* Calculates the present value of the swap product, converted to the specified currency.
* <p>
* The present value of the product is the value on the valuation date.
* This is the discounted forecast value.
* The result is converted to the specified currency.
*
* @param swap the product
* @param currency the currency to convert to
* @param provider the rates provider
* @return the present value of the swap product in the specified currency
*/
public CurrencyAmount presentValue(ResolvedSwap swap, Currency currency, RatesProvider provider) {
double totalPv = 0;
for (ResolvedSwapLeg leg : swap.getLegs()) {
double pv = legPricer.presentValueInternal(leg, provider);
totalPv += (pv * provider.fxRate(leg.getCurrency(), currency));
}
return CurrencyAmount.of(currency, totalPv);
}
/**
* Calculates the present value of the swap product.
* <p>
* The present value of the product is the value on the valuation date.
* This is the discounted forecast value.
* The result is expressed using the payment currency of each leg.
*
* @param swap the product
* @param provider the rates provider
* @return the present value of the swap product
*/
public MultiCurrencyAmount presentValue(ResolvedSwap swap, RatesProvider provider) {
return swapValue(provider, swap, legPricer::presentValueInternal);
}
/**
* Calculates the forecast value of the swap product.
* <p>
* The forecast value of the product is the value on the valuation date without present value discounting.
* The result is expressed using the payment currency of each leg.
*
* @param swap the product
* @param provider the rates provider
* @return the forecast value of the swap product
*/
public MultiCurrencyAmount forecastValue(ResolvedSwap swap, RatesProvider provider) {
return swapValue(provider, swap, legPricer::forecastValueInternal);
}
//-------------------------------------------------------------------------
// calculate present or forecast value for the swap
private static MultiCurrencyAmount swapValue(
RatesProvider provider,
ResolvedSwap swap,
ToDoubleBiFunction<ResolvedSwapLeg, RatesProvider> legFn) {
if (swap.isCrossCurrency()) {
return swap.getLegs().stream()
.map(leg -> CurrencyAmount.of(leg.getCurrency(), legFn.applyAsDouble(leg, provider)))
.collect(toMultiCurrencyAmount());
} else {
Currency currency = swap.getLegs().iterator().next().getCurrency();
double total = 0d;
for (ResolvedSwapLeg leg : swap.getLegs()) {
total += legFn.applyAsDouble(leg, provider);
}
return MultiCurrencyAmount.of(currency, total);
}
}
//-------------------------------------------------------------------------
/**
* Calculates the accrued interest since the last payment.
* <p>
* This determines the payment period applicable at the valuation date and calculates
* the accrued interest since the last payment.
*
* @param swap the product
* @param provider the rates provider
* @return the accrued interest of the swap product
*/
public MultiCurrencyAmount accruedInterest(ResolvedSwap swap, RatesProvider provider) {
MultiCurrencyAmount result = MultiCurrencyAmount.empty();
for (ResolvedSwapLeg leg : swap.getLegs()) {
result = result.plus(legPricer.accruedInterest(leg, provider));
}
return result;
}
//-------------------------------------------------------------------------
/**
* Computes the par rate for swaps with a fixed leg.
* <p>
* The par rate is the common rate on all payments of the fixed leg for which the total swap present value is 0.
* <p>
* At least one leg must be a fixed leg. The par rate will be computed with respect to the first fixed leg
* in which all the payments are fixed payments with a unique accrual period (no compounding) and no FX reset.
* If the fixed leg is compounding, the par rate is computed only when the number of fixed coupon payments is 1 and
* accrual factor of each sub-period is 1
*
* @param swap the product
* @param provider the rates provider
* @return the par rate
*/
public double parRate(ResolvedSwap swap, RatesProvider provider) {
// find fixed leg
ResolvedSwapLeg fixedLeg = fixedLeg(swap);
Currency ccyFixedLeg = fixedLeg.getCurrency();
// other payments (not fixed leg coupons) converted in fixed leg currency
double otherLegsConvertedPv = 0.0;
for (ResolvedSwapLeg leg : swap.getLegs()) {
if (leg != fixedLeg) {
double pvLocal = legPricer.presentValueInternal(leg, provider);
otherLegsConvertedPv += (pvLocal * provider.fxRate(leg.getCurrency(), ccyFixedLeg));
}
}
double fixedLegEventsPv = legPricer.presentValueEventsInternal(fixedLeg, provider);
if (fixedLeg.getPaymentPeriods().size() > 1) { // try multiperiod par-rate
// PVBP
double pvbpFixedLeg = legPricer.pvbp(fixedLeg, provider);
// Par rate
return -(otherLegsConvertedPv + fixedLegEventsPv) / pvbpFixedLeg;
}
SwapPaymentPeriod firstPeriod = fixedLeg.getPaymentPeriods().get(0);
ArgChecker.isTrue(firstPeriod instanceof RatePaymentPeriod, "PaymentPeriod must be instance of RatePaymentPeriod");
RatePaymentPeriod payment = (RatePaymentPeriod) firstPeriod;
if (payment.getAccrualPeriods().size() == 1) { // no compounding
// PVBP
double pvbpFixedLeg = legPricer.pvbp(fixedLeg, provider);
// Par rate
return -(otherLegsConvertedPv + fixedLegEventsPv) / pvbpFixedLeg;
}
// try Compounding
Triple<Boolean, Integer, Double> fixedCompounded = checkFixedCompounded(fixedLeg);
ArgChecker.isTrue(fixedCompounded.getFirst(),
"Swap should have a fixed leg and for one payment it should be based on compunding witout spread.");
double notional = payment.getNotional();
double df = provider.discountFactor(ccyFixedLeg, payment.getPaymentDate());
return Math.pow(-(otherLegsConvertedPv + fixedLegEventsPv) / (notional * df) + 1.0d,
1.0 / fixedCompounded.getSecond()) - 1.0d;
}
/**
* Computes the par spread for swaps.
* <p>
* The par spread is the common spread on all payments of the first leg for which the total swap present value is 0.
* <p>
* The par spread will be computed with respect to the first leg. For that leg, all the payments have a unique
* accrual period or multiple accrual periods with Flat compounding and no FX reset.
*
* @param swap the product
* @param provider the rates provider
* @return the par rate
*/
public double parSpread(ResolvedSwap swap, RatesProvider provider) {
ResolvedSwapLeg referenceLeg = swap.getLegs().get(0);
Currency ccyReferenceLeg = referenceLeg.getCurrency();
if (referenceLeg.getPaymentPeriods().size() > 1) { // try multiperiod par-spread
double convertedPv = presentValue(swap, ccyReferenceLeg, provider).getAmount();
double pvbp = legPricer.pvbp(referenceLeg, provider);
return -convertedPv / pvbp;
}
SwapPaymentPeriod firstPeriod = referenceLeg.getPaymentPeriods().get(0);
ArgChecker.isTrue(firstPeriod instanceof RatePaymentPeriod, "PaymentPeriod must be instance of RatePaymentPeriod");
RatePaymentPeriod payment = (RatePaymentPeriod) firstPeriod;
if (payment.getAccrualPeriods().size() == 1) { // no compounding
double convertedPv = presentValue(swap, ccyReferenceLeg, provider).getAmount();
// PVBP
double pvbpFixedLeg = legPricer.pvbp(referenceLeg, provider);
// Par rate
return -convertedPv / pvbpFixedLeg;
}
// try Compounding
Triple<Boolean, Integer, Double> fixedCompounded = checkFixedCompounded(referenceLeg);
ArgChecker.isTrue(fixedCompounded.getFirst(),
"Swap should have a fixed leg and for one payment it should be based on compunding witout spread.");
double df = provider.discountFactor(ccyReferenceLeg, referenceLeg.getPaymentPeriods().get(0).getPaymentDate());
double convertedPv = presentValue(swap, ccyReferenceLeg, provider).getAmount();
double referenceConvertedPv = legPricer.presentValue(referenceLeg, provider).getAmount();
double notional = ((RatePaymentPeriod) referenceLeg.getPaymentPeriods().get(0)).getNotional();
double parSpread =
Math.pow(-(convertedPv - referenceConvertedPv) / (df * notional) + 1.0d, 1.0d / fixedCompounded.getSecond()) -
(1.0d + fixedCompounded.getThird());
return parSpread;
}
//-------------------------------------------------------------------------
/**
* Calculates the present value sensitivity of the swap product.
* <p>
* The present value sensitivity of the product is the sensitivity of the present value to
* the underlying curves.
*
* @param swap the product
* @param provider the rates provider
* @return the present value curve sensitivity of the swap product
*/
public PointSensitivityBuilder presentValueSensitivity(ResolvedSwap swap, RatesProvider provider) {
return swapValueSensitivity(swap, provider, legPricer::presentValueSensitivity);
}
/**
* Calculates the present value sensitivity of the swap product converted in a given currency.
* <p>
* The present value sensitivity of the product is the sensitivity of the present value to
* the underlying curves.
*
* @param swap the product
* @param currency the currency to convert to
* @param provider the rates provider
* @return the present value curve sensitivity of the swap product converted in the given currency
*/
public PointSensitivityBuilder presentValueSensitivity(ResolvedSwap swap, Currency currency, RatesProvider provider) {
PointSensitivityBuilder builder = PointSensitivityBuilder.none();
for (ResolvedSwapLeg leg : swap.getLegs()) {
PointSensitivityBuilder ls = legPricer.presentValueSensitivity(leg, provider);
PointSensitivityBuilder lsConverted =
ls.withCurrency(currency).multipliedBy(provider.fxRate(leg.getCurrency(), currency));
builder = builder.combinedWith(lsConverted);
}
return builder;
}
/**
* Calculates the forecast value sensitivity of the swap product.
* <p>
* The forecast value sensitivity of the product is the sensitivity of the forecast value to
* the underlying curves.
*
* @param swap the product
* @param provider the rates provider
* @return the forecast value curve sensitivity of the swap product
*/
public PointSensitivityBuilder forecastValueSensitivity(ResolvedSwap swap, RatesProvider provider) {
return swapValueSensitivity(swap, provider, legPricer::forecastValueSensitivity);
}
// calculate present or forecast value sensitivity for the swap
private static PointSensitivityBuilder swapValueSensitivity(
ResolvedSwap swap,
RatesProvider provider,
BiFunction<ResolvedSwapLeg, RatesProvider, PointSensitivityBuilder> legFn) {
PointSensitivityBuilder builder = PointSensitivityBuilder.none();
for (ResolvedSwapLeg leg : swap.getLegs()) {
builder = builder.combinedWith(legFn.apply(leg, provider));
}
return builder;
}
/**
* Calculates the par rate curve sensitivity for a swap with a fixed leg.
* <p>
* The par rate is the common rate on all payments of the fixed leg for which the total swap present value is 0.
* <p>
* At least one leg must be a fixed leg. The par rate will be computed with respect to the first fixed leg.
* All the payments in that leg should be fixed payments with a unique accrual period (no compounding) and no FX reset.
*
* @param swap the product
* @param provider the rates provider
* @return the par rate curve sensitivity of the swap product
*/
public PointSensitivityBuilder parRateSensitivity(ResolvedSwap swap, RatesProvider provider) {
ResolvedSwapLeg fixedLeg = fixedLeg(swap);
Currency ccyFixedLeg = fixedLeg.getCurrency();
// other payments (not fixed leg coupons) converted in fixed leg currency
double otherLegsConvertedPv = 0.0;
for (ResolvedSwapLeg leg : swap.getLegs()) {
if (leg != fixedLeg) {
double pvLocal = legPricer.presentValueInternal(leg, provider);
otherLegsConvertedPv += (pvLocal * provider.fxRate(leg.getCurrency(), ccyFixedLeg));
}
}
double fixedLegEventsPv = legPricer.presentValueEventsInternal(fixedLeg, provider);
double pvbpFixedLeg = legPricer.pvbp(fixedLeg, provider);
// Backward sweep
double otherLegsConvertedPvBar = -1.0d / pvbpFixedLeg;
double fixedLegEventsPvBar = -1.0d / pvbpFixedLeg;
double pvbpFixedLegBar = (otherLegsConvertedPv + fixedLegEventsPv) / (pvbpFixedLeg * pvbpFixedLeg);
PointSensitivityBuilder pvbpFixedLegDr = legPricer.pvbpSensitivity(fixedLeg, provider);
PointSensitivityBuilder fixedLegEventsPvDr = legPricer.presentValueSensitivityEventsInternal(fixedLeg, provider);
PointSensitivityBuilder otherLegsConvertedPvDr = PointSensitivityBuilder.none();
for (ResolvedSwapLeg leg : swap.getLegs()) {
if (leg != fixedLeg) {
PointSensitivityBuilder pvLegDr = legPricer.presentValueSensitivity(leg, provider)
.multipliedBy(provider.fxRate(leg.getCurrency(), ccyFixedLeg));
otherLegsConvertedPvDr = otherLegsConvertedPvDr.combinedWith(pvLegDr);
}
}
otherLegsConvertedPvDr = otherLegsConvertedPvDr.withCurrency(ccyFixedLeg);
return pvbpFixedLegDr.multipliedBy(pvbpFixedLegBar)
.combinedWith(fixedLegEventsPvDr.multipliedBy(fixedLegEventsPvBar))
.combinedWith(otherLegsConvertedPvDr.multipliedBy(otherLegsConvertedPvBar));
}
/**
* Calculates the par spread curve sensitivity for a swap.
* <p>
* The par spread is the common spread on all payments of the first leg for which the total swap present value is 0.
* <p>
* The par spread is computed with respect to the first leg. For that leg, all the payments have a unique
* accrual period (no compounding) and no FX reset.
*
* @param swap the product
* @param provider the rates provider
* @return the par spread curve sensitivity of the swap product
*/
public PointSensitivityBuilder parSpreadSensitivity(ResolvedSwap swap, RatesProvider provider) {
ResolvedSwapLeg referenceLeg = swap.getLegs().get(0);
Currency ccyReferenceLeg = referenceLeg.getCurrency();
double convertedPv = presentValue(swap, ccyReferenceLeg, provider).getAmount();
PointSensitivityBuilder convertedPvDr = presentValueSensitivity(swap, ccyReferenceLeg, provider);
if (referenceLeg.getPaymentPeriods().size() > 1) { // try multiperiod par-spread
double pvbp = legPricer.pvbp(referenceLeg, provider);
// Backward sweep
double convertedPvBar = -1d / pvbp;
double pvbpBar = convertedPv / (pvbp * pvbp);
PointSensitivityBuilder pvbpDr = legPricer.pvbpSensitivity(referenceLeg, provider);
return convertedPvDr.multipliedBy(convertedPvBar).combinedWith(pvbpDr.multipliedBy(pvbpBar));
}
SwapPaymentPeriod firstPeriod = referenceLeg.getPaymentPeriods().get(0);
ArgChecker.isTrue(firstPeriod instanceof RatePaymentPeriod, "PaymentPeriod must be instance of RatePaymentPeriod");
RatePaymentPeriod payment = (RatePaymentPeriod) firstPeriod;
if (payment.getAccrualPeriods().size() == 1) { // no compounding
// PVBP
double pvbp = legPricer.pvbp(referenceLeg, provider);
// Backward sweep
double convertedPvBar = -1d / pvbp;
double pvbpBar = convertedPv / (pvbp * pvbp);
PointSensitivityBuilder pvbpDr = legPricer.pvbpSensitivity(referenceLeg, provider);
return convertedPvDr.multipliedBy(convertedPvBar).combinedWith(pvbpDr.multipliedBy(pvbpBar));
}
// try Compounding
Triple<Boolean, Integer, Double> fixedCompounded = checkFixedCompounded(referenceLeg);
ArgChecker.isTrue(fixedCompounded.getFirst(),
"Swap should have a fixed leg and for one payment it should be based on compunding witout spread.");
double df = provider.discountFactor(ccyReferenceLeg, referenceLeg.getPaymentPeriods().get(0).getPaymentDate());
PointSensitivityBuilder dfDr = provider.discountFactors(ccyReferenceLeg)
.zeroRatePointSensitivity(referenceLeg.getPaymentPeriods().get(0).getPaymentDate());
double referenceConvertedPv = legPricer.presentValue(referenceLeg, provider).getAmount();
PointSensitivityBuilder referenceConvertedPvDr = legPricer.presentValueSensitivity(referenceLeg, provider);
double notional = ((RatePaymentPeriod) referenceLeg.getPaymentPeriods().get(0)).getNotional();
PointSensitivityBuilder dParSpreadDr =
convertedPvDr.combinedWith(referenceConvertedPvDr.multipliedBy(-1)).multipliedBy(-1.0d / (df * notional))
.combinedWith(dfDr.multipliedBy((convertedPv - referenceConvertedPv) / (df * df * notional)))
.multipliedBy(1.0d / fixedCompounded.getSecond() *
Math.pow(-(convertedPv - referenceConvertedPv) / (df * notional) + 1.0d,
1.0d / fixedCompounded.getSecond() - 1.0d));
return dParSpreadDr;
}
//-------------------------------------------------------------------------
/**
* Calculates the future cash flows of the swap product.
* <p>
* Each expected cash flow is added to the result.
* This is based on {@link #forecastValue(ResolvedSwap, RatesProvider)}.
*
* @param swap the product
* @param provider the rates provider
* @return the cash flow
*/
public CashFlows cashFlows(ResolvedSwap swap, RatesProvider provider) {
return swap.getLegs().stream()
.map(leg -> legPricer.cashFlows(leg, provider))
.reduce(CashFlows.NONE, CashFlows::combinedWith);
}
//-------------------------------------------------------------------------
/**
* Explains the present value of the swap product.
* <p>
* This returns explanatory information about the calculation.
*
* @param swap the product
* @param provider the rates provider
* @return the explanatory information
*/
public ExplainMap explainPresentValue(ResolvedSwap swap, RatesProvider provider) {
ExplainMapBuilder builder = ExplainMap.builder();
builder.put(ExplainKey.ENTRY_TYPE, "Swap");
for (ResolvedSwapLeg leg : swap.getLegs()) {
builder.addListEntryWithIndex(
ExplainKey.LEGS, child -> legPricer.explainPresentValueInternal(leg, provider, child));
}
return builder.build();
}
//-------------------------------------------------------------------------
/**
* Calculates the currency exposure of the swap product.
*
* @param swap the product
* @param provider the rates provider
* @return the currency exposure of the swap product
*/
public MultiCurrencyAmount currencyExposure(ResolvedSwap swap, RatesProvider provider) {
MultiCurrencyAmount ce = MultiCurrencyAmount.empty();
for (ResolvedSwapLeg leg : swap.getLegs()) {
ce = ce.plus(legPricer.currencyExposure(leg, provider));
}
return ce;
}
/**
* Calculates the current cash of the swap product.
*
* @param swap the product
* @param provider the rates provider
* @return the current cash of the swap product
*/
public MultiCurrencyAmount currentCash(ResolvedSwap swap, RatesProvider provider) {
MultiCurrencyAmount ce = MultiCurrencyAmount.empty();
for (ResolvedSwapLeg leg : swap.getLegs()) {
ce = ce.plus(legPricer.currentCash(leg, provider));
}
return ce;
}
//-------------------------------------------------------------------------
// checking that at least one leg is a fixed leg and returning the first one
private ResolvedSwapLeg fixedLeg(ResolvedSwap swap) {
List<ResolvedSwapLeg> fixedLegs = swap.getLegs(SwapLegType.FIXED);
if (fixedLegs.isEmpty()) {
throw new IllegalArgumentException("Swap must contain a fixed leg");
}
return fixedLegs.get(0);
}
// Checks if the leg is a fixed leg with one payment and compounding
// This type of leg is used in zero-coupon inflation swaps
// When returning a 'true' for the first element, the second element is the number of periods which are used in
// par rate/spread computation and the third element is the common fixed rate
private Triple<Boolean, Integer, Double> checkFixedCompounded(ResolvedSwapLeg leg) {
if (leg.getPaymentEvents().size() != 0) {
return Triple.of(false, 0, 0.0d); // No event
}
RatePaymentPeriod ratePaymentPeriod = (RatePaymentPeriod) leg.getPaymentPeriods().get(0);
if (ratePaymentPeriod.getCompoundingMethod() == CompoundingMethod.NONE) {
return Triple.of(false, 0, 0.0d); // Should be compounded
}
ImmutableList<RateAccrualPeriod> accrualPeriods = ratePaymentPeriod.getAccrualPeriods();
int nbAccrualPeriods = accrualPeriods.size();
double fixedRate = 0;
for (int i = 0; i < nbAccrualPeriods; i++) {
if (!(accrualPeriods.get(i).getRateComputation() instanceof FixedRateComputation)) {
return Triple.of(false, 0, 0.0d); // Should be fixed period
}
if ((i > 0) && (((FixedRateComputation) accrualPeriods.get(i).getRateComputation()).getRate() != fixedRate)) {
return Triple.of(false, 0, 0.0d); // All fixed rates should be the same
}
fixedRate = ((FixedRateComputation) accrualPeriods.get(i).getRateComputation()).getRate();
if (accrualPeriods.get(i).getSpread() != 0) {
return Triple.of(false, 0, 0.0d); // Should have no spread
}
if (accrualPeriods.get(i).getGearing() != 1.0d) {
return Triple.of(false, 0, 0.0d); // Should have a gearing of 1.
}
if (accrualPeriods.get(i).getYearFraction() != 1.0d) {
return Triple.of(false, 0, 0.0d); // Should have a year fraction of 1.
}
}
return Triple.of(true, nbAccrualPeriods, fixedRate);
}
}