Esc

    The API is under testing and may change before release.

    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.

    Rust
    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(())
    }
    Python
    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 O1O_1 and O2O_2 is worth V=V(O1)+V(O2)V = V(O_1) + V(O_2), each option priced off the market's own surface at its own strike. Its quote is the vol σˉ\bar\sigma that solves

    V(O1; σˉ)+V(O2; σˉ)=V,V(O_1;\,\bar\sigma) + V(O_2;\,\bar\sigma) = V,

    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)
    O1O_1, O2O_2 the options FxOption::new, EuropeanSwaption::new products.fx_option, products.european_swaption
    VV its value AnyInstrument::npv Instrument.npv
    σˉ\bar\sigma 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 at premium; with no premium it uses the strategy's own value on market.
    • 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.