use crate::cashflows::Dividend;
use crate::errors::QlResult;
use crate::fail;
use crate::instruments::{Greeks, OneAssetOptionEngine, OneAssetOptionResults, OptionArguments};
use crate::methods::finitedifferences::meshers::{
FdmMesher, FdmMesherComposite, fdm_black_scholes_mesher,
};
use crate::methods::finitedifferences::solvers::{
FdmBlackScholesSolver, FdmSchemeDesc, FdmSolverDesc,
};
use crate::methods::finitedifferences::stepconditions::FdmStepConditionComposite;
use crate::methods::finitedifferences::utilities::{FdmInnerValueCalculator, fdm_log_inner_value};
use crate::patterns::observable::{AsObservable, Observable};
use crate::payoff::Payoff;
use crate::pricingengine::{Arguments, PricingEngine, Results};
use crate::processes::GeneralizedBlackScholesProcess;
use crate::shared::{Shared, shared};
use crate::stochasticprocess::StochasticProcess1D;
use crate::types::Size;
const DIRECTION: Size = 0;
const EPS: f64 = 0.0001;
const SCALE_FACTOR: f64 = 1.5;
const C_POINT_DENSITY: f64 = 0.1;
pub struct FdBlackScholesVanillaEngine {
base: OneAssetOptionEngine,
process: Shared<GeneralizedBlackScholesProcess>,
dividends: Vec<Shared<dyn Dividend>>,
t_grid: Size,
x_grid: Size,
damping_steps: Size,
scheme_desc: FdmSchemeDesc,
}
impl FdBlackScholesVanillaEngine {
pub fn new(
process: Shared<GeneralizedBlackScholesProcess>,
t_grid: Size,
x_grid: Size,
damping_steps: Size,
scheme_desc: FdmSchemeDesc,
) -> FdBlackScholesVanillaEngine {
FdBlackScholesVanillaEngine::with_cash_dividends(
process,
Vec::new(),
t_grid,
x_grid,
damping_steps,
scheme_desc,
)
}
pub fn with_cash_dividends(
process: Shared<GeneralizedBlackScholesProcess>,
dividends: Vec<Shared<dyn Dividend>>,
t_grid: Size,
x_grid: Size,
damping_steps: Size,
scheme_desc: FdmSchemeDesc,
) -> FdBlackScholesVanillaEngine {
let base =
OneAssetOptionEngine::new(OptionArguments::default(), OneAssetOptionResults::default());
base.register_with(process.observable());
FdBlackScholesVanillaEngine {
base,
process,
dividends,
t_grid,
x_grid,
damping_steps,
scheme_desc,
}
}
}
impl AsObservable for FdBlackScholesVanillaEngine {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl PricingEngine for FdBlackScholesVanillaEngine {
fn arguments_mut(&mut self) -> &mut dyn Arguments {
self.base.arguments_mut()
}
fn results(&self) -> &dyn Results {
self.base.results()
}
fn reset(&mut self) {
self.base.reset();
}
fn calculate(&mut self) -> QlResult<()> {
let arguments = self.base.arguments();
let Some(exercise) = &arguments.exercise else {
fail!("no exercise given");
};
let exercise = Shared::clone(exercise);
let Some(payoff) = &arguments.payoff else {
fail!("no payoff given");
};
let payoff = Shared::clone(payoff);
let exercise_date = exercise.last_date();
let maturity = self.process.time(&exercise_date)?;
let strike = payoff.strike();
let equity_mesher = fdm_black_scholes_mesher(
self.x_grid,
&self.process,
maturity,
strike,
None,
None,
EPS,
SCALE_FACTOR,
Some((strike, C_POINT_DENSITY)),
&self.dividends,
0.0,
)?;
let mesher = shared(FdmMesherComposite::new(vec![equity_mesher])) as Shared<dyn FdmMesher>;
let calculator = shared(fdm_log_inner_value(
payoff as Shared<dyn Payoff>,
Shared::clone(&mesher),
DIRECTION,
)) as Shared<dyn FdmInnerValueCalculator>;
let risk_free = self.process.risk_free_rate().current_link()?;
let Some(day_counter) = risk_free.day_counter() else {
fail!("no day counter provided for the risk-free curve");
};
let condition = FdmStepConditionComposite::vanilla_composite(
&self.dividends,
&exercise,
Shared::clone(&mesher),
Shared::clone(&calculator),
risk_free.reference_date()?,
&day_counter,
)?;
let solver_desc = FdmSolverDesc {
mesher,
bc_set: Vec::new(),
condition,
calculator,
maturity,
time_steps: self.t_grid,
damping_steps: self.damping_steps,
};
let solver = FdmBlackScholesSolver::new(
Shared::clone(&self.process),
strike,
solver_desc,
self.scheme_desc,
);
let spot = self.process.x0()?;
let value = solver.value_at(spot)?;
let delta = solver.delta_at(spot)?;
let gamma = solver.gamma_at(spot)?;
let theta = solver.theta_at(spot)?;
let results = self.base.results_mut();
results.instrument.value = Some(value);
results.greeks = Greeks {
delta: Some(delta),
gamma: Some(gamma),
theta,
..Greeks::default()
};
Ok(())
}
}
#[cfg(test)]
mod test_fd_engines {
use std::time::Instant;
use super::super::test_market::{market, today};
use super::FdBlackScholesVanillaEngine;
use crate::instrument::Instrument;
use crate::instruments::{EuropeanOption, PlainVanillaPayoff};
use crate::methods::finitedifferences::solvers::FdmSchemeDesc;
use crate::option::OptionType::{self, Call, Put};
use crate::pricingengine::PricingEngine;
use crate::shared::{Shared, SharedMut, shared, shared_mut};
use crate::time::date::Date;
use crate::types::{Rate, Real, Size, Volatility};
const T_GRID: Size = 500;
const X_GRID: Size = 500;
const UNDERLYING: Real = 100.0;
const VALUE_TOLERANCE: Real = 1.0e-4;
const DELTA_TOLERANCE: Real = 1.0e-6;
const GAMMA_TOLERANCE: Real = 1.0e-6;
const THETA_TOLERANCE: Real = 1.0e-3;
fn relative_error(x1: Real, x2: Real, reference: Real) -> Real {
if reference != 0.0 {
(x1 - x2).abs() / reference
} else {
(x1 - x2).abs()
}
}
fn fd_option(
market: &super::super::test_market::Market,
option_type: OptionType,
strike: Real,
expiry: Date,
) -> EuropeanOption {
let payoff = shared(PlainVanillaPayoff::new(option_type, strike));
let exercise = shared(crate::exercise::EuropeanExercise::new(expiry));
let mut option = EuropeanOption::new(payoff, exercise, Shared::clone(&market.settings));
let engine = shared_mut(FdBlackScholesVanillaEngine::new(
Shared::clone(&market.process),
T_GRID,
X_GRID,
0,
FdmSchemeDesc::douglas(),
));
option
.base_mut()
.set_pricing_engine(engine as SharedMut<dyn PricingEngine>);
option
}
#[test]
fn fd_engine_matches_the_analytic_engine_over_the_market_sweep() {
let started = Instant::now();
let market = market();
let expiry = today() + 360;
let q_rates: [Rate; 2] = [0.00, 0.05];
let r_rates: [Rate; 3] = [0.01, 0.05, 0.15];
let vols: [Volatility; 3] = [0.11, 0.50, 1.20];
let mut worst = [
("value", 0.0),
("delta", 0.0),
("gamma", 0.0),
("theta", 0.0),
];
for option_type in [Call, Put] {
for strike in [75.0, 100.0, 125.0] {
let mut reference = market.option(option_type, strike, expiry);
let mut option = fd_option(&market, option_type, strike, expiry);
for q in q_rates {
for r in r_rates {
for vol in vols {
market.set(UNDERLYING, q, r, vol);
let value = option.npv().unwrap();
let mut checks =
vec![("value", reference.npv().unwrap(), value, VALUE_TOLERANCE)];
if value > UNDERLYING * 1.0e-5 {
checks.push((
"delta",
reference.delta().unwrap(),
option.delta().unwrap(),
DELTA_TOLERANCE,
));
checks.push((
"gamma",
reference.gamma().unwrap(),
option.gamma().unwrap(),
GAMMA_TOLERANCE,
));
checks.push((
"theta",
reference.theta().unwrap(),
option.theta().unwrap(),
THETA_TOLERANCE,
));
}
for (name, expected, calculated, tolerance) in checks {
let error = relative_error(expected, calculated, UNDERLYING);
assert!(
error <= tolerance,
"{name} of {option_type:?} K={strike} q={q} r={r} v={vol}: \
analytic {expected} vs finite difference {calculated} \
(relative error {error} over {tolerance})"
);
for slot in &mut worst {
if slot.0 == name && error > slot.1 {
slot.1 = error;
}
}
}
}
}
}
}
}
println!(
"testFdEngines: {:?} for 108 combinations; worst relative errors {worst:?}",
started.elapsed()
);
}
}
#[cfg(test)]
mod test_fd_values {
use super::super::test_market::{Market, market, time_to_days, today};
use super::FdBlackScholesVanillaEngine;
use crate::exercise::{AmericanExercise, Exercise};
use crate::instrument::Instrument;
use crate::instruments::{OneAssetOption, PlainVanillaPayoff};
use crate::methods::finitedifferences::solvers::FdmSchemeDesc;
use crate::option::OptionType::{self, Call, Put};
use crate::pricingengine::PricingEngine;
use crate::shared::{Shared, SharedMut, shared, shared_mut};
use crate::types::{Rate, Real, Size, Time, Volatility};
const T_GRID: Size = 100;
const X_GRID: Size = 400;
const TOLERANCE: Real = 8.0e-2;
struct JuValue {
option_type: OptionType,
strike: Real,
spot: Real,
q: Rate,
r: Rate,
t: Time,
vol: Volatility,
expected: Real,
}
fn price(market: &Market, ju: &JuValue) -> Real {
market.set(ju.spot, ju.q, ju.r, ju.vol);
let exercise = AmericanExercise::over(today(), today() + time_to_days(ju.t)).unwrap();
let mut option = OneAssetOption::new(
shared(PlainVanillaPayoff::new(ju.option_type, ju.strike)),
shared(exercise) as Shared<dyn Exercise>,
Shared::clone(&market.settings),
);
let engine = shared_mut(FdBlackScholesVanillaEngine::new(
Shared::clone(&market.process),
T_GRID,
X_GRID,
0,
FdmSchemeDesc::douglas(),
));
option
.base_mut()
.set_pricing_engine(engine as SharedMut<dyn PricingEngine>);
option.npv().unwrap()
}
#[test]
fn american_options_reproduce_the_ju_values() {
let market = market();
let rows = [
JuValue {
option_type: Put,
strike: 40.0,
spot: 40.0,
q: 0.0,
r: 0.0488,
t: 0.3333,
vol: 0.2,
expected: 1.576,
},
JuValue {
option_type: Put,
strike: 45.0,
spot: 40.0,
q: 0.0,
r: 0.0488,
t: 0.5833,
vol: 0.2,
expected: 5.260,
},
JuValue {
option_type: Call,
strike: 100.0,
spot: 100.0,
q: 0.07,
r: 0.03,
t: 3.0,
vol: 0.2,
expected: 9.065,
},
JuValue {
option_type: Call,
strike: 100.0,
spot: 120.0,
q: 0.07,
r: 0.03,
t: 3.0,
vol: 0.2,
expected: 21.398,
},
];
for ju in &rows {
let calculated = price(&market, ju);
let error = (calculated - ju.expected).abs();
println!(
"testFdValues: {:?} K={} S={} t={}: Ju {} finite difference {calculated}",
ju.option_type, ju.strike, ju.spot, ju.t, ju.expected
);
assert!(
error <= TOLERANCE,
"{:?} K={} S={} q={} r={} t={} v={}: Ju {} vs finite difference {calculated} \
(absolute error {error} over {TOLERANCE})",
ju.option_type,
ju.strike,
ju.spot,
ju.q,
ju.r,
ju.t,
ju.vol,
ju.expected
);
}
}
}
#[cfg(test)]
mod test_fd_earliest_exercise_date {
use super::super::AnalyticEuropeanEngine;
use super::FdBlackScholesVanillaEngine;
use crate::exercise::{AmericanExercise, EuropeanExercise, Exercise};
use crate::handle::Handle;
use crate::instrument::Instrument;
use crate::instruments::{OneAssetOption, PlainVanillaPayoff};
use crate::interestrate::Compounding;
use crate::methods::finitedifferences::solvers::FdmSchemeDesc;
use crate::option::OptionType::Put;
use crate::pricingengine::PricingEngine;
use crate::processes::{BlackScholesMertonProcess, GeneralizedBlackScholesProcess};
use crate::quotes::{Quote, SimpleQuote};
use crate::settings::Settings;
use crate::shared::{Shared, SharedMut, shared, shared_mut};
use crate::termstructures::volatility::{BlackConstantVol, BlackVolTermStructure};
use crate::termstructures::yields::FlatForward;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::date::{Date, Month};
use crate::time::daycounters::actual365fixed::Actual365Fixed;
use crate::time::frequency::Frequency;
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
use crate::types::{Rate, Real, Size, Volatility};
pub(super) const S0: Real = 80.0;
pub(super) const STRIKE: Real = 100.0;
const SIGMA: Volatility = 0.25;
const R: Rate = 0.05;
const Q: Rate = 0.0;
pub(super) const T_GRID: Size = 200;
pub(super) const X_GRID: Size = 200;
pub(super) fn today() -> Date {
Date::new(15, Month::January, 2025)
}
fn flat_rate(rate: Rate) -> Handle<dyn YieldTermStructure> {
Handle::new(shared(FlatForward::with_rate(
today(),
rate,
Actual365Fixed::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>)
}
pub(super) fn process() -> Shared<GeneralizedBlackScholesProcess> {
let spot = Handle::new(shared(SimpleQuote::new(S0)) as Shared<dyn Quote>);
let vol = Handle::new(shared(BlackConstantVol::new(
today(),
None,
SIGMA,
Actual365Fixed::new(),
)) as Shared<dyn BlackVolTermStructure>);
shared(BlackScholesMertonProcess::new(
spot,
flat_rate(Q),
flat_rate(R),
vol,
))
}
fn american_price(
settings: &Shared<Settings<Date>>,
process: &Shared<GeneralizedBlackScholesProcess>,
earliest: Date,
maturity: Date,
) -> Real {
let exercise = AmericanExercise::over(earliest, maturity).unwrap();
let mut option = OneAssetOption::new(
shared(PlainVanillaPayoff::new(Put, STRIKE)),
shared(exercise) as Shared<dyn Exercise>,
Shared::clone(settings),
);
let engine = shared_mut(FdBlackScholesVanillaEngine::new(
Shared::clone(process),
T_GRID,
X_GRID,
0,
FdmSchemeDesc::douglas(),
));
option
.base_mut()
.set_pricing_engine(engine as SharedMut<dyn PricingEngine>);
option.npv().unwrap()
}
#[test]
fn narrowing_the_exercise_window_lowers_the_price_toward_the_european_one() {
let settings = shared(Settings::new());
settings.set_evaluation_date(today());
let process = process();
let maturity = today() + Period::new(1, TimeUnit::Years);
let full = american_price(&settings, &process, today(), maturity);
let mid = american_price(
&settings,
&process,
maturity - Period::new(6, TimeUnit::Months),
maturity,
);
let late = american_price(
&settings,
&process,
maturity - Period::new(3, TimeUnit::Months),
maturity,
);
let mut european = OneAssetOption::new(
shared(PlainVanillaPayoff::new(Put, STRIKE)),
shared(EuropeanExercise::new(maturity)) as Shared<dyn Exercise>,
Shared::clone(&settings),
);
let analytic = shared_mut(AnalyticEuropeanEngine::new(Shared::clone(&process)));
european
.base_mut()
.set_pricing_engine(analytic as SharedMut<dyn PricingEngine>);
let euro = european.npv().unwrap();
println!("testFdEarliestExerciseDate: full {full} 6M {mid} 3M {late} european {euro}");
assert!(
full - euro > 1.0,
"the early-exercise premium should be significant: full {full} european {euro}"
);
assert!(
full - late > 0.01,
"restricting the exercise window should reduce the price: full {full} late {late}"
);
assert!(
late > euro + 0.01,
"the restricted American should exceed the European: late {late} european {euro}"
);
assert!(
mid >= late - 1e-8,
"a wider window should give a higher price: 6M {mid} 3M {late}"
);
assert!(
full >= mid - 1e-8,
"the full window should give the highest price: full {full} 6M {mid}"
);
}
}
#[cfg(test)]
mod test_fd_bermudan {
use super::FdBlackScholesVanillaEngine;
use super::test_fd_earliest_exercise_date::{S0, STRIKE, T_GRID, X_GRID, process, today};
use crate::exercise::{AmericanExercise, BermudanExercise, EuropeanExercise, Exercise};
use crate::instrument::Instrument;
use crate::instruments::{OneAssetOption, PlainVanillaPayoff};
use crate::methods::finitedifferences::solvers::FdmSchemeDesc;
use crate::option::OptionType::Put;
use crate::pricingengine::PricingEngine;
use crate::processes::GeneralizedBlackScholesProcess;
use crate::settings::Settings;
use crate::shared::{Shared, SharedMut, shared, shared_mut};
use crate::time::date::Date;
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
use crate::types::Real;
fn fd_price(
settings: &Shared<Settings<Date>>,
process: &Shared<GeneralizedBlackScholesProcess>,
exercise: Shared<dyn Exercise>,
) -> Real {
let mut option = OneAssetOption::new(
shared(PlainVanillaPayoff::new(Put, STRIKE)),
exercise,
Shared::clone(settings),
);
let engine = shared_mut(FdBlackScholesVanillaEngine::new(
Shared::clone(process),
T_GRID,
X_GRID,
0,
FdmSchemeDesc::douglas(),
));
option
.base_mut()
.set_pricing_engine(engine as SharedMut<dyn PricingEngine>);
option.npv().unwrap()
}
fn every(months: &[i32]) -> Shared<dyn Exercise> {
let dates = months
.iter()
.map(|m| today() + Period::new(*m, TimeUnit::Months))
.collect();
shared(BermudanExercise::new(dates, false).unwrap()) as Shared<dyn Exercise>
}
fn maturity() -> Date {
today() + Period::new(1, TimeUnit::Years)
}
fn fixture() -> (
Shared<Settings<Date>>,
Shared<GeneralizedBlackScholesProcess>,
) {
let settings = shared(Settings::new());
settings.set_evaluation_date(today());
(settings, process())
}
#[test]
fn binding_market_regressions_cover_european_american_and_quarterly_bermudan() {
let (settings, process) = fixture();
let rows: [(&str, Shared<dyn Exercise>, [Real; 4]); 3] = [
(
"European",
shared(EuropeanExercise::new(maturity())),
[
18.266147644485358,
-0.714_918_249_077_874_9,
0.016_981_361_087_847_3,
0.377_587_111_591_224_7,
],
),
(
"American",
shared(AmericanExercise::over(today(), maturity()).unwrap()),
[
20.357667204554883,
-0.859_029_794_934_689_8,
0.026600330114210077,
-0.863_258_093_716_587,
],
),
(
"Bermudan",
every(&[3, 6, 9, 12]),
[
19.954434523211695,
-0.817_505_453_287_635_2,
0.019414167279763642,
0.385_210_817_369_876_67,
],
),
];
for (name, exercise, expected) in rows {
let mut option = OneAssetOption::new(
shared(PlainVanillaPayoff::new(Put, STRIKE)),
exercise,
Shared::clone(&settings),
);
let engine = shared_mut(FdBlackScholesVanillaEngine::new(
Shared::clone(&process),
T_GRID,
X_GRID,
0,
FdmSchemeDesc::douglas(),
));
option
.base_mut()
.set_pricing_engine(engine as SharedMut<dyn PricingEngine>);
let actual = [
option.npv().unwrap(),
option.delta().unwrap(),
option.gamma().unwrap(),
option.theta().unwrap(),
];
for (field, (actual, expected)) in actual.into_iter().zip(expected).enumerate() {
assert!(
(actual - expected).abs() <= 1.0e-10 * expected.abs(),
"{name} field {field}: {actual} != {expected}"
);
}
}
}
#[test]
fn a_single_exercise_date_at_expiry_degenerates_to_the_european_price() {
let (settings, process) = fixture();
let euro = fd_price(
&settings,
&process,
shared(EuropeanExercise::new(maturity())) as Shared<dyn Exercise>,
);
let bermudan = fd_price(&settings, &process, every(&[12]));
let error = (bermudan - euro).abs();
assert!(
error <= 1.0e-5,
"a Bermudan exercisable only at expiry should be the European option: \
Bermudan {bermudan} european {euro} (absolute error {error})"
);
}
#[test]
fn dense_exercise_dates_price_above_the_european_and_under_the_american() {
let (settings, process) = fixture();
let euro = fd_price(
&settings,
&process,
shared(EuropeanExercise::new(maturity())) as Shared<dyn Exercise>,
);
let monthly = fd_price(
&settings,
&process,
every(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
);
let american = fd_price(
&settings,
&process,
shared(AmericanExercise::over(today(), maturity()).unwrap()) as Shared<dyn Exercise>,
);
println!("testFdBermudan: S0={S0} european {euro} monthly {monthly} american {american}");
assert!(
monthly > euro + 1.0,
"monthly exercise should capture most of the early-exercise premium: \
monthly {monthly} european {euro}"
);
assert!(
monthly <= american,
"a Bermudan cannot beat the American it is a subset of: \
monthly {monthly} american {american}"
);
}
#[test]
fn price_is_monotone_in_the_exercise_date_set() {
let (settings, process) = fixture();
let quarterly = fd_price(&settings, &process, every(&[3, 6, 9, 12]));
let monthly = fd_price(
&settings,
&process,
every(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
);
assert!(
monthly >= quarterly - 1.0e-8,
"more exercise dates should not lower the price: \
quarterly {quarterly} monthly {monthly}"
);
}
}
#[cfg(test)]
mod test_fd_engine_with_non_constant_parameters {
use super::super::AnalyticEuropeanEngine;
use super::super::test_market::today;
use super::FdBlackScholesVanillaEngine;
use crate::exercise::EuropeanExercise;
use crate::handle::Handle;
use crate::instrument::Instrument;
use crate::instruments::{EuropeanOption, PlainVanillaPayoff};
use crate::interestrate::Compounding;
use crate::math::interpolations::flat::BackwardFlat;
use crate::methods::finitedifferences::solvers::FdmSchemeDesc;
use crate::option::OptionType::Call;
use crate::pricingengine::PricingEngine;
use crate::processes::GeneralizedBlackScholesProcess;
use crate::quotes::{Quote, SimpleQuote};
use crate::settings::Settings;
use crate::shared::{Shared, SharedMut, shared, shared_mut};
use crate::termstructures::volatility::{BlackConstantVol, BlackVolTermStructure};
use crate::termstructures::yields::{FlatForward, ForwardCurve};
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::daycounters::actual360::Actual360;
use crate::time::daycounters::actual365fixed::Actual365Fixed;
use crate::time::frequency::Frequency;
use crate::types::{Real, Size, Volatility};
const UNDERLYING: Real = 190.0;
const STRIKE: Real = 190.0;
const VOLATILITY: Volatility = 0.20;
const T_GRID: Size = 200;
const X_GRID: Size = 201;
const TOLERANCE: Real = 0.01;
fn process() -> GeneralizedBlackScholesProcess {
let day_counter = Actual360::new();
let spot = shared(SimpleQuote::new(UNDERLYING)) as Shared<dyn Quote>;
let vol = shared(BlackConstantVol::new(
today(),
None,
VOLATILITY,
day_counter.clone(),
)) as Shared<dyn BlackVolTermStructure>;
let dates = vec![
today(),
today() + 90,
today() + 180,
today() + 270,
today() + 360,
];
let forwards = vec![0.0, 0.001, 0.002, 0.005, 0.01];
let risk_free =
shared(ForwardCurve::new(dates, forwards, day_counter.clone(), BackwardFlat).unwrap())
as Shared<dyn YieldTermStructure>;
let dividend = shared(FlatForward::with_rate(
today(),
0.0,
Actual365Fixed::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>;
GeneralizedBlackScholesProcess::new(
Handle::new(spot),
Handle::new(dividend),
Handle::new(risk_free),
Handle::new(vol),
)
}
#[test]
fn fd_engine_matches_the_analytic_engine_under_a_time_varying_rate() {
let settings = shared(Settings::new());
settings.set_evaluation_date(today());
let process = shared(process());
let payoff = shared(PlainVanillaPayoff::new(Call, STRIKE));
let exercise = shared(EuropeanExercise::new(today() + 360));
let mut option = EuropeanOption::new(payoff, exercise, Shared::clone(&settings));
let analytic = shared_mut(AnalyticEuropeanEngine::new(Shared::clone(&process)));
option
.base_mut()
.set_pricing_engine(analytic as SharedMut<dyn PricingEngine>);
let expected = option.npv().unwrap();
let finite_difference = shared_mut(FdBlackScholesVanillaEngine::new(
Shared::clone(&process),
T_GRID,
X_GRID,
0,
FdmSchemeDesc::douglas(),
));
option
.base_mut()
.set_pricing_engine(finite_difference as SharedMut<dyn PricingEngine>);
let calculated = option.npv().unwrap();
let error = (expected - calculated).abs();
assert!(
error <= TOLERANCE,
"analytic {expected} vs finite difference {calculated} \
(absolute error {error} over {TOLERANCE})"
);
}
}
#[cfg(test)]
mod test_fd_spot_cash_dividends {
use crate::cashflows::dividend_vector;
use crate::exercise::{AmericanExercise, EuropeanExercise, Exercise};
use crate::handle::Handle;
use crate::instrument::Instrument;
use crate::instruments::{OneAssetOption, PlainVanillaPayoff};
use crate::interestrate::Compounding;
use crate::methods::finitedifferences::solvers::FdmSchemeDesc;
use crate::option::OptionType::{self, Call, Put};
use crate::pricingengine::PricingEngine;
use crate::pricingengines::vanilla::AnalyticDividendEuropeanEngine;
use crate::pricingengines::vanilla::test_market::quote_handle;
use crate::processes::{BlackScholesMertonProcess, GeneralizedBlackScholesProcess};
use crate::quotes::SimpleQuote;
use crate::settings::Settings;
use crate::shared::{Shared, SharedMut, shared, shared_mut};
use crate::termstructures::volatility::{BlackConstantVol, BlackVolTermStructure};
use crate::termstructures::yields::FlatForward;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::date::{Date, Month};
use crate::time::daycounter::DayCounter;
use crate::time::daycounters::actual360::Actual360;
use crate::time::daycounters::actual365fixed::Actual365Fixed;
use crate::time::frequency::Frequency;
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
use crate::types::{Real, Size};
use super::FdBlackScholesVanillaEngine;
const DEFAULT_GRID: Size = 100;
struct Market {
settings: Shared<Settings<Date>>,
spot: Shared<SimpleQuote>,
q_rate: Shared<SimpleQuote>,
r_rate: Shared<SimpleQuote>,
vol: Shared<SimpleQuote>,
process: Shared<GeneralizedBlackScholesProcess>,
}
impl Market {
fn new(today: Date, day_counter: DayCounter) -> Market {
let settings = shared(Settings::new());
settings.set_evaluation_date(today);
let quote = || shared(SimpleQuote::new(0.0));
let (spot, q_rate, r_rate, vol) = (quote(), quote(), quote(), quote());
let flat = |q: &Shared<SimpleQuote>| {
Handle::new(shared(FlatForward::new(
today,
quote_handle(q),
day_counter.clone(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>)
};
let vol_ts = shared(BlackConstantVol::with_quote(
today,
None,
quote_handle(&vol),
day_counter.clone(),
)) as Shared<dyn BlackVolTermStructure>;
let process = shared(BlackScholesMertonProcess::new(
quote_handle(&spot),
flat(&q_rate),
flat(&r_rate),
Handle::new(vol_ts),
));
Market {
settings,
spot,
q_rate,
r_rate,
vol,
process,
}
}
fn set(&self, spot: Real, q: Real, r: Real, vol: Real) {
self.spot.set_value(spot);
self.q_rate.set_value(q);
self.r_rate.set_value(r);
self.vol.set_value(vol);
}
fn option(
&self,
option_type: OptionType,
strike: Real,
exercise: &Shared<dyn Exercise>,
dividends: Option<(&[Date], &[Real])>,
(t_grid, x_grid): (Size, Size),
) -> OneAssetOption {
let process = Shared::clone(&self.process);
let engine = match dividends {
None => FdBlackScholesVanillaEngine::new(
process,
t_grid,
x_grid,
0,
FdmSchemeDesc::douglas(),
),
Some((dates, amounts)) => FdBlackScholesVanillaEngine::with_cash_dividends(
process,
dividend_vector(dates, amounts).unwrap(),
t_grid,
x_grid,
0,
FdmSchemeDesc::douglas(),
),
};
let mut option = OneAssetOption::new(
shared(PlainVanillaPayoff::new(option_type, strike)),
Shared::clone(exercise),
Shared::clone(&self.settings),
);
option
.base_mut()
.set_pricing_engine(shared_mut(engine) as SharedMut<dyn PricingEngine>);
option
}
}
fn european(date: Date) -> Shared<dyn Exercise> {
shared(EuropeanExercise::new(date))
}
fn american(earliest: Date, latest: Date) -> Shared<dyn Exercise> {
shared(AmericanExercise::over(earliest, latest).unwrap())
}
type Cached = [(OptionType, bool, Real, Real, Real, Real); 4];
#[allow(
clippy::excessive_precision,
reason = "Preserve the exact digits QuantLib printed"
)]
const CACHED: Cached = [
(
Call,
false,
6.1960282059877851,
0.35044346362393469,
0.011801864944369449,
-3.6653085886903991,
),
(
Call,
true,
9.3876035416433616,
0.50439288657051151,
0.014825610960493722,
-4.4369085588700861,
),
(
Put,
false,
19.549295792091659,
-0.57167700814074907,
0.011801370341950067,
-6.8200224270321117,
),
(
Put,
true,
19.554593543297607,
-0.57210536069765239,
0.011835102196332071,
-6.8366745831229414,
),
];
#[allow(
clippy::excessive_precision,
reason = "Preserve the exact digits QuantLib printed"
)]
const CACHED_TWO: Cached = [
(
Call,
false,
4.7378522931954965,
0.29403066310229198,
0.011428251048714312,
-3.7809558264983743,
),
(
Call,
true,
7.9057708867966641,
0.49147953278341305,
0.01871370359184878,
-6.1496514215378504,
),
(
Put,
false,
22.763590143100142,
-0.62808693189317422,
0.011427611391172732,
-6.748690838527442,
),
(
Put,
true,
22.772363862240606,
-0.6287703683761644,
0.011478776299872184,
-6.7740649495887144,
),
];
fn check_cached(months: &[i32], amounts: &[Real], cached: &Cached) {
let today = Date::new(27, Month::February, 2021);
let market = Market::new(today, Actual360::new());
market.set(100.0, 0.08, 0.04, 0.3);
let maturity = today + Period::new(12, TimeUnit::Months);
assert_eq!(maturity, Date::new(27, Month::February, 2022));
let dates: Vec<Date> = months
.iter()
.map(|&m| today + Period::new(m, TimeUnit::Months))
.collect();
for &(option_type, is_american, npv, delta, gamma, theta) in cached {
let exercise = if is_american {
american(today, maturity)
} else {
european(maturity)
};
let mut option = market.option(
option_type,
100.0,
&exercise,
Some((&dates, amounts)),
(100, 400),
);
for (name, value, expected) in [
("npv", option.npv().unwrap(), npv),
("delta", option.delta().unwrap(), delta),
("gamma", option.gamma().unwrap(), gamma),
("theta", option.theta().unwrap(), theta),
] {
assert!(
(value - expected).abs() < 1.0e-8,
"{dates:?} {option_type:?} american {is_american} {name}: \
calculated {value} expected {expected}"
);
}
}
}
#[test]
fn mid_life_cash_dividend_reproduces_the_cached_quantlib_values() {
check_cached(&[10], &[10.0], &CACHED);
}
#[test]
fn two_cash_dividends_reproduce_the_cached_quantlib_values() {
check_cached(&[4, 10], &[5.0, 10.0], &CACHED_TWO);
}
#[test]
fn a_dividend_after_maturity_is_dropped() {
check_cached(&[10, 14], &[10.0, 7.0], &CACHED);
}
fn check_fd_greeks(today: Date, exercise: &Shared<dyn Exercise>) {
let market = Market::new(today, Actual365Fixed::new());
let u = 100.0;
let last = exercise.last_date();
let mut dates = Vec::new();
let mut d = today + Period::new(3, TimeUnit::Months);
while d < last {
dates.push(d);
d = d + Period::new(6, TimeUnit::Months);
}
let amounts = vec![5.0; dates.len()];
for option_type in [Call, Put] {
for strike in [50.0, 99.5, 100.0, 100.5, 150.0] {
let mut option = market.option(
option_type,
strike,
exercise,
Some((&dates, &amounts)),
(DEFAULT_GRID, DEFAULT_GRID),
);
for q in [0.00, 0.10, 0.20] {
for r in [0.01, 0.05, 0.15] {
for v in [0.05, 0.20, 0.50] {
market.set(u, q, r, v);
let value = option.npv().unwrap();
let delta = option.delta().unwrap();
let gamma = option.gamma().unwrap();
if value <= u * 1.0e-5 {
continue;
}
let du = u * 1.0e-4;
market.spot.set_value(u + du);
let (value_p, delta_p) =
(option.npv().unwrap(), option.delta().unwrap());
market.spot.set_value(u - du);
let (value_m, delta_m) =
(option.npv().unwrap(), option.delta().unwrap());
market.spot.set_value(u);
for (name, expected, calculated, tolerance) in [
("delta", (value_p - value_m) / (2.0 * du), delta, 5.0e-3),
("gamma", (delta_p - delta_m) / (2.0 * du), gamma, 7.0e-3),
] {
let error = (expected - calculated).abs() / u;
assert!(
error <= tolerance,
"{name} {option_type:?} K {strike} q {q} r {r} v {v} \
expiry {last:?}: expected {expected} calculated \
{calculated} error {error}"
);
}
}
}
}
}
}
}
fn greeks_today() -> Date {
Date::new(15, Month::June, 2026)
}
#[test]
fn european_greeks_match_bump_and_reprice() {
let today = greeks_today();
for years in [1, 2] {
let expiry = today + Period::new(years, TimeUnit::Years);
check_fd_greeks(today, &european(expiry));
}
}
#[test]
fn american_greeks_match_bump_and_reprice() {
let today = greeks_today();
for years in [1, 2] {
let expiry = today + Period::new(years, TimeUnit::Years);
check_fd_greeks(today, &american(today, expiry));
}
}
fn degenerate_fixture(r: Real) -> (Market, Date, Date) {
let today = Date::new(27, Month::February, 2005);
let market = Market::new(today, Actual360::new());
market.set(54.625, 0.0, r, 0.282922);
(market, today, Date::new(13, Month::April, 2005))
}
fn check_fd_degenerate(market: &Market, today: Date, exercise: &Shared<dyn Exercise>) {
let grid = (100, 300);
let reference = market
.option(Call, 55.0, exercise, None, grid)
.npv()
.unwrap();
let mut dates = Vec::new();
let mut amounts = Vec::new();
for i in 0..=6 {
if i > 0 {
dates.push(today + i);
amounts.push(0.0);
}
let value = market
.option(Call, 55.0, exercise, Some((&dates, &amounts)), grid)
.npv()
.unwrap();
assert!(
(reference - value).abs() <= 1.0e-6,
"{i} null dividends: reference {reference} value {value}"
);
}
}
#[test]
fn european_null_dividends_leave_the_price_unchanged() {
let (market, today, expiry) = degenerate_fixture(0.052706);
check_fd_degenerate(&market, today, &european(expiry));
}
#[test]
fn american_null_dividends_leave_the_price_unchanged() {
let (market, today, expiry) = degenerate_fixture(0.052706);
check_fd_degenerate(&market, today, &american(today, expiry));
}
fn check_fd_dividend_at_t_zero(market: &Market, today: Date, exercise: &Shared<dyn Exercise>) {
let (dates, amounts) = ([today], [1.0]);
let mut option = market.option(Call, 55.0, exercise, Some((&dates, &amounts)), (50, 400));
let calculated = option.npv().unwrap();
assert!(option.theta().is_err());
let mut reference = OneAssetOption::new(
shared(PlainVanillaPayoff::new(Call, 55.0)),
european(exercise.last_date()),
Shared::clone(&market.settings),
);
let engine = AnalyticDividendEuropeanEngine::new(
Shared::clone(&market.process),
dividend_vector(&dates, &amounts).unwrap(),
);
reference
.base_mut()
.set_pricing_engine(shared_mut(engine) as SharedMut<dyn PricingEngine>);
let expected = reference.npv().unwrap();
assert!(
(calculated - expected).abs() <= 1.0e-4,
"calculated {calculated} expected {expected}"
);
}
#[test]
fn european_dividend_today_matches_the_analytic_engine() {
let (market, today, expiry) = degenerate_fixture(0.0);
check_fd_dividend_at_t_zero(&market, today, &european(expiry));
}
#[test]
fn american_dividend_today_matches_the_analytic_engine() {
let (market, today, expiry) = degenerate_fixture(0.0);
check_fd_dividend_at_t_zero(&market, today, &american(today, expiry));
}
}