use std::cell::{Cell, RefCell};
use crate::errors::QlResult;
use crate::handle::Handle;
use crate::math::array::Array;
use crate::math::generallinearleastsquares::GeneralLinearLeastSquares;
use crate::math::statistics::{IncrementalStatistics, MeanStdDev, Statistics};
use crate::math::timegrid::TimeGrid;
use crate::methods::montecarlo::{EarlyExercisePathPricer, Path, PathPricer};
use crate::require;
use crate::shared::Shared;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::types::{DiscountFactor, Real, Size};
pub struct LongstaffSchwartzPathPricer {
path_pricer: Shared<dyn EarlyExercisePathPricer<Path, State = Real>>,
v: Vec<Box<dyn Fn(Real) -> Real>>,
df: Vec<DiscountFactor>,
len: Size,
calibration_phase: Cell<bool>,
coeff: RefCell<Vec<Array>>,
paths: RefCell<Vec<Path>>,
exercise_probability: RefCell<IncrementalStatistics>,
}
impl LongstaffSchwartzPathPricer {
pub fn new(
times: &TimeGrid,
path_pricer: Shared<dyn EarlyExercisePathPricer<Path, State = Real>>,
term_structure: &Handle<dyn YieldTermStructure>,
) -> QlResult<Self> {
let len = times.size();
require!(len >= 2, "at least two exercise times required");
let curve = term_structure.current_link()?;
let t = times.times();
let mut df = Vec::with_capacity(len - 1);
for i in 0..len - 1 {
df.push(curve.discount(t[i + 1], false)? / curve.discount(t[i], false)?);
}
Ok(LongstaffSchwartzPathPricer {
v: path_pricer.basis_system(),
path_pricer,
df,
len,
calibration_phase: Cell::new(true),
coeff: RefCell::new(vec![Array::new(); len - 2]),
paths: RefCell::new(Vec::new()),
exercise_probability: RefCell::new(IncrementalStatistics::new()),
})
}
pub fn calibrate(&self) -> QlResult<()> {
let paths = std::mem::take(&mut *self.paths.borrow_mut());
let mut prices: Vec<Real> = paths
.iter()
.map(|path| self.path_pricer.value(path, self.len - 1))
.collect();
for i in (1..self.len - 1).rev() {
let mut itm: Vec<(Size, Real, Real)> = Vec::new();
let mut x: Vec<Real> = Vec::new();
let mut y: Vec<Real> = Vec::new();
for (j, path) in paths.iter().enumerate() {
let exercise = self.path_pricer.value(path, i);
if exercise > 0.0 {
let state = self.path_pricer.state(path, i);
itm.push((j, state, exercise));
x.push(state);
y.push(self.df[i] * prices[j]);
}
}
let fit = if self.v.len() <= x.len() {
GeneralLinearLeastSquares::new(&x, &y, &self.v)?
.coefficients()
.clone()
} else {
Array::with_size(self.v.len())
};
for price in prices.iter_mut() {
*price *= self.df[i];
}
for (j, state, exercise) in itm {
if self.continuation(&fit, state) < exercise {
prices[j] = exercise;
}
}
self.coeff.borrow_mut()[i - 1] = fit;
}
self.calibration_phase.set(false);
Ok(())
}
pub fn exercise_probability(&self) -> QlResult<Real> {
self.exercise_probability.borrow().mean()
}
fn continuation(&self, coeff: &Array, state: Real) -> Real {
(0..self.v.len()).map(|l| coeff[l] * self.v[l](state)).sum()
}
fn coefficients(&self, i: Size) -> Array {
self.coeff.borrow()[i - 1].clone()
}
}
impl PathPricer<Path> for LongstaffSchwartzPathPricer {
fn price(&self, path: &Path) -> Real {
if self.calibration_phase.get() {
self.paths.borrow_mut().push(path.clone());
return 0.0;
}
let mut price = self.path_pricer.value(path, self.len - 1);
let mut exercised = price > 0.0;
for i in (1..self.len - 1).rev() {
price *= self.df[i];
let exercise = self.path_pricer.value(path, i);
if exercise > 0.0 {
let state = self.path_pricer.state(path, i);
if self.continuation(&self.coefficients(i), state) < exercise {
price = exercise;
exercised = true;
}
}
}
self.exercise_probability
.borrow_mut()
.add(if exercised { 1.0 } else { 0.0 })
.expect("a unit-weighted 0/1 indicator is a valid sample");
price * self.df[0]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::interestrate::Compounding;
use crate::methods::montecarlo::{LsmBasisSystem, PolynomialType};
use crate::shared::shared;
use crate::termstructures::yields::FlatForward;
use crate::time::date::{Date, Month};
use crate::time::daycounters::actual360::Actual360;
use crate::time::frequency::Frequency;
use crate::types::Time;
const STRIKE: Real = 100.0;
const TOL: Real = 1e-10;
struct AmericanPut;
impl EarlyExercisePathPricer<Path> for AmericanPut {
type State = Real;
fn value(&self, path: &Path, t: Size) -> Real {
(STRIKE - path[t]).max(0.0)
}
fn state(&self, path: &Path, t: Size) -> Real {
path[t]
}
fn basis_system(&self) -> Vec<Box<dyn Fn(Real) -> Real>> {
LsmBasisSystem::path_basis_system(1, PolynomialType::Monomial)
}
}
fn grid() -> TimeGrid {
TimeGrid::new(3.0, 3).unwrap()
}
fn path(spots: [Real; 4]) -> Path {
Path::new(grid(), Array::from(spots)).unwrap()
}
fn half_step_curve() -> Handle<dyn YieldTermStructure> {
Handle::new(shared(FlatForward::with_rate(
Date::new(15, Month::June, 2026),
(2.0 as Time).ln(),
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>)
}
fn pricer() -> LongstaffSchwartzPathPricer {
LongstaffSchwartzPathPricer::new(&grid(), shared(AmericanPut), &half_step_curve()).unwrap()
}
fn calibration_paths() -> [Path; 3] {
[
path([100.0, 90.0, 80.0, 96.0]),
path([100.0, 98.0, 90.0, 108.0]),
path([100.0, 94.0, 110.0, 76.0]),
]
}
fn calibrated() -> LongstaffSchwartzPathPricer {
let lsm = pricer();
for p in calibration_paths() {
lsm.price(&p);
}
lsm.calibrate().unwrap();
lsm
}
#[test]
fn the_calibration_phase_buffers_and_reports_zero() {
let lsm = pricer();
for (n, p) in calibration_paths().into_iter().enumerate() {
assert_eq!(lsm.price(&p), 0.0);
assert_eq!(lsm.paths.borrow().len(), n + 1);
}
assert!(lsm.calibration_phase.get());
assert!(lsm.exercise_probability().is_err(), "nothing priced yet");
}
#[test]
fn calibrate_reproduces_the_hand_computed_coefficients() {
let lsm = calibrated();
let late = lsm.coefficients(2);
let early = lsm.coefficients(1);
assert!((late[0] - 18.0).abs() < TOL, "got {}", late[0]);
assert!((late[1] + 0.2).abs() < TOL, "got {}", late[1]);
assert!((early[0] - 65.75).abs() < TOL, "got {}", early[0]);
assert!((early[1] + 0.625).abs() < TOL, "got {}", early[1]);
assert!(!lsm.calibration_phase.get());
assert!(lsm.paths.borrow().is_empty(), "the buffer is released");
}
#[test]
fn the_pricing_phase_returns_the_hand_computed_prices() {
let lsm = calibrated();
let [p1, p2, p3] = calibration_paths();
assert!((lsm.price(&p1) - 5.0).abs() < TOL);
assert!((lsm.price(&p2) - 2.5).abs() < TOL);
assert!((lsm.price(&p3) - 3.0).abs() < TOL);
}
#[test]
fn the_exercise_probability_counts_a_terminal_exercise() {
let lsm = calibrated();
for p in calibration_paths() {
lsm.price(&p);
}
lsm.price(&path([100.0, 130.0, 140.0, 150.0]));
assert!((lsm.exercise_probability().unwrap() - 0.75).abs() < TOL);
}
#[test]
fn a_continuation_equal_to_the_exercise_value_holds() {
let lsm = pricer();
lsm.calibrate().unwrap();
lsm.coeff.borrow_mut()[0] = Array::from([0.0, 1.0]);
assert!((lsm.price(&path([100.0, 50.0, 110.0, 60.0])) - 5.0).abs() < TOL);
}
#[test]
fn too_few_itm_paths_zero_the_coefficients_and_force_exercise() {
let lsm = pricer();
lsm.price(&path([100.0, 90.0, 80.0, 96.0]));
lsm.calibrate().unwrap();
assert_eq!(lsm.coefficients(1), Array::with_size(2));
assert_eq!(lsm.coefficients(2), Array::with_size(2));
assert!((lsm.price(&path([100.0, 98.0, 90.0, 108.0])) - 1.0).abs() < TOL);
}
}