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.
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(())
}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 on the -year swap rate and paying at , the linear terminal swap rate model gives
where is the forward swap rate, the annuity of its fixed leg, its last payment, with mean reversion , , and under a flat normal vol (with a smile, the variance is replicated off it). The slope is positive when the payment follows the fixing, so the CMS rate sits above the forward. The swap receives 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 |
|---|---|---|---|
| the mean reversion | the mean_reversion argument |
mean_reversion= |
|
| the swaption vol | MarketContext::with_swaption_vols |
MarketContext.with_swaption_vols |
|
| 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 |
| 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 |
, 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.