Straddle
This page lists everything you need to book and quote a straddle or a strangle: the function, what it accepts, how the strategy's single vol is found, and an example you can run. A straddle is a call and a put at the same strike, held together; a strangle is the same pair at two strikes. Desks quote either as one number, a vol, rather than as two premiums.
Quick example
The example reads its rates from the desk sheets; load them once and it runs as shown.
This builds a EUR/USD market at 1.10 with a one-expiry smile, and books an FX straddle at 1.13 and a strangle from 1.10 to 1.16, each expiring on 1 October 2027. It reads the one vol the straddle is quoted on and reprices it at that vol. It then books a swaption straddle at the forward swap rate on a SOFR market, and shows that an FX option and a swaption aren't accepted as one strategy.
use chrono::NaiveDate;
use helixio_docs_sheets::{EUR_CLOSE, SOFR_CLOSE_DATE, USD_SOFR_CLOSE};
use helixrs::{
AnyInstrument, CollateralSpec, Currency, CurrencyPair, DeltaConvention, EuropeanSwaption,
FxOption, FxRates, FxVolSmile, FxVolSurface, MarketContext, OptionKind, QuoteSheet,
RateVolSurface, Side, Straddle, Strike, SwaptionKind, SwaptionUnderlying, Termination,
VolConvention, template,
};
type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
const SPOT: f64 = 1.1000;
const NOTIONAL: f64 = 1_000_000.0;
fn date((year, month, day): (i32, u32, u32)) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
}
/// A one-expiry EUR/USD surface from its three quotes.
fn smile(close: NaiveDate, atm: f64, rr: f64, bf: f64) -> Outcome<FxVolSurface> {
let (eur, usd) = ("EUR".parse()?, "USD".parse()?);
Ok(FxVolSurface::new(
CurrencyPair::new(eur, usd),
DeltaConvention::Spot,
close,
vec![(date((2027, 10, 1)), FxVolSmile::from_quotes(atm, rr, bf)?)],
)?)
}
fn fx(kind: OptionKind, strike: f64) -> Outcome<AnyInstrument> {
let (eur, usd): (Currency, Currency) = ("EUR".parse()?, "USD".parse()?);
Ok(AnyInstrument::FxOption(FxOption::new(
eur,
usd,
NOTIONAL,
kind,
date((2027, 10, 1)),
date((2027, 10, 5)),
Strike::Absolute(strike),
)))
}
fn strategy(first: AnyInstrument, second: AnyInstrument) -> Outcome<AnyInstrument> {
Ok(AnyInstrument::Straddle(Box::new(Straddle::new(first, second)?)))
}
fn underlying(close: NaiveDate, rate: f64, side: Side) -> Outcome<AnyInstrument> {
Ok(template("USD-SOFR-OIS")?
.swap(close)?
.with_maturity("1Y5Y".parse::<Termination>()?)?
.with_notional(10_000_000.0)
.with_fixed_rate(rate)
.with_side(side)
.build()?)
}
fn swaption(close: NaiveDate, strike: f64, kind: SwaptionKind) -> Outcome<AnyInstrument> {
let side = if kind == SwaptionKind::Payer {
Side::Payer
} else {
Side::Receiver
};
let AnyInstrument::Ois(swap) = underlying(close, strike, side)? else {
unreachable!("a SOFR swap is an overnight indexed swap")
};
let option = EuropeanSwaption::new(
SwaptionUnderlying::Ois(swap),
date((2027, 10, 1)),
strike,
kind,
)?;
Ok(AnyInstrument::EuropeanSwaption(Box::new(option)))
}
fn main() -> Outcome<()> {
let close = date(SOFR_CLOSE_DATE);
let (eur, usd): (Currency, Currency) = ("EUR".parse()?, "USD".parse()?);
let rates = MarketContext::as_of(close)
.with_recipe_quotes(
&CollateralSpec::Currency(usd),
&QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?,
)?
.with_recipe_quotes(
&CollateralSpec::Currency(eur),
&QuoteSheet::from_rows(EUR_CLOSE.iter().copied())?,
)?
.with_fx_rates(FxRates::from_quotes(&[(CurrencyPair::new(eur, usd), SPOT)])?);
let market = rates.clone().with_fx_vols(smile(close, 0.082, -0.008, 0.0025)?);
// A straddle at 1.13 and a strangle from 1.10 to 1.16, each worth its two legs.
let straddle = strategy(fx(OptionKind::Call, 1.13)?, fx(OptionKind::Put, 1.13)?)?;
let strangle = strategy(fx(OptionKind::Put, 1.10)?, fx(OptionKind::Call, 1.16)?)?;
let legs = fx(OptionKind::Call, 1.13)?.npv(&market)? + fx(OptionKind::Put, 1.13)?.npv(&market)?;
println!("straddle {:.6}", straddle.npv(&market)?);
println!("its legs {legs:.6}");
println!("strangle {:.6}", strangle.npv(&market)?);
// The strategy's quote: the one flat vol that reprices both legs to their sum.
// Priced on a smile flat at that vol, the straddle is worth what it was, to
// the tolerance the vol was solved to.
let quote = straddle.implied_vol(&market, None)?;
let flat = rates.clone().with_fx_vols(smile(close, quote, 0.0, 0.0)?);
println!("quoted vol {quote:.8}");
println!("repriced {:.6}", straddle.npv(&flat)?);
// The same on swaptions: a payer and a receiver at the forward swap rate.
let swaptions = MarketContext::as_of(close)
.with_recipe_quotes(
&CollateralSpec::Currency(usd),
&QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?,
)?
.with_swaption_vols(
usd,
RateVolSurface::new(
vec![1.0, 2.0, 5.0],
vec![2.0, 5.0, 10.0],
&[
vec![0.0100, 0.0105, 0.0110],
vec![0.0102, 0.0107, 0.0112],
vec![0.0100, 0.0104, 0.0108],
],
VolConvention::Normal,
)?,
);
let forward = underlying(close, 0.0, Side::Payer)?.par_rate(&swaptions)?;
let rates_straddle = strategy(
swaption(close, forward, SwaptionKind::Payer)?,
swaption(close, forward, SwaptionKind::Receiver)?,
)?;
let both = swaption(close, forward, SwaptionKind::Payer)?.npv(&swaptions)?
+ swaption(close, forward, SwaptionKind::Receiver)?.npv(&swaptions)?;
println!("straddle {:.6}", rates_straddle.npv(&swaptions)?);
println!("its legs {both:.6}");
println!(
"quoted vol {:.8}",
rates_straddle.implied_vol(&swaptions, None)?
);
// An FX option and a swaption are not one strategy, and are refused as one.
let refused = Straddle::new(
fx(OptionKind::Call, 1.13)?,
swaption(close, forward, SwaptionKind::Payer)?,
)
.is_err();
println!(
"mixed strategy refused {}",
if refused { "yes" } else { "no" }
);
Ok(())
}import datetime as dt
import helixio as hlx
from desk_sheets import EUR_CLOSE, SOFR_CLOSE_DATE, USD_SOFR_CLOSE
SPOT = 1.1000
EXPIRY, DELIVERY = dt.date(2027, 10, 1), dt.date(2027, 10, 5)
NOTIONAL = 1_000_000.0
rates = (
hlx.MarketContext(SOFR_CLOSE_DATE)
.with_recipe_quotes("USD", USD_SOFR_CLOSE)
.with_recipe_quotes("EUR", EUR_CLOSE)
.with_fx_rates([("EUR", "USD", SPOT)])
)
def smile(atm, rr, bf):
"""A one-expiry EUR/USD surface from its three quotes."""
return hlx.vols.FxVolSurface("EUR", "USD", SOFR_CLOSE_DATE, [(EXPIRY, atm, rr, bf)])
def fx(kind, strike):
return hlx.products.fx_option(
"EUR", "USD", notional_base=NOTIONAL, kind=kind, expiry=EXPIRY, delivery=DELIVERY, strike=strike
)
market = rates.with_fx_vols(smile(0.082, -0.008, 0.0025))
# A straddle at 1.13 and a strangle from 1.10 to 1.16, each worth its two legs.
straddle = hlx.products.straddle(fx("call", 1.13), fx("put", 1.13))
strangle = hlx.products.straddle(fx("put", 1.10), fx("call", 1.16))
legs = fx("call", 1.13).npv(market) + fx("put", 1.13).npv(market)
print(f"straddle {straddle.npv(market):.6f}")
print(f"its legs {legs:.6f}")
print(f"strangle {strangle.npv(market):.6f}")
# The strategy's quote: the one flat vol that reprices both legs to their sum.
# Priced on a smile flat at that vol, the straddle is worth what it was, to
# the tolerance the vol was solved to.
quote = straddle.implied_vol(market)
flat = rates.with_fx_vols(smile(quote, 0.0, 0.0))
print(f"quoted vol {quote:.8f}")
print(f"repriced {straddle.npv(flat):.6f}")
# The same on swaptions: a payer and a receiver at the forward swap rate.
swaptions = (
hlx.MarketContext(SOFR_CLOSE_DATE)
.with_recipe_quotes("USD", USD_SOFR_CLOSE)
.with_swaption_vols(
"USD",
hlx.vols.RateVolSurface(
[1.0, 2.0, 5.0],
[2.0, 5.0, 10.0],
[[0.0100, 0.0105, 0.0110], [0.0102, 0.0107, 0.0112], [0.0100, 0.0104, 0.0108]],
"normal",
),
)
)
sofr = hlx.templates.get("USD-SOFR-OIS")
def swaption(strike, kind):
swap = sofr.swap(SOFR_CLOSE_DATE).with_maturity("1Y5Y").with_notional(10_000_000.0)
swap = swap.with_fixed_rate(strike).with_side(kind).build()
return hlx.products.european_swaption(swap, EXPIRY, strike=strike, kind=kind)
forward = sofr.swap(SOFR_CLOSE_DATE).with_maturity("1Y5Y").with_notional(10_000_000.0)
forward = forward.with_fixed_rate(0.0).with_side("payer").build().par_rate(swaptions)
rates_straddle = hlx.products.straddle(swaption(forward, "payer"), swaption(forward, "receiver"))
both = swaption(forward, "payer").npv(swaptions) + swaption(forward, "receiver").npv(swaptions)
print(f"straddle {rates_straddle.npv(swaptions):.6f}")
print(f"its legs {both:.6f}")
print(f"quoted vol {rates_straddle.implied_vol(swaptions):.8f}")
# An FX option and a swaption are not one strategy, and are refused as one.
try:
hlx.products.straddle(fx("call", 1.13), swaption(forward, "payer"))
refused = False
except ValueError:
refused = True
print(f"mixed strategy refused {'yes' if refused else 'no'}")straddle 70736.094846
its legs 70736.094846
strangle 46597.837443
quoted vol 0.08207241
repriced 70736.094851
straddle 353344.743603
its legs 353344.743603
quoted vol 0.01050055
mixed strategy refused yes
Each strategy is worth exactly its two options. The strangle, struck further from the money, costs less. Priced on a smile flat at its quoted vol, the straddle is worth what it was, to the tolerance the vol was solved to.
How it's priced
A strategy of options and is worth , each option priced off the market's own surface at its own strike. Its quote is the vol that solves
with both options priced at the one flat vol.
API
products.straddle(first, second) books two options of one strategy as one product: a straddle,
or a strangle when the strikes differ.
| Argument | Type | Default | Meaning |
|---|---|---|---|
first |
Instrument |
required | an FX option from products.fx_option, or a European swaption from products.european_swaption |
second |
Instrument |
required | the other option of the same strategy: on the same currency pair, or a swaption in the same currency |
Instrument.npv(market) values the strategy, and Instrument.implied_vol(market, premium=None)
returns its quote. In Rust the strategy is AnyInstrument::Straddle(Straddle::new(first, second)), quoted with
AnyInstrument::implied_vol(market, None).
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| the trade | two options as one product | Straddle::new(first, second) |
products.straddle(first, second) |
| , | the options | FxOption::new, EuropeanSwaption::new |
products.fx_option, products.european_swaption |
| its value | AnyInstrument::npv |
Instrument.npv |
|
| its quote | AnyInstrument::implied_vol |
Instrument.implied_vol |
Defaults and limits
- One strategy: the two options must be FX options on one pair or European swaptions in one
currency. Any other pair raises
ValueError. - The quote:
implied_vol(market, premium)solves for the vol that prices the strategy atpremium; with no premium it uses the strategy's own value onmarket. - The market: an FX strategy needs the pair's spot, both currencies' curves and an FX vol surface; see FX options. A swaption strategy needs a swaption vol surface; see swaptions.
See also
- Source:
crates/helixrs/src/instruments/straddle.rs - Related pages:
- How FX products work, for how FX vols are quoted.
- How rates products work, for swaptions.
- FX options, for each leg of an FX strategy.
- Swaptions, for each leg of a swaption strategy.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.