Bond future
This page lists what you need to work with a bond future: the conversion-factor calls for CME, Eurex and ICE, the basket analysis with its basis, implied repo and cheapest to deliver, the futures position, and examples you can run. A bond future is an exchange-traded contract to deliver a government bond in a set month, chosen by the seller from a basket the exchange lists.
Quick example
The example reads its swap quotes from the desk sheets; load them once and it runs as shown.
This takes a 10-year Treasury-style contract for June 2026 delivery, with three deliverable notes, on 2 March 2026. It reads each note's CME conversion factor, analyses the basket at a quoted 113.00 against a SOFR curve and a general-collateral repo curve stated in the file, and values a position of ten contracts.
use chrono::{Datelike, NaiveDate};
use helixio_docs_sheets::USD_CLOSE_MARCH;
use helixrs::{
AnyInstrument, BondFuture, BondFuturePosition, CmeMaturityRounding, CollateralSpec, Currency,
Curve, DayCountFraction, Deliverable, FixedRateBond, Frequency, Interpolation,
MarketContext, PiecewiseDiscount, PriceBasis, QuoteSheet, cme_conversion_factor, forward_price,
};
fn date(year: i32, month: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
}
/// A discount curve through continuously compounded zero rates.
fn curve_on(reference: NaiveDate, zero_rates: &[(i32, f64)]) -> Curve {
let pillars: Vec<(NaiveDate, f64)> = zero_rates
.iter()
.map(|&(years, rate)| {
let at = date(reference.year() + years, reference.month(), reference.day());
let t = (at - reference).num_days() as f64 / 365.0;
(at, (-rate * t).exp())
})
.collect();
Curve::Piecewise(
PiecewiseDiscount::from_discount_factors(reference, &pillars)
.expect("well-formed pillars")
.with_interpolation(Interpolation::LogLinear)
.expect("a valid scheme")
.with_rate_day_count(DayCountFraction::Act365Fixed),
)
}
fn note(currency: Currency, issue: NaiveDate, maturity: NaiveDate, coupon: f64) -> FixedRateBond {
FixedRateBond::bullet(
currency,
issue,
maturity,
coupon,
Frequency::SemiAnnual,
DayCountFraction::ActActIcma,
100.0,
)
.expect("a well-formed bond")
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let valuation = date(2026, 3, 2);
let delivery_month = date(2026, 6, 1);
let delivery = date(2026, 6, 30);
let usd = Currency::new("USD").expect("a registered currency");
let notes = [
("A", note(usd, date(2025, 5, 15), date(2035, 5, 15), 0.0425)),
("B", note(usd, date(2024, 8, 15), date(2034, 8, 15), 0.03875)),
("C", note(usd, date(2024, 2, 15), date(2034, 2, 15), 0.0400)),
];
// CME's factor is the price of one dollar of the note at a 6% yield, on its
// remaining life from the first day of the delivery month, rounded down to
// a quarter for the 10-year contract.
let factors: Vec<f64> = notes
.iter()
.map(|(_, bond)| {
cme_conversion_factor(bond.coupon, bond.maturity(), delivery_month, CmeMaturityRounding::Quarter)
})
.collect();
for ((key, bond), factor) in notes.iter().zip(&factors) {
println!("{key} {:.3}% {} factor {factor:.4}", bond.coupon * 100.0, bond.maturity());
}
// Each note is priced spot off the discount curve and carried to delivery
// on the Treasury general-collateral repo curve, stated here from its zero
// rates.
let repo = curve_on(
valuation,
&[(1, 0.0380), (2, 0.0370), (3, 0.0365), (5, 0.0370), (7, 0.0380), (10, 0.0395)],
);
let sheet = QuoteSheet::from_rows(USD_CLOSE_MARCH.iter().copied())?;
let market = MarketContext::as_of(valuation)
.with_recipe_quotes(&CollateralSpec::Currency(usd), &sheet)?
.with_repo_curve("USD", repo);
let deliverables: Vec<Deliverable> = notes
.iter()
.zip(&factors)
.map(|((key, bond), factor)| Deliverable {
key: (*key).to_owned(),
bond: bond.clone(),
conversion_factor: *factor,
repo_group: None,
})
.collect();
let future = BondFuture::new(
delivery,
deliverables.iter().map(|d| (d.key.clone(), d.conversion_factor)).collect(),
)?;
let quoted = 113.0;
let analysis = future.analyze_resolved(&market, &deliverables, quoted)?;
println!("key factor gross basis net basis implied repo coupons");
for row in &analysis.rows {
println!(
"{} {:.4} {:11.6} {:9.6} {:11.6}% {:.3}",
row.key,
row.conversion_factor,
row.gross_basis,
row.net_basis,
row.implied_repo * 100.0,
row.interim_coupons
);
}
let theoretical = analysis.theoretical_price;
println!("cheapest to deliver {}", analysis.ctd);
println!("theoretical price {theoretical:.6}");
// Ten contracts, each on 100,000 face, bought at the quoted price.
// Margined daily, the position is worth the move from the traded price,
// undiscounted.
let long = BondFuturePosition::from_deliverables(delivery, deliverables.clone(), 10.0, 100_000.0, quoted);
let value = AnyInstrument::BondFuturePosition(Box::new(long.clone())).npv(&market)?;
let short = AnyInstrument::BondFuturePosition(Box::new(long.short())).npv(&market)?;
println!("position value {value:.2}");
println!("held short {short:.2}");
Ok(())
}import datetime as dt
import math
import helixio as hlx
from desk_sheets import USD_CLOSE_MARCH
VALUATION = dt.date(2026, 3, 2)
DELIVERY_MONTH = dt.date(2026, 6, 1)
DELIVERY = dt.date(2026, 6, 30)
SEMI = hlx.dates.Frequency.SemiAnnual
ICMA = hlx.dates.DayCountFraction.ActActIcma
QUARTER = hlx.products.CmeMaturityRounding.Quarter
NOTES = [
("A", hlx.products.fixed_rate_bond("USD", dt.date(2025, 5, 15), dt.date(2035, 5, 15), coupon=0.0425, frequency=SEMI, day_count=ICMA)),
("B", hlx.products.fixed_rate_bond("USD", dt.date(2024, 8, 15), dt.date(2034, 8, 15), coupon=0.03875, frequency=SEMI, day_count=ICMA)),
("C", hlx.products.fixed_rate_bond("USD", dt.date(2024, 2, 15), dt.date(2034, 2, 15), coupon=0.0400, frequency=SEMI, day_count=ICMA)),
]
def curve_on(reference, zero_rates):
"""A discount curve through continuously compounded zero rates."""
pillars = []
for years, rate in zero_rates:
at = dt.date(reference.year + years, reference.month, reference.day)
t = (at - reference).days / 365.0
pillars.append((at, math.exp(-rate * t)))
return hlx.curves.DiscountCurve(reference, pillars)
# CME's factor is the price of one dollar of the note at a 6% yield, on its
# remaining life from the first day of the delivery month, rounded down to a
# quarter for the 10-year contract.
factors = [
hlx.products.cme_conversion_factor(note.coupon(), note.maturity(), DELIVERY_MONTH, QUARTER)
for _, note in NOTES
]
for (key, note), factor in zip(NOTES, factors):
print(f"{key} {note.coupon() * 100:.3f}% {note.maturity()} factor {factor:.4f}")
# Each note is priced spot off the discount curve and carried to delivery on
# the Treasury general-collateral repo curve, stated here from its zero rates.
repo = curve_on(VALUATION, [(1, 0.0380), (2, 0.0370), (3, 0.0365), (5, 0.0370), (7, 0.0380), (10, 0.0395)])
market = hlx.MarketContext(VALUATION).with_recipe_quotes("USD", USD_CLOSE_MARCH).with_repo_curve("USD", repo)
future = hlx.products.bond_future(
DELIVERY, [(note, factor, key) for (key, note), factor in zip(NOTES, factors)]
)
quoted = 113.0
analysis = hlx.bonds.future_analysis(future, market, quoted)
print("key factor gross basis net basis implied repo coupons")
for row in analysis.rows():
print(
f"{row.key()} {row.conversion_factor():.4f} {row.gross_basis():11.6f} {row.net_basis():9.6f} "
f"{row.implied_repo() * 100:11.6f}% {row.interim_coupons():.3f}"
)
theoretical = analysis.theoretical_price()
print(f"cheapest to deliver {analysis.ctd()}")
print(f"theoretical price {theoretical:.6f}")
# Ten contracts, each on 100,000 face, bought at the quoted price. Margined
# daily, the position is worth the move from the traded price, undiscounted.
position = hlx.products.bond_future_position(future, contracts=10.0, contract_size=100_000.0, traded_price=quoted)
short = hlx.products.bond_future_position(future, contracts=10.0, contract_size=100_000.0, traded_price=quoted, short=True)
value = position.npv(market)
print(f"position value {value:.2f}")
print(f"held short {short.npv(market):.2f}")A 4.250% 2035-05-15 factor 0.8821
B 3.875% 2034-08-15 factor 0.8665
C 4.000% 2034-02-15 factor 0.8806
key factor gross basis net basis implied repo coupons
A 0.8821 0.944381 0.814905 1.352578% 2.125
B 0.8665 0.233707 0.185067 3.206703% 0.000
C 0.8806 -0.273798 -0.350159 4.828277% 0.000
cheapest to deliver C
theoretical price 112.602363
position value -3976.37
held short 3976.37
Note C has the smallest net basis, so it's the cheapest to deliver, and it also earns the highest implied repo. Only note A pays a coupon before delivery. The theoretical price is C's forward clean price over its factor; it sits below the 113.00 traded, so the long position has lost money.
How it's priced
With the futures price, each bond's factor, and its spot and forward clean prices, its spot dirty price, the invoice, and coupons paid before delivery:
A position of contracts of size traded at is worth , undiscounted. Each exchange's factor formula, and why the cheapest bond drives the price, are covered in how bonds work.
API
| Argument | Type | Default | Meaning |
|---|---|---|---|
products.cme_conversion_factor(coupon, maturity, delivery_month, rounding) |
float, date, date, products.CmeMaturityRounding |
— | CME's factor: Quarter for the 10-year contract, Month for the shorter ones |
products.eurex_conversion_factor(coupon, interest_start, first_coupon, maturity, delivery, *, notional_coupon) |
float, four dates, float |
notional_coupon=0.06 |
the Eurex factor for Schatz, Bobl, Bund and Buxl |
products.ice_gilt_conversion_factor(gilt, delivery_month) |
FixedRateBond, date |
— | the ICE Long Gilt factor, at a 4% gilt yield |
products.bond_future(delivery, basket, *, repo_day_count) |
date, list[(bond, factor, key)], DayCountFraction |
repo_day_count=None, the currency's money-market day count |
the contract and its deliverables |
bonds.future_analysis(future, market, quoted) |
— | — | each deliverable's basis and implied repo, the CTD and the theoretical price |
products.bond_future_position(future, *, contracts, contract_size, traded_price, short) |
— | short=False |
a position, margined daily |
In Rust, the factor calls are cme_conversion_factor, eurex_conversion_factor(.., 0.06) and
ice_gilt_conversion_factor(&gilt, month, 0.04); the contract is BondFuture::new(delivery, keys_and_factors)? with Deliverable values, analysed by analyze_resolved(&market, &deliverables, quoted)?; the position is BondFuturePosition::from_deliverables, with .short().
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| , , | each bond's basis and implied repo | row.gross_basis, row.net_basis, row.implied_repo |
row.gross_basis(), row.net_basis(), row.implied_repo() |
| coupons paid before delivery | row.interim_coupons |
row.interim_coupons() |
|
| the day count of | the implied repo's basis | row.repo_day_count |
row.repo_day_count() |
| , , | spot and forward prices | forward_price(&bond, &market, date, basis, repo_key) |
bonds.forward_price(bond, market, date, price_basis, repo_key) |
| CTD, | cheapest to deliver, theoretical price | analysis.ctd, analysis.theoretical_price |
analysis.ctd(), analysis.theoretical_price() |
| the position's value | MarketView::npv on AnyInstrument::BondFuturePosition |
Instrument.npv |
Defaults and limits
- Market inputs: the currency's discount curve for spot prices, and a repo curve under the
currency (
MarketContext.with_repo_curve) for the carry to delivery. - Implied repo day count: ACT/360 for USD, EUR, CHF, SEK, NOK and DKK, and ACT/365 Fixed for every other currency, unless the contract states another.
- Rounding: the CME factor is rounded to four decimals, Eurex's to six and ICE's to seven, as each exchange publishes.
- The position isn't discounted, because it's margined daily.
More examples
The same basket, with CME month rounding beside quarter rounding, a Eurex factor for a Bund-style bond and an ICE factor for a long gilt, and the implied repo requoted on ACT/365 Fixed.
use chrono::{Datelike, NaiveDate};
use helixio_docs_sheets::USD_CLOSE_MARCH;
use helixrs::{
BondFuture, CmeMaturityRounding, CollateralSpec, Currency, Curve, DayCountFraction, Deliverable,
FixedRateBond, Frequency, Interpolation, MarketContext, PiecewiseDiscount, PriceBasis,
QuoteSheet, YieldQuoteConvention, cme_conversion_factor, eurex_conversion_factor,
ice_gilt_conversion_factor,
};
fn date(year: i32, month: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
}
/// A discount curve through continuously compounded zero rates.
fn curve_on(reference: NaiveDate, zero_rates: &[(i32, f64)]) -> Curve {
let pillars: Vec<(NaiveDate, f64)> = zero_rates
.iter()
.map(|&(years, rate)| {
let at = date(reference.year() + years, reference.month(), reference.day());
let t = (at - reference).num_days() as f64 / 365.0;
(at, (-rate * t).exp())
})
.collect();
Curve::Piecewise(
PiecewiseDiscount::from_discount_factors(reference, &pillars)
.expect("well-formed pillars")
.with_interpolation(Interpolation::LogLinear)
.expect("a valid scheme")
.with_rate_day_count(DayCountFraction::Act365Fixed),
)
}
fn note(currency: Currency, issue: NaiveDate, maturity: NaiveDate, coupon: f64) -> FixedRateBond {
FixedRateBond::bullet(
currency,
issue,
maturity,
coupon,
Frequency::SemiAnnual,
DayCountFraction::ActActIcma,
100.0,
)
.expect("a well-formed bond")
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let valuation = date(2026, 3, 2);
let delivery_month = date(2026, 6, 1);
let delivery = date(2026, 6, 30);
let usd = Currency::new("USD").expect("a registered currency");
let notes = [
("A", note(usd, date(2025, 5, 15), date(2035, 5, 15), 0.0425)),
("B", note(usd, date(2024, 8, 15), date(2034, 8, 15), 0.03875)),
("C", note(usd, date(2024, 2, 15), date(2034, 2, 15), 0.0400)),
];
// Month rounding keeps more of the life than quarter rounding; below a 6%
// coupon that lowers the factor, which is why the 5-year contract's run
// lower.
let mut rounded = Vec::new();
for (key, bond) in ¬es {
let quarter =
cme_conversion_factor(bond.coupon, bond.maturity(), delivery_month, CmeMaturityRounding::Quarter);
let month =
cme_conversion_factor(bond.coupon, bond.maturity(), delivery_month, CmeMaturityRounding::Month);
println!("{key} quarter {quarter:.4} month {month:.4}");
rounded.push((quarter, month));
}
// Eurex prices an annual euro bond at the contract's 6% notional coupon
// from the delivery day; this Bund-style bond delivers on 10 June 2026.
// ICE's Long Gilt factor is the gilt's clean price per unit nominal at a
// 4% gilt yield, settling on the first day of the delivery month.
let eurex = eurex_conversion_factor(
0.026,
date(2025, 2, 15),
date(2026, 2, 15),
date(2035, 2, 15),
date(2026, 6, 10),
0.06,
);
let gbp = Currency::new("GBP").expect("a registered currency");
let gilt = note(gbp, date(2019, 1, 22), date(2049, 7, 22), 0.04375);
let ice = ice_gilt_conversion_factor(&gilt, delivery_month, 0.04);
println!("Eurex, 2.60% 2035 Bund {eurex:.6}");
println!("ICE, 4.375% 2049 gilt {ice:.7}");
// The implied repo is quoted on the currency's money-market basis, ACT/360
// for a dollar note. Stating another day count requotes it.
let repo = curve_on(
valuation,
&[(1, 0.0380), (2, 0.0370), (3, 0.0365), (5, 0.0370), (7, 0.0380), (10, 0.0395)],
);
let sheet = QuoteSheet::from_rows(USD_CLOSE_MARCH.iter().copied())?;
let market = MarketContext::as_of(valuation)
.with_recipe_quotes(&CollateralSpec::Currency(usd), &sheet)?
.with_repo_curve("USD", repo);
let deliverables: Vec<Deliverable> = notes
.iter()
.zip(&rounded)
.map(|((key, bond), (quarter, _))| Deliverable {
key: (*key).to_owned(),
bond: bond.clone(),
conversion_factor: *quarter,
repo_group: None,
})
.collect();
let future = BondFuture::new(
delivery,
deliverables.iter().map(|d| (d.key.clone(), d.conversion_factor)).collect(),
)?;
let on_360 = future.analyze_resolved(&market, &deliverables, 113.0)?;
let on_365 = future
.clone()
.with_repo_day_count(DayCountFraction::Act365Fixed)
.analyze_resolved(&market, &deliverables, 113.0)?;
for (row, other) in on_360.rows.iter().zip(&on_365.rows) {
println!(
"{} ACT/360 {:.6}% ACT/365F {:.6}%",
row.key,
row.implied_repo * 100.0,
other.implied_repo * 100.0
);
}
Ok(())
}import datetime as dt
import math
import helixio as hlx
from desk_sheets import USD_CLOSE_MARCH
VALUATION = dt.date(2026, 3, 2)
DELIVERY_MONTH = dt.date(2026, 6, 1)
DELIVERY = dt.date(2026, 6, 30)
SEMI = hlx.dates.Frequency.SemiAnnual
ICMA = hlx.dates.DayCountFraction.ActActIcma
QUARTER = hlx.products.CmeMaturityRounding.Quarter
MONTH = hlx.products.CmeMaturityRounding.Month
NOTES = [
("A", hlx.products.fixed_rate_bond("USD", dt.date(2025, 5, 15), dt.date(2035, 5, 15), coupon=0.0425, frequency=SEMI, day_count=ICMA)),
("B", hlx.products.fixed_rate_bond("USD", dt.date(2024, 8, 15), dt.date(2034, 8, 15), coupon=0.03875, frequency=SEMI, day_count=ICMA)),
("C", hlx.products.fixed_rate_bond("USD", dt.date(2024, 2, 15), dt.date(2034, 2, 15), coupon=0.0400, frequency=SEMI, day_count=ICMA)),
]
def curve_on(reference, zero_rates):
"""A discount curve through continuously compounded zero rates."""
pillars = []
for years, rate in zero_rates:
at = dt.date(reference.year + years, reference.month, reference.day)
t = (at - reference).days / 365.0
pillars.append((at, math.exp(-rate * t)))
return hlx.curves.DiscountCurve(reference, pillars)
# Month rounding keeps more of the life than quarter rounding; below a 6%
# coupon that lowers the factor, which is why the 5-year contract's run lower.
for key, note in NOTES:
quarter = hlx.products.cme_conversion_factor(note.coupon(), note.maturity(), DELIVERY_MONTH, QUARTER)
month = hlx.products.cme_conversion_factor(note.coupon(), note.maturity(), DELIVERY_MONTH, MONTH)
print(f"{key} quarter {quarter:.4f} month {month:.4f}")
# Eurex prices an annual euro bond at the contract's 6% notional coupon from
# the delivery day; this Bund-style bond delivers on 10 June 2026. ICE's Long
# Gilt factor is the gilt's clean price per unit nominal at a 4% gilt yield,
# settling on the first day of the delivery month.
eurex = hlx.products.eurex_conversion_factor(
0.026, dt.date(2025, 2, 15), dt.date(2026, 2, 15), dt.date(2035, 2, 15), dt.date(2026, 6, 10)
)
gilt = hlx.products.fixed_rate_bond("GBP", dt.date(2019, 1, 22), dt.date(2049, 7, 22), coupon=0.04375, frequency=SEMI, day_count=ICMA)
ice = hlx.products.ice_gilt_conversion_factor(gilt, DELIVERY_MONTH)
print(f"Eurex, 2.60% 2035 Bund {eurex:.6f}")
print(f"ICE, 4.375% 2049 gilt {ice:.7f}")
# The implied repo is quoted on the currency's money-market basis, ACT/360 for
# a dollar note. Stating another day count requotes it.
repo = curve_on(VALUATION, [(1, 0.0380), (2, 0.0370), (3, 0.0365), (5, 0.0370), (7, 0.0380), (10, 0.0395)])
market = hlx.MarketContext(VALUATION).with_recipe_quotes("USD", USD_CLOSE_MARCH).with_repo_curve("USD", repo)
basket = [
(note, hlx.products.cme_conversion_factor(note.coupon(), note.maturity(), DELIVERY_MONTH, QUARTER), key)
for key, note in NOTES
]
on_360 = hlx.bonds.future_analysis(hlx.products.bond_future(DELIVERY, basket), market, 113.0).rows()
on_365 = hlx.bonds.future_analysis(
hlx.products.bond_future(DELIVERY, basket, repo_day_count=hlx.dates.DayCountFraction.Act365Fixed), market, 113.0
).rows()
for row, other in zip(on_360, on_365):
print(f"{row.key()} ACT/360 {row.implied_repo() * 100:.6f}% ACT/365F {other.implied_repo() * 100:.6f}%")A quarter 0.8821 month 0.8805
B quarter 0.8665 month 0.8643
C quarter 0.8806 month 0.8785
Eurex, 2.60% 2035 Bund 0.774794
ICE, 4.375% 2049 gilt 1.0562140
A ACT/360 1.352578% ACT/365F 1.371364%
B ACT/360 3.206703% ACT/365F 3.251241%
C ACT/360 4.828277% ACT/365F 4.895336%
Month rounding keeps more of each note's life, which lowers the factor for a coupon below 6%. The ACT/365 Fixed rate is the ACT/360 rate times 365/360 for every note, coupon or not, because both day counts count the same actual days.
See also
- Source:
crates/helixrs/src/instruments/bond_derivatives.rs, with the basket analysis incrates/helixrs/src/market/pricers.rs - Related pages:
- How bonds work, for the factors and the cheapest to deliver.
- Bond future option, for options on the contract.
- Fixed-rate bond, for the deliverable notes.
- Repo, for the financing the implied repo is compared with.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.