Repo spread curves
This page lists what you need to mark repo financing spreads: general collateral (GC) against the overnight curve, and a bond trading special against GC. It covers the arguments, how spreads chain over a base, and the difference between a key's stored factor and the composed factor a financed trade reads.
Quick example
This marks GC and one special, chains them over the SOFR curve, and reads the special's composed factor, checking it against the chain formula below. The example reads its quotes from the desk sheets; load them once and it runs as shown.
use chrono::NaiveDate;
use helixio_docs_sheets::{SOFR_CLOSE_DATE, USD_SOFR_CLOSE};
use helixrs::{
CollateralSpec, Curve, DayCountFraction, MarketContext, QuoteSheet, RepoSpread, YieldCurve,
};
type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
fn date((year, month, day): (i32, u32, u32)) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
}
fn main() -> Outcome<()> {
let reference = date(SOFR_CLOSE_DATE);
let six_months = date((2027, 4, 1));
let accrual = |d: NaiveDate| (d - reference).num_days() as f64 / 360.0;
// GC trades five basis points under SOFR at one month and ten at one year; one
// Treasury issue trades special, thirty basis points under GC.
let gc = RepoSpread::new(
reference,
&[(date((2026, 11, 2)), -0.0005), (date((2027, 10, 1)), -0.0010)],
DayCountFraction::Act360,
)?;
let special = RepoSpread::new(reference, &[(date((2027, 10, 1)), -0.0030)], DayCountFraction::Act360)?;
println!("day count {}", gc.day_count().code());
println!("GC at 6M {:.8}", gc.spread(six_months)?);
// The special sits over GC and GC over the SOFR discount curve; the market
// records each base.
let sheet = QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?;
let market = MarketContext::as_of(reference)
.with_recipe_quotes(&CollateralSpec::Currency("USD".parse()?), &sheet)?
.with_repo_spread("USGC", Curve::RepoSpread(gc.clone()), "discount(USD)".parse()?)
.with_repo_spread("UST-2035", Curve::RepoSpread(special.clone()), "repo(USGC)".parse()?);
let over = |key: &str| market.repo_base(key).map(ToString::to_string).unwrap_or_default();
println!("repo(USGC) over {}", over("USGC"));
println!("repo(UST-2035) over {}", over("UST-2035"));
// Each key stores its own spread: its factor alone is 1/(1 + s tau). The factor a
// trade financed on the special reads is the chain resolved for pricing, and
// `repo_discount` reads it: the spreads along the chain add to the base in 1/P.
let read = |id: &str| -> Outcome<f64> {
let (curve, _) = market.curve(&id.parse()?).expect("held");
Ok(curve.view::<f64>().discount(six_months)?)
};
let stored = read("repo(USGC)")?;
let base = read("discount(USD)")?;
let special_6m = market.repo_discount("UST-2035", six_months)?;
println!("SOFR factor 6M {base:.10}");
println!("special factor 6M {special_6m:.10}");
// The formula, composed by hand from the stored spreads, gives the same factor.
let composed = 1.0
/ (1.0 / base + (gc.spread(six_months)? + special.spread(six_months)?) * accrual(six_months));
println!("by hand {composed:.10}");
Ok(())
}import datetime as dt
import helixio as hlx
from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE
REFERENCE = SOFR_CLOSE_DATE
SIX_MONTHS = dt.date(2027, 4, 1)
def accrual(d: dt.date) -> float:
return (d - REFERENCE).days / 360.0
# GC trades five basis points under SOFR at one month and ten at one year; one
# Treasury issue trades special, thirty basis points under GC.
gc = hlx.curves.RepoSpreadCurve(
REFERENCE, [(dt.date(2026, 11, 2), -0.0005), (dt.date(2027, 10, 1), -0.0010)]
)
special = hlx.curves.RepoSpreadCurve(REFERENCE, [(dt.date(2027, 10, 1), -0.0030)])
print(f"day count {gc.day_count()}")
print(f"GC at 6M {gc.spread(SIX_MONTHS):.8f}")
# The special sits over GC and GC over the SOFR discount curve; the market
# records each base.
market = (
hlx.MarketContext(REFERENCE)
.with_recipe_quotes("USD", USD_SOFR_CLOSE)
.with_repo_spread("USGC", gc, "discount(USD)")
.with_repo_spread("UST-2035", special, "repo(USGC)")
)
print(f"repo(USGC) over {market.repo_base('USGC')}")
print(f"repo(UST-2035) over {market.repo_base('UST-2035')}")
# Each key stores its own spread: its factor alone is 1/(1 + s tau). The factor a
# trade financed on the special reads is the chain resolved for pricing, and
# `repo_discount` reads it: the spreads along the chain add to the base in 1/P.
base = market.curve("discount(USD)").discount(SIX_MONTHS)
special_6m = market.repo_discount("UST-2035", SIX_MONTHS)
print(f"SOFR factor 6M {base:.10f}")
print(f"special factor 6M {special_6m:.10f}")
# The formula, composed by hand from the stored spreads, gives the same factor.
composed = 1.0 / (
1.0 / base + (gc.spread(SIX_MONTHS) + special.spread(SIX_MONTHS)) * accrual(SIX_MONTHS)
)
print(f"by hand {composed:.10f}")day count ACT/360
GC at 6M -0.00072523
repo(USGC) over discount(USD)
repo(UST-2035) over repo(USGC)
SOFR factor 6M 0.9790816261
special factor 6M 0.9808903021
by hand 0.9808903021
How it's priced
A repo spread is simple, linear between dated pillars and flat outside them, accruing on ACT/360 with year fraction . Declared over a base with factor , it composes as
A spread declared over another repo key adds to that key's spread, so along the chain special, GC, SOFR:
How GC and specials trade is covered in how curves work.
API
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| a repo spread | RepoSpread::new with a DayCountFraction, read with RepoSpread::spread |
curves.RepoSpreadCurve, read with curves.RepoSpreadCurve.spread |
|
| base | the key a spread is declared over | MarketContext::with_repo_spread(key, curve, over), read with MarketContext::repo_base |
MarketContext.with_repo_spread(key, curve, base), read with MarketContext.repo_base |
| a key's own stored factor | MarketContext::curve on repo(KEY) |
MarketContext.curve on repo(KEY) |
|
| the composed factor a financed trade reads | MarketContext::repo_discount(key, date) |
MarketContext.repo_discount(key, date) |
curves.RepoSpreadCurve(...):
| Argument | Type | Default | Meaning |
|---|---|---|---|
reference |
date |
required | the spread's reference date |
pillars |
list[tuple[date, float]] |
required | (date, simple spread) pairs |
day_count |
str or DayCountFraction |
"ACT/360" |
the money-market day count the spread accrues on |
curves.RepoSpreadCurve.from_term_rates(base, quotes, day_count="ACT/360") builds the spread over
base that reprices term repos quoted as (maturity, simple rate).
Defaults and limits
!!! warning "A key's stored factor is not what a trade reads"
Each key stores only its own spread: read alone with curve, its factor is
. The chain composes when a repo key is resolved for pricing.
Read repo_discount for the factor a trade financed on that key sees.
- Outside the pillars: linear between pillars, flat outside them.
- Risk: each key's pillars report risk on the repo axis, so a move in SOFR, in GC and in a special's own mark each stay on their own axis.
See also
- Source:
crates/helixrs/src/curves/spread.rs,crates/helixrs/src/market/store.rs - Related pages:
- Spread curves, for a continuously compounded spread.
- Day counts, for ACT/360.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.