Testing Cheapest-to-Deliver Switches Under Parallel Yield Shifts
Summary
The document sets up a small Treasury futures example to investigate whether parallel yield curve shifts can change which bond is cheapest to deliver. It uses two hypothetical deliverable bonds, prices them from a shifted curve, applies conversion factors, and compares their implied bases across a range of spread shifts. The stated motivation is to understand CTD switching before building an option-adjusted DV01 model.
The example provides code and made-up inputs, but reports no output or explanation for why the CTD does not switch. Its basis calculation and bond pricing conventions therefore need to be checked before drawing conclusions. In particular, the bond clean price is computed at a fixed yield while the curve is shifted, so the displayed bond values may not reflect the shifted curve as intended. The exercise is illustrative rather than evidence of how a real Treasury futures basket behaves; actual CTD analysis also depends on delivery economics and consistent settlement, accrued-interest, and conversion-factor treatment.
Key ideas
- A CTD analysis compares candidate deliverable bonds after applying futures conversion factors.
- Parallel curve shifts can be used to explore whether relative delivery economics change.
- The example gives hypothetical bond inputs and code but no observed CTD switch or result.
- A fixed yield used to calculate clean prices may prevent the shifted curve from affecting bond valuations as intended.
Tags
Full text
# Minimal example of CTD switch due to parallel yield curve shift
# Minimal example of CTD switch due to parallel yield curve shift
I am trying to produce a minimal example using completely made up numbers where I can observe a CTD switch due to a parallel yield curve shift. The end goal is to build a simple option-adjusted DV01 model. At this stage, I don't understand why I can't observe a CTD switch given the large parallel shifts I am applying to the curve. Please see my code below
```
import QuantLib as ql
# Function to create a treasury bond
def create_treasury_bond(
bond_issue_date,
bond_maturity_date,
coupon_rate,
business_convention=ql.Following,
coupon_frequency=ql.Period(ql.Semiannual),
day_count=ql.ActualActual(ql.ActualActual.ISDA),
calendar=ql.UnitedStates(ql.UnitedStates.GovernmentBond),
face_amount=100.0,
settlement_days=0,
end_of_month=False,
date_generation=ql.DateGeneration.Forward
):
schedule = ql.Schedule(bond_issue_date,
bond_maturity_date,
coupon_frequency,
calendar,
business_convention,
business_convention,
date_generation,
end_of_month)
security = ql.FixedRateBond(settlement_days,
face_amount,
schedule,
[coupon_rate],
day_count)
return security
# Common settings
calc_date = ql.Date(30,11,2015)
ql.Settings.instance().evaluationDate = calc_date
day_count = ql.ActualActual(ql.ActualActual.ISDA)
settlement_days = 0
calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond)
business_convention=ql.Following
futures_price = 125
# Yield curve
r = ql.SimpleQuote(0.02)
yield_curve = ql.FlatForward(0, ql.TARGET(), ql.QuoteHandle(r), ql.Actual360())
yield_curve_handle = ql.YieldTermStructureHandle(yield_curve)
# Deliverable basket
basket = [(2, ql.Date(15,8,2022), 100),
(10.5, ql.Date(15,2,2045), 150)
]
# Function to determine CTD
def get_ctd_data(futures_price, basket, yield_curve_handle):
securities = []
min_basis = 100; min_basis_index = -1
for i, b in enumerate(basket):
coupon, maturity, price = b
issue = maturity - ql.Period(10, ql.Years)
s = create_treasury_bond(issue, maturity, coupon/100.)
bond_engine = ql.DiscountingBondEngine(yield_curve_handle)
s.setPricingEngine(bond_engine)
cf = ql.BondFunctions.cleanPrice(s,0.06,
day_count, ql.Compounded,
ql.Semiannual, calc_date)/100.
adjusted_futures_price = futures_price * cf
basis = price-adjusted_futures_price
if basis< min_basis:
min_basis = basis
min_basis_index = i
securities.append((s,cf, basis))
ctd_info = basket[min_basis_index]
ctd_bond, ctd_cf, basis = securities[min_basis_index]
ctd_price = ctd_info[2]
return ctd_info, ctd_bond, ctd_cf, ctd_price, min_basis
# Shifted yield curve
spread = ql.SimpleQuote(0.0)
shifted_yield_curve = ql.ZeroSpreadedTermStructure(yield_curve_handle,
ql.QuoteHandle(spread))
shifted_yield_curve_handle = ql.YieldTermStructureHandle(shifted_yield_curve)
# Shift yield curve and calculate the CTD information
for i in range(-100, 100, 10):
spread.setValue(i/10000.0)
ctd_info, ctd_bond, ctd_cf, ctd_price, min_basis = get_ctd_data(futures_price,
basket,
shifted_yield_curve_handle)
print("%-30s = %lf" % ("Minimum Basis", min_basis))
print("%-30s = %lf" % ("Conversion Factor", ctd_cf))
print("%-30s = %lf" % ("CTD coupon", ctd_info[0]))
print("%-30s = %s" % ("CTD maturity", ctd_info[1]))
print("%-30s = %lf" % ("CTD price", ctd_info[2]))
```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.