Esc

    The API is under testing and may change before release.

    CMS coupons

    This page lists what you need to price a constant-maturity-swap (CMS) coupon: the rate functions, the coupon and strip constructors, and the market inputs they read. A CMS coupon pays a swap rate, the par rate of a swap of fixed tenor, observed at the coupon's fixing and paid at its payment date. A CMS spread coupon pays the difference of two such rates.

    Quick example

    This reads the 10-year EUR swap rate fixing in October 2027 and its convexity-adjusted CMS rate, prices a CMS call and put, a 10Y-less-2Y spread rate, and the same 10-year rate as a coupon in a leg. The example reads its quotes from the desk sheets; load them once and it runs as shown. The swaption vols are stated in the program, because no recipe builds them.

    Rust
    use chrono::NaiveDate;
    use helixio_docs_sheets::{EUR_CLOSE, SOFR_CLOSE_DATE};
    use helixrs::{
        AnyCoupon, CmsCoupon, CollateralSpec, Direction, Leg, MarketContext, OptionKind, QuoteSheet,
        RateVolSurface, SwapIndex, VolConvention, YieldCurve, cms_option, cms_rate,
        cms_spread_coupon_rate, leg_cashflow_records,
    };
    
    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 (fixing, payment, reversion) = (date((2027, 10, 1)), date((2028, 1, 5)), 0.01);
    
        // Curves from the sheet; swaption vols, which no recipe builds, joined with the
        // market's verb: normal vols by expiry and swap tenor, both in years.
        let swaption_vols = RateVolSurface::new(
            vec![0.5, 1.0, 2.0, 5.0],
            vec![2.0, 10.0, 20.0],
            &[
                vec![0.0070, 0.0068, 0.0066],
                vec![0.0072, 0.0070, 0.0068],
                vec![0.0075, 0.0072, 0.0070],
                vec![0.0078, 0.0074, 0.0071],
            ],
            VolConvention::Normal,
        )?;
        let sheet = QuoteSheet::from_rows(EUR_CLOSE.iter().copied())?;
        let market = MarketContext::as_of(close)
            .with_recipe_quotes(&CollateralSpec::Currency("EUR".parse()?), &sheet)?
            .with_swaption_vols("EUR".parse()?, swaption_vols);
        let ten = SwapIndex::new("EUR-EURIBOR-3M-IRS", "10Y".parse()?)?;
        let two = SwapIndex::new("EUR-EURIBOR-3M-IRS", "2Y".parse()?)?;
        let projection = ten.projection();
        let factors = ten.risk_factors(&projection);
    
        // The 10Y swap rate fixing on 1 October 2027, paid on 5 January 2028.
        let ten_year = market.priced(&factors, |view| cms_rate(&ten, fixing, payment, reversion, view))?;
        let (forward, cms) = (ten_year.forward, ten_year.rate);
        println!("forward swap rate  {forward:.8}");
        println!("CMS rate           {cms:.8}");
    
        // A CMS call and put at one strike, replicated off the same smile.
        let strike = 0.026;
        let option = |kind| {
            market.priced(&factors, |view| cms_option(&ten, fixing, payment, reversion, strike, kind, view))
        };
        let (call, put) = (option(OptionKind::Call)?, option(OptionKind::Put)?);
        println!("call  {call:.8}");
        println!("put   {put:.8}");
        println!("call less put  {:.8}", call - put);
        println!("CMS less K     {:.8}", cms - strike);
    
        // A 10Y-less-2Y spread coupon. With no option on it the correlation does not
        // enter, and it pays the difference of the two adjusted rates.
        let cms_2y = market.priced(&factors, |view| cms_rate(&two, fixing, payment, reversion, view))?.rate;
        let spread = market.priced(&factors, |view| {
            cms_spread_coupon_rate(&ten, &two, fixing, payment, reversion, 0.8, 1.0, 0.0, None, view)
        })?;
        println!("2Y CMS rate  {cms_2y:.8}");
        println!("10Y less 2Y  {spread:.8}");
    
        // The same rate as a coupon in a leg: accruing from 5 October 2027, it fixes two
        // TARGET days before, on 1 October, and its table carries the rate above.
        let coupon = CmsCoupon::on_index(
            &ten,
            date((2027, 10, 5)),
            payment,
            payment,
            10_000_000.0,
            1.0,
            0.0,
            "ACT/360".parse()?,
            reversion,
        );
        let leg = Leg::new(vec![AnyCoupon::Cms(coupon)], Direction::Receive);
        let eur = CollateralSpec::Currency("EUR".parse()?);
        let projections = leg.projections();
        let row = market.priced(&leg.risk_factors(&eur, &projections), |view| {
            Ok(leg_cashflow_records(&leg, &view.disc, view, &view.fixings, close)?)
        })?;
        let fixes = row["fixing_date"].dates().expect("dates")[0].expect("a fixing date");
        let rate = row["rate"].numbers().expect("numbers")[0];
        let npv = leg.npv_on(&market, &eur)?;
        println!("fixes on  {fixes}");
        println!("rate      {rate:.8}");
        println!("npv       {npv:.2}");
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    from desk_sheets import EUR_CLOSE, SOFR_CLOSE_DATE
    
    INDEX, FIXING, PAYMENT, REVERSION = "EUR-EURIBOR-3M-IRS", dt.date(2027, 10, 1), dt.date(2028, 1, 5), 0.01
    
    # Curves from the sheet; swaption vols, which no recipe builds, joined with the
    # market's verb: normal vols by expiry and swap tenor, both in years.
    swaption_vols = hlx.vols.RateVolSurface(
        [0.5, 1.0, 2.0, 5.0],
        [2.0, 10.0, 20.0],
        [[0.0070, 0.0068, 0.0066], [0.0072, 0.0070, 0.0068], [0.0075, 0.0072, 0.0070], [0.0078, 0.0074, 0.0071]],
        vol_convention="normal",
    )
    market = (
        hlx.MarketContext(SOFR_CLOSE_DATE)
        .with_recipe_quotes("EUR", EUR_CLOSE)
        .with_swaption_vols("EUR", swaption_vols)
    )
    
    # The 10Y swap rate fixing on 1 October 2027, paid on 5 January 2028.
    forward, cms = hlx.rates.cms_rate(market, INDEX, "10Y", FIXING, PAYMENT, REVERSION)
    print(f"forward swap rate  {forward:.8f}")
    print(f"CMS rate           {cms:.8f}")
    
    # A CMS call and put at one strike, replicated off the same smile.
    STRIKE = 0.026
    call = hlx.rates.cms_option(market, INDEX, "10Y", FIXING, PAYMENT, REVERSION, strike=STRIKE, kind="Call")
    put = hlx.rates.cms_option(market, INDEX, "10Y", FIXING, PAYMENT, REVERSION, strike=STRIKE, kind="Put")
    print(f"call  {call:.8f}")
    print(f"put   {put:.8f}")
    print(f"call less put  {call - put:.8f}")
    print(f"CMS less K     {cms - STRIKE:.8f}")
    
    # A 10Y-less-2Y spread coupon. With no option on it the correlation does not
    # enter, and it pays the difference of the two adjusted rates.
    _, cms_2y = hlx.rates.cms_rate(market, INDEX, "2Y", FIXING, PAYMENT, REVERSION)
    spread = hlx.rates.cms_spread_coupon_rate(market, INDEX, "10Y", "2Y", FIXING, PAYMENT, REVERSION, correlation=0.8)
    print(f"2Y CMS rate  {cms_2y:.8f}")
    print(f"10Y less 2Y  {spread:.8f}")
    
    # The same rate as a coupon in a leg: accruing from 5 October 2027, it fixes two
    # TARGET days before, on 1 October, and its table carries the rate above.
    coupon = hlx.legs.cms_coupon(
        INDEX, "10Y", dt.date(2027, 10, 5), PAYMENT, PAYMENT, notional=10_000_000.0, day_count="ACT/360"
    )
    leg = hlx.Leg([coupon], "Receive", "EUR")
    row = leg.cashflow_records(market).to_dict()
    npv = leg.npv(market)
    print(f"fixes on  {row['fixing_date'][0]}")
    print(f"rate      {row['rate'][0]:.8f}")
    print(f"npv       {npv:.2f}")
    forward swap rate  0.02666239
    CMS rate           0.02690566
    call  0.00326702
    put   0.00236136
    call less put  0.00090566
    CMS less K     0.00090566
    2Y CMS rate  0.02331825
    10Y less 2Y  0.00358741
    fixes on  2027-10-01
    rate      0.02690566
    npv       67062.74

    The CMS rate sits above the forward swap rate. The call less the put is the CMS rate less the strike, the spread rate is the 10-year CMS rate less the 2-year one, and the coupon fixes two TARGET days before it accrues, at the rate the function gives.

    How it's priced

    Let S(t)S(t) be the index's forward swap rate, with annuity A(t)A(t). Paid at TpT_p rather than on the swap's own dates, its expectation under the payment measure exceeds the forward. Under the linear terminal swap rate model, P(t,Tp)/A(t)≈α+β S(t)P(t, T_p)/A(t) \approx \alpha + \beta\, S(t), and the adjusted rate is

    CMS=S0+βα+βS0 Var⁡[S(Tf)],\mathrm{CMS} = S_0 + \frac{\beta}{\alpha + \beta S_0}\, \operatorname{Var}\bigl[S(T_f)\bigr],

    with TfT_f the fixing date and the variance read off the swaption smile. The slope β\beta is positive, so CMS>S0\mathrm{CMS} > S_0 whenever the rate has any variance. A call and a put at KK are replicated off the same smile, so

    C(K)−P(K)=CMS−K.C(K) - P(K) = \mathrm{CMS} - K.

    A spread coupon CMSa−CMSb\mathrm{CMS}_a - \mathrm{CMS}_b with no option on it is linear in the two rates, so the correlation doesn't enter. Why a CMS rate is worth more than the forward is in how cashflows work.

    API

    Symbol Meaning Rust Python
    index a swap index: template and tenor SwapIndex::new named by template and tenor
    S0S_0, CMS\mathrm{CMS} the forward and the adjusted rate cms_rate → CmsRate { forward, rate } rates.cms_rate → (forward, rate)
    C(K)C(K), P(K)P(K) CMS caplet and floorlet, as rates cms_option with OptionKind rates.cms_option with kind=
    coupon rate what a coupon pays, with any option cms_coupon_rate rates.cms_coupon_rate
    spread rate CMSa−CMSb\mathrm{CMS}_a - \mathrm{CMS}_b cms_spread_coupon_rate rates.cms_spread_coupon_rate
    coupon a CMS coupon CmsCoupon::on_index legs.cms_coupon
    strips one coupon per period cms_leg, cms_spread_leg legs.cms_leg, legs.cms_spread_leg
    vols the swaption smile MarketContext::with_swaption_vols MarketContext.with_swaption_vols
    correlation the market's mark for a pair MarketContext::with_cms_spread_correlation MarketContext.with_cms_spread_correlation

    rates.cms_rate(...) and rates.cms_option(...):

    Argument Type Default Meaning
    market MarketContext required curves and the currency's swaption vols
    template str required the swap index's template, such as "EUR-EURIBOR-3M-IRS"
    tenor str required the swap's tenor, such as "10Y"
    fixing date required when the rate is observed
    payment date required when it's paid
    mean_reversion float required the model's mean reversion; 0.01 is the coupon default
    strike float required (option) the option's strike
    kind str required (option) "Call" or "Put"

    legs.cms_coupon(...):

    Argument Type Default Meaning
    template, tenor str required the swap index
    accrual_start, accrual_end, payment_date date required the coupon's dates; it fixes the index's lag before accrual_start
    notional float required the size
    day_count str required the accrual day count
    gearing, spread float 1.0, 0.0 the coupon pays gearing × CMS + spread
    mean_reversion float 0.01 as above

    legs.cms_spread_leg adds minus_tenor, the second rate's tenor, and correlation, a number or the name of the market's mark for the pair.

    Defaults and limits

    • Vols: a CMS coupon reads the currency's swaption vols even with no option on it, for the convexity adjustment. A market without them refuses the coupon with the surface named.
    • Correlation matters only for a spread coupon with an option on it. It's either a number in the contract or the market's mark, set with with_cms_spread_correlation.
    • Options on the coupon: caps, floors and digitals attach as on any floating leg; see coupon options.

    See also

    • Source: crates/helixrs/src/market/cms.rs, crates/helixrs/src/cashflows/coupons.rs
    • Related pages:
      • CMS swap, the traded product.
      • Swaptions, for the smile the adjustment reads.

    From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.