use crate::error::{Error, Result};
use crate::markets::forex::quotes::forwardpoints::{FXForwardHelper, FXForwardQuote};
use crate::markets::forex::quotes::volsurface::{FXDeltaVolPillar, FXVolQuote, FXVolSurface};
use crate::markets::termstructures::yieldcurve::{
InterpolationMethodEnum, YieldTermMarketData, YieldTermStructure,
};
use crate::time::calendars::Calendar;
use crate::time::daycounters::DayCounters;
use chrono::{Duration, NaiveDate};
use iso_currency::Currency;
#[derive(Debug)]
pub struct FxMarketContext {
pub valuation_date: NaiveDate,
pub spot: f64,
pub pair: (Currency, Currency),
pub domestic_curve: YieldTermStructure,
pub foreign_curve: YieldTermStructure,
pub forwards: FXForwardHelper,
pub vol_surface: FXVolSurface,
}
impl FxMarketContext {
#[allow(clippy::too_many_arguments)]
pub fn new(
valuation_date: NaiveDate,
spot: f64,
pair: (Currency, Currency),
domestic_curve: YieldTermStructure,
foreign_curve: YieldTermStructure,
forwards: FXForwardHelper,
vol_surface: FXVolSurface,
) -> Self {
Self {
valuation_date,
spot,
pair,
domestic_curve,
foreign_curve,
forwards,
vol_surface,
}
}
#[allow(clippy::too_many_arguments)]
pub fn from_raw_quotes(
valuation_date: NaiveDate,
spot: f64,
pair: (Currency, Currency),
domestic_ir: YieldTermMarketData,
foreign_ir: YieldTermMarketData,
fx_forward_quotes: Vec<FXForwardQuote>,
vol_pillars: Vec<FXDeltaVolPillar>,
domestic_calendar: Box<dyn Calendar>,
foreign_calendar: Box<dyn Calendar>,
day_counter: Box<dyn DayCounters>,
day_counter_foreign: Box<dyn DayCounters>,
) -> Result<Self> {
let domestic_curve = YieldTermStructure::new(
domestic_calendar,
day_counter,
valuation_date,
domestic_ir.get_stripped_curve()?,
);
let foreign_curve = YieldTermStructure::new(
foreign_calendar,
day_counter_foreign,
valuation_date,
foreign_ir.get_stripped_curve()?,
);
let forwards = FXForwardHelper::new(valuation_date, spot, fx_forward_quotes);
let vol_surface = FXVolSurface::new(valuation_date, vol_pillars)?;
Ok(Self::new(
valuation_date,
spot,
pair,
domestic_curve,
foreign_curve,
forwards,
vol_surface,
))
}
pub fn forward_at(&self, date: NaiveDate, calendar: &dyn Calendar) -> Result<f64> {
self.forwards.get_forward(date, calendar)?.ok_or_else(|| {
Error::InvalidData(format!(
"FxMarketContext.forward_at({}): outside forward range ({} → ...)",
date, self.valuation_date,
))
})
}
pub fn rate_d(&self, date: NaiveDate) -> Result<f64> {
self.domestic_curve
.zero_rate(date, &InterpolationMethodEnum::StepFunctionForward)
}
pub fn rate_f(&self, date: NaiveDate) -> Result<f64> {
self.foreign_curve
.zero_rate(date, &InterpolationMethodEnum::StepFunctionForward)
}
pub fn discount_d(&self, date: NaiveDate) -> Result<f64> {
self.domestic_curve
.discount(date, &InterpolationMethodEnum::StepFunctionForward)
}
pub fn discount_f(&self, date: NaiveDate) -> Result<f64> {
self.foreign_curve
.discount(date, &InterpolationMethodEnum::StepFunctionForward)
}
pub fn implied_vol(&self, expiry: NaiveDate, strike: f64) -> Result<f64> {
self.vol_surface.volatility(expiry, strike)
}
pub fn for_linear(
valuation_date: NaiveDate,
spot: f64,
pair: (Currency, Currency),
domestic_curve: YieldTermStructure,
foreign_curve: YieldTermStructure,
forwards: FXForwardHelper,
) -> Result<Self> {
let vol_surface = trivial_vol_surface(valuation_date)?;
Ok(Self::new(
valuation_date,
spot,
pair,
domestic_curve,
foreign_curve,
forwards,
vol_surface,
))
}
}
fn trivial_vol_surface(valuation_date: NaiveDate) -> Result<FXVolSurface> {
let pillar_expiry = valuation_date + Duration::days(365);
let pillar = FXDeltaVolPillar {
expiry: pillar_expiry,
forward: 1.0,
quotes: vec![
FXVolQuote::Atm(0.10),
FXVolQuote::Put {
delta: 0.25,
vol: 0.10,
},
FXVolQuote::Call {
delta: 0.25,
vol: 0.10,
},
],
};
FXVolSurface::new(valuation_date, vec![pillar])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::markets::forex::quotes::volsurface::FXVolQuote;
fn toy_vol_pillar(expiry: NaiveDate, forward: f64) -> FXDeltaVolPillar {
FXDeltaVolPillar {
expiry,
forward,
quotes: vec![
FXVolQuote::Atm(0.08),
FXVolQuote::Put {
delta: 0.25,
vol: 0.082,
},
FXVolQuote::Call {
delta: 0.25,
vol: 0.079,
},
],
}
}
#[test]
fn new_round_trip_holds_inputs_verbatim() {
use crate::markets::termstructures::yieldcurve::StrippedCurve;
use crate::time::calendars::target::Target;
use crate::time::daycounters::actual365fixed::Actual365Fixed;
let val = NaiveDate::from_ymd_opt(2026, 4, 22).unwrap();
let expiry = NaiveDate::from_ymd_opt(2027, 4, 22).unwrap();
let strip = vec![StrippedCurve {
first_settle_date: val,
date: expiry,
market_rate: 0.035,
zero_rate: 0.035,
discount: 0.966,
source: crate::markets::termstructures::yieldcurve::InterestRateQuoteEnum::OIS,
hidden_pillar: false,
}];
let d_curve = YieldTermStructure::new(
Box::new(Target),
Box::new(Actual365Fixed::default()),
val,
strip.clone(),
);
let f_curve = YieldTermStructure::new(
Box::new(Target),
Box::new(Actual365Fixed::default()),
val,
strip,
);
let forwards = FXForwardHelper::new(val, 1.17, vec![]);
let surface = FXVolSurface::new(val, vec![toy_vol_pillar(expiry, 1.19)]).unwrap();
let ctx = FxMarketContext::new(
val,
1.17,
(Currency::USD, Currency::EUR),
d_curve,
f_curve,
forwards,
surface,
);
assert_eq!(ctx.valuation_date, val);
assert!((ctx.spot - 1.17).abs() < 1e-15);
let iv = ctx.implied_vol(expiry, 1.19).unwrap();
assert!((iv - 0.08).abs() < 1e-3, "ATM iv {}", iv);
}
}