use std::cell::RefCell;
use crate::errors::QlResult;
use crate::math::array::Array;
use crate::math::interpolations::Interpolation;
use crate::math::interpolations::cubic::{CubicInterpolation, MonotonicCubicNaturalSpline};
use crate::methods::finitedifferences::operators::FdmLinearOpComposite;
use crate::methods::finitedifferences::stepconditions::{
FdmSnapshotCondition, FdmStepConditionComposite,
};
use crate::shared::{Shared, SharedMut, shared};
use crate::types::{Real, Time};
use super::{FdmBackwardSolver, FdmSchemeDesc, FdmSolverDesc};
const ONE_DAY: Time = 1.0 / 365.0;
const CAPTURE_FRACTION: Real = 0.99;
pub struct Fdm1DimSolver {
solver_desc: FdmSolverDesc,
scheme_desc: FdmSchemeDesc,
op: SharedMut<dyn FdmLinearOpComposite>,
theta_condition: Shared<FdmSnapshotCondition>,
conditions: Shared<FdmStepConditionComposite>,
x: Vec<Real>,
initial_values: Vec<Real>,
interpolation: RefCell<Option<CubicInterpolation>>,
}
impl Fdm1DimSolver {
pub fn new(
solver_desc: FdmSolverDesc,
scheme_desc: FdmSchemeDesc,
op: SharedMut<dyn FdmLinearOpComposite>,
) -> Self {
let theta_condition = shared(FdmSnapshotCondition::new(theta_time(&solver_desc)));
let conditions =
FdmStepConditionComposite::join_conditions(&theta_condition, &solver_desc.condition);
let layout = Shared::clone(solver_desc.mesher.layout());
let mut x = vec![0.0; layout.size()];
let mut initial_values = vec![0.0; layout.size()];
for iter in layout.iter() {
initial_values[iter.index()] = solver_desc
.calculator
.avg_inner_value(&iter, solver_desc.maturity);
x[iter.index()] = solver_desc.mesher.location(&iter, 0);
}
Fdm1DimSolver {
solver_desc,
scheme_desc,
op,
theta_condition,
conditions,
x,
initial_values,
interpolation: RefCell::new(None),
}
}
pub fn interpolate_at(&self, x: Real) -> QlResult<Real> {
self.read_off(|spline| spline.value(x))
}
pub fn derivative_x(&self, x: Real) -> QlResult<Real> {
self.read_off(|spline| spline.derivative(x))
}
pub fn derivative_xx(&self, x: Real) -> QlResult<Real> {
self.read_off(|spline| spline.second_derivative(x))
}
pub fn theta_at(&self, x: Real) -> QlResult<Option<Real>> {
if self.conditions.stopping_times().first() == Some(&0.0) {
return Ok(None);
}
let value = self.interpolate_at(x)?;
let captured = self.theta_condition.values();
let capture_spline = MonotonicCubicNaturalSpline::new(self.x.clone(), captured.to_vec())?;
Ok(Some(
(capture_spline.value(x)? - value) / self.theta_condition.time(),
))
}
fn calculate(&self) -> QlResult<()> {
let built = self.interpolation.borrow().is_some();
if built {
return Ok(());
}
let mut rhs = Array::from(self.initial_values.clone());
FdmBackwardSolver::new(
self.op.clone(),
self.solver_desc.bc_set.clone(),
Some(Shared::clone(&self.conditions)),
self.scheme_desc,
)
.rollback(
&mut rhs,
self.solver_desc.maturity,
0.0,
self.solver_desc.time_steps,
self.solver_desc.damping_steps,
)?;
let spline = MonotonicCubicNaturalSpline::new(self.x.clone(), rhs.to_vec())?;
*self.interpolation.borrow_mut() = Some(spline);
Ok(())
}
fn read_off<T>(&self, read: impl FnOnce(&CubicInterpolation) -> QlResult<T>) -> QlResult<T> {
self.calculate()?;
let cached = self.interpolation.borrow();
let spline = cached
.as_ref()
.expect("calculate leaves the interpolation built");
read(spline)
}
}
fn theta_time(solver_desc: &FdmSolverDesc) -> Time {
let first_stopping_time = solver_desc
.condition
.stopping_times()
.first()
.copied()
.unwrap_or(solver_desc.maturity);
CAPTURE_FRACTION * ONE_DAY.min(first_stopping_time)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::instruments::CashOrNothingPayoff;
use crate::methods::finitedifferences::meshers::FdmMesher;
use crate::methods::finitedifferences::operators::FdmLinearOp;
use crate::methods::finitedifferences::schemes::testops;
use crate::methods::finitedifferences::utilities::{
FdmInnerValueCalculator, fdm_log_inner_value,
};
use crate::option::OptionType::Put;
use crate::payoff::Payoff;
use crate::shared::shared_mut;
use crate::types::Size;
const COEFFICIENT: Real = 0.4;
const MATURITY: Time = 0.75;
const STEPS: Size = 10;
const DAMPING_STEPS: Size = 2;
const STRIKE: Real = 100.0;
const CASH: Real = 10.0;
const CAPTURE_TIME: Time = 0.99 / 365.0;
const PROBE: Real = 4.5;
const INTERIOR: Size = 2;
struct CountingComposite {
set_times: Shared<RefCell<Size>>,
}
impl FdmLinearOp for CountingComposite {
fn apply(&self, r: &Array) -> Array {
COEFFICIENT * r
}
}
impl FdmLinearOpComposite for CountingComposite {
fn size(&self) -> Size {
1
}
fn set_time(&mut self, _t1: Time, _t2: Time) -> QlResult<()> {
*self.set_times.borrow_mut() += 1;
Ok(())
}
fn apply_mixed(&self, r: &Array) -> Array {
Array::with_size(r.size())
}
fn apply_direction(&self, _direction: Size, r: &Array) -> Array {
COEFFICIENT * r
}
fn solve_splitting(&self, _direction: Size, r: &Array, s: Real) -> QlResult<Array> {
Ok(r / (1.0 + s * COEFFICIENT))
}
fn preconditioner(&self, r: &Array, s: Real) -> QlResult<Array> {
self.solve_splitting(0, r, s)
}
}
fn calculator(mesher: &Shared<dyn FdmMesher>) -> Shared<dyn FdmInnerValueCalculator> {
let payoff = shared(CashOrNothingPayoff::new(Put, STRIKE, CASH)) as Shared<dyn Payoff>;
shared(fdm_log_inner_value(payoff, Shared::clone(mesher), 0))
}
fn empty_condition() -> Shared<FdmStepConditionComposite> {
shared(FdmStepConditionComposite::new(&[], Vec::new()))
}
fn desc(
mesher: &Shared<dyn FdmMesher>,
condition: Shared<FdmStepConditionComposite>,
maturity: Time,
) -> FdmSolverDesc {
FdmSolverDesc {
mesher: Shared::clone(mesher),
bc_set: Vec::new(),
condition,
calculator: calculator(mesher),
maturity,
time_steps: STEPS,
damping_steps: DAMPING_STEPS,
}
}
fn solver(
mesher: &Shared<dyn FdmMesher>,
condition: Shared<FdmStepConditionComposite>,
) -> Fdm1DimSolver {
Fdm1DimSolver::new(
desc(mesher, condition, MATURITY),
FdmSchemeDesc::douglas(),
testops::scaled_composite(&[COEFFICIENT]),
)
}
fn capture_time(condition: Shared<FdmStepConditionComposite>, maturity: Time) -> Time {
let mesher = testops::mesher();
let solver = Fdm1DimSolver::new(
desc(&mesher, condition, maturity),
FdmSchemeDesc::douglas(),
testops::scaled_composite(&[COEFFICIENT]),
);
solver.theta_condition.time()
}
fn rolled_by_hand(
mesher: &Shared<dyn FdmMesher>,
condition: &Shared<FdmStepConditionComposite>,
) -> CubicInterpolation {
let calculator = calculator(mesher);
let layout = Shared::clone(mesher.layout());
let mut x = vec![0.0; layout.size()];
let mut rhs = Array::with_size(layout.size());
for iter in layout.iter() {
rhs[iter.index()] = calculator.avg_inner_value(&iter, MATURITY);
x[iter.index()] = mesher.location(&iter, 0);
}
let snapshot = shared(FdmSnapshotCondition::new(CAPTURE_TIME));
let joined = FdmStepConditionComposite::join_conditions(&snapshot, condition);
FdmBackwardSolver::new(
testops::scaled_composite(&[COEFFICIENT]),
Vec::new(),
Some(joined),
FdmSchemeDesc::douglas(),
)
.rollback(&mut rhs, MATURITY, 0.0, STEPS, DAMPING_STEPS)
.unwrap();
MonotonicCubicNaturalSpline::new(x, rhs.to_vec()).unwrap()
}
#[test]
fn the_grid_is_seeded_with_the_cell_averaged_payoff_at_maturity() {
let mesher = testops::mesher();
let solver = solver(&mesher, empty_condition());
let calculator = calculator(&mesher);
let expected: Vec<Real> = mesher
.layout()
.iter()
.map(|iter| calculator.avg_inner_value(&iter, MATURITY))
.collect();
assert_eq!(solver.initial_values, expected);
}
#[test]
fn the_grid_coordinates_are_the_mesher_locations() {
let mesher = testops::mesher();
let solver = solver(&mesher, empty_condition());
assert_eq!(solver.x, mesher.locations(0).to_vec());
}
#[test]
fn the_read_offs_come_from_the_rolled_back_grid() {
let mesher = testops::mesher();
let condition = empty_condition();
let solver = solver(&mesher, Shared::clone(&condition));
let expected = rolled_by_hand(&mesher, &condition);
assert_eq!(
solver.interpolate_at(PROBE).unwrap(),
expected.value(PROBE).unwrap()
);
assert_eq!(
solver.derivative_x(PROBE).unwrap(),
expected.derivative(PROBE).unwrap()
);
assert_eq!(
solver.derivative_xx(PROBE).unwrap(),
expected.second_derivative(PROBE).unwrap()
);
}
#[test]
fn the_rolled_grid_is_not_the_seed() {
let mesher = testops::mesher();
let solver = solver(&mesher, empty_condition());
let rolled = solver.interpolate_at(solver.x[INTERIOR]).unwrap();
assert!(
(rolled - solver.initial_values[INTERIOR]).abs() > 1e-3,
"the rollback left the interior node at its seed {}: {rolled}",
solver.initial_values[INTERIOR]
);
}
#[test]
fn the_capture_sits_just_inside_the_day_before_today() {
assert_eq!(capture_time(empty_condition(), MATURITY), CAPTURE_TIME);
}
#[test]
fn a_maturity_inside_that_day_pulls_the_capture_in() {
assert_eq!(capture_time(empty_condition(), 0.001), 0.99 * 0.001);
}
#[test]
fn the_first_stopping_time_pulls_the_capture_in() {
let condition = shared(FdmStepConditionComposite::new(
&[vec![0.5, 0.002]],
Vec::new(),
));
assert_eq!(capture_time(condition, MATURITY), 0.99 * 0.002);
}
#[test]
fn the_theta_is_the_captured_grid_less_the_result_over_the_capture_time() {
let mesher = testops::mesher();
let solver = solver(&mesher, empty_condition());
let theta = solver
.theta_at(PROBE)
.unwrap()
.expect("a capture away from today has a theta");
let captured = solver.theta_condition.values();
assert!(
!captured.is_empty(),
"the capture did not fire through the joined conditions"
);
let capture_spline =
MonotonicCubicNaturalSpline::new(solver.x.clone(), captured.to_vec()).unwrap();
let expected = (capture_spline.value(PROBE).unwrap()
- solver.interpolate_at(PROBE).unwrap())
/ CAPTURE_TIME;
assert_eq!(theta, expected);
}
#[test]
fn a_stopping_time_on_today_has_no_theta() {
let mesher = testops::mesher();
let condition = shared(FdmStepConditionComposite::new(
&[vec![0.0, 0.5]],
Vec::new(),
));
let solver = solver(&mesher, condition);
assert_eq!(solver.theta_condition.time(), 0.0);
assert_eq!(solver.theta_at(PROBE).unwrap(), None);
}
#[test]
fn the_rollback_runs_once_however_many_reads_follow() {
let mesher = testops::mesher();
let set_times = shared(RefCell::new(0));
let op = shared_mut(CountingComposite {
set_times: Shared::clone(&set_times),
});
let solver = Fdm1DimSolver::new(
desc(&mesher, empty_condition(), MATURITY),
FdmSchemeDesc::douglas(),
op,
);
solver.interpolate_at(PROBE).unwrap();
let after_first_read = *set_times.borrow();
solver.derivative_x(PROBE).unwrap();
solver.derivative_xx(PROBE).unwrap();
solver.theta_at(PROBE).unwrap();
assert!(after_first_read >= STEPS + DAMPING_STEPS);
assert_eq!(*set_times.borrow(), after_first_read);
}
}