FX forwards
This page lists what you need to book and value an outright FX forward: the constructor's arguments, how it's priced, and an example you can run. An FX forward agrees today to exchange two currencies on a future date at a fixed rate, the way an importer locks in the cost of a future payment.
Quick example
The example reads its quotes from the desk sheets; load them once and it runs as shown.
This marks a dollar and a euro curve with a EUR/USD spot of 1.10, works out the forward to October 2027 by covered interest parity, values a forward buying a million euros at 1.12, and checks that a forward struck at the market's forward is worth nothing. A forward whose value date has passed has settled, so it's worth nothing too.
use chrono::NaiveDate;
use helixio_docs_sheets::{EUR_CLOSE, SOFR_CLOSE_DATE, USD_SOFR_CLOSE};
use helixrs::{
AnyInstrument, CollateralSpec, Currency, CurrencyPair, FxForward, FxRates, MarketContext,
QuoteSheet, YieldCurve,
};
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 discount(market: &MarketContext, key: &str, on: NaiveDate) -> Outcome<f64> {
let (curve, _) = market.curve(&key.parse()?).expect("a pinned curve");
Ok(curve.view::<f64>().discount(on)?)
}
fn main() -> Outcome<()> {
let close = date(SOFR_CLOSE_DATE);
let spot = 1.1000;
let value_date = date((2027, 10, 5));
let notional = 1_000_000.0;
let (eur, usd): (Currency, Currency) = ("EUR".parse()?, "USD".parse()?);
// Each currency's curve from its recipe, 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), spot)])?);
// Covered interest parity: F = S × P_EUR / P_USD.
let p_usd = discount(&market, "discount(USD)", value_date)?;
let p_eur = discount(&market, "discount(EUR)", value_date)?;
let forward = spot * p_eur / p_usd;
println!("forward EUR/USD {forward:.6}");
// Buy a million euros at 1.12 dollars each: worth the difference to the
// forward, on the euro notional, discounted in dollars.
let trade = FxForward::new(eur, usd, notional, 1.12, value_date);
let npv = AnyInstrument::FxForward(trade).npv(&market)?;
let by_parity = (forward - 1.12) * notional * p_usd;
println!("npv (USD) {npv:.2}");
println!("by parity {by_parity:.2}");
// Struck at the forward, the trade is worth nothing.
let fair = AnyInstrument::FxForward(FxForward::new(eur, usd, notional, forward, value_date));
println!("at the forward {:.2}", fair.npv(&market)?.abs());
// A forward whose value date is already past has exchanged its currencies.
let settled = AnyInstrument::FxForward(FxForward::new(eur, usd, notional, 1.05, date((2026, 9, 30))));
println!("settled npv {:.2}", settled.npv(&market)?);
Ok(())
}import datetime as dt
import helixio as hlx
from desk_sheets import EUR_CLOSE, SOFR_CLOSE_DATE, USD_SOFR_CLOSE
SPOT = 1.1000
VALUE_DATE = dt.date(2027, 10, 5)
NOTIONAL = 1_000_000.0
# Each currency's curve from its recipe, 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", SPOT)])
)
# Covered interest parity: F = S × P_EUR / P_USD.
p_usd = market.curve("discount(USD)").discount(VALUE_DATE)
p_eur = market.curve("discount(EUR)").discount(VALUE_DATE)
forward = SPOT * p_eur / p_usd
print(f"forward EUR/USD {forward:.6f}")
# Buy a million euros at 1.12 dollars each: worth the difference to the
# forward, on the euro notional, discounted in dollars.
trade = hlx.products.fx_forward("EUR", "USD", base_amount=NOTIONAL, contract_rate=1.12, value_date=VALUE_DATE)
npv = trade.npv(market)
by_parity = (forward - 1.12) * NOTIONAL * p_usd
print(f"npv (USD) {npv:.2f}")
print(f"by parity {by_parity:.2f}")
# Struck at the forward, the trade is worth nothing.
fair = hlx.products.fx_forward("EUR", "USD", base_amount=NOTIONAL, contract_rate=forward, value_date=VALUE_DATE)
print(f"at the forward {abs(fair.npv(market)):.2f}")
# A forward whose value date is already past has exchanged its currencies.
settled = hlx.products.fx_forward(
"EUR", "USD", base_amount=NOTIONAL, contract_rate=1.05, value_date=dt.date(2026, 9, 30)
)
print(f"settled npv {settled.npv(market):.2f}")forward EUR/USD 1.126932
npv (USD) 6633.87
by parity 6633.87
at the forward 0.00
settled npv 0.00
Buying euros at 1.12 when the market's forward is about 1.127 is worth the difference on a million euros, discounted in dollars, and the library's value matches the parity formula to the cent.
How it's priced
With the covered-interest-parity forward, a forward buying base at contract rate is worth
in the quote currency. Why the forward takes that form is covered in how FX products work.
API
| Argument | Type | Default | Meaning |
|---|---|---|---|
base |
str |
required | the base currency code |
quote |
str |
required | the quote currency code |
base_amount |
float |
required | units of base bought; negative sells them |
contract_rate |
float |
None |
the agreed rate, quote per base; give this or quote_amount |
quote_amount |
float |
None |
the quote amount exchanged for base_amount, in place of contract_rate |
value_date |
date |
required | the day the currencies are exchanged |
settlement_index, fixing_date, settlement_currency |
str, date, str |
None |
make the forward non-deliverable; see NDF |
In Rust the forward is FxForward::new(base, quote, base_amount, contract_rate, value_date).
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| the spot, quote per base | MarketContext::with_fx_rates with FxRates::from_quotes |
MarketContext.with_fx_rates |
|
| each currency's discount | with_recipe_quotes per currency, read with YieldCurve::discount |
with_recipe_quotes per currency, read with curves.DiscountCurve.discount |
|
| the forward's terms | FxForward::new(base, quote, base_amount, contract_rate, value_date) |
products.fx_forward(base, quote, base_amount=, contract_rate=, value_date=) |
|
| the forward's value | AnyInstrument::npv |
Instrument.npv |
Defaults and limits
- The rate: give exactly one of
contract_rateandquote_amount; neither or both raisesValueError. - The market: a discount curve for each currency and the pair's spot, set with
with_fx_rates. The spot is the rate for exchange on the valuation date. - Currency of the value: the npv is in the quote currency, dollars for EUR/USD.
- Settled forwards: a forward whose value date is on or before the valuation date has exchanged its currencies, and is worth nothing.
- Dating: the constructor takes the value date you give it. Each pair's spot lag and calendars are in the FX conventions catalog.
See also
- Source:
crates/helixrs/src/instruments/fx.rs,crates/helixrs/src/marketdata/fx.rs - Related pages:
- How FX products work, the ideas behind the forward.
- FX swaps, for a near and a far forward together.
- NDF, for a forward settled in cash.
- Quote sheets and recipes, for the two curves.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.