Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Bonds and issuer optionality

Bond valuation combines contractual coupon and principal payments with a market quotation. For a straight fixed-rate bond, QuantSupport solves the spread implied by a price anchor and provides yield and duration analytics. For a stepped-coupon bond with issuer calls, valuation also requires a short-rate model and an exercise calculation.

InstrumentPricerSupported analytics
FixedRateBondBondPricervalue, cashflows, yield to maturity, modified duration, Z-spread, sensitivities, accrued amount
SteppedCouponBondTreeBasedBondPricer or BackwardFourierEvolutionBondPricervalue, cashflows, OAS, sensitivities, accrued amount

Stepped-coupon bonds use price or spread anchors. Their supported analytics are value, cashflows, OAS, sensitivities and accrued amount; yield-to-maturity and modified-duration requests leave those result fields absent.

Contractual accrued amount

We use Request::AccruedAmount(start, end) to request signed coupon interest between two dates. The result is an undiscounted currency amount in EvaluationResults::accrued_amount(), using the context base currency when configured. Principal is excluded, and the requested dates are clipped to each coupon period. Adjacent windows reconcile to the complete coupon.

The request uses numeric coupon accrual without solving a spread, normalizing a clean or dirty quote, or recording a differentiation tape. A callable bond reports contractual interest assuming its scheduled coupons continue; this measure does not include future exercise-probability weighting. Accrual alone requires no tree or Fourier valuation model.

Pricing anchors

Every bond trade stores exactly one required BondPricingAnchor. Pricers are reusable and take no quote in their constructors:

  • CleanPrice(value) is a price per bond units excluding accrued interest.
  • DirtyPrice(value) is a price per bond units including accrued interest.
  • ZSpread(value) is a continuously compounded spread for a straight bond.
  • OAS(value) is an option-adjusted spread for a bond with issuer calls.

A clean or dirty price anchor implies Z-spread for a straight bond and OAS for a bond with issuer calls. The pricer rejects OAS on a straight bond and Z-spread on a callable bond.

The solved or supplied spread becomes an independent AD variable. The common sensitivity table therefore reports curve risk together with either dPV/dZSpread or dPV/dOAS.

Straight fixed-rate bonds

Only coupons and principal strictly after settlement enter valuation. A clean quote is converted to dirty value by adding accrued interest. Z-spread z solves

\[ B_{\rm dirty}(S;z)=\sum_{T_i>S}CF_i \frac{P(0,T_i)}{P(0,S)} \exp\!\left[-z\,\tau(S,T_i)\right]. \]

Here \(S\) is settlement and \(\tau\) uses the spread day counter. Quoted prices are normalized to the bond’s units; the expression above is the settlement value before that normalization.

The solver expands a bracket and bisects inside it. Yield to maturity is solved independently under the fixed bond’s day-count, frequency, and compounding convention. Modified duration is -(dP/dy)/P, using the analytic derivative of each yield discount factor.

Request::Value remains signed position PV at the evaluation date. A quoted price anchors the spread; it does not redefine PV.

Stepped-coupon contract

The builder delegates date generation to MakeSchedule. In addition to payment_frequency, it accepts a calendar, business-day convention, termination-date convention, date-generation rule, end-of-month flag, first-coupon date and next-to-last-coupon date. The final date defaults to the same adjustment convention as intermediate dates. Python exposes the same schedule controls.

Side belongs to SteppedCouponBondTrade or FixedRateBondTrade, together with the pricing anchor and optional settlement/spread day-count terms. Instrument builders do not accept side. Cashflows describe the contract; the pricer applies the trade direction to PV and sensitivities, and claim decomposition applies that direction to XVA claims.

SteppedCouponBond is built from three independent schedules:

  1. coupons is positional. Entry zero applies to the first generated coupon period, entry one to the second, and so on. If the payment schedule is longer, the last coupon definition repeats through maturity. A list longer than the generated schedule is rejected rather than silently truncated.
  2. notional_percentages is also positional and uses the same carry-forward rule. Each value is the fraction of original principal outstanding during that period. The first value must be 1.0; values must remain in (0, 1] and cannot increase. Extra entries beyond the generated schedule are rejected.
  3. call_schedule is not positional. Each entry supplies start_date, an optional end_date, and strike. Without an end date it is a discrete call. Otherwise it is an inclusive exercise window. Windows are sorted, must follow the bond start and end before final payment, and cannot overlap, including at a shared endpoint. The legacy serialized date field remains accepted as an alias for start_date.

A coupon schedule contains the existing CashflowType<T> variants:

FixedRateCoupon(FixedRateCoupon<T>)
FloatingRateCoupon(FloatingRateCoupon<T>)
OptionEmbeddedCoupon(OptionEmbeddedCoupon<T>)

For a floating fixing L, the all-in coupon rate is

\[ R=\min\!\left(\max(L+\text{spread},\text{floor}),\text{cap}\right), \]

with a missing floor interpreted as negative infinity and a missing cap as positive infinity. Fixed coupons use the bond’s coupon day counter. Floating coupons use the referenced index convention.

An outstanding-notional reduction creates a principal redemption at the end of that coupon period. The remaining principal is redeemed at maturity.

Call entries give the issuer the right to redeem; holder puts are unsupported. Strikes are quoted in bond units and exclude same-day coupons. During backward valuation the engine first discounts continuation, then applies the issuer obstacle min(V, K), then adds contractual cashflows mapped to that date. Consequently, same-day coupon and scheduled amortization are paid before the remaining bond is called.

The following Rust fragment illustrates the contract and position terms. Its context and market configuration are supplied separately.

let bond = MakeSteppedCouponBond::<DualFwd>::default()
    .with_identifier("USD-STEP-CALL-2031".into())
    .with_start_date(Date::new(2025, 1, 1))
    .with_maturity_date(Date::new(2031, 1, 1))
    .with_notional(1_000_000.0)
    .with_units(100.0)
    .with_currency(Currency::USD)
    .with_payment_frequency(Frequency::Semiannual)
    .with_coupons(vec![
        CashflowType::fixed(0.045),
        CashflowType::floating(
            MarketIndex::SOFR,
            0.006,
            Some(0.0),
            Some(0.06),
        ),
    ])
    .with_notional_percentages(vec![1.0, 0.90, 0.75])
    .with_call_schedule(vec![
        CallScheduleEntry::window(
          Date::new(2028, 1, 1), Date::new(2028, 6, 30), 101.0,
        ),
        CallScheduleEntry::new(Date::new(2029, 1, 1), 100.0),
    ])
    .build()?;

let trade = SteppedCouponBondTrade::new(
    bond, Date::new(2025, 1, 1), 1_000_000.0, Side::LongReceive,
    BondPricingAnchor::OAS(0.0075),
);
let mut pricer = TreeBasedBondPricer::new();
  pricer.set_discount_policy(Box::new(
    FixedIncomeDiscountPolicy::new(false)
      .with_risk_free_index(Currency::USD, MarketIndex::SOFR),
  ));

The stepped bond and its builder have no discount index. Discounting is a pricing choice resolved by the pricer’s DiscountPolicy. Straight-bond pricing also requires a policy; an embedded instrument index does not bypass it. A policy can explicitly choose whether to prefer legacy instrument indices.

CashflowType::fixed and CashflowType::floating create coupon templates. The builder rebinds existing cashflows to the generated dates and outstanding notional, preserving rates, spreads, payoff operations and fixed-rate conventions unless a day counter is overridden.

Floating-coupon projection convention

Historical in-advance coupons use the fixing stored on their accrual-start date. Future coupons use the current forward curve for their reference index, then apply spread, floor, and cap. The resulting amount remains an AD expression, so its forward-curve sensitivity appears in the ordinary risk table.

The callable engines use the Hull–White short-rate state to value the issuer option. Future floating coupons are projected from valuation-date forwards and remain deterministic within that exercise calculation. Their forward-curve sensitivity is retained, but the engines omit stochastic future coupon fixings.

Context-level pricer and model selection

Choose the callable engine through a PricerConfiguration for InstrumentType::SteppedCouponBond. The context uses the same configuration mechanism for other product families. Each model assignment associates a typed ModelConfiguration with a MarketIndex.

A tree configuration for the stepped-coupon bond is:

{
  "instrument_type": "SteppedCouponBond",
  "pricer": {
    "TreeBased": {
      "time_grid": {
        "steps_per_year": 96,
        "max_steps": 4096,
        "day_counter": "Actual365"
      }
    }
  },
  "models": [{
    "market_index": "SOFR",
    "model": {
      "HullWhite": {
        "alpha": 0.05,
        "parameter_source": { "Fixed": { "sigma": 0.01 } }
      }
    }
  }]
}

Backward Fourier evolution uses the same model assignment:

{
  "instrument_type": "SteppedCouponBond",
  "pricer": {
    "BackwardFourierEvolution": {
      "time_grid": {
        "steps_per_year": 96,
        "max_steps": 4096,
        "day_counter": "Actual365"
      },
      "fourier_grid": {
        "points": 512,
        "standard_deviations": 10.0
      }
    }
  },
  "models": [{
    "market_index": "SOFR",
    "model": {
      "HullWhite": {
        "alpha": 0.05,
        "parameter_source": { "Fixed": { "sigma": 0.01 } }
      }
    }
  }]
}

The configured model is resolved using the policy-selected discount index. Automatic Python dispatch reads the same configuration and instantiates the selected engine.

Hull-White dynamics and curve fit

Both callable engines use the one-factor Hull-White state

\[ dx_t=-\alpha x_t\,dt+\sigma\,dW_t,\qquad r_t=x_t+\phi(t). \]

For one interval dt, the exact transition is

\[ E[x_{t+\Delta}\mid x_t=x]=e^{-\alpha\Delta}x,\qquad \operatorname{Var}[x_{t+\Delta}\mid x_t] =\sigma^2\frac{1-e^{-2\alpha\Delta}}{2\alpha}, \]

with limit sigma^2 dt when alpha is zero. The deterministic shift phi(t) fits the initial discount curve. OAS is added to the short rate after that fit, so changing OAS does not recalibrate drift.

Contractual payments and discrete call dates are mapped to their nearest time grid points. Call windows permit exercise at every numerical time inside the inclusive interval in both engines, not only at endpoints. A window that already started remains exercisable at settlement until its end date. Strikes use principal remaining after amortization mapped to the exercise step. Increasing steps_per_year improves time resolution; max_steps caps total work.

Recombining tree

TreeBasedBondPricer places nodes at spacing sqrt(3 Var[dx]). At each node it selects the nearest middle successor to the conditional mean and assigns three probabilities that match the first two conditional moments. State prices propagate forward, and the deterministic shift at each layer is chosen so their discounted sum matches the input curve discount factor. The bond value is then obtained by backward induction with the call obstacle.

Backward Fourier evolution

BackwardFourierEvolutionBondPricer uses a uniform state grid. If \(V_{i+1}(x)\) is the next-step value and \(\eta\) the Gaussian innovation, first form the convolution

\[ g_i(y)=\mathcal F^{-1}\!\left( \mathcal F(V_{i+1})(k) e^{-\tfrac12\operatorname{Var}(\eta)k^2}\right)(y). \]

The conditional expectation is then \(E[V_{i+1}(e^{-\alpha\Delta}x+\eta)]=g_i(e^{-\alpha\Delta}x)\). Separating convolution from evaluation at the mean avoids confusing the Fourier multiplier with the mean-reversion coordinate transformation.

The implementation performs an FFT, multiplies by the Gaussian characteristic function, and performs an inverse FFT. It interpolates at the mean-reverted state, discounts by exp(-(x + phi_i + OAS)dt), and applies the same call obstacle as the tree.

fourier_grid.points must be a power of two. More points reduce interpolation and spectral error. standard_deviations controls the truncated state-domain half-width; increasing it reduces periodic wrap-around at the cost of a wider grid.

The Fourier curve fit uses Gaussian integrated-state variance. With I_n = dt sum_(i<n) x_i, Gaussianity gives

\[ E[e^{-I_n}]=e^{\tfrac12\operatorname{Var}(I_n)},\qquad \sum_{i<n}\phi_i\Delta=-\log P(0,t_n) +\tfrac12\operatorname{Var}(I_n). \]

This supplies an independent numerical construction of the same initial-curve fit used by the tree.

OAS inversion and risk

For a clean or dirty price anchor, the selected callable engine is evaluated inside a bracketed scalar root solve until model dirty value equals quoted dirty value. The converged OAS is then placed on the AD tape for final valuation. Request::Sensitivities contains all discount/forward-curve pillar adjoints and one additional row:

<identifier>.OAS    dPV/dOAS

This is the partial derivative with OAS treated as an independent market factor. Curve and volatility sensitivities hold that spread fixed.

Validation and examples

Tests cover invalid schedules, carry-forward behavior, amortization, clean/dirty spread round trips, AD-versus-central-difference spread risk, extreme OAS, call boundaries, tree refinement, a Hull-White closed-form zero-coupon bond-option benchmark, and agreement between the tree and Fourier engines for fixed and mixed capped/floored coupons.

Runnable examples are:

  • cargo run -p valuation-bonds prices the same mixed-coupon instrument with both engines.
  • cargo run --manifest-path examples/scripting/Cargo.toml --bin stepped-coupon-bond compares the native tree with an equivalent scripted deterministic payoff.
  • bindings/python/examples/valuation/bonds/stepped_coupon_bond.ipynb constructs and prices the product from Python with typed terms and auxiliary model settings.
  • bindings/python/examples/scripting/scripted_cashflows.ipynb explains the scripted underlying and its deterministic native comparison.

Calibrated Hull–White parameters retain the entire piecewise sigma schedule. Transition variance integration splits at every sigma boundary, using the first sigma from time zero and extrapolating the last beyond its final pillar. Calibration uses the curve’s reference date and day counter while preserving the configured step density and maximum steps. Callable valuation currently requires settlement to equal the context evaluation date.

Volatility sensitivities chain the calibration Jacobian through sampled surface or cube quotes to their original risk pillars. That schedule assembles independent constant-sigma fits, not a joint repricing calibration. The diagonal forward-difference Jacobian includes volatility inputs only: sigma’s response to curve quotes and changes in calibration strikes is not included. Cube calibration must select at most one underlying tenor per expiry and stay within the source domain.