Treasury Curve Pricing: Bill Conventions, Settlement, and Curve Choice
Summary
The document addresses discrepancies when using on-the-run Treasury yields and coupon-strip yields to build discount curves and price a Treasury bond. The response identifies the coupon-strip curve in the example as likely erroneous because its yields sit far above the on-the-run curve. It also explains that a Treasury bill modeled as a fixed-rate bond can still be priced from discount factors, but that this representation does not make all quoted rate measures equivalent.
Bill settlement timing matters: bills and bonds settle after the trade date, so directly equating a curve discount factor with a bill price can mislead. Discount rate, simple rate, and yield to maturity use different conventions and can produce different figures for the same instrument. Finally, a bond repriced from the same calibrated curve and market price will have a zero Z-spread by construction. That result confirms internal repricing, not independent evidence that the curve is generally accurate. The discussion offers a diagnosis of one setup, not a universal curve-building prescription.
Key ideas
- A coupon-strip curve far above the on-the-run curve may indicate faulty input or construction.
- Bills can be priced from discount factors even when represented with a fixed-rate bond model.
- Settlement conventions affect comparisons between bill prices and curve discount factors.
- Bill discount rates, simple rates, and yields to maturity are distinct measures.
- A zero Z-spread against the price used to calibrate a curve is expected and does not independently validate it.
Tags
Full text
# Pricing a bond using different curve, otr vs c-strip
# Pricing a bond using different curve, otr vs c-strip
I am trying to train building curve and price different bonds. Yet I get different results depending on the curve I use. All the data I am using is data observed on the 2024-9-17.
My goal is quite simple I want to price the bond with ISIN :US91282CLJ89 using an OTR curve and using the Coupon Strip curve.
First on the 2024-9-17 this is what my two curves look like (blue line is the YTM of the OTR and green is YTM of C-strip) [![enter image description here][1]][1]
Fo me it's a bit weird to see such a difference for the C-strip curve. The C-Strip curve yields is so much above the on-the-run curve.
Then using rateslib I define my on-the-run bonds:
```
bond = [FixedRateBond(effective=datetime(2024, 9, 17),termination=datetime(2024,10,15),convention='ActActISMA',frequency='S', fixed_rate=0.0,curves='curve'), FixedRateBond(effective=datetime(2024, 9, 17),termination=datetime(2024, 11, 12), convention='ActActISMA',frequency='S', fixed_rate=0.0,curves='curve'),
FixedRateBond(effective=datetime(2024, 9, 17),termination=datetime(2024, 12, 19),convention='ActActISMA',frequency='S', fixed_rate=0.0,curves='curve'), FixedRateBond(effective=datetime(2024, 9, 17),termination=datetime(2025, 3, 20), convention='ActActISMA',frequency='S', fixed_rate=0.0,curves='curve'),
FixedRateBond(effective=datetime(2024, 9, 17),termination=datetime(2025, 9, 4),convention='ActActISMA',frequency='S', fixed_rate=0.0,curves='curve'), FixedRateBond(effective=datetime(2024, 9, 17),termination=datetime(2026, 8, 31),convention='ActActISMA',frequency='S', fixed_rate=3.75,curves='curve'),
FixedRateBond(effective=datetime(2024, 9, 17),termination=datetime(2027, 9, 15), convention='ActActISMA',frequency='S', fixed_rate=3.375,curves='curve'), FixedRateBond(effective=datetime(2024, 9, 17),termination=datetime(2029, 8, 31),convention='ActActISMA',frequency='S', fixed_rate=3.625,curves='curve'),
FixedRateBond(effective=datetime(2024, 9, 17),termination=datetime(2031, 8, 31),convention='ActActISMA',frequency='S', fixed_rate=3.75,curves='curve'), FixedRateBond(effective=datetime(2024, 9, 17),termination=datetime(2034,8,15),convention='ActActISMA',frequency='S', fixed_rate=3.875,curves='curve'),
FixedRateBond(effective=datetime(2024, 9, 17),termination=datetime(2044,8,15),convention='ActActISMA',frequency='S', fixed_rate=4.125,curves='curve'), FixedRateBond(effective=datetime(2024, 9, 17),termination=datetime(2054, 8, 15),convention='ActActISMA',frequency='S', fixed_rate=4.25,curves='curve')]
clean_price = [99.638313, 99.26323, 98.795375, 97.776677, 96.264532, 100.294922, 99.789062, 100.871094, 101.371094, 101.929688, 101.390625, 105.179688]
curve_nodes = {b.kwargs['termination']: 1.0 for b in bond}
curve_nodes[datetime(2024, 9, 17)] = 1.0
curve_nodes = dict(sorted(curve_nodes.items()))
curve = Curve(
nodes= curve_nodes,
id="curve",
interpolation='log_linear'
)
solver = Solver(
curves=[curve],
instruments=bond,
s=clean_price,
weights=[1.] * len(bond)
)
```
First thing I found weird here is that I get a discount factor of: 0.996249 for the 2024-10-15 ( so at the maturity of the first t-bill).
So the first T-Bill YTM should be:
```
(1-0.996249) / (0.996249 * 28/366) * 100 = 4.921553604714073
```
But doing:
```
bond[0].rate(curves=curve, metric='ytm')
```
I get a different result of: 4.862566
Why is that?
Otherwise now that I have my discount curve based on the on-the-run curve I can price the bond I want which is the following bond:
```
bond1 = FixedRateBond(effective=datetime(2024, 9, 17), termination=datetime(2031,8,31),convention='ActActISMA',frequency='S', fixed_rate=3.75)
```
Then doing
```
bond1.rate(curves=curve, metric='ytm')
```
where I get: 3.526054 with a Z-spread of:
```
bond1.oaspread(curves=curve, price=101.371094) # = 1.0115364265816287e-08
```
which I guess means this curve is not too bad for pricing since the Z-spread is quite small?
Now If I do exactly the same thing but instead I use the Coupon-Strip curve instead of the OTR curve I get the following resut:
```
bond1.rate(curves=curve_strip, metric='ytm') # = 5.475895
```
which is completely off from what I get using the OTR curve.
Any ideas of why? Is this a good way to price a bond to use the OTR curve and doing this logic, or an other method should be used?
Thanks
## Answer by Attack68 (score 1, accepted)
https://quant.stackexchange.com/a/80660
There is a lot going on here. Your c-strip curve is hundreds of basis points above the OTR curve (which looks accurate by the way). It must be erroneous.
Secondly you have modelled a `Bill` as a `FixedRateBond`. It doesn't matter considering the price for a remaining cashflow is derived from the curve in the same way (just using discount factors).
Thirdly, bills and bonds settle T+1, so a direct comparison of the discount factor of the curve and equating that with a price of a bill is inaccurate.
Fourthly 'rates' on Bills come in different forms.
```
bill = Bill(effective=dt(2024, 9, 10), termination=dt(2024, 10, 15), spec="us_gbb", curves="curve")
bill.rate(solver=solver, metric="price")
# 99.638313
bill.rate(solver=solver, metric="discount_rate")
# 4.822493
bill.rate(solver=solver, metric="simple_rate")
# 4.839999
bill.ytm(solver=solver, metric="ytm") # this is a comparison with a single period FixedRateBond of one whole period.
# 4.972566
```
YTM calculations are not the same as discount rate or simple rate calculations.
Fifth, the price of the off the run priced from the curve is 101.371094.
```
bond1 = FixedRateBond(effective=datetime(2024, 9, 17), termination=datetime(2031,8,31),convention='ActActISMA',frequency='S', fixed_rate=3.75, curves="curve")
bond1.rate(solver=solver, metric="clean_price")
# 101.371094
```
This will obviously return a Z-spread of zero for that same price because the curve does not need to shift by any amount to re-price it correctly.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.