Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 27

Pricing a Floating Rate Note with Python

Recommended resources

Introduction

In our last lesson, we established the theoretical foundation for pricing floating rate notes (FRNs), focusing on the general discount margin method. We learned that this involves forecasting future coupons using forward rates plus a contracted spread, and then discounting all cash flows using a market-driven discount curve composed of spot rates plus a market spread.

Today, we will translate that theory into practice. Your goal is to implement a Python function capable of pricing an FRN using a given term structure. To do this efficiently and robustly, we will leverage QuantLib, a premier open-source library for quantitative finance. You will see how the abstract concepts from our previous lesson map directly onto concrete components within the library, such as yield curves, indices, and pricing engines. We will also address a crucial real-world detail: the necessity of handling historical index fixings for accurate valuation.


Core Components for FRN Pricing in QuantLib

Pricing an instrument in QuantLib involves assembling several building blocks that represent different aspects of the financial product and the market environment. Let's start by understanding the key components for an FRN. The following article, while focused on capped/floored floaters, provides an excellent code structure that we will adapt for a standard FRN.

Pricing Capped and Floored Floating-Rate Bonds with QuantLib

This article from Data Dataninjago demonstrates how to construct and price floating-rate instruments in QuantLib.

Focus on the Python code in the second section of the article. Don't worry about the cap/floor logic for now. Instead, pay attention to the main objects being created: ql.Settings.instance().evaluationDate: Setting the valuation date. ql.ZeroCurve: Building a yield curve from spot rates. ql.RelinkableYieldTermStructureHandle: A handle to pass the curve around. ql.Euribor3M: The reference index for floating coupons. Notice how it takes the yield curve handle. ql.Schedule: Defining the bond's payment dates. ql.FloatingRateBond: The main instrument object. ql.DiscountingBondEngine: The engine that performs the valuation. Trace how these objects are created and linked together in the create_floating_rate_bond function.

As you can see from the code, pricing is not a single function call but a process of constructing and connecting independent components. Let's break down how this mirrors the theory.

  1. Term Structure (ql.ZeroCurve): This represents the spot rate curve, , which is our foundation for both forecasting and discounting.
  2. Reference Index (ql.IborIndex): This object (e.g., ql.Euribor3M) represents the floating reference rate. By linking it to our ZeroCurve, we are telling QuantLib to use the curve's implied forward rates to forecast future coupon payments. This directly implements the "forecast cash flows" step from our model.
  3. Instrument (ql.FloatingRateBond): This object defines the bond's contractual terms: its face value, maturity, payment schedule, and—importantly—its contracted spread (the spreads parameter in the constructor).
  4. Pricing Engine (ql.DiscountingBondEngine): This is the calculator. It takes the cash flows defined by the FloatingRateBond object and discounts them to the evaluation date. The key here is which curve it uses for discounting.

Implementing the Discount Margin Method

The crucial part of the discount margin method is the potential difference between the contracted spread (used for coupons) and the market spread (used for discounting). We can model this in QuantLib by creating two distinct yield curves:

  1. Forecasting Curve: The base spot rate curve. We pass this to the IborIndex to calculate forward rates for coupon forecasting.
  2. Discounting Curve: The base spot rate curve plus the market spread. We achieve this using ql.ZeroSpreadedTermStructure, which wraps the original curve and adds a spread to it. This new curve is then passed to the DiscountingBondEngine.

This separation perfectly implements the two-step valuation process we derived.


A Practical Necessity: Handling Past Fixings

When you price a bond, the coupon for the current period is not a forecast; its rate was already set (or "fixed") on the last reset date. A pricing engine needs this historical value to calculate the next cash flow and accrued interest correctly. If the fixing is not provided, QuantLib will raise an error.

The following resource clearly illustrates this problem and its solution.

Pricing over a range of days

This post from Luigi Ballabio's "Implementing QuantLib" blog dives into the practical issue of past fixings.

Read the section "A complication: past fixings". Note the RuntimeError that occurs when pricing a FloatingRateBond without its required fixing. The solution is to use the index.addFixing(date, rate) method to supply the known historical rate to the index object.

This is a critical step in any real-world implementation. Before pricing, you must load all historical fixings for the reference index that are relevant to the bond's past coupon periods, especially the one governing the current coupon.


Python Implementation

Now let's synthesize these concepts into a complete Python function. The code below defines a function price_floater that encapsulates the entire process. It takes the bond's characteristics, the spot curve data, and both the contracted and market spreads as input.

Note how we construct two separate curve handles:

  • forecast_handle: Points to the base ZeroCurve. Used by the Euribor3M index.
  • discount_handle: Points to the ZeroSpreadedTermStructure. Used by the DiscountingBondEngine.
import QuantLib as ql

def price_floater(
    eval_date,
    settlement_date,
    maturity_date,
    contracted_spread,
    market_spread,
    face_value,
    spot_curve_dates,
    spot_curve_rates,
    fixing_date,
    fixing_rate
):
    """
    Prices a Floating Rate Note using the discount margin method in QuantLib.

    Args:
        eval_date (ql.Date): The date of evaluation.
        settlement_date (ql.Date): The settlement date of the bond.
        maturity_date (ql.Date): The maturity date of the bond.
        contracted_spread (float): The spread over the index specified in the bond contract.
        market_spread (float): The currently required market spread (discount margin).
        face_value (float): The principal amount of the bond.
        spot_curve_dates (list[ql.Date]): Dates for the spot curve points.
        spot_curve_rates (list[float]): Rates for the spot curve points.
        fixing_date (ql.Date): The date of the last known coupon fixing.
        fixing_rate (float): The rate of the last known coupon fixing.
    
    Returns:
        dict: A dictionary containing the clean price, dirty price, and accrued interest.
    """
    



    # 1. Set Evaluation Date
    ql.Settings.instance().evaluationDate = eval_date
    



    # 2. Construct the base spot curve for forecasting
    day_counter = ql.Actual360()
    calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond)
    
    spot_curve = ql.ZeroCurve(spot_curve_dates, spot_curve_rates, day_counter)
    forecast_handle = ql.YieldTermStructureHandle(spot_curve)
    



    # 3. Construct the discounting curve by adding the market spread
    # The market spread is a continuous-compounded rate
    spread_quote = ql.SimpleQuote(market_spread)
    spread_handle = ql.QuoteHandle(spread_quote)
    discount_curve = ql.ZeroSpreadedTermStructure(forecast_handle, spread_handle)
    discount_handle = ql.YieldTermStructureHandle(discount_curve)




    # 4. Define the reference index and add the required fixing
    # This index is used for FORECASTING future coupons
    index = ql.Euribor3M(forecast_handle)
    index.addFixing(fixing_date, fixing_rate)




    # 5. Define the bond's schedule
    schedule = ql.Schedule(
        settlement_date,
        maturity_date,
        ql.Period(ql.Quarterly),
        calendar,
        ql.ModifiedFollowing,
        ql.ModifiedFollowing,
        ql.DateGeneration.Backward,
        False
    )




    # 6. Construct the Floating Rate Bond instrument
    bond = ql.FloatingRateBond(
        settlementDays=2,
        faceAmount=face_value,
        schedule=schedule,
        index=index,
        paymentDayCounter=day_counter,
        spreads=[contracted_spread]
    )




    # 7. Create the pricing engine with the DISCOUNTING curve
    engine = ql.DiscountingBondEngine(discount_handle)
    bond.setPricingEngine(engine)




    # 8. Calculate and return prices
    return {
        "dirty_price": bond.dirtyPrice(),
        "clean_price": bond.cleanPrice(),
        "accrued_interest": bond.accruedAmount()
    }



```grasp
{
  "type": "exercise",
  "id": "c351a06b-022b-45d6-98e6-9a02fa443142"
}

--- Example Usage ---

if name == 'main':

# Market and bond data
today = ql.Date(15, 5, 2023)




# Spot curve data (upward sloping)
spot_dates = [ql.Date(15, 5, 2023), ql.Date(15, 5, 2024), ql.Date(15, 5, 2028), ql.Date(15, 5, 2033)]
spot_rates = [0.03, 0.035, 0.04, 0.042]




# Bond details
bond_settlement = ql.Date(17, 5, 2023)
bond_maturity = ql.Date(17, 5, 2028)




# Previous coupon was set on Feb 17, 2023
last_fixing_date = ql.Date(15, 2, 2023) # Fixing is usually T-2
last_fixing_rate = 0.028 # 2.8%




# Spreads
bond_contracted_spread = 0.0050 # 50 bps
current_market_spread = 0.0075 # 75 bps, market demands a higher spread




# Since market spread > contracted spread, we expect the bond to trade at a discount.

price_info = price_floater(
    eval_date=today,
    settlement_date=bond_settlement,
    maturity_date=bond_maturity,
    contracted_spread=bond_contracted_spread,
    market_spread=current_market_spread,
    face_value=100,
    spot_curve_dates=spot_dates,
    spot_curve_rates=spot_rates,
    fixing_date=last_fixing_date,
    fixing_rate=last_fixing_rate
)

print(f"Evaluation Date: {today}")
print(f"Contracted Spread: {bond_contracted_spread*100:.2f}%")
print(f"Market Spread (Discount Margin): {current_market_spread*100:.2f}%")
print("-" * 30)
print(f"Clean Price: {price_info['clean_price']:.4f}")
print(f"Accrued Interest: {price_info['accrued_interest']:.4f}")
print(f"Dirty Price: {price_info['dirty_price']:.4f}")

Running this script demonstrates the full workflow. Because the required market spread (75 bps) is higher than the bond's contracted spread (50 bps), the bond is less attractive, and its clean price is below par, as expected.


```grasp
{
  "type": "exercise",
  "id": "c96a046e-a696-4477-8def-471af7612cbd"
}

Conclusion

In this lesson, you have successfully bridged the gap between theory and practice by implementing a Python function to price a floating rate note.

Key Takeaways:

  • Component-Based Pricing: We assembled a FloatingRateBond by combining market data (ZeroCurve), instrument definitions (Schedule, IborIndex), and a valuation model (DiscountingBondEngine).
  • Implementing the Discount Margin: The key to correctly modeling the market spread was to use two different curves: a base curve for forecasting linked to the index, and a spreaded curve for discounting linked to the pricing engine.
  • The Importance of Fixings: We saw that providing historical fixings via index.addFixing() is a mandatory step for pricing an FRN on any date after its first coupon period has begun.
  • QuantLib as a Tool: You've seen how a library like QuantLib handles the complex machinery of date calculations and cash flow generation, allowing you to focus on correctly applying the financial logic.

In our next lesson, we will leverage this understanding and our new price_floater function to explore a more complex instrument: the inverse floating rate note. You will see how it can be deconstructed into a long position in a fixed-rate bond and a short position in a standard floater, making its pricing a direct application of what we've learned today.