Pricing Callable Amortizing Danish Mortgage Bonds in QuantLib
Summary
The document describes an attempted QuantLib approach to pricing callable, fixed-rate Danish mortgage bonds with and without amortization. It extends a callable fixed-rate bond class to build coupon cash flows from a schedule of changing notionals, constructs a yield curve from swap rates, and supplies call dates to a Hull–White tree pricing engine. The author compares the model price with a market quote and observes that the modeled price is lower. They also find that adding amortization lowers the model price, contrary to their market experience.
These observations identify implementation and calibration questions rather than establish a pricing method. The document does not resolve whether the custom cash flows correctly represent the bond’s amortization, explain the price difference, or validate the model against market data. Its example uses a particular curve setup and tree configuration, so the results cannot establish general behavior for callable mortgage bonds.
Key ideas
- The example models amortization by assigning a changing notional to each period in a fixed-rate leg.
- It builds a yield curve from swap-rate helpers and prices callability with a Hull–White tree engine.
- The author reports a modeled price below a market quote and lower modeled prices after adding amortization.
- The document raises, but does not answer, questions about cash-flow construction, model setup, and calibration.
Tags
Full text
# Pricing a amortizing callable danish mortgage bond using quantlib
# Pricing a amortizing callable danish mortgage bond using quantlib
I am trying to price a callable fixed rate amortizing danish mortgage bond. since QuantLib only has `CallableFixedRateBond` i have created a dervived class called `CallableAmortizedBond` which looks like `AmortizingFixedRateBond`:
```
class CallableAmortizedBond : public CallableFixedRateBond
{
public:
CallableAmortizedBond(
Natural settlementDays,
Real faceAmount,
const Schedule &schedule,
const vector<Rate> &coupons,
const DayCounter &accrualDayCounter,
BusinessDayConvention paymentConvention,
Real redemption,
const Date &issueDate,
const vector<Rate> ¬ionals,
const CallabilitySchedule &putCallSchedule)
: CallableFixedRateBond(settlementDays, faceAmount, schedule, coupons, accrualDayCounter, paymentConvention, redemption, issueDate, putCallSchedule)
{
cashflows_ =
FixedRateLeg(schedule)
.withNotionals(notionals)
.withCouponRates(coupons, accrualDayCounter)
.withPaymentAdjustment(paymentConvention);
}
};
```
I have added notionals with inspiration from:
```
Schedule sinkingSchedule(const Date& startDate,
const Period& maturityTenor,
const Frequency& sinkingFrequency,
const Calendar& paymentCalendar)
```
Which can look like this for different bonds:
For bond with no amortization it will be a vector of faceValues e.g. `[100.0 .. 100.0]` For bonds with partial amortization it will be with faceValues until amortization starts and then fully amortizing: `[100.0, 100.0 .. 99.0 97.0 .. 0.0]` For bonds with full amortization it will be `[100.0, 99.0 .. 0.0]`
I use swaprates to create a `YieldTermStructure` with the following values:
```
2Y -> -0.13
3Y -> -0.09
4Y -> -0.03
5Y -> -0.03
6Y -> 0.1
7Y -> 0.18
8Y -> 0.25
9Y -> 0.32
10Y -> 0.39
12Y -> 0.51
15Y -> 0.65
```
like this:
```
Handle<YieldTermStructure> calculateTermStructure(rust::Vec<SwapRates> swapRates, RustDate now)
{
Calendar calendar = TARGET();
Date settlementDate(now.day, getMonthFromInt(now.month), now.year);
Frequency swFixedLegFrequency = Annual;
BusinessDayConvention swFixedLegConvention = Unadjusted;
DayCounter swFixedLegDayCounter = dayCounter;
DayCounter termStructureDayCounter = dayCounter;
ext::shared_ptr<IborIndex> swFloatingLegIndex(new Euribor1Y);
const Period forwardStart(1 * Days);
std::vector<ext::shared_ptr<RateHelper>> swapInstruments;
for (auto const &swapRate : swapRates)
{
ext::shared_ptr<Quote> simpleQuote(new SimpleQuote(swapRate.rate));
ext::shared_ptr<RateHelper> swapRateHelper(new SwapRateHelper(
Handle<Quote>(simpleQuote), swapRate.maturity * Years,
calendar, swFixedLegFrequency,
swFixedLegConvention, swFixedLegDayCounter,
swFloatingLegIndex, Handle<Quote>(), forwardStart));
swapInstruments.push_back(swapRateHelper);
}
ext::shared_ptr<YieldTermStructure> swapTermStructure(
new PiecewiseYieldCurve<Discount, LogLinear>(
settlementDate, swapInstruments,
termStructureDayCounter));
Handle<YieldTermStructure> termStructure(swapTermStructure);
termStructure->enableExtrapolation();
return termStructure;
}
```
I create the `CallabilitySchedule` using the following function:
```
CallabilitySchedule getCallSchedule(rust::Vec<RustDate> callSchedule)
{
CallabilitySchedule callabilitySchedule;
Real callPrice = 100.;
for (auto const &callRustRate : callSchedule)
{
Bond::Price bondCallPrice(callPrice, Bond::Price::Clean);
Date callDate(callRustRate.day, getMonthFromInt(callRustRate.month), callRustRate.year);
callabilitySchedule.push_back(
ext::make_shared<Callability>(
bondCallPrice,
Callability::Call,
callDate));
}
return callabilitySchedule;
}
```
Which i then use to price a callable mortgage bond:
```
Integer gridIntervals = 40;
Real reversionParameter = .03;
// output price/yield results for varying volatility parameter
Real sigma = QL_EPSILON; // core dumps if zero on Cygwin
ext::shared_ptr<ShortRateModel> hw0(
new HullWhite(termStructure, reversionParameter, sigma));
ext::shared_ptr<PricingEngine> engine0(
new TreeCallableFixedRateBondEngine(hw0, gridIntervals));
ext::shared_ptr<PricingEngine> bondEngine(
new DiscountingBondEngine(termStructure));
vector<Rate> notionalRates;
for (auto const ¬ional : notionals)
{
notionalRates.push_back(notional);
}
CallableAmortizedBond callableAmortizedBond(settlementDays, faceAmount, schedule,
vector<Rate>(1, coupon),
dayCounter, paymentConvention,
redemption, datedDate, notionalRates, callabilitySchedule);
callableAmortizedBond.setPricingEngine(engine0);
return callableAmortizedBond.cleanPrice();
```
The price returned using `.cleanPrice()` is much lower than the price found on Nasdaq. For example this bond has a last price of `91.210` but my price using the current swap rates is `88.542` with no amortization.
Another issue is that with amortization i get lower prices than without. Which is opposite of what happens in the real world. So is this correct way to price callable amortizing mortgage bonds? And is it the correct way to add amortization for the callable bond class?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.