Structured notes
This page lists what you need to price a structured note: the constructor, the price per 100 it reports, the spread a quote implies over the issuer's discounting, the issuer's credit, and an example you can run. A structured note is a bond whose coupons are a leg built to order, such as a floating, capped or CMS rate; the investor takes the issuer's credit along with it.
Quick example
The example reads its quotes from the desk sheets; load them once and it runs as shown.
This wraps two years of quarterly EURIBOR 3M coupons on ten million, from 5 October 2026, as a note. It values the note as its coupons plus its discounted face, finds the spread a 97.00 quote implies and pins it to reprice the quote, and weights the note by its issuer's survival on a 2% hazard.
use chrono::NaiveDate;
use helixio_docs_sheets::{EUR_CLOSE, SOFR_CLOSE_DATE};
use helixrs::{
AnyIndex, AnyInstrument, CollateralSpec, CreditCurveId, Curve, Direction, FlatHazard,
FlatSpread, LegTerms, MarketContext, PaymentRule, QuoteSheet, RiskFactors, Schedule,
StructuredNote, Survival, YieldCurve, calendar, ibor_leg_on_index, index, note_dirty_price,
note_z_spread,
};
type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
const FACE: f64 = 10_000_000.0;
const ISSUER: &str = "ACME/SeniorUnSec/MM14/EUR";
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 eur = CollateralSpec::Currency("EUR".parse()?);
// The coupons are an ordinary leg; the note receives it and redeems the face
// on the last payment date.
let sheet = QuoteSheet::from_rows(EUR_CLOSE.iter().copied())?;
let market = MarketContext::as_of(close).with_recipe_quotes(&eur, &sheet)?;
let target = calendar("EUTA")?;
let quarterly = Schedule::generate(
date((2026, 10, 5)),
date((2028, 10, 5)),
"3M".parse()?,
target.as_ref(),
"ModFollowing".parse()?,
"None".parse()?,
"ShortInitial".parse()?,
false,
None,
None,
)?;
let terms = LegTerms {
notional: FACE,
day_count: "ACT/360".parse()?,
currency: "EUR".parse()?,
direction: Direction::Receive,
payment: PaymentRule::none(),
};
let Some(AnyIndex::Ibor(euribor)) = index("EUR-EURIBOR-3M") else {
panic!("a registered term index");
};
let leg = ibor_leg_on_index(&quarterly, &terms, &euribor, 1.0, 0.0);
let lifted = market.lift::<f64>();
let view = market.view(&lifted)?;
let resolved = view.resolve(&RiskFactors::discounted_by(&eur).projecting("EUR-EURIBOR-3M"))?;
let coupons: f64 = leg.npv::<f64, _>(&resolved.disc, &resolved, &resolved.fixings)?;
let note = StructuredNote::new(&leg, FACE, eur.clone())?;
let plain = AnyInstrument::StructuredNote(Box::new(note.clone()));
let value = plain.npv(&market)?;
let last_payment = leg
.coupons()
.iter()
.map(|coupon| coupon.payment_date())
.max()
.expect("a coupon");
let face_today = FACE * resolved.disc.discount(last_payment)?;
let dirty = note_dirty_price(&plain, &market)?;
println!("coupons {coupons:.2}");
println!("face, discounted {face_today:.2}");
println!("note {value:.2}");
println!("per 100 {dirty:.6}");
// A quote below the risk-free value implies a spread on the issuer's
// discounting. Pinned under the note's key, that spread gives the quote back.
let quote = 97.0;
let z = note_z_spread(&plain, &market, quote)?;
let pinned = market
.clone()
.with_security_spread("ACME-NOTE", Curve::FlatSpread(FlatSpread::new(close, z)?));
let marked = AnyInstrument::StructuredNote(Box::new(note.clone().spread_by("ACME-NOTE")));
println!("quoted per 100 {quote:.2}");
println!("z-spread {:.4} bp", z * 10_000.0);
println!("repriced per 100 {:.6}", note_dirty_price(&marked, &pinned)?);
// Tagged to its issuer, the note is weighted by the issuer's survival and
// recovers part of its face on default. With no default risk it is the plain
// note; with some, it is worth less.
let issuer: CreditCurveId = ISSUER.parse()?;
let with_hazard = |hazard: f64| -> Outcome<MarketContext> {
let curve = Survival::FlatHazard(FlatHazard::new(close, hazard, "ACT/365.FIXED".parse()?)?);
Ok(market.clone().with_credit_curve(issuer.clone(), curve, Some(0.4)))
};
let tagged = AnyInstrument::StructuredNote(Box::new(note.issued_by(issuer.clone())));
let riskless = tagged.npv(&with_hazard(0.0)?)?;
let risky = tagged.npv(&with_hazard(0.02)?)?;
println!("no default risk {riskless:.2}");
println!("hazard 2% {risky:.2}");
Ok(())
}import datetime as dt
import helixio as hlx
from desk_sheets import EUR_CLOSE, SOFR_CLOSE_DATE
FACE = 10_000_000.0
ISSUER = "ACME/SeniorUnSec/MM14/EUR"
# The coupons are an ordinary leg; the note receives it and redeems the face
# on the last payment date.
market = hlx.MarketContext(SOFR_CLOSE_DATE).with_recipe_quotes("EUR", EUR_CLOSE)
target = hlx.Calendar("EUTA")
quarterly = hlx.dates.schedule(dt.date(2026, 10, 5), dt.date(2028, 10, 5), "3M", target)
paying = quarterly.paying(0, target)
leg = hlx.Leg(
hlx.legs.ibor_leg(quarterly, paying, "EUR", notional=FACE, index="EUR-EURIBOR-3M", day_count="ACT/360"),
"Receive",
"EUR",
)
note = hlx.products.structured_note(leg, face=FACE)
value = note.npv(market)
face_today = FACE * market.curve("discount(EUR)").discount(paying[-1])
dirty = hlx.bonds.note_dirty_price(note, market)
print(f"coupons {leg.npv(market):.2f}")
print(f"face, discounted {face_today:.2f}")
print(f"note {value:.2f}")
print(f"per 100 {dirty:.6f}")
# A quote below the risk-free value implies a spread on the issuer's
# discounting. Pinned under the note's key, that spread gives the quote back.
quote = 97.0
z = hlx.bonds.note_z_spread(note, market, quote)
pinned = market.with_security_spread("ACME-NOTE", hlx.curves.SpreadCurve.flat(SOFR_CLOSE_DATE, z))
marked = hlx.products.structured_note(leg, face=FACE, security_spread="ACME-NOTE")
print(f"quoted per 100 {quote:.2f}")
print(f"z-spread {z * 10_000:.4f} bp")
print(f"repriced per 100 {hlx.bonds.note_dirty_price(marked, pinned):.6f}")
# Tagged to its issuer, the note is weighted by the issuer's survival and
# recovers part of its face on default. With no default risk it is the plain
# note; with some, it is worth less.
def with_hazard(hazard):
curve = hlx.curves.SurvivalCurve.flat_hazard(SOFR_CLOSE_DATE, hazard)
return market.with_credit_curve(ISSUER, curve, 0.4)
tagged = hlx.products.structured_note(leg, face=FACE, issuer=ISSUER)
riskless = tagged.npv(with_hazard(0.0))
risky = tagged.npv(with_hazard(0.02))
print(f"no default risk {riskless:.2f}")
print(f"hazard 2% {risky:.2f}")coupons 419208.38
face, discounted 9603774.19
note 10022982.57
per 100 100.229826
quoted per 100 97.00
z-spread 165.6827 bp
repriced per 100 97.000000
no default risk 10022982.57
hazard 2% 9788359.35
The note is its coupons plus its discounted face. A 97.00 quote sits below that risk-free value, so it implies a positive spread on the issuer's discounting; pinned under the note's key, the spread gives the quote back. Tagged to its issuer, the note is worth the plain note with no default risk, and less on a 2% hazard.
How it's priced
With coupons worth on the market and face paid at the last payment date :
A flat spread on the issuer's discounting scales each flow at time by , with the coupons still projected spread-free. Tagged to its issuer, each flow is weighted by the issuer's survival, and each period adds a recovery of face weighted by the chance of default in it. How a note's price carries its issuer is covered in how bonds work.
API
| Argument | Type | Default | Meaning |
|---|---|---|---|
leg |
Leg |
required | the coupons, received; any leg helixio builds: fixed, floating, collared, CMS or CMS spread |
face |
float |
required | the face redeemed on the last payment date |
issuer |
str |
None |
tags the issuer, so a credit curve and recovery value the note risky |
security_spread |
str |
None |
a key whose spread curve the note discounts under |
products.structured_note(leg, *, face, issuer, security_spread) builds it; in Rust,
StructuredNote::new(&leg, face, collateral)?, with .issued_by(credit) and .spread_by(key).
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| the coupon leg | Leg::npv |
Leg.npv |
|
| the note | AnyInstrument::npv on AnyInstrument::StructuredNote |
Instrument.npv |
|
| the note per 100 of face | note_dirty_price(¬e, &market)? |
bonds.note_dirty_price(note, market) |
|
| the z-spread of a quote | note_z_spread(¬e, &market, quote)? |
bonds.note_z_spread(note, market, quote) |
Defaults and limits
- The leg is received. A leg the holder would pay is refused.
- Market inputs: whatever the leg needs (its discount and projection curves, and any fixings or
vols), plus a credit curve and recovery when the note is tagged to an issuer, or a spread curve
under its
security_spreadkey. - The z-spread applies to the discounting only; the coupons are projected spread-free.
See also
- Source:
crates/helixrs/src/instruments/structured.rs - Related pages:
- How bonds work, for how a note carries its issuer.
- Legs and coupons, for the coupons a note wraps.
- Build a leg from a schedule, step by step.
- Spread measures, for the same spreads on a fixed-rate bond.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.