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.
- Term Structure (
ql.ZeroCurve): This represents the spot rate curve, , which is our foundation for both forecasting and discounting. - Reference Index (
ql.IborIndex): This object (e.g.,ql.Euribor3M) represents the floating reference rate. By linking it to ourZeroCurve, 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. - Instrument (
ql.FloatingRateBond): This object defines the bond's contractual terms: its face value, maturity, payment schedule, and—importantly—its contracted spread (thespreadsparameter in the constructor). - Pricing Engine (
ql.DiscountingBondEngine): This is the calculator. It takes the cash flows defined by theFloatingRateBondobject 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:
- Forecasting Curve: The base spot rate curve. We pass this to the
IborIndexto calculate forward rates for coupon forecasting.
- 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 theDiscountingBondEngine.
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.
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 baseZeroCurve. Used by theEuribor3Mindex.discount_handle: Points to theZeroSpreadedTermStructure. Used by theDiscountingBondEngine.
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
FloatingRateBondby 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.