Esc

    The API is under testing and may change before release.

    NDF

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

    Quick example

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

    This marks a dollar curve, an onshore won curve from KOFR swaps and an offshore won curve from NDF outrights, with a USD/KRW spot of 1390. It drafts a six-month NDF buying a million dollars at 1370 from the KRW-USD-NDF template, reads the fixing it settles on, values it, and strikes a second NDF at the quoted six-month outright.

    Rust
    use chrono::NaiveDate;
    use helixio_docs_sheets::{SOFR_CLOSE_DATE, Sheet, USD_SOFR_CLOSE};
    use helixrs::{
        AnyInstrument, AnyTemplate, CollateralSpec, Currency, CurrencyPair, FxRates, MarketContext,
        Period, QuoteSheet, 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")
    }
    
    const SPOT: f64 = 1390.0;
    const KOFR: Sheet = &[("KRW-KOFR-OIS", "1Y", 0.0255), ("KRW-KOFR-OIS", "2Y", 0.0262), ("KRW-KOFR-OIS", "5Y", 0.0275)];
    const NDF_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),
    ];
    
    fn main() -> Outcome<()> {
        let close = date(SOFR_CLOSE_DATE);
        let value_date = date((2027, 4, 6));
        let (usd, krw): (Currency, Currency) = ("USD".parse()?, "KRW".parse()?);
    
        // Dollars from the SOFR close; won onshore from the KOFR swaps; won offshore,
        // under dollar collateral, from NDF outrights; and the spot, won per dollar.
        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(KOFR.iter().copied())?)?
            .with_fx_rates(FxRates::from_quotes(&[(CurrencyPair::new(usd, krw), SPOT)])?)
            .with_recipe_quotes(&"KRW-CSA-USD".parse()?, &QuoteSheet::from_rows(NDF_OUTRIGHTS.iter().copied())?)?;
        for (role, key) in market.contents() {
            if role == "discount" || role == "fx" {
                println!("{role:<9} {key}");
            }
        }
    
        // A six-month NDF buying a million dollars against won at 1370. The template
        // dates it: spot from the trade date, value six months on, and the fixing two
        // business days before value.
        let ndf_template = template("KRW-USD-NDF")?;
        let AnyTemplate::Ndf(terms) = &ndf_template else {
            unreachable!("KRW-USD-NDF is an NDF template")
        };
        let six_months: Period = "6M".parse()?;
        let ndf = AnyInstrument::FxForward(ndf_template.ndf(close, six_months, 1_000_000.0, 1370.0, false)?);
        let needed = ndf.fixings_needed(value_date);
        let (fixing_key, fixing_date) = &needed[0];
        println!("spot        {}", terms.spot_date(close));
        println!("fixes on    {fixing_key} {fixing_date}");
    
        // The won difference is paid in dollars at the fixing, so the NDF values in
        // dollars: the offshore forward's difference from the contract rate, per unit of
        // that forward, on the notional, discounted on the dollar curve. The onshore
        // forward is a different number, and the NDF does not read it.
        let read = |key: &str| -> Outcome<f64> {
            let (curve, _) = market.curve(&key.parse()?).expect("a pinned curve");
            Ok(curve.view::<f64>().discount(value_date)?)
        };
        let p_usd = read("discount(USD)")?;
        let p_offshore = read("discount(KRW-CSA-USD)")?;
        let p_onshore = read("discount(KRW)")?;
        let offshore = SPOT * p_usd / p_offshore;
        let onshore = SPOT * p_usd / p_onshore;
        let npv = ndf.npv(&market)?;
        let by_formula = 1_000_000.0 * (1.0 - 1370.0 / offshore) * p_usd;
        println!("offshore    {offshore:.4}");
        println!("onshore     {onshore:.4}");
        println!("npv ({})   {npv:.2}", ndf.currency());
        println!("by formula  {by_formula:.2}");
    
        // The offshore curve is marked from the outrights, so the offshore forward to
        // the six-month value date is the quoted outright, and an NDF struck there is
        // worth nothing.
        let quoted = AnyInstrument::FxForward(ndf_template.ndf(close, six_months, 1_000_000.0, 1380.0, false)?);
        println!("at 1380.0   {:.2}", quoted.npv(&market)?.abs());
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE
    
    SPOT = 1390.0
    VALUE_DATE = dt.date(2027, 4, 6)
    NDF_OUTRIGHTS = {"KRW-USD-NDF": {"1M": 1388.5, "3M": 1385.0, "6M": 1380.0, "1Y": 1371.0, "2Y": 1355.0}}
    
    # Dollars from the SOFR close; won onshore from the KOFR swaps; won offshore,
    # under dollar collateral, from NDF outrights; and the spot, won per dollar.
    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", SPOT)])
        .with_recipe_quotes("KRW-CSA-USD", NDF_OUTRIGHTS)
    )
    for role, key in market.contents():
        if role in ("discount", "fx"):
            print(f"{role:<9} {key}")
    
    # A six-month NDF buying a million dollars against won at 1370. The template
    # dates it: spot from the trade date, value six months on, and the fixing two
    # business days before value.
    template = hlx.templates.get("KRW-USD-NDF")
    ndf = template.ndf(SOFR_CLOSE_DATE, "6M", notional=1_000_000.0, forward_rate=1370.0)
    [(fixing_key, fixing_date)] = ndf.fixings_needed(VALUE_DATE)
    print(f"spot        {template.spot_date(SOFR_CLOSE_DATE)}")
    print(f"fixes on    {fixing_key} {fixing_date}")
    
    # The won difference is paid in dollars at the fixing, so the NDF values in
    # dollars: the offshore forward's difference from the contract rate, per unit of
    # that forward, on the notional, discounted on the dollar curve. The onshore
    # forward is a different number, and the NDF does not read it.
    p_usd = market.curve("discount(USD)").discount(VALUE_DATE)
    p_offshore = market.curve("discount(KRW-CSA-USD)").discount(VALUE_DATE)
    p_onshore = market.curve("discount(KRW)").discount(VALUE_DATE)
    offshore = SPOT * p_usd / p_offshore
    onshore = SPOT * p_usd / p_onshore
    npv = ndf.npv(market)
    by_formula = 1_000_000.0 * (1.0 - 1370.0 / offshore) * p_usd
    print(f"offshore    {offshore:.4f}")
    print(f"onshore     {onshore:.4f}")
    print(f"npv ({ndf.npv_ccy()})   {npv:.2f}")
    print(f"by formula  {by_formula:.2f}")
    
    # The offshore curve is marked from the outrights, so the offshore forward to
    # the six-month value date is the quoted outright, and an NDF struck there is
    # worth nothing.
    quoted = template.ndf(SOFR_CLOSE_DATE, "6M", notional=1_000_000.0, forward_rate=1380.0)
    print(f"at 1380.0   {abs(quoted.npv(market)):.2f}")
    discount  KRW
    discount  USD
    discount  KRW-CSA-USD
    fx        USD/KRW
    spot        2026-10-06
    fixes on    FX-USDKRW 2027-04-02
    offshore    1380.0000
    onshore     1377.9203
    npv (USD)   7090.46
    by formula  7090.46
    at 1380.0   0.00

    The template dates the trade: spot from the trade date, the value date six months on, and the fixing two business days before it. The value is in dollars and matches the formula below, read at the offshore forward. The onshore forward is a different number, and the NDF doesn't read it. Struck at the quoted outright, the NDF is worth nothing, because the offshore curve is marked from those outrights.

    How it's priced

    An NDF buying NN dollars against won at contract rate KK pays N(1−K/X)N(1 - K/X) dollars at the fixing XX. Before the fixing, it's worth

    V=N(1−KFoff(T))PUSD(T),Foff(T)=S PUSD(T)PKRWoff(T),V = N\left(1 - \frac{K}{F^{\text{off}}(T)}\right) P_{\text{USD}}(T), \qquad F^{\text{off}}(T) = S\,\frac{P_{\text{USD}}(T)}{P^{\text{off}}_{\text{KRW}}(T)},

    where PKRWoffP^{\text{off}}_{\text{KRW}} is the won discount under dollar collateral. Why the offshore forward differs from the onshore one is covered in how FX products work.

    API

    Argument Type Default Meaning
    templates.get(code) str required the market's NDF terms: KRW-USD-NDF, INR-USD-NDF, BRL-USD-NDF and the rest; Rust template(code)?, an AnyTemplate::Ndf
    Template.ndf(trade_date, tenor, ...) date, str required drafts the NDF; spot, value and fixing dates come from the template; Rust AnyTemplate::ndf(trade_date, tenor, notional, rate, short)
    notional float required the settlement currency bought against the local one
    forward_rate float required the contract rate, local per settlement currency
    short bool False sell the settlement currency instead
    Template.spot_date(trade_date) date — the spot date the template's calendars give; Rust NdfTemplate::spot_date
    Symbol Meaning Rust Python
    XX the fixing it reads AnyInstrument::fixings_needed Instrument.fixings_needed
    PKRWoffP^{\text{off}}_{\text{KRW}} the offshore curve, from NDF outrights with_recipe_quotes(&"KRW-CSA-USD".parse()?, &sheet), read as discount(KRW-CSA-USD) with_recipe_quotes("KRW-CSA-USD", sheet), read as discount(KRW-CSA-USD)
    VV its value, in dollars AnyInstrument::npv Instrument.npv
    the npv currency the settlement currency, USD AnyInstrument::currency Instrument.npv_ccy

    Defaults and limits

    • The market: the dollar curve, the pair's spot, and the offshore curve under KRW-CSA-USD, which the offshore recipe marks from the pair's NDF outrights. The onshore curve may be on the market too; the NDF doesn't read it.
    • Settlement: the npv is in the settlement currency, dollars. Multiplied by the spot, it's the won value a won valuation would give.
    • The fixing: two business days before the value date, on the source the template names.
    • Templates: one per market, plus crosses settled in euros, sterling or yen. Each market's terms are in the non-deliverable conventions catalog.

    See also

    • Source: crates/helixrs/src/statics/templates/ndf.rs, crates/helixrs/src/instruments/fx.rs
    • Related pages:
      • How FX products work, the ideas behind the offshore forward.
      • FX forwards, for the deliverable forward.
      • NDO, for the option on the same fixing.
      • NDS and NDCCS, for swaps whose every payment settles like an NDF.

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