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.
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(())
}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 dollars against won at contract rate pays dollars at the fixing . Before the fixing, it's worth
where 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 |
|---|---|---|---|
| the fixing it reads | AnyInstrument::fixings_needed |
Instrument.fixings_needed |
|
| 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) |
|
| 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.