Repo
This page lists what you need to book and value a repo: the template's repo call and its
arguments, the GC curve it reads, classic and buy/sell-back coupons, and the margin position. A
repo sells a bond for cash and buys it back later for the cash plus interest; a reverse repo is the
other side, lending the cash.
Quick example
The example reads its quotes from the desk sheets; load them once and it runs as shown.
This puts general collateral 10bp under SOFR on the USD close, then repos 10 million of a ten-year 4.25% bond at 99.5 for six months at 4.00%. It values the repo and the reverse, compares a classic repo with a buy/sell-back across the bond's coupon date. To read a repo's margin position and haircut, see value a repo and its haircut.
use chrono::NaiveDate;
use helixio_docs_sheets::{SOFR_CLOSE_DATE, USD_SOFR_CLOSE, USD_SOFR_PRINTS};
use helixrs::{
AnyInstrument, AnyTemplate, CollateralSpec, CouponTreatment, Curve, DayCountFraction,
FixedRateBond, FixingKey, FixingStore, Frequency, MarketContext, QuoteSheet, Repo, RepoRate,
RepoSide, RepoSpread, RepoTerms, package_value, template,
};
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 close = date(SOFR_CLOSE_DATE);
let start = date((2026, 10, 2));
// Rates from the sheet, and general collateral 10bp under SOFR, pinned as a
// repo spread curve over the USD discount curve.
let sheet = QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?;
let prints: Vec<(FixingKey, NaiveDate, f64)> = USD_SOFR_PRINTS
.iter()
.map(|(index, day, value)| (FixingKey::Typed((*index).to_owned()), date(*day), *value))
.collect();
let gc = RepoSpread::new(close, &[(date((2027, 10, 1)), -0.0010)], DayCountFraction::Act360)?;
let market = MarketContext::as_of(close)
.with_fixings(FixingStore::from_observations_checked(&prints)?)
.with_recipe_quotes(&CollateralSpec::Currency("USD".parse()?), &sheet)?
.with_repo_spread("USD", Curve::RepoSpread(gc), "discount(USD)".parse()?);
let usd = "USD".parse()?;
let bond = FixedRateBond::bullet(
usd,
date((2025, 8, 15)),
date((2035, 8, 15)),
0.0425,
Frequency::SemiAnnual,
DayCountFraction::ActActIcma,
100.0,
)?;
// Six months on the USD GC template at 4.00%, on 10 million of the bond at 99.5.
let AnyTemplate::Repo(gc_repo) = template("USD-GC-REPO")? else {
unreachable!("a repo template")
};
let repo = |side, coupons, margin_ratio| -> Outcome<AnyInstrument> {
let end = gc_repo.end_after(start, "6M".parse()?);
let terms = RepoTerms::from_template(&gc_repo, side, start, end, RepoRate::Fixed(0.0400), margin_ratio)?;
let built = Repo::from_collateral(terms, bond.clone(), 10_000_000.0, 99.5)?.with_coupons(coupons);
Ok(AnyInstrument::Repo(Box::new(built)))
};
let borrow = repo(RepoSide::Repo, CouponTreatment::Classic, 1.0)?;
let lend = repo(RepoSide::ReverseRepo, CouponTreatment::Classic, 1.0)?;
println!("repo {:.2}", borrow.npv(&market)?);
println!("reverse {:.2}", lend.npv(&market)?);
// The bond pays a coupon inside the term. A classic repo manufactures it back
// to the seller; a buy/sell-back folds it into the repurchase price. The cash
// legs differ by just under the coupon; the packages, each with its collateral,
// differ by far less.
let classic = borrow.clone();
let sell_back = repo(RepoSide::Repo, CouponTreatment::BuySellBack, 1.0)?;
let coupon = 10_000_000.0 * 0.0425 / 2.0;
let gap = sell_back.npv(&market)? - classic.npv(&market)?;
println!("one coupon {coupon:.2}");
println!("cash legs differ by {gap:.2}");
println!(
"packages {:.2} {:.2}",
package_value(&classic, &market)?,
package_value(&sell_back, &market)?
);
Ok(())
}import datetime as dt
import helixio as hlx
from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE, USD_SOFR_PRINTS
START = dt.date(2026, 10, 2)
# Rates from the sheet, and general collateral 10bp under SOFR, pinned as a
# repo spread curve over the USD discount curve.
gc = hlx.curves.RepoSpreadCurve(SOFR_CLOSE_DATE, [(dt.date(2027, 10, 1), -0.0010)])
market = (
hlx.MarketContext(SOFR_CLOSE_DATE)
.with_fixings(USD_SOFR_PRINTS)
.with_recipe_quotes("USD", USD_SOFR_CLOSE)
.with_repo_spread("USD", gc, "discount(USD)")
)
bond = hlx.products.fixed_rate_bond(
"USD", dt.date(2025, 8, 15), dt.date(2035, 8, 15),
coupon=0.0425, frequency="Semiannual", day_count="ACT/ACT.ICMA",
)
# Six months on the USD GC template at 4.00%, on 10 million of the bond at 99.5.
def repo(side, coupons="classic", margin_ratio=1.0):
return hlx.templates.get("USD-GC-REPO").repo(
START, "6M", side=side, collateral=bond, price=99.5,
rate=0.0400, nominal=10_000_000.0, coupons=coupons, margin_ratio=margin_ratio,
)
borrow, lend = repo("repo"), repo("reverse")
print(f"repo {borrow.npv(market):.2f}")
print(f"reverse {lend.npv(market):.2f}")
# The bond pays a coupon inside the term. A classic repo manufactures it back
# to the seller; a buy/sell-back folds it into the repurchase price. The cash
# legs differ by just under the coupon; the packages, each with its collateral,
# differ by far less.
classic, sell_back = borrow, repo("repo", coupons="buy_sell_back")
coupon = 10_000_000.0 * 0.0425 / 2.0
gap = sell_back.npv(market) - classic.npv(market)
print(f"one coupon {coupon:.2f}")
print(f"cash legs differ by {gap:.2f}")
print(f"packages {hlx.bonds.package_value(classic, market):.2f} {hlx.bonds.package_value(sell_back, market):.2f}")repo 6350.63
reverse -6350.63
one coupon 212500.00
cash legs differ by 209196.80
packages 6350.63 6267.45
The repo and the reverse hold the same value with opposite signs. The buy/sell-back's cash leg is worth just under one coupon more, because the buyer keeps the coupon and lowers the repurchase price to match.
How it's priced
Cash is lent against nominal of a bond at price per 100, with margin ratio :
At the end date the cash comes back with interest at the repo rate over the accrual fraction . The borrower's value is the cash received at the start date less the repayment, both discounted on the collateral's repo curve . Once the repo has started, only the repayment is left:
The repo curve adds the GC spread to the overnight curve in simple-rate space, , where is the curve's own year fraction to , not the repo's accrual.
The collateral required is times the exposure , the haircut is , and the excess is the collateral's value less what's required. Why a repo is held to a margin is covered in how financing works.
API
Template.repo(start, end, *, side, collateral, price, ...) on a repo template such as
templates.get("USD-GC-REPO"):
| Argument | Type | Default | Meaning |
|---|---|---|---|
start |
date |
required | when the cash moves |
end |
date or tenor str |
required | the repurchase date, or a tenor from start; "open" books an open repo, ended on a business day's notice |
side |
"repo" or "reverse" |
required | borrowing cash against the bond, or lending it |
collateral |
FixedRateBond |
required | the bond |
price |
float |
required | the collateral's price per 100 the cash is sized at |
rate |
float |
none | the fixed repo rate; give this or spread |
spread |
float |
none | a spread over the template's compounded overnight index, in place of rate; makes the repo floating |
nominal |
float |
none | the bond's face; the cash follows from it, the price and the margin ratio |
cash |
float |
none | the cash lent, in place of nominal |
margin_ratio |
float |
1.0 |
collateral value over cash: 1.02 is 102% margin, a 1.96% haircut |
coupons |
"classic" or "buy_sell_back" |
"classic" |
who gets a coupon paid during the term |
repo_key |
str |
the template's GC key | the repo curve the cash leg discounts on |
isin |
str |
none | the collateral's ISIN: a repo curve filed under it prices first |
repo_group |
str |
none | the collateral's repo group, such as a specialness bucket: tried after the ISIN, before the GC curve |
margin_basis |
"clean" or "dirty" |
the template's | the price the margin ratio applies to |
interest_dates |
list[date] |
none | pays interest at each date, compounding a floating repo each period |
fx_rate |
float |
none | cash units per collateral unit, for a bond in another currency |
In Rust the same terms are RepoTerms::from_template(&template, side, start, end, RepoRate::Fixed(r), margin_ratio),
then Repo::from_collateral(terms, bond, nominal, price) and .with_coupons(CouponTreatment::BuySellBack).
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| GC as a spread over the overnight curve | RepoSpread::new, MarketContext::with_repo_spread |
curves.RepoSpreadCurve, MarketContext.with_repo_spread |
|
| from | cash following the collateral | Repo::from_collateral |
Template.repo(.., nominal=) |
| the cash leg's value | AnyInstrument::npv |
Instrument.npv |
|
| the package | the cash leg with its collateral | package_value |
bonds.package_value |
| , the haircut, the excess | the margin position | repo_margin → RepoMargin |
bonds.repo_margin → dict |
bonds.repo_margin(repo, market, *, price=None) returns exposure, collateral_value,
required, excess (negative is a call) and haircut. The collateral is priced off its curves,
or at price per 100 if you give one.
Defaults and limits
- What the market needs: the currency's discount curve and a repo spread curve under the
repo's key. Add the GC curve with
with_repo_spread(currency, curve, "discount(USD)"). - What the template holds: the currency, day count, settlement and the default GC key; see the repo conventions.
- Rate: give exactly one of
rateandspread. - Size: give exactly one of
nominalandcash. - Haircut: the haircut is
1 - 1/margin_ratio, so 102% margin is a 1.96% haircut, not 2%. - Cross-currency: with
fx_rate, the repo is classic, and a buy/sell-back or a package value is refused.
See also
- Source:
crates/helixrs/src/instruments/repo.rs,crates/helixrs/src/statics/templates/repo.rs,crates/helixrs/src/market/bond_readings.rs - Related pages:
- How financing works, the ideas behind repo and margin.
- Value a repo and its haircut, step by step.
- Repo spread curves, for .
- Bond forward, for a bond carried on the same curve.
- Repo conventions, for each template's terms.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.