Esc

    The API is under testing and may change before release.

    CMS swap

    This page lists what you need to value a constant-maturity swap (CMS): the calls for a CMS rate and a CMS swap, their arguments and defaults, and the convexity adjustment the library applies. A CMS leg pays, each period, the par rate of a longer swap fixing at the period start.

    Quick example

    The example reads its quotes from the desk sheets; load them once and it runs as shown.

    This reads the 10-year EUR CMS rate for a coupon fixing in two years, then values a five-year swap receiving that rate quarterly against 2.50% fixed, and reads its par spread.

    Rust
    use chrono::NaiveDate;
    use helixio_docs_sheets::{EUR_CLOSE, SOFR_CLOSE_DATE};
    use helixrs::{
        AnyInstrument, CmsSwap, CollateralSpec, Direction, Leg, MarketContext, QuoteSheet,
        RateVolSurface, Schedule, SwapIndex, Termination, VolConvention, calendar, cms_rate, template,
    };
    
    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")
    }
    
    const INDEX: &str = "EUR-EURIBOR-3M-IRS";
    
    fn main() -> Outcome<()> {
        let close = date(SOFR_CLOSE_DATE);
        let vols = RateVolSurface::new(
            vec![0.25, 1.0, 5.0, 10.0],
            vec![1.0, 10.0, 30.0],
            &[
                vec![0.0080; 3],
                vec![0.0080; 3],
                vec![0.0080; 3],
                vec![0.0080; 3],
            ],
            VolConvention::Normal,
        )?;
        let market = MarketContext::as_of(close)
            .with_recipe_quotes(
                &CollateralSpec::Currency("EUR".parse()?),
                &QuoteSheet::from_rows(EUR_CLOSE.iter().copied())?,
            )?
            .with_swaption_vols("EUR".parse()?, vols);
    
        // A coupon fixing in two years and paid a quarter later: the forward 10-year
        // swap rate, adjusted upward for paying in arrears of the swap's annuity.
        let index = SwapIndex::new(INDEX, "10Y".parse()?)?;
        let projection = index.projection();
        let got = market.priced(&index.risk_factors(&projection), |resolved| {
            cms_rate(&index, date((2028, 10, 3)), date((2029, 1, 5)), 0.01, resolved)
        })?;
        println!("forward swap rate   {:.8}", got.forward);
        println!("cms rate            {:.8}", got.rate);
    
        // Receive the 10-year CMS rate quarterly for five years, pay 2.50% fixed on the
        // same template's fixed leg.
        let vanilla = template(INDEX)?
            .swap(close)?
            .with_maturity("5Y".parse::<Termination>()?)?
            .with_notional(10_000_000.0)
            .with_fixed_rate(0.0250)
            .build()?;
        let dates = vanilla.contract_dates();
        let schedule = Schedule::generate(
            dates[0].1,
            dates[1].1,
            "3M".parse()?,
            calendar("EUTA")?.as_ref(),
            "ModFollowing".parse()?,
            "None".parse()?,
            "ShortInitial".parse()?,
            false,
            None,
            None,
        )?;
        let fixed_paid = Leg::new(
            vanilla.legs().expect("a swap has legs")[0]
                .coupons()
                .to_vec(),
            Direction::Pay,
        );
        let cms = |spread: f64| -> Outcome<AnyInstrument> {
            let swap = CmsSwap::new(
                SwapIndex::new(INDEX, "10Y".parse()?)?,
                &schedule,
                "ACT/360".parse()?,
                10_000_000.0,
                1.0,
                spread,
                0.01,
                Direction::Receive,
                fixed_paid.clone(),
            )?;
            Ok(AnyInstrument::CmsSwap(Box::new(swap)))
        };
        let npv = cms(0.0)?.npv(&market)?;
        let spread = cms(0.0)?.par_spread(&market)?;
        println!("npv                 {npv:.2}");
        println!("par spread          {spread:.8}");
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    from desk_sheets import EUR_CLOSE, SOFR_CLOSE_DATE
    
    VOLS = hlx.vols.RateVolSurface([0.25, 1.0, 5.0, 10.0], [1.0, 10.0, 30.0], [[0.0080] * 3] * 4, "normal")
    market = (
        hlx.MarketContext(SOFR_CLOSE_DATE)
        .with_recipe_quotes("EUR", EUR_CLOSE)
        .with_swaption_vols("EUR", VOLS)
    )
    INDEX = "EUR-EURIBOR-3M-IRS"
    
    # A coupon fixing in two years and paid a quarter later: the forward 10-year
    # swap rate, adjusted upward for paying in arrears of the swap's annuity.
    forward, rate = hlx.rates.cms_rate(market, INDEX, "10Y", dt.date(2028, 10, 3), dt.date(2029, 1, 5), 0.01)
    print(f"forward swap rate   {forward:.8f}")
    print(f"cms rate            {rate:.8f}")
    
    # Receive the 10-year CMS rate quarterly for five years, pay 2.50% fixed on the
    # same template's fixed leg.
    vanilla = hlx.templates.get(INDEX).swap(SOFR_CLOSE_DATE).with_maturity("5Y")\
        .with_notional(10_000_000.0).with_fixed_rate(0.0250).build()
    dates = dict(vanilla.contract_dates())
    schedule = hlx.dates.schedule(
        dates["effective"], dates["termination"], "3M", hlx.Calendar("EUTA"), "ModFollowing"
    )
    fixed_paid = hlx.Leg(vanilla.legs()[0].coupons(), "Pay", "EUR")
    
    def cms(spread=0.0):
        return hlx.products.cms_swap(
            INDEX, "10Y", schedule, "ACT/360", notional=10_000_000.0, other=fixed_paid, spread=spread
        )
    
    npv = cms().npv(market)
    spread = cms().par_spread(market)
    print(f"npv                 {npv:.2f}")
    print(f"par spread          {spread:.8f}")
    forward swap rate   0.02751390
    cms rate            0.02815059
    npv                 187822.26
    par spread          -0.00390961

    The CMS rate sits above the forward swap rate, and the 10-year rate is above 2.50%, so the receiver is in the money.

    How it's priced

    For a coupon fixing at TT on the nn-year swap rate SS and paying at TpT_p, the linear terminal swap rate model gives

    E[S]=S0+a Var⁡(S)α0,α0=P(Tp)A0,a=P(Tp) (γ−G(Tp))P(Tn) G(Tn)+S0A0γ,\mathbb{E}[S] = S_0 + \frac{a\,\operatorname{Var}(S)}{\alpha_0}, \qquad \alpha_0 = \frac{P(T_p)}{A_0}, \qquad a = \frac{P(T_p)\,(\gamma - G(T_p))}{P(T_n)\,G(T_n) + S_0 A_0 \gamma},

    where S0S_0 is the forward swap rate, A0=∑iτiP(Ti)A_0 = \sum_i \tau_i P(T_i) the annuity of its fixed leg, TnT_n its last payment, G(x)=(1−e−κ(x−T))/κG(x) = (1 - e^{-\kappa (x - T)})/\kappa with mean reversion κ\kappa, γ=∑iτiP(Ti)G(Ti)/A0\gamma = \sum_i \tau_i P(T_i) G(T_i) / A_0, and Var⁡(S)=σ2T\operatorname{Var}(S) = \sigma^2 T under a flat normal vol (with a smile, the variance is replicated off it). The slope aa is positive when the payment follows the fixing, so the CMS rate sits above the forward. The swap receives ∑iNτiP(Ti)(E[Si]+s)\sum_i N\tau_i P(T_i)(\mathbb{E}[S_i] + s) against the other leg; its par spread makes it worth nothing. Why a CMS coupon's expectation sits above the forward swap rate is covered in how rates products work.

    API

    Symbol Meaning Rust Python
    κ\kappa the mean reversion the mean_reversion argument mean_reversion=
    σ\sigma the swaption vol MarketContext::with_swaption_vols MarketContext.with_swaption_vols
    S0,E[S]S_0, \mathbb{E}[S] the forward and the CMS rate cms_rate inside MarketContext::priced on a SwapIndex rates.cms_rate
    the swap a CMS leg against another leg CmsSwap::new products.cms_swap
    s∗s^\ast the par spread AnyInstrument::par_spread Instrument.par_spread

    rates.cms_rate(market, template, tenor, fixing, payment, mean_reversion) returns (forward, rate); every argument is required. products.cms_swap takes:

    Argument Type Default Meaning
    template str required the swap index's fixed-versus-IBOR template, such as "EUR-EURIBOR-3M-IRS"
    tenor str required the swap index's tenor, such as "10Y"
    schedule Schedule required the CMS leg's periods
    day_count str or DayCountFraction required the CMS leg's accrual day count
    notional float required the notional of both legs
    other Leg required the opposite leg, on the other side
    gearing float 1.0 the multiplier on the CMS rate
    spread float 0.0 added to the geared rate
    mean_reversion float 0.01 κ\kappa, for the convexity adjustment
    direction Direction or str "Receive" the CMS leg's side
    cap, floor float or None None collar every coupon's rate, gearing times CMS plus spread
    call, put tuple or None None a digital on each coupon, spelled as Leg.with_digital spells one
    naked bool False the digital's naked flag, as on Leg.with_digital
    gap float one basis point the digital's replication gap, as on Leg.with_digital

    In Rust, CmsSwap::new(index, schedule, day_count, notional, gearing, spread, mean_reversion, direction, other) takes the same values positionally, with the index a SwapIndex.

    Defaults and limits

    • The vols: the adjustment needs swaption vols on the market (with_swaption_vols); see swaptions.
    • The index: the swap index's conventions are those of its template; see rates conventions.
    • Coupons: the CMS coupon, with its caps, floors and spread options, is built on CMS coupons.

    See also

    • Source: crates/helixrs/src/market/cms.rs
    • Related pages:
      • CMS coupons, for the coupon.
      • Swaptions, for the vols the adjustment reads.

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