Skip to content
All library documents

Solving for Floating-Rate Bond Discount Margin with QuantLib

Article Quant Q&A · Author: Kyle

Summary

The document describes approaches to calculating the discount margin of a floating-rate bond with QuantLib. The initial method builds a spreaded forward curve whose spread is controlled by an observable quote, then uses a one-dimensional numerical solver to adjust that spread until the bond’s calculated clean price matches a supplied market price. The bond setup includes a LIBOR-linked coupon, a schedule, curve inputs, and pricing conventions.

An updated answer presents a more general cashflow-based method: vary a spreaded discount curve, calculate the discount margin of the leg, and solve for the spread that matches the target margin. It also discusses accounting for coupon spread and accrued interest when moving between clean and dirty prices. The author says the later implementation was checked by a colleague, but the excerpt omits part of the final method and supplies no independent benchmark or validation data. Results depend on correct curve, schedule, settlement, and day-count inputs.

Key ideas

  • A spreaded yield curve can represent candidate discount margins for a floating-rate bond.
  • A one-dimensional solver can search for the spread that matches a target price or margin.
  • Cashflow discounting must account for settlement dates and the bond’s coupon spread.
  • Clean prices and dirty prices differ by accrued interest, which matters when solving for margin.
  • The example’s reported calculation is implementation-specific and is not independently benchmarked.

Tags

Full text
# Calculating Discount Margin on a floating rate bond using QuantLib


# Calculating Discount Margin on a floating rate bond using QuantLib












Going off Luigi's hint on this answer: Setting up Schedule for an amortizing floater in QuantLib

I was able to cobble something together but I'm unable to verify if it's correct. TLDR: I was able to generate this cashflow and got a Discount Margin value of 121.42091071060361 for the price input of 90.00. Any insights is appreciated.

First, let's set up the bond: It's a LIBOR+1.77 (vectors come from a third party).













Setting up the bond: I use `InterpolatedForwardCurve` to setup my floating rates vectors and feed it to `ForwardSpreadedTermStructure` in order to accept a spread value. The basic idea is that since `SimpleQuote`, your input spread, is an observable, all changes to it will trigger recalculations in your bond class. I use this input in order to find the spread at which price exactly equals the current price (the price is an input; going from price to get DM).

DMFinder Function: My DMFinder inherits ISolver1d accepts a price as input. The `value(x)` implementation accepts a Spread value and tries to solve for `v` where the the resulting price exactly matches the original price; i.e. the spread value where the price exactly results in a null price at the current DM. As advised by @LuigiBallabio I'm also keeping an instance of the Bond class inside the `DMFinder` class to call `cleanPrice()` as the `SimpleQuote` value changes.

```
private class DMFinder : ISolver1d
{
    private readonly FloatingRateBond bond_;
    private SimpleQuote spread_;
    private double price_;

    public DMFinder(FloatingRateBond bond, double price, SimpleQuote spread)
    {
        this.bond_ = bond;
        this.price_ = price;
        this.spread_ = spread;
    }

    public override double value(double v)
    {
        this.spread.setValue(v);
        var solvedPrice = bond.cleanPrice();
        return this.price - solvedPrice;
    }
}
```

Getting the DM through Solver1D:

```
public static double DM(FloatingRateBond bond, double price, SimpleQuote spread, double accuracy = 1.0e-10, int maxIterations = 100, double guess = 0.05)
{
    var solver = new FiniteDifferenceNewtonSafe();
    solver.SetMaxEvaluations(maxIterations);

    var objFunction = new DMFinder(bond, price, spread);
    var dm = solver.Solve(objFunction, accuracy, guess, guess / 10.0);
    return dm;
}
```

Full bond setup: Full code below.

```
var settleDate = new Date(15, Month.Jan, 2018);                         // Settle date
var settlementDays = 3;                                                 // Settle day number, usually T+3
var faceAmount = 12000000.00;                                           // Current face
var issueDate = new Date(3, Month.Jun, 1997);                           // Issue Date
var maturity = new Date(3, Month.Jun, 2037);                            // Maturity Date

var thirty360 = new Thirty360();                                        // Day counter, using 30/360 convention.
var calendar = new UnitedStates(UnitedStates.Market.Settlement);        // Using USA settlement calendar for holidays/weekends detection
settleDate = calendar.adjust(settleDate);

// Evaluation must be a business day
var today = calendar.advance(settleDate, -settlementDays, TimeUnit.Days);
Settings.setEvaluationDate(today);      // Set business day to today

// An observable, holds the Spread argument
var spread = new SimpleQuote(0.0);

// Vectors are here: https://pastebin.com/HLYWsyux
var yieldCurve = new InterpolatedForwardCurve<Linear>(VECTOR_DATES, VECTORS, thirty360, calendar, new Linear());
yieldCurve.enableExtrapolation();
var spreadedYieldCurve = new ForwardSpreadedTermStructure(new Handle<YieldTermStructure>(yieldCurve), new Handle<Quote>(spread));
spreadedYieldCurve.enableExtrapolation();

var discountingTs = new Handle<YieldTermStructure>(spreadedYieldCurve);
var indexTs = new Handle<YieldTermStructure>(spreadedYieldCurve);

var index = new Libor("USD Libor",          // Family name
    new Period(Frequency.Monthly),          // Frequency of of rates
    2,                                      // Settlement days
    new USDCurrency(),                      // Base currency
    calendar,                               // Calendar used
    thirty360,                              // Day counting convention
    indexTs);                               // Class containing vectors and dates, as well as interpolation

// Generates the payment schedule, always start at issue date
var schedule = new Schedule(issueDate,
    maturity,
    new Period(Frequency.Quarterly),
    calendar,
    BusinessDayConvention.ModifiedFollowing,
    BusinessDayConvention.ModifiedFollowing,
    DateGeneration.Rule.Forward,
    false);

// Setup the bond, issued 06/03/1997, libor+177, matures 06/03/2037
// Next payment: 03/05/2018
var bond = new FloatingRateBond(settlementDays,
    faceAmount,
    schedule,
    index,
    thirty360,
    schedule.businessDayConvention(),
    0,                                  // Fixing days
    new List<double>() { 1, 1.0 },      // LIBOR multiplier (for example, 0.80 * libor)
    new List<double>() { 1, 0.0177 },   // The spread, or "libor plus" => in this case,  x is 0.0177
    new List<double?>() { },            // Caps
    new List<double?>() { },            // Floors
    true,                               // Index fixing in arrears?
    100,                                // Percent of redemption at maturity
    issueDate);                         // When bond was issued

var bondEngine = new DiscountingBondEngine(discountingTs);
bond.setPricingEngine(bondEngine);

// Sets the coupon pricer to the bond
var pricer = new BlackIborCouponPricer();
var vol = 0.0;
var volatility = new ConstantOptionletVolatility(3,
    calendar,
    BusinessDayConvention.ModifiedFollowing,
    vol,
    new Thirty360());

pricer.setCapletVolatility(new Handle<OptionletVolatilityStructure>(volatility));
HelperFunctions.setCouponPricer(bond.cashflows(), pricer);

// now calculate DM
var dm = Cashflows.DM(bond, 90, spread) * 100;
```

## Answer by Kyle (score 1)

https://quant.stackexchange.com/a/38090

Update (2018-10-09):

This solution is more correct. It's a class that solves for the DM using the class `ForwardSpreadedTermStructure`.

```
public class DMFinder : ISolver1d
{
    private readonly List<Cashflow> leg_;
    private readonly double dm_;
    private readonly DayCounter dayCounter_;
    private readonly Compounding compounding_;
    private readonly Frequency frequency_;
    private readonly bool includeSettlementDateFlows_;
    private readonly Date settlementDate_, npvDate_;
    private readonly Handle<YieldTermStructure> discountCurve_;
    private readonly ForwardSpreadedTermStructure forwardSpreadedTs_;
    private readonly SimpleQuote spread_;

    public DMFinder(List<Cashflow> leg,
        double dm,
        Handle<YieldTermStructure> discountCurve,
        DayCounter dayCounter,
        Compounding comp,
        Frequency freq,
        bool includeSettlementDateFlows,
        Date settlementDate,
        Date npvDate)
    {
        leg_ = leg;
        dm_ = dm;
        dayCounter_ = dayCounter;
        compounding_ = comp;
        frequency_ = freq;
        includeSettlementDateFlows_ = includeSettlementDateFlows;
        settlementDate_ = settlementDate;
        npvDate_ = npvDate;
        discountCurve_ = discountCurve;

        if (settlementDate_ == null) settlementDate_ = Settings.EvaluationDate();
        if (npvDate_ == null) npvDate_ = settlementDate_;

        // Get the spread, starting at 0.0
        // This will value will be what the solver will try to solve for
        spread_ = new SimpleQuote(0.0);

        // Declare a new Spreaded Discount Curve using the given curve and the spread
        forwardSpreadedTs_ = new ForwardSpreadedTermStructure(discountCurve_, new Handle<Quote>(spread_));
        forwardSpreadedTs_.EnableExtrapolation(discountCurve_.Instance.AllowsExtrapolation());
    }

    public override double Value(double v)
    {
        // The solver will set the value v continuously until a solution is found
        this.spread_.SetValue(v);
        var solvedDm = Cashflows.DM(leg_, forwardSpreadedTs_, includeSettlementDateFlows_, settlementDate_, npvDate_);

        return dm_ - solvedDm;
    }
}
```

This function discounts the cashflows given the spread:

```
/// <summary>
/// Given a discount curve, calculate the Discount Margin of the cashflows when
/// discounted against the given curve.
/// </summary>
/// <param name="leg">The schedule of cashflows.</param>
/// <param name="discountCurve">The discount curve that is spreaded via a simple quote.</param>
/// <param name="includeSettlementDateFlows">Specifies whether we want to include settlement date flows.</param>
/// <param name="settlementDate">the settlement date.</param>
/// <param name="npvDate">the npv date.</param>
/// <returns>The total calculated npv</returns>
public static double DM(Leg leg,
    ForwardSpreadedTermStructure discountCurve,
    bool includeSettlementDateFlows,
    Date settlementDate = null,
    Date npvDate = null)
{
    if (leg.Empty()) return 0.0;

    if (settlementDate == null)
        settlementDate = Settings.EvaluationDate();

    if (npvDate == null)
        npvDate = settlementDate;

    var iborCoupon = leg.FirstOrDefault(c => c is IborCoupon);
    double cfSpread = 0.0;
    if (iborCoupon != null)
        cfSpread = (iborCoupon as IborCoupon).Spread();

    // If the cashflow's spread is non-zero, take it away from the quote handler
    // inside the ForwardSpreadedTermStruture
    if(cfSpread > 0.0)
    {
        var dmSpreadHandler = discountCurve.Spread();
        var dmSpread = dmSpreadHandler.Instance.Value();

        // Take the cashflow's spread off of the  discount curve's spread
        // By updating the value, we notify the ForwardSpreadedTermStructure to use the new value
        (dmSpreadHandler.Instance as SimpleQuote).SetValue(dmSpread - cfSpread);
    }

    double totalDm = 0.0;

    // Loop through the cashflows and calculate the total DM, based on the discount curve
    foreach(var period in leg
        .Where(l => !l.HasOccurred(settlementDate, includeSettlementDateFlows))
        .GroupBy(l => l.Date())
        .OrderBy(p => p.Key))
    {
        var date = period.Key;

        // In each period, discount against the curve for each amount
        foreach(var cashflow in period)
        {
            totalDm += cashflow.Amount() * discountCurve.Discount(date);
        }
    }

    return totalDm / discountCurve.Discount(npvDate);
}
```

===

This is what I ended up implementing and has been verified to work by a colleague.

Quite similar to calculating yield, the DM function takes a `cleanPrice` and derives a `dirtyPrice` from the accrued amount.

```
public static double DM(Bond bond,
    double cleanPrice,
    DayCounter dayCounter,
    Compounding compounding,
    Frequency frequency,
    Date settlementDate = null,
    double accuracy = 1.0e-10,
    int maxIterations = 100,
    double guess = 0.05)
{
    if (settlementDate == null)
        settlementDate = bond.SettlementDate();

    Thrower.AssertValid(BondFunctions.IsTradable(bond, settlementDate), () =>
    $"non tradable at {settlementDate} (maturity being {bond.MaturityDate()})");

    double dirtyPrice = cleanPrice + bond.AccruedAmount(settlementDate);
    dirtyPrice /= 100.0 / bond.Notional(settlementDate);

    return Cashflows.DM(bond.Cashflows(), dirtyPrice,
                            dayCounter, compounding, frequency,
                            false, settlementDate, settlementDate,
                            accuracy, maxIterations, guess);
}
```

Once you have the target NPV (the `dirtyPrice`), you feed it to the next function, which does the actual calculations:

```
public static double DM(Leg leg,
    double npv,
    DayCounter dayCounter,
    Compounding compounding,
    Frequency frequency,
    bool includeSettlementDateFlows,
    Date settlementDate = null,
    Date npvDate = null,
    double accuracy = 1.0e-10,
    int maxIterations = 100,
    double guess = 0.05)
{
    var solver = new FiniteDifferenceNewtonSafe();
    solver.SetMaxEvaluations(maxIterations);
    var objFunction = new DMFinder(leg,
        npv,
        dayCounter,
        compounding,
        frequency,
        includeSettlementDateFlows,
        settlementDate,
        npvDate);

    return solver.Solve(objFunction, accuracy, guess, guess / 10.0);
}
```

And this is the `DMFinder` function. It solves for DM by given a `discountRate` and stops solving when a given `discountRate` results in the original DM (NPV).

```
private class DMFinder : ISolver1d
{
    private Leg leg;
    private double dm;
    private DayCounter dayCounter;
    private Compounding compounding;
    private Frequency frequency;
    private bool includeSettlementDateFlows;
    private Date settlementDate, npvDate;

    public DMFinder(Leg leg,
        double dm,
        DayCounter dayCounter,
        Compounding comp,
        Frequency freq,
        bool includeSettlementDateFlows,
        Date settlementDate,
        Date npvDate)
    {
        this.leg = leg;
        this.dm = dm;
        this.dayCounter = dayCounter;
        this.compounding = comp;
        this.frequency = freq;
        this.includeSettlementDateFlows = includeSettlementDateFlows;
        this.settlementDate = settlementDate;
        this.npvDate = npvDate;

        if (settlementDate == null)
            settlementDate = Settings.EvaluationDate();

        if (npvDate == null)
            this.npvDate = this.settlementDate;
    }

    public override double Value(double v)
    {
        var discountRate = new InterestRate(v, this.dayCounter, this.compounding, this.frequency);
        var solvedDm = Cashflows.DM(this.leg, discountRate, this.includeSettlementDateFlows, this.settlementDate, this.npvDate);
        return this.dm - solvedDm;
    }
}
```

And Finally, this is where the total DM calc happens. This is needed by the solver. It takes a given `discountRate` and calculates total DM. Note that it doesn't taken into account the given Spread (the floater margin):

```
/// <summary>
/// Gets the total DM given a discount rate.
/// </summary>
/// <param name="leg">The cashflows.</param>
/// <param name="dm">The discount rate.</param>
/// <param name="includeSettlementDateFlows">Indicates whether to includ cashflows that occurred on the settlement date.</param>
/// <param name="settlementDate">The settlement date.</param>
/// <param name="npvDate">The NPV date.</param>
/// <returns>The total DM.</returns>
public static double DM(Leg leg,
    InterestRate dm,
    bool includeSettlementDateFlows,
    Date settlementDate = null,
    Date npvDate = null)
{
    if (leg.Empty()) return 0.0;

    if (settlementDate == null)
        settlementDate = Settings.EvaluationDate();

    if (npvDate == null)
        npvDate = settlementDate;

    var totalDm = 0.0;
    var discount = 1.0;
    var lastRate = 0.0;
    var lastSpread = 0.0;
    var lastDate = npvDate;
    Date refStartDate, refEndDate;

    for(var i = 0; i< leg.Count; ++i)
    {
        if (leg[i].HasOccurred(settlementDate, includeSettlementDateFlows))
            continue;

        var couponDate = leg[i].Date();
        var amount = leg[i].Amount();
        if (leg[i].TradingExCoupon(settlementDate))
            amount = 0.0;

        var coupon = leg[i] as Coupon;
        if(coupon != null)
        {
            refStartDate = coupon.ReferencePeriodStart;
            refEndDate = coupon.ReferencePeriodEnd;
        }
        else
        {
            if (lastDate == npvDate)
            {
                // we don't have a previous coupon date,
                // so we fake it
                refStartDate = couponDate - new Period(1, TimeUnit.Years);
            }
            else
            {
                refStartDate = lastDate;
            }
            refEndDate = couponDate;
        }

        double l, h;
        if (settlementDate > refStartDate && settlementDate < refEndDate)
            l = dm.DayCounter().YearFraction(settlementDate, refEndDate, refStartDate, refEndDate);
        else
        {
            l = dm.DayCounter().YearFraction(lastDate, couponDate, refStartDate, refEndDate);
            // if (coupon != null) l = coupon.AccrualPeriod();
            // else l = dm.DayCounter().YearFraction(lastDate, couponDate, refStartDate, refEndDate);
        }

        double rate;
        if (coupon != null) rate = coupon.Rate();
        else rate = lastRate; // Fake the last rate if the coupon doesn't come with a rate

        var spread = 0.0;
        if ((leg[i] as IborCoupon) != null) spread = (leg[i] as IborCoupon).Spread();
        else spread = lastSpread;

        h = rate + dm.Rate() - spread;
        discount /= (1 + (l * h));

        totalDm += discount * amount;

        lastDate = couponDate;
        lastRate = rate;
        lastSpread = spread;
    }

    return totalDm;
}
```

Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.