Esc

    The API is under testing and may change before release.

    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.

    Rust
    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(())
    }
    Python
    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 F(T)=S PEUR(T)/PUSD(T)F(T) = S\,P_{\text{EUR}}(T) / P_{\text{USD}}(T) the covered-interest-parity forward, a forward buying NN base at contract rate KK is worth

    V=N (F(T)−K) PUSD(T)V = N\,(F(T) - K)\,P_{\text{USD}}(T)

    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
    SS the spot, quote per base MarketContext::with_fx_rates with FxRates::from_quotes MarketContext.with_fx_rates
    PEUR,PUSDP_{\text{EUR}}, P_{\text{USD}} each currency's discount with_recipe_quotes per currency, read with YieldCurve::discount with_recipe_quotes per currency, read with curves.DiscountCurve.discount
    N,K,TN, K, T 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=)
    VV the forward's value AnyInstrument::npv Instrument.npv

    Defaults and limits

    • The rate: give exactly one of contract_rate and quote_amount; neither or both raises ValueError.
    • 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.