Esc

    The API is under testing and may change before release.

    Price an FX option from a delta quote

    You'll price an FX option that's quoted by its delta rather than its strike, the way an FX desk quotes a 25-delta call, and read back the strike and vol it resolves to.

    Before you start

    • helixio installed: see installing.
    • A discount curve for each currency of the pair, and the pair's spot. Here that's a EUR close, a USD SOFR close and the EUR/USD spot.
    • The pair's vol quotes for each expiry: the at-the-money vol, the 25-delta risk reversal and the 25-delta butterfly.

    Steps

    1. Mark both currencies' curves and the spot on one market.
    2. Build the surface from each expiry's three quotes with vols.FxVolSurface, and pin it with with_fx_vols.
    3. Book the option by its delta: products.fx_option(..., delta=0.25) in Python, Strike::AtDelta(0.25) in Rust. Give the option's own delta, unsigned, for a put as well as a call.
    4. Read what it resolved to with diagnostics: the strike, the vol and the delta. Then value it.
    5. If you need to book it at a fixed strike, rebook it at the resolved strike. It prices the same.
    Rust
    use chrono::NaiveDate;
    use helixrs::{
        AnyInstrument, CollateralSpec, Currency, CurrencyPair, DeltaConvention, DeltaQuote, FxOption,
        FxRates, FxVolSmile, FxVolSurface, MarketContext, OptionKind, PricingRiskFactors, QuoteSheet,
        Strike,
    };
    
    type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
    
    fn date(year: i32, month: u32, day: u32) -> NaiveDate {
        NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
    }
    
    fn main() -> Outcome<()> {
        let close = date(2026, 10, 1);
        let eur_close = [
            ("EUR-ESTR-OIS", "1Y", 0.0195),
            ("EUR-ESTR-OIS", "2Y", 0.0200),
            ("EUR-ESTR-OIS", "5Y", 0.0215),
            ("EUR-ESTR-OIS", "10Y", 0.0240),
            ("EUR-ESTR-OIS", "20Y", 0.0262),
            ("EUR-ESTR-OIS", "30Y", 0.0265),
            ("EUR-EURIBOR-3M-IRS", "1Y", 0.0210),
            ("EUR-EURIBOR-3M-IRS", "2Y", 0.0216),
            ("EUR-EURIBOR-3M-IRS", "5Y", 0.0232),
            ("EUR-EURIBOR-3M-IRS", "10Y", 0.0257),
            ("EUR-EURIBOR-3M-IRS", "20Y", 0.0278),
            ("EUR-EURIBOR-3M-IRS", "30Y", 0.0280),
        ];
        let usd_sofr_close = [
            ("USD-SOFR-OIS", "1M", 0.0391),
            ("USD-SOFR-OIS", "3M", 0.0402),
            ("USD-SOFR-OIS", "1Y", 0.0439),
            ("USD-SOFR-OIS", "2Y", 0.0459),
            ("USD-SOFR-OIS", "3Y", 0.0468),
            ("USD-SOFR-OIS", "4Y", 0.0469),
            ("USD-SOFR-OIS", "5Y", 0.0471),
            ("USD-SOFR-OIS", "7Y", 0.0475),
            ("USD-SOFR-OIS", "10Y", 0.0483),
            ("USD-SOFR-OIS", "15Y", 0.0494),
            ("USD-SOFR-OIS", "20Y", 0.0499),
            ("USD-SOFR-OIS", "30Y", 0.0492),
        ];
        let (expiry, delivery) = (date(2027, 4, 1), date(2027, 4, 5));
        let (eur, usd): (Currency, Currency) = ("EUR".parse()?, "USD".parse()?);
    
        // Step 1: mark both currencies' curves and the spot, dollars per euro.
        let market = 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), 1.0950)])?);
    
        // Step 2: build the surface from each expiry's ATM vol, 25-delta risk reversal
        // and 25-delta butterfly, and pin it on the market.
        let surface = FxVolSurface::new(
            CurrencyPair::new(eur, usd),
            DeltaConvention::Spot,
            close,
            vec![
                (date(2027, 1, 4), FxVolSmile::from_quotes(0.0770, -0.0060, 0.0019)?),
                (expiry, FxVolSmile::from_quotes(0.0790, -0.0065, 0.0021)?),
            ],
        )?;
        let market = market.with_fx_vols(surface.clone());
    
        // Step 3: book the 25-delta call on two million euros by its delta.
        let call = FxOption::new(eur, usd, 2_000_000.0, OptionKind::Call, expiry, delivery, Strike::AtDelta(0.25));
    
        // Step 4: read the strike and vol it resolved to, and its price.
        let resolved = market.priced(&call.risk_factors()?, |v| call.diagnostics(v))?;
        let call_node = surface.view::<f64>().vol_at_quote(expiry, "25C".parse::<DeltaQuote>()?)?;
        let npv = AnyInstrument::FxOption(call).npv(&market)?;
        println!("forward     {:.6}", resolved.forward);
        println!("strike      {:.6}", resolved.strike);
        println!("vol         {:.6}", resolved.vol);
        println!("25C node    {call_node:.6}");
        println!("delta       {:.6}", resolved.delta);
        println!("npv (USD)   {npv:.2}");
    
        // Step 5: the same option at that absolute strike prices the same.
        let at_strike = AnyInstrument::FxOption(FxOption::new(
            eur,
            usd,
            2_000_000.0,
            OptionKind::Call,
            expiry,
            delivery,
            Strike::Absolute(resolved.strike),
        ));
        println!("at strike   {:.2}", at_strike.npv(&market)?);
    
        Ok(())
    }
    Python
    from datetime import date
    
    import helixio as hlx
    
    SOFR_CLOSE_DATE = date(2026, 10, 1)
    EUR_CLOSE = {
        "EUR-ESTR-OIS": {"1Y": 0.0195, "2Y": 0.0200, "5Y": 0.0215, "10Y": 0.0240, "20Y": 0.0262, "30Y": 0.0265},
        "EUR-EURIBOR-3M-IRS": {"1Y": 0.0210, "2Y": 0.0216, "5Y": 0.0232, "10Y": 0.0257, "20Y": 0.0278, "30Y": 0.0280},
    }
    USD_SOFR_CLOSE = {
        "USD-SOFR-OIS": {
            "1M": 0.0391, "3M": 0.0402, "1Y": 0.0439, "2Y": 0.0459, "3Y": 0.0468, "4Y": 0.0469,
            "5Y": 0.0471, "7Y": 0.0475, "10Y": 0.0483, "15Y": 0.0494, "20Y": 0.0499, "30Y": 0.0492,
        },
    }
    EXPIRY, DELIVERY = date(2027, 4, 1), date(2027, 4, 5)
    
    # Step 1: mark both currencies' curves and the spot, dollars per euro.
    market = (
        hlx.MarketContext(SOFR_CLOSE_DATE)
        .with_recipe_quotes("USD", USD_SOFR_CLOSE)
        .with_recipe_quotes("EUR", EUR_CLOSE)
        .with_fx_rates([("EUR", "USD", 1.0950)])
    )
    
    # Step 2: build the surface from each expiry's ATM vol, 25-delta risk reversal
    # and 25-delta butterfly, and pin it on the market.
    surface = hlx.vols.FxVolSurface(
        "EUR",
        "USD",
        SOFR_CLOSE_DATE,
        [(date(2027, 1, 4), 0.0770, -0.0060, 0.0019), (EXPIRY, 0.0790, -0.0065, 0.0021)],
    )
    market = market.with_fx_vols(surface)
    
    # Step 3: book the 25-delta call on two million euros by its delta.
    call = hlx.products.fx_option(
        "EUR", "USD", notional_base=2_000_000.0, kind="call", expiry=EXPIRY, delivery=DELIVERY, delta=0.25
    )
    
    # Step 4: read the strike and vol it resolved to, and its price.
    resolved = call.diagnostics(market)
    npv = call.npv(market)
    print(f"forward     {resolved.forward():.6f}")
    print(f"strike      {resolved.strike():.6f}")
    print(f"vol         {resolved.vol():.6f}")
    print(f"25C node    {surface.vol_at_quote(EXPIRY, '25C'):.6f}")
    print(f"delta       {resolved.delta():.6f}")
    print(f"npv (USD)   {npv:.2f}")
    
    # Step 5: the same option at that absolute strike prices the same.
    at_strike = hlx.products.fx_option(
        "EUR", "USD", notional_base=2_000_000.0, kind="call", expiry=EXPIRY, delivery=DELIVERY, strike=resolved.strike()
    )
    print(f"at strike   {at_strike.npv(market):.2f}")
    forward     1.107834
    strike      1.150922
    vol         0.077850
    25C node    0.077850
    delta       0.250000
    npv (USD)   17528.69
    at strike   17528.69

    Check it worked

    • The resolved delta is exactly the one you quoted, 0.25 here.
    • On a smile that quotes that delta, the vol is the node's vol: the 25-delta call reads 25C.
    • A call quoted at 25 delta is struck above the forward.
    • Rebooked at the resolved strike, the option prices the same, to the cent.
    • FX options, every argument of the option and the surface.
    • How FX products work, why the strike and the vol are solved together.
    • FX vol surface, how each pair quotes its deltas.

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