#[cfg(test)]
mod tests {
use crate::derivatives::basic::{BasicInfo, Direction, Style};
use crate::derivatives::forex::basic::{FXDerivatives, FXUnderlying};
use crate::derivatives::forex::option::{FXVanillaOption, OptionType};
use crate::error::Result;
use crate::markets::forex::market_context::FxMarketContext;
use crate::markets::forex::quotes::forwardpoints::FXForwardQuote;
use crate::markets::forex::quotes::volsurface::{FXDeltaVolPillar, FXVolQuote};
use crate::markets::termstructures::yieldcurve::{
InterestRateQuoteEnum, StrippedCurve, YieldTermStructure,
};
use crate::math::optimize::NelderMeadOptions;
use crate::models::common::calibration::Calibration;
use crate::models::forex::market_data::smile_strip;
use crate::models::forex::sabr::{SabrParams, SabrSimulator};
use crate::models::forex::sabr_calibrator::SabrSmileCalibrator;
use crate::time::calendars::{Target, UnitedStates};
use crate::time::daycounters::actual365fixed::Actual365Fixed;
use crate::time::period::Period;
use chrono::NaiveDate;
use iso_currency::Currency;
fn eurusd_ctx() -> FxMarketContext {
let val = NaiveDate::from_ymd_opt(2026, 4, 22).unwrap();
let exp_1y = NaiveDate::from_ymd_opt(2027, 4, 22).unwrap();
let spot = 1.17095;
let forward = 1.1865;
let usd_strip = vec![
StrippedCurve {
first_settle_date: val,
date: exp_1y,
market_rate: 0.0369,
zero_rate: 0.0370,
discount: (-0.0370_f64).exp(),
source: InterestRateQuoteEnum::OIS,
hidden_pillar: false,
},
StrippedCurve {
first_settle_date: val,
date: NaiveDate::from_ymd_opt(2028, 4, 22).unwrap(),
market_rate: 0.0363,
zero_rate: 0.0364,
discount: (-2.0 * 0.0364_f64).exp(),
source: InterestRateQuoteEnum::OIS,
hidden_pillar: false,
},
];
let eur_strip = vec![
StrippedCurve {
first_settle_date: val,
date: exp_1y,
market_rate: 0.0237,
zero_rate: 0.0238,
discount: (-0.0238_f64).exp(),
source: InterestRateQuoteEnum::OIS,
hidden_pillar: false,
},
StrippedCurve {
first_settle_date: val,
date: NaiveDate::from_ymd_opt(2028, 4, 22).unwrap(),
market_rate: 0.0246,
zero_rate: 0.0247,
discount: (-2.0 * 0.0247_f64).exp(),
source: InterestRateQuoteEnum::OIS,
hidden_pillar: false,
},
];
let d_curve = YieldTermStructure::new(
Box::new(UnitedStates::default()),
Box::new(Actual365Fixed::default()),
val,
usd_strip,
);
let f_curve = YieldTermStructure::new(
Box::new(Target),
Box::new(Actual365Fixed::default()),
val,
eur_strip,
);
let fwd_quotes = vec![
FXForwardQuote {
tenor: Period::SPOT,
value: 0.0,
},
FXForwardQuote {
tenor: Period::Years(1),
value: (forward - spot) * 10_000.0,
},
FXForwardQuote {
tenor: Period::Years(2),
value: 1.1984_f64.sub_f(&spot) * 10_000.0,
},
];
let vol_pillars = vec![FXDeltaVolPillar {
expiry: exp_1y,
forward,
quotes: vec![
FXVolQuote::Atm(0.0663),
FXVolQuote::Put {
delta: 0.25,
vol: 0.06855,
},
FXVolQuote::Call {
delta: 0.25,
vol: 0.07125,
},
FXVolQuote::Put {
delta: 0.10,
vol: 0.077225,
},
FXVolQuote::Call {
delta: 0.10,
vol: 0.082775,
},
],
}];
let forwards = crate::markets::forex::quotes::forwardpoints::FXForwardHelper::new(
val, spot, fwd_quotes,
);
let vol_surface =
crate::markets::forex::quotes::volsurface::FXVolSurface::new(val, vol_pillars).unwrap();
FxMarketContext::new(
val,
spot,
(Currency::USD, Currency::EUR),
d_curve,
f_curve,
forwards,
vol_surface,
)
}
trait SubF {
fn sub_f(&self, rhs: &Self) -> Self;
}
impl SubF for f64 {
fn sub_f(&self, rhs: &Self) -> Self {
*self - *rhs
}
}
#[test]
fn raw_market_context_prices_fx_vanilla_option() -> Result<()> {
let ctx = eurusd_ctx();
let expiry = NaiveDate::from_ymd_opt(2027, 4, 22).unwrap();
let option = FXVanillaOption {
basic_info: BasicInfo {
trade_date: ctx.valuation_date,
style: Style::FXCall,
direction: Direction::Buy,
expiry_date: expiry,
delivery_date: expiry,
},
asset: FXUnderlying::EURUSD,
option_type: OptionType::Call,
notional_currency: Currency::from_code("EUR").unwrap(),
notional_amounts: 1_000_000.0,
strike: 1.20,
volatility: ctx.implied_vol(expiry, 1.20)?,
};
let pv = option.mtm(&ctx)?;
assert!(
pv.value < 0.0,
"buyer PV should be negative, got {}",
pv.value
);
assert!(
pv.value.abs() < 100_000.0,
"buyer premium {} implausibly large on EUR 1M notional",
pv.value,
);
Ok(())
}
#[test]
fn raw_market_context_calibrates_and_simulates_sabr() -> Result<()> {
let ctx = eurusd_ctx();
let expiry = NaiveDate::from_ymd_opt(2027, 4, 22).unwrap();
let forward = 1.1865;
let strikes = vec![1.05, 1.12, 1.19, 1.26, 1.33];
let strip = smile_strip(
&ctx.vol_surface,
ctx.valuation_date,
expiry,
forward,
&strikes,
)?;
let calibrator = SabrSmileCalibrator {
initial: SabrParams::new(0.07, 0.5, -0.20, 0.30),
};
let report = calibrator.calibrate(
&strip,
NelderMeadOptions {
max_iter: 500,
ftol: 1.0e-10,
xtol: 1.0e-8,
step_frac: 0.10,
},
)?;
assert!(
report.rmse < 20.0e-4,
"raw-market SABR calibration RMSE {:.3} bp > 20 bp",
report.rmse * 10_000.0
);
let mut sim = SabrSimulator::new(report.params, forward, 42);
let t = 1.0_f64;
let terms = sim.simulate(t, 200, 2_000);
let mean: f64 = terms.iter().map(|s| s.forward).sum::<f64>() / 2_000.0;
assert!(
(mean - forward).abs() / forward < 0.01,
"SABR MC mean {} off from forward {} by {:.2} %",
mean,
forward,
(mean - forward).abs() / forward * 100.0,
);
Ok(())
}
}