Esc

    The API is under testing and may change before release.

    NDO

    This page lists what you need to draft and value a non-deliverable option (NDO): its templates, the arguments they take, the forward it reads, and an example you can run. An NDO is an FX option on a currency that can't be delivered offshore, settled in cash in dollars. It's to an FX option what an NDF is to a forward.

    Quick example

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

    This marks the same won market as the NDF page, adds a USD/KRW smile per expiry, and drafts a six-month dollar call against won at 1380 from the KRW-USD-NDO template. It reads the fixing the option expires on and the forward it prices on, compares it with a deliverable call on the same dates, and checks that a call less a put at 1370 is the NDF at 1370.

    Rust
    use chrono::NaiveDate;
    use helixio_docs_sheets::{SOFR_CLOSE_DATE, Sheet, USD_SOFR_CLOSE};
    use helixrs::{
        AnyInstrument, CollateralSpec, Currency, CurrencyPair, DeltaConvention, FxOption, FxRates,
        FxVolSmile, FxVolSurface, MarketContext, OptionKind, Period, PricingRiskFactors, QuoteSheet,
        Strike, YieldCurve, 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")
    }
    
    fn main() -> Outcome<()> {
        let close = date(SOFR_CLOSE_DATE);
        let (usd, krw): (Currency, Currency) = ("USD".parse()?, "KRW".parse()?);
        let won: Sheet = &[("KRW-KOFR-OIS", "1Y", 0.0255), ("KRW-KOFR-OIS", "2Y", 0.0262), ("KRW-KOFR-OIS", "5Y", 0.0275)];
        let outrights: Sheet = &[
            ("KRW-USD-NDF", "1M", 1388.5),
            ("KRW-USD-NDF", "3M", 1385.0),
            ("KRW-USD-NDF", "6M", 1380.0),
            ("KRW-USD-NDF", "1Y", 1371.0),
            ("KRW-USD-NDF", "2Y", 1355.0),
        ];
        let six_months: Period = "6M".parse()?;
    
        // The NDF market — dollars, won onshore and won offshore from NDF outrights —
        // and a won smile per expiry from its three quotes.
        let surface = FxVolSurface::new(
            CurrencyPair::new(usd, krw),
            DeltaConvention::Spot,
            close,
            vec![
                (date((2027, 1, 4)), FxVolSmile::from_quotes(0.090, 0.010, 0.0030)?),
                (date((2027, 4, 1)), FxVolSmile::from_quotes(0.095, 0.011, 0.0032)?),
                (date((2027, 10, 1)), FxVolSmile::from_quotes(0.100, 0.012, 0.0035)?),
            ],
        )?;
        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(krw), &QuoteSheet::from_rows(won.iter().copied())?)?
            .with_fx_rates(FxRates::from_quotes(&[(CurrencyPair::new(usd, krw), 1390.0)])?)
            .with_recipe_quotes(&"KRW-CSA-USD".parse()?, &QuoteSheet::from_rows(outrights.iter().copied())?)?
            .with_fx_vols(surface);
    
        // A six-month dollar call against won at 1380, drafted from its template. It
        // expires on the fixing the NDF terms give for six months.
        let ndo_template = template("KRW-USD-NDO")?;
        let call_terms = ndo_template.ndo(close, six_months, 1_000_000.0, 1380.0, OptionKind::Call)?;
        let call = AnyInstrument::FxOption(call_terms.clone());
        let needed = call.fixings_needed(date((2027, 4, 5)));
        let (fixing_key, fixing_date) = &needed[0];
        let diagnostics = market.priced(&call_terms.risk_factors()?, |v| call_terms.diagnostics(v))?;
        println!("fixes on    {fixing_key} {fixing_date}");
        println!("forward     {:.4}", diagnostics.forward);
        println!("vol         {:.6}", diagnostics.vol);
        let ndo_npv = call.npv(&market)?;
        println!("npv         {ndo_npv:.2} {}", call.currency());
    
        // The option's forward is the offshore forward to the value date. A deliverable
        // call on the same dates exchanges won, so it reads the onshore forward and is
        // worth something else.
        let delivery = date((2027, 4, 6));
        let read = |key: &str| -> Outcome<f64> {
            let (curve, _) = market.curve(&key.parse()?).expect("a pinned curve");
            Ok(curve.view::<f64>().discount(delivery)?)
        };
        let p_usd = read("discount(USD)")?;
        let offshore = 1390.0 * p_usd / read("discount(KRW-CSA-USD)")?;
        let onshore = 1390.0 * p_usd / read("discount(KRW)")?;
        let deliverable_terms = FxOption::new(
            usd,
            krw,
            1_000_000.0,
            OptionKind::Call,
            *fixing_date,
            delivery,
            Strike::Absolute(1380.0),
        );
        let deliverable_forward =
            market.priced(&deliverable_terms.risk_factors()?, |v| deliverable_terms.diagnostics(v))?.forward;
        let deliverable = AnyInstrument::FxOption(deliverable_terms);
        println!("offshore    {offshore:.4}");
        println!("onshore     {onshore:.4}");
        println!("deliverable reads {deliverable_forward:.4}");
        println!("deliverable {:.0} {}", deliverable.npv(&market)?, deliverable.currency());
    
        // A call less a put at one strike is the NDF at that strike. All three value
        // in dollars, the settlement currency, so the two sides compare as they are.
        // At 1370, below the offshore forward, both are positive.
        let call = AnyInstrument::FxOption(ndo_template.ndo(close, six_months, 1_000_000.0, 1370.0, OptionKind::Call)?);
        let put = AnyInstrument::FxOption(ndo_template.ndo(close, six_months, 1_000_000.0, 1370.0, OptionKind::Put)?);
        let ndf = AnyInstrument::FxForward(template("KRW-USD-NDF")?.ndf(close, six_months, 1_000_000.0, 1370.0, false)?);
        let difference = call.npv(&market)? - put.npv(&market)?;
        println!("call - put  {difference:.2} {}", call.currency());
        println!("ndf         {:.2} {}", ndf.npv(&market)?, ndf.currency());
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE
    
    # The NDF market — dollars, won onshore and won offshore from NDF outrights —
    # and a won smile per expiry from its three quotes.
    surface = hlx.vols.FxVolSurface(
        "USD",
        "KRW",
        SOFR_CLOSE_DATE,
        [
            (dt.date(2027, 1, 4), 0.090, 0.010, 0.0030),
            (dt.date(2027, 4, 1), 0.095, 0.011, 0.0032),
            (dt.date(2027, 10, 1), 0.100, 0.012, 0.0035),
        ],
    )
    market = (
        hlx.MarketContext(SOFR_CLOSE_DATE)
        .with_recipe_quotes("USD", USD_SOFR_CLOSE)
        .with_recipe_quotes("KRW", {"KRW-KOFR-OIS": {"1Y": 0.0255, "2Y": 0.0262, "5Y": 0.0275}})
        .with_fx_rates([("USD", "KRW", 1390.0)])
        .with_recipe_quotes("KRW-CSA-USD", {"KRW-USD-NDF": {"1M": 1388.5, "3M": 1385.0, "6M": 1380.0, "1Y": 1371.0, "2Y": 1355.0}})
        .with_fx_vols(surface)
    )
    
    # A six-month dollar call against won at 1380, drafted from its template. It
    # expires on the fixing the NDF terms give for six months.
    call = hlx.templates.get("KRW-USD-NDO").ndo(SOFR_CLOSE_DATE, "6M", notional=1_000_000.0, strike=1380.0, kind="call")
    [(fixing_key, fixing_date)] = call.fixings_needed(dt.date(2027, 4, 5))
    diagnostics = call.diagnostics(market)
    print(f"fixes on    {fixing_key} {fixing_date}")
    print(f"forward     {diagnostics.forward():.4f}")
    print(f"vol         {diagnostics.vol():.6f}")
    ndo_npv = call.npv(market)
    print(f"npv         {ndo_npv:.2f} {call.npv_ccy()}")
    
    # The option's forward is the offshore forward to the value date. A deliverable
    # call on the same dates exchanges won, so it reads the onshore forward and is
    # worth something else.
    delivery = dt.date(2027, 4, 6)
    p_usd = market.curve("discount(USD)").discount(delivery)
    offshore = 1390.0 * p_usd / market.curve("discount(KRW-CSA-USD)").discount(delivery)
    onshore = 1390.0 * p_usd / market.curve("discount(KRW)").discount(delivery)
    deliverable = hlx.products.fx_option(
        "USD", "KRW", notional_base=1_000_000.0, kind="call", expiry=fixing_date, delivery=delivery, strike=1380.0
    )
    deliverable_forward = deliverable.diagnostics(market).forward()
    print(f"offshore    {offshore:.4f}")
    print(f"onshore     {onshore:.4f}")
    print(f"deliverable reads {deliverable_forward:.4f}")
    print(f"deliverable {deliverable.npv(market):.0f} {deliverable.npv_ccy()}")
    
    # A call less a put at one strike is the NDF at that strike. All three value
    # in dollars, the settlement currency, so the two sides compare as they are.
    # At 1370, below the offshore forward, both are positive.
    call = hlx.templates.get("KRW-USD-NDO").ndo(SOFR_CLOSE_DATE, "6M", notional=1_000_000.0, strike=1370.0, kind="call")
    put = hlx.templates.get("KRW-USD-NDO").ndo(SOFR_CLOSE_DATE, "6M", notional=1_000_000.0, strike=1370.0, kind="put")
    ndf = hlx.templates.get("KRW-USD-NDF").ndf(SOFR_CLOSE_DATE, "6M", notional=1_000_000.0, forward_rate=1370.0)
    print(f"call - put  {call.npv(market) - put.npv(market):.2f} {call.npv_ccy()}")
    print(f"ndf         {ndf.npv(market):.2f} {ndf.npv_ccy()}")
    fixes on    FX-USDKRW 2027-04-02
    forward     1380.0000
    vol         0.094935
    npv         26235.50 USD
    offshore    1380.0000
    onshore     1377.9203
    deliverable reads 1377.9203
    deliverable 35508453 KRW
    call - put  7090.46 USD
    ndf         7090.46 USD

    The NDO reads the offshore forward and values in dollars. The deliverable call exchanges won, so it reads the onshore forward and values in won: a different trade with a different price. The call less the put at 1370 matches the NDF to the cent, both in dollars.

    How it's priced

    The NDO is priced by Garman-Kohlhagen on the pair's surface, as an FX option is, expiring on the fixing date and paying on the value date. The forward it reads is the offshore one,

    Foff(T)=S PUSD(T)PKRWoff(T),F^{\text{off}}(T) = S\,\frac{P_{\text{USD}}(T)}{P^{\text{off}}_{\text{KRW}}(T)},

    and a call less a put at one strike is the NDF at that strike, CNDO−PNDO=VNDF(K)C_{\text{NDO}} - P_{\text{NDO}} = V_{\text{NDF}}(K). Why the payoff keeps its shape when it's paid in dollars is covered in how FX products work.

    API

    Argument Type Default Meaning
    templates.get(code) str required the market's NDO terms: KRW-USD-NDO, INR-USD-NDO and the rest; Rust template(code)?, an AnyTemplate::Ndo
    Template.ndo(trade_date, tenor, ...) date, str required drafts the option, expiring on the fixing the NDF terms give for the tenor; Rust AnyTemplate::ndo(trade_date, tenor, notional, strike, kind)
    notional float required the settlement currency the option is on
    strike float required an absolute strike, local per settlement currency
    kind "call" or "put" required a call on the settlement currency, or a put; Rust OptionKind

    A non-deliverable option can also be built directly with products.fx_option(..., settlement_index=, fixing_date=, settlement_currency=); see FX options.

    Symbol Meaning Rust Python
    expiry the NDF fixing for the tenor AnyInstrument::fixings_needed Instrument.fixings_needed
    CNDOC_{\text{NDO}} its value, in dollars AnyInstrument::npv Instrument.npv
    the npv currency dollars, the settlement currency AnyInstrument::currency Instrument.npv_ccy
    FoffF^{\text{off}} the forward it reads market.priced(&option.risk_factors()?, |v| option.diagnostics(v)), .forward option.diagnostics(market).forward()
    the deliverable option the vanilla on the same dates, read onshore FxOption::new products.fx_option

    Defaults and limits

    • The market: the NDF market, the dollar curve, the pair's spot and the offshore curve under KRW-CSA-USD, plus the pair's surface pinned with with_fx_vols.
    • Exercise: European only, on the fixing date. The payoff is converted at that fixing and paid on the value date.
    • Settlement: the npv is in the settlement currency, dollars. A deliverable option on the same pair values in the quote currency, won, and reads the onshore forward.
    • Vols: quoted as for any FX option; see the FX vol surface catalog. Each market's NDO terms are in the non-deliverable conventions catalog.

    See also

    • Source: crates/helixrs/src/statics/templates/ndo.rs, crates/helixrs/src/instruments/fx.rs
    • Related pages:
      • How FX products work, the ideas behind the option.
      • FX options, for the deliverable option and the surface.
      • NDF, for the forward on the same fixing.

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