Step curves
This page lists what you need to build a step curve: a discount curve whose pillars sit where the overnight rate changes, each tagged as a central-bank meeting, a turn, or an ordinary date. It covers the knot tags, the constructor's arguments, and how the forwards behave between knots.
Quick example
This builds a SOFR step curve from a policy path, prints the forward over each window, and checks the forward is flat within a window. 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;
use helixrs::{Curve, KnotKind, StepForward, 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);
// The overnight rate the desk expects, window by window: unchanged to the
// October decision, cut at October and December, a premium over the year end,
// then held. Each knot is tagged with the event that starts there.
let path = [
(date((2026, 10, 29)), "meeting:FED", 0.0390),
(date((2026, 12, 10)), "meeting:FED", 0.0365),
(date((2026, 12, 31)), "turn", 0.0340),
(date((2027, 1, 4)), "anon", 0.0440),
(date((2027, 10, 1)), "anon", 0.0340),
];
let (mut factors, mut factor, mut start) = (Vec::new(), 1.0_f64, reference);
let mut knots = Vec::new();
for (end, tag, rate) in path {
factor *= (-rate * (end - start).num_days() as f64 / 365.0).exp();
factors.push(factor);
knots.push((end, KnotKind::from_tag(tag, end)?));
start = end;
}
let step = StepForward::from_discount_factors(reference, &knots, &factors)?;
let tags: Vec<String> = step.knot_kinds().iter().map(KnotKind::tag).collect();
println!("knots {}", tags.join(", "));
let curve = Curve::Step(step);
let view = curve.view::<f64>();
// Between two knots the forward is the path's rate for that window.
let mut start = reference;
for (end, _, rate) in path {
let forward = view.forward_rate(start, end)?;
println!("{start} - {end} {forward:.6}");
start = end;
}
// Inside a window the forward is the same whichever part of it is read.
let first = view.forward_rate(date((2026, 11, 2)), date((2026, 11, 9)))?;
let second = view.forward_rate(date((2026, 11, 30)), date((2026, 12, 7)))?;
println!("two weeks inside one window {first:.8} {second:.8}");
Ok(())
}import datetime as dt
import math
import helixio as hlx
from desk_sheets import SOFR_CLOSE_DATE
REFERENCE = SOFR_CLOSE_DATE
# The overnight rate the desk expects, window by window: unchanged to the
# October decision, cut at October and December, a premium over the year end,
# then held. Each knot is tagged with the event that starts there.
PATH = [
(dt.date(2026, 10, 29), "meeting:FED", 0.0390),
(dt.date(2026, 12, 10), "meeting:FED", 0.0365),
(dt.date(2026, 12, 31), "turn", 0.0340),
(dt.date(2027, 1, 4), "anon", 0.0440),
(dt.date(2027, 10, 1), "anon", 0.0340),
]
factors, factor, start = [], 1.0, REFERENCE
for end, _, rate in PATH:
factor *= math.exp(-rate * (end - start).days / 365.0)
factors.append(factor)
start = end
curve = hlx.curves.DiscountCurve.step_forward(REFERENCE, [(d, tag) for d, tag, _ in PATH], factors)
print(f"knots {', '.join(curve.knot_tags())}")
# Between two knots the forward is the path's rate for that window.
start = REFERENCE
for end, tag, rate in PATH:
forward = curve.forward_rate(start, end)
print(f"{start} - {end} {forward:.6f}")
start = end
# Inside a window the forward is the same whichever part of it is read.
first = curve.forward_rate(dt.date(2026, 11, 2), dt.date(2026, 11, 9))
second = curve.forward_rate(dt.date(2026, 11, 30), dt.date(2026, 12, 7))
print(f"two weeks inside one window {first:.8f} {second:.8f}")knots meeting:FED, meeting:FED, turn, anon, anon
2026-10-01 - 2026-10-29 0.039000
2026-10-29 - 2026-12-10 0.036500
2026-12-10 - 2026-12-31 0.034000
2026-12-31 - 2027-01-04 0.044000
2027-01-04 - 2027-10-01 0.034000
two weeks inside one window 0.03650000 0.03650000
Each window's forward is the path's rate for that window.
How it's priced
Let the policy path set the overnight rate over the window between knots, with the reference date. Compounding continuously on ACT/365, the factor at each knot is
The curve is log-linear between knots, so the forward over any part of a window is that window's rate:
Why the front end moves in steps, and what a turn is, is covered in how curves work.
API
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| with identity | a knot | (date, KnotKind::from_tag(tag, date)?) |
(date, tag) with tag "meeting:FED", "turn" or "anon" |
| the curve from knots and factors | StepForward::from_discount_factors, wrapped in Curve::Step |
curves.DiscountCurve.step_forward |
|
| — | each knot's identity | StepForward::knot_kinds, read with KnotKind::tag |
curves.DiscountCurve.knot_tags |
| the forward | YieldCurve::forward_rate |
curves.DiscountCurve.forward_rate |
curves.DiscountCurve.step_forward(...):
| Argument | Type | Default | Meaning |
|---|---|---|---|
reference |
date |
required | the curve's reference date |
knots |
list[tuple[date, str]] |
required | (date, tag) pairs; the tag is "meeting:FED" (any bank code), "turn" or "anon" |
discount_factors |
list[float] |
required | one discount factor per knot |
Defaults and limits
- Identity: risk read back from a step curve is keyed by each knot's tag, so you see your exposure to a meeting rather than to a date.
- Where meeting knots land: on the dates the bank's
meeting schedule gives;
dates.meeting_datesanddates.turn_dateslist them in Python. - Solved from quotes: a step curve solved from meeting quotes is shown in curve helpers.
See also
- Source:
crates/helixrs/src/curves/step.rs,crates/helixrs/src/statics/meetings.rs - Related pages:
- Meeting schedules, for where meeting knots land.
- Curve helpers, for a step curve solved from quotes.
- Interpolation, for log-linear's flat forwards.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.