use crate::instrument::Instrument;
use crate::instruments::CashOrNothingPayoff;
use crate::math::array::Array;
use crate::math::interpolations::Interpolation;
use crate::math::interpolations::cubic::MonotonicCubicNaturalSpline;
use crate::methods::finitedifferences::meshers::{
FdmMesher, FdmMesherComposite, fdm_black_scholes_mesher,
};
use crate::methods::finitedifferences::operators::{
FdmBlackScholesOp, FdmLinearOpComposite, FdmLinearOpLayout,
};
use crate::methods::finitedifferences::solvers::{FdmBackwardSolver, FdmSchemeDesc};
use crate::methods::finitedifferences::utilities::{FdmInnerValueCalculator, fdm_log_inner_value};
use crate::option::OptionType::Put;
use crate::pricingengines::vanilla::test_market::{Market, market, time_to_days, today};
use crate::shared::{Shared, SharedMut, shared, shared_mut};
use crate::stochasticprocess::StochasticProcess1D;
use crate::time::date::Date;
use crate::types::{Rate, Real, Size, Time, Volatility};
const SPOT: Real = 100.0;
const STRIKE: Real = 100.0;
const CASH: Real = 10.0;
const RATE: Rate = 0.06;
const VOL: Volatility = 0.35;
const MATURITY: Time = 0.75;
const X_GRID: Size = 400;
const CS_STEPS: Size = 25;
const DAMPING_STEPS: Size = 3;
const REL_TOL: Real = 2e-3;
fn fixture() -> Market {
let market = market();
market.set(SPOT, RATE, RATE, VOL);
market
}
fn expiry() -> Date {
today() + time_to_days(MATURITY)
}
fn payoff() -> Shared<CashOrNothingPayoff> {
shared(CashOrNothingPayoff::new(Put, STRIKE, CASH))
}
fn analytic(market: &Market) -> (Real, Real) {
let mut option = market.option_with_payoff(payoff(), expiry());
(option.npv().unwrap(), option.gamma().unwrap())
}
fn mesher(market: &Market) -> Shared<FdmMesherComposite> {
let equity = fdm_black_scholes_mesher(
X_GRID,
&market.process,
MATURITY,
STRIKE,
None,
None,
0.0001,
1.5,
Some((STRIKE, 0.01)),
&[],
0.0,
)
.unwrap();
shared(FdmMesherComposite::new(vec![equity]))
}
fn seed(mesher: &Shared<FdmMesherComposite>) -> (Vec<Real>, Array) {
let calculator =
fdm_log_inner_value(payoff(), Shared::clone(mesher) as Shared<dyn FdmMesher>, 0);
let layout: &Shared<FdmLinearOpLayout> = mesher.layout();
let mut rhs = Array::with_size(layout.size());
let mut x = vec![0.0; layout.size()];
for iter in layout.iter() {
rhs[iter.index()] = calculator.avg_inner_value(&iter, MATURITY);
x[iter.index()] = mesher.location(&iter, 0);
}
(x, rhs)
}
fn rollback(
market: &Market,
mesher: &Shared<FdmMesherComposite>,
rhs: &mut Array,
damping_steps: Size,
) {
let map = shared_mut(
FdmBlackScholesOp::new(
Shared::clone(mesher) as Shared<dyn FdmMesher>,
&market.process,
STRIKE,
0,
)
.unwrap(),
);
let mut solver = FdmBackwardSolver::new(
map as SharedMut<dyn FdmLinearOpComposite>,
Vec::new(),
None,
FdmSchemeDesc::douglas(),
);
solver
.rollback(rhs, MATURITY, 0.0, CS_STEPS, damping_steps)
.unwrap();
}
fn read_off(x: Vec<Real>, rhs: &Array) -> (Real, Real) {
let spline = MonotonicCubicNaturalSpline::new(x, rhs.to_vec()).unwrap();
let log_spot = SPOT.ln();
let pv = spline.value(log_spot).unwrap();
let gamma = (spline.second_derivative(log_spot).unwrap()
- spline.derivative(log_spot).unwrap())
/ (SPOT * SPOT);
(pv, gamma)
}
fn finite_difference(market: &Market, damping_steps: Size) -> (Real, Real) {
let mesher = mesher(market);
let (x, mut rhs) = seed(&mesher);
rollback(market, &mesher, &mut rhs, damping_steps);
read_off(x, &rhs)
}
fn relative_error(calculated: Real, expected: Real) -> Real {
(calculated - expected).abs() / expected.abs()
}
#[test]
fn crank_nicolson_with_damping_prices_the_digital_put() {
let market = fixture();
let (expected_pv, expected_gamma) = analytic(&market);
let (calculated_pv, calculated_gamma) = finite_difference(&market, DAMPING_STEPS);
assert!(
expected_gamma > 0.0,
"the analytic gamma must be positive for a relative band to mean anything: \
{expected_gamma}"
);
assert!(
relative_error(calculated_pv, expected_pv) <= REL_TOL,
"PV of the digital option: expected {expected_pv}, calculated {calculated_pv}, \
rel. error {} against {REL_TOL}",
relative_error(calculated_pv, expected_pv)
);
assert!(
relative_error(calculated_gamma, expected_gamma) <= REL_TOL,
"gamma of the digital option: expected {expected_gamma}, calculated \
{calculated_gamma}, rel. error {} against {REL_TOL}",
relative_error(calculated_gamma, expected_gamma)
);
}
#[test]
fn the_expiry_is_exactly_the_maturity_the_grid_is_built_for() {
let market = fixture();
assert_eq!(expiry() - today(), 270);
assert_eq!(market.process.time(&expiry()).unwrap(), MATURITY);
}
#[test]
fn the_critical_point_makes_the_grid_non_uniform() {
let market = fixture();
let mesher = mesher(&market);
let locations = mesher.fdm_1d_meshers()[0].locations();
let gaps: Vec<Real> = locations.windows(2).map(|pair| pair[1] - pair[0]).collect();
let at_strike = gaps
.iter()
.enumerate()
.min_by(|a, b| a.1.total_cmp(b.1))
.map(|(i, _)| i)
.unwrap();
let narrowest = gaps[at_strike];
let widest = gaps.iter().copied().fold(Real::MIN, Real::max);
assert!(
widest > 10.0 * narrowest,
"the grid is near-uniform: gaps run {narrowest} to {widest}"
);
assert!(
(locations[at_strike] - STRIKE.ln()).abs() < 0.05,
"the narrowest gap is at {} rather than at ln(strike) {}",
locations[at_strike],
STRIKE.ln()
);
}
#[test]
fn the_seed_averages_across_the_strike_cell() {
let market = fixture();
let mesher = mesher(&market);
let (x, rhs) = seed(&mesher);
let partial: Vec<Size> = (0..rhs.size())
.filter(|&i| rhs[i] > 0.0 && rhs[i] < CASH)
.collect();
assert_eq!(
partial.len(),
1,
"expected exactly one averaged cell, found {partial:?}"
);
let straddling = partial[0];
assert!(
x[straddling] < STRIKE.ln() && STRIKE.ln() < x[straddling + 1],
"the averaged cell at {} does not straddle ln(strike) {}",
x[straddling],
STRIKE.ln()
);
assert_eq!(rhs[straddling - 1], CASH);
assert_eq!(rhs[straddling + 1], 0.0);
}
#[test]
fn dropping_the_damping_steps_breaks_the_gamma() {
let market = fixture();
let (_, expected_gamma) = analytic(&market);
let (_, damped) = finite_difference(&market, DAMPING_STEPS);
let (_, undamped) = finite_difference(&market, 0);
assert!(
relative_error(undamped, expected_gamma) > REL_TOL,
"the undamped roll still meets the oracle: {undamped} against {expected_gamma}"
);
assert!(
relative_error(undamped, expected_gamma) > relative_error(damped, expected_gamma),
"the undamped roll is no worse than the damped one: {undamped} vs {damped}"
);
}