How curves work
This page explains why each kind of curve exists, what a desk uses it for, and the choices behind how it reads between its points. Read it when you want the reasoning; each curve's reference page has the arguments and a program you can run.
What every curve shares
Every curve here answers one question: what is a payment, or a survival, or a price level, worth on a given date, seen from the curve's reference date? A curve stores a few numbers at dated points, its pillars, and reads between them under an interpolation. Most curves are solved from market quotes rather than written by hand, and a solved curve remembers where it came from, which is what lets it report risk against those quotes; provenance explains how.
Discount curves
A desk rarely writes discount factors down. It marks a curve from swap and futures quotes, and the solve produces the factors. But the factors are what the curve is: curves pass between systems as dated factors, a regulator's test curve is published that way, and a flat curve at one rate is the first curve a model is checked on.
At a pillar the curve returns its factor exactly; between pillars it reads log-linearly by default. Past the last pillar it keeps the forward rate of its last segment rather than refusing, so a date beyond the curve's end still prices. The short way to build and read one is on discount curves.
Step curves
A central bank sets its policy rate at scheduled meetings, and the overnight rate follows it from the day a decision takes effect until the next one. Between decisions it barely moves. The front end of a curve is therefore a path of steps, one per meeting, and a desk quotes it that way: meeting swaps covering one window each, and futures that average across several.
At year end, and less so at quarter ends, funding briefly costs more as banks shrink their balance sheets. A desk marks that premium, a turn, over the few days it lasts, and the curve needs a pillar at each end of it.
That's why a step curve's pillars aren't anonymous dates. Each is a meeting, a turn or an ordinary date, and risk read back from the curve is keyed by that identity, so you see your exposure to the December decision rather than to 10 December. Between knots the curve is log-linear, so the forward over any part of a window is that window's rate. See step curves.
Spread curves
A corporate bond pays in dollars, but a holder discounts its coupons at more than the dollar curve: the issuer may default, and the bond may be harder to sell. A desk expresses that extra discount as a spread, flat for a quick view, or dated when it has a term structure from the issuer's other bonds or credit default swaps. A spread pinned under a security's key reaches every product that names the security.
A flat spread is declared data, so it carries no risk of its own. A dated spread's pillars are free parameters, and they report risk on the security-spread axis. See spread curves.
Repo spread curves
Bonds are financed by repo: lent out against cash, overnight or for a term, at a repo rate. Most government bonds finance at the general collateral (GC) rate, a few basis points from the overnight index. A bond in demand trades special: you can borrow cash against it more cheaply, so its repo rate sits below GC. Desks quote both as simple spreads, GC over the overnight curve and the special over GC.
Building the curve that way keeps each mark on its own axis. A move in the overnight rate moves every repo rate, a move in GC moves every special, and a special's own mark moves only its own bond. Each key stores only its own spread; the chain is composed when a trade financed on that key is priced. See repo spread curves.
Survival curves
Default risk is quoted through credit default swaps, as a spread or an upfront, and a survival curve is solved from those quotes. Every quote assumes a recovery, the fraction of face a holder gets back on default. A survival curve without its recovery has no units, so a market keeps the two together.
An issuer's curves differ by seniority, by the documentation clause that defines a credit event, and by currency. That's why a market files each curve under the full identity (entity, seniority, clause and currency) and refuses a bare entity: two curves on one issuer can't collide. See survival curves.
Inflation curves
A statistics agency publishes a price index monthly, with a lag. A desk's inflation curve starts from the last published level and forecasts the rest, usually from zero-coupon inflation swaps.
Prices don't rise evenly through the year: energy, clothing sales and holidays move the index by month in a pattern that repeats. A curve that ignored it would misprice a trade fixing in a month whose seasonal effect differs from the year's average. So an inflation curve can carry twelve monthly factors that reshape its level, relative to the month of its first node; only their relative shape matters. See inflation curves.
Parametric fits
A government bond curve is built from bonds, not swaps, and there are more bonds than a desk wants pillars. Bootstrapping through every bond would reproduce each price and pass every quirk of supply, special repo and coupon into the curve's forwards. A parametric curve does the opposite: it fits one smooth shape across all of them and reports how far each bond sits from it. A bond priced above the curve's model price is rich; one below is cheap.
The fit weights each bond's price error by the inverse of its duration by default, so in effect it fits yields rather than prices. Nelson-Siegel's level, slope and hump give a curve that settles to its long-run level far out. See parametric fits.
Reading a curve
A desk quotes curves as rates, not factors: a five-year zero rate, a five-year-five-year forward. Those rates depend on a convention, because a rate is a log factor divided by a year fraction, and the year fraction depends on a day count. A curve is told which day count it forms rates on, and changing it changes every rate without moving any factor.
A zero rate read off a curve is a property of the curve. It isn't the rate an index fixes at, which is simple, on the index's own day count, over the index's own period.
Reading a curve shows the short way: ask the curve for its zero rate and its forward. The program below rebuilds both from the curve's discount factors, by hand, and prints each beside the curve's own value. It reads its quotes from the desk sheets.
use chrono::NaiveDate;
use helixio_docs_sheets::{SOFR_CLOSE_DATE, USD_SOFR_CLOSE};
use helixrs::{CollateralSpec, MarketContext, QuoteSheet, 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 years(start: NaiveDate, end: NaiveDate) -> f64 {
(end - start).num_days() as f64 / 365.0
}
fn main() -> Outcome<()> {
let close = date(SOFR_CLOSE_DATE);
let sheet = QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?;
let market = MarketContext::as_of(close)
.with_recipe_quotes(&CollateralSpec::Currency("USD".parse()?), &sheet)?;
let (curve, _) = market.curve(&"discount(USD)".parse()?).expect("the USD curve");
let view = curve.view::<f64>();
let (five, ten) = (date((2031, 10, 1)), date((2036, 10, 1)));
let zero = -view.discount(five)?.ln() / years(close, five);
println!("zero 5Y by hand {zero:.8}");
println!("zero 5Y curve {:.8}", view.zero_rate(five)?);
let forward = (view.discount(five)?.ln() - view.discount(ten)?.ln())
/ (years(close, ten) - years(close, five));
println!("5Y-10Y by hand {forward:.8}");
println!("5Y-10Y curve {:.8}", view.forward_rate(five, ten)?);
Ok(())
}import datetime as dt
import math
import helixio as hlx
from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE
curve = hlx.MarketContext(SOFR_CLOSE_DATE).with_recipe_quotes("USD", USD_SOFR_CLOSE).curve("discount(USD)")
FIVE, TEN = dt.date(2031, 10, 1), dt.date(2036, 10, 1)
def years(start: dt.date, end: dt.date) -> float:
return (end - start).days / 365.0
zero = -math.log(curve.discount(FIVE)) / years(SOFR_CLOSE_DATE, FIVE)
print(f"zero 5Y by hand {zero:.8f}")
print(f"zero 5Y curve {curve.zero_rate(FIVE):.8f}")
forward = (math.log(curve.discount(FIVE)) - math.log(curve.discount(TEN))) / (
years(SOFR_CLOSE_DATE, TEN) - years(SOFR_CLOSE_DATE, FIVE)
)
print(f"5Y-10Y by hand {forward:.8f}")
print(f"5Y-10Y curve {curve.forward_rate(FIVE, TEN):.8f}")zero 5Y by hand 0.04669935
zero 5Y curve 0.04669935
5Y-10Y by hand 0.04933518
5Y-10Y curve 0.04933518
The two agree to the printed digits. The forward agrees to round-off rather than to the bit: the curve stores log factors, and the log of a factor can differ from the stored log in the last bit.
See also
- Source:
crates/helixrs/src/curves/ - Related pages:
- Interpolation, for how a curve reads between its pillars.
- Provenance, for how a solved curve reports risk against its quotes.
- Quote sheets and recipes, for building curves from quotes.
- Credit calibration and inflation calibration, for solving survival and inflation curves.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.