Price an FX option from a delta quote
You'll price an FX option that's quoted by its delta rather than its strike, the way an FX desk quotes a 25-delta call, and read back the strike and vol it resolves to.
Before you start
- helixio installed: see installing.
- A discount curve for each currency of the pair, and the pair's spot. Here that's a EUR close, a USD SOFR close and the EUR/USD spot.
- The pair's vol quotes for each expiry: the at-the-money vol, the 25-delta risk reversal and the 25-delta butterfly.
Steps
- Mark both currencies' curves and the spot on one market.
- Build the surface from each expiry's three quotes with
vols.FxVolSurface, and pin it withwith_fx_vols. - Book the option by its delta:
products.fx_option(..., delta=0.25)in Python,Strike::AtDelta(0.25)in Rust. Give the option's own delta, unsigned, for a put as well as a call. - Read what it resolved to with
diagnostics: the strike, the vol and the delta. Then value it. - If you need to book it at a fixed strike, rebook it at the resolved strike. It prices the same.
Rust
use chrono::NaiveDate;
use helixrs::{
AnyInstrument, CollateralSpec, Currency, CurrencyPair, DeltaConvention, DeltaQuote, FxOption,
FxRates, FxVolSmile, FxVolSurface, MarketContext, OptionKind, PricingRiskFactors, QuoteSheet,
Strike,
};
type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
fn date(year: i32, month: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
}
fn main() -> Outcome<()> {
let close = date(2026, 10, 1);
let eur_close = [
("EUR-ESTR-OIS", "1Y", 0.0195),
("EUR-ESTR-OIS", "2Y", 0.0200),
("EUR-ESTR-OIS", "5Y", 0.0215),
("EUR-ESTR-OIS", "10Y", 0.0240),
("EUR-ESTR-OIS", "20Y", 0.0262),
("EUR-ESTR-OIS", "30Y", 0.0265),
("EUR-EURIBOR-3M-IRS", "1Y", 0.0210),
("EUR-EURIBOR-3M-IRS", "2Y", 0.0216),
("EUR-EURIBOR-3M-IRS", "5Y", 0.0232),
("EUR-EURIBOR-3M-IRS", "10Y", 0.0257),
("EUR-EURIBOR-3M-IRS", "20Y", 0.0278),
("EUR-EURIBOR-3M-IRS", "30Y", 0.0280),
];
let usd_sofr_close = [
("USD-SOFR-OIS", "1M", 0.0391),
("USD-SOFR-OIS", "3M", 0.0402),
("USD-SOFR-OIS", "1Y", 0.0439),
("USD-SOFR-OIS", "2Y", 0.0459),
("USD-SOFR-OIS", "3Y", 0.0468),
("USD-SOFR-OIS", "4Y", 0.0469),
("USD-SOFR-OIS", "5Y", 0.0471),
("USD-SOFR-OIS", "7Y", 0.0475),
("USD-SOFR-OIS", "10Y", 0.0483),
("USD-SOFR-OIS", "15Y", 0.0494),
("USD-SOFR-OIS", "20Y", 0.0499),
("USD-SOFR-OIS", "30Y", 0.0492),
];
let (expiry, delivery) = (date(2027, 4, 1), date(2027, 4, 5));
let (eur, usd): (Currency, Currency) = ("EUR".parse()?, "USD".parse()?);
// Step 1: mark both currencies' curves 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), 1.0950)])?);
// Step 2: build the surface from each expiry's ATM vol, 25-delta risk reversal
// and 25-delta butterfly, and pin it on the market.
let surface = FxVolSurface::new(
CurrencyPair::new(eur, usd),
DeltaConvention::Spot,
close,
vec![
(date(2027, 1, 4), FxVolSmile::from_quotes(0.0770, -0.0060, 0.0019)?),
(expiry, FxVolSmile::from_quotes(0.0790, -0.0065, 0.0021)?),
],
)?;
let market = market.with_fx_vols(surface.clone());
// Step 3: book the 25-delta call on two million euros by its delta.
let call = FxOption::new(eur, usd, 2_000_000.0, OptionKind::Call, expiry, delivery, Strike::AtDelta(0.25));
// Step 4: read the strike and vol it resolved to, and its price.
let resolved = market.priced(&call.risk_factors()?, |v| call.diagnostics(v))?;
let call_node = surface.view::<f64>().vol_at_quote(expiry, "25C".parse::<DeltaQuote>()?)?;
let npv = AnyInstrument::FxOption(call).npv(&market)?;
println!("forward {:.6}", resolved.forward);
println!("strike {:.6}", resolved.strike);
println!("vol {:.6}", resolved.vol);
println!("25C node {call_node:.6}");
println!("delta {:.6}", resolved.delta);
println!("npv (USD) {npv:.2}");
// Step 5: the same option at that absolute strike prices the same.
let at_strike = AnyInstrument::FxOption(FxOption::new(
eur,
usd,
2_000_000.0,
OptionKind::Call,
expiry,
delivery,
Strike::Absolute(resolved.strike),
));
println!("at strike {:.2}", at_strike.npv(&market)?);
Ok(())
}Python
from datetime import date
import helixio as hlx
SOFR_CLOSE_DATE = date(2026, 10, 1)
EUR_CLOSE = {
"EUR-ESTR-OIS": {"1Y": 0.0195, "2Y": 0.0200, "5Y": 0.0215, "10Y": 0.0240, "20Y": 0.0262, "30Y": 0.0265},
"EUR-EURIBOR-3M-IRS": {"1Y": 0.0210, "2Y": 0.0216, "5Y": 0.0232, "10Y": 0.0257, "20Y": 0.0278, "30Y": 0.0280},
}
USD_SOFR_CLOSE = {
"USD-SOFR-OIS": {
"1M": 0.0391, "3M": 0.0402, "1Y": 0.0439, "2Y": 0.0459, "3Y": 0.0468, "4Y": 0.0469,
"5Y": 0.0471, "7Y": 0.0475, "10Y": 0.0483, "15Y": 0.0494, "20Y": 0.0499, "30Y": 0.0492,
},
}
EXPIRY, DELIVERY = date(2027, 4, 1), date(2027, 4, 5)
# Step 1: mark both currencies' curves 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", 1.0950)])
)
# Step 2: build the surface from each expiry's ATM vol, 25-delta risk reversal
# and 25-delta butterfly, and pin it on the market.
surface = hlx.vols.FxVolSurface(
"EUR",
"USD",
SOFR_CLOSE_DATE,
[(date(2027, 1, 4), 0.0770, -0.0060, 0.0019), (EXPIRY, 0.0790, -0.0065, 0.0021)],
)
market = market.with_fx_vols(surface)
# Step 3: book the 25-delta call on two million euros by its delta.
call = hlx.products.fx_option(
"EUR", "USD", notional_base=2_000_000.0, kind="call", expiry=EXPIRY, delivery=DELIVERY, delta=0.25
)
# Step 4: read the strike and vol it resolved to, and its price.
resolved = call.diagnostics(market)
npv = call.npv(market)
print(f"forward {resolved.forward():.6f}")
print(f"strike {resolved.strike():.6f}")
print(f"vol {resolved.vol():.6f}")
print(f"25C node {surface.vol_at_quote(EXPIRY, '25C'):.6f}")
print(f"delta {resolved.delta():.6f}")
print(f"npv (USD) {npv:.2f}")
# Step 5: the same option at that absolute strike prices the same.
at_strike = hlx.products.fx_option(
"EUR", "USD", notional_base=2_000_000.0, kind="call", expiry=EXPIRY, delivery=DELIVERY, strike=resolved.strike()
)
print(f"at strike {at_strike.npv(market):.2f}")forward 1.107834
strike 1.150922
vol 0.077850
25C node 0.077850
delta 0.250000
npv (USD) 17528.69
at strike 17528.69
Check it worked
- The resolved delta is exactly the one you quoted, 0.25 here.
- On a smile that quotes that delta, the vol is the node's vol: the 25-delta call reads
25C. - A call quoted at 25 delta is struck above the forward.
- Rebooked at the resolved strike, the option prices the same, to the cent.
Related
- FX options, every argument of the option and the surface.
- How FX products work, why the strike and the vol are solved together.
- FX vol surface, how each pair quotes its deltas.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.