use crate::errors::QlResult;
use crate::math::statistics::{GeneralStatistics, Statistics};
use crate::methods::montecarlo::PathGen;
use crate::types::{Real, Size};
pub trait PathPricer<P> {
fn price(&self, path: &P) -> Real;
}
impl<P, F: Fn(&P) -> Real> PathPricer<P> for F {
fn price(&self, path: &P) -> Real {
self(path)
}
}
pub struct MonteCarloModel<PG, P, S = GeneralStatistics> {
path_generator: PG,
path_pricer: P,
sample_accumulator: S,
antithetic_variate: bool,
}
impl<PG, P, S> MonteCarloModel<PG, P, S>
where
PG: PathGen,
P: PathPricer<PG::PathType>,
S: Statistics,
{
pub fn new(
path_generator: PG,
path_pricer: P,
sample_accumulator: S,
antithetic_variate: bool,
) -> QlResult<Self> {
Ok(MonteCarloModel {
path_generator,
path_pricer,
sample_accumulator,
antithetic_variate,
})
}
pub fn add_samples(&mut self, samples: Size) -> QlResult<()> {
for _ in 0..samples {
let sample = self.path_generator.next()?;
let price = self.path_pricer.price(&sample.value);
if self.antithetic_variate {
let antithetic = self.path_generator.antithetic()?;
let price2 = self.path_pricer.price(&antithetic.value);
self.sample_accumulator
.add_weighted((price + price2) / 2.0, sample.weight)?;
} else {
self.sample_accumulator.add_weighted(price, sample.weight)?;
}
}
Ok(())
}
pub fn sample_accumulator(&self) -> &S {
&self.sample_accumulator
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handle::{Handle, RelinkableHandle};
use crate::interestrate::Compounding;
use crate::math::array::Array;
use crate::math::matrix::Matrix;
use crate::math::randomnumbers::rngtraits::{McRngTraits, PseudoRandom};
use crate::math::statistics::MeanStdDev;
use crate::math::timegrid::TimeGrid;
use crate::methods::montecarlo::{MultiPath, MultiPathGenerator, Path, PathGenerator, Sample};
use crate::patterns::observable::{AsObservable, Observable};
use crate::processes::{BlackScholesMertonProcess, StochasticProcessArray};
use crate::quotes::make_quote_handle;
use crate::shared::{Shared, shared};
use crate::stochasticprocess::{StochasticProcess, StochasticProcess1D};
use crate::termstructures::volatility::{BlackConstantVol, BlackVolTermStructure};
use crate::termstructures::yields::FlatForward;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::calendars::target::Target;
use crate::time::date::{Date, Month};
use crate::time::daycounters::actual360::Actual360;
use crate::time::frequency::Frequency;
use crate::types::{Rate, Time, Volatility};
const SPOT: Real = 100.0;
const R: Rate = 0.05;
const Q: Rate = 0.02;
const VOL: Volatility = 0.20;
fn reference() -> Date {
Date::new(15, Month::June, 2026)
}
fn flat_yield(rate: Rate) -> Handle<dyn YieldTermStructure> {
Handle::new(shared(FlatForward::with_rate(
reference(),
rate,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>)
}
fn gbs_process() -> Shared<dyn StochasticProcess1D> {
let spot = make_quote_handle(SPOT);
let vol = RelinkableHandle::new(shared(BlackConstantVol::new(
reference(),
Some(Target::new()),
VOL,
Actual360::new(),
)) as Shared<dyn BlackVolTermStructure>);
shared(BlackScholesMertonProcess::new(
spot.handle(),
flat_yield(Q),
flat_yield(R),
vol.handle(),
)) as Shared<dyn StochasticProcess1D>
}
fn gbs_1d(spot: Real, r: Rate, q: Rate, sigma: Volatility) -> Shared<dyn StochasticProcess1D> {
let quote = make_quote_handle(spot);
let vol = RelinkableHandle::new(shared(BlackConstantVol::new(
reference(),
Some(Target::new()),
sigma,
Actual360::new(),
)) as Shared<dyn BlackVolTermStructure>);
shared(BlackScholesMertonProcess::new(
quote.handle(),
flat_yield(q),
flat_yield(r),
vol.handle(),
)) as Shared<dyn StochasticProcess1D>
}
fn model<P: PathPricer<Path>>(
pricer: P,
steps: Size,
seed: u32,
) -> MonteCarloModel<PathGenerator<<PseudoRandom as McRngTraits>::RsgType>, P> {
let generator = PseudoRandom::make_sequence_generator(steps, seed).unwrap();
let pg = PathGenerator::new(gbs_process(), 1.0, steps, generator, false).unwrap();
MonteCarloModel::new(pg, pricer, GeneralStatistics::new(), false).unwrap()
}
struct WeightStub {
forward: Sample<Real>,
anti: Sample<Real>,
}
impl PathGen for WeightStub {
type PathType = Real;
fn next(&mut self) -> QlResult<Sample<Real>> {
Ok(self.forward)
}
fn antithetic(&mut self) -> QlResult<Sample<Real>> {
Ok(self.anti)
}
fn dimension(&self) -> Size {
1
}
}
#[test]
fn constant_pricer_reproduces_its_value_and_count() {
const K: Real = 3.5;
const N: Size = 128;
let mut m = model(|_: &Path| K, 12, 42);
m.add_samples(N).unwrap();
assert_eq!(m.sample_accumulator().mean().unwrap(), K);
assert_eq!(m.sample_accumulator().samples(), N);
}
#[test]
fn error_estimate_shrinks_as_inverse_sqrt_n() {
const N: Size = 4_000;
const STEPS: Size = 4;
let terminal = |path: &Path| path.back();
let mut m_n = model(terminal, STEPS, 42);
m_n.add_samples(N).unwrap();
let se_n = m_n.sample_accumulator().error_estimate().unwrap();
let mut m_4n = model(terminal, STEPS, 42);
m_4n.add_samples(4 * N).unwrap();
let se_4n = m_4n.sample_accumulator().error_estimate().unwrap();
let ratio = se_4n / se_n;
assert!(
(0.4..=0.6).contains(&ratio),
"se(4N)/se(N) = {ratio}, expected ~0.5 for 1/sqrt(N) scaling"
);
}
#[test]
fn antithetic_averages_pair_under_the_forward_weight() {
const N: Size = 16;
const P1: Real = 4.0;
const P2: Real = 10.0;
const W1: Real = 2.0;
const W2: Real = 3.0;
let stub = WeightStub {
forward: Sample::new(P1, W1),
anti: Sample::new(P2, W2),
};
let mut m =
MonteCarloModel::new(stub, |x: &Real| *x, GeneralStatistics::new(), true).unwrap();
m.add_samples(N).unwrap();
let acc = m.sample_accumulator();
assert_eq!(acc.samples(), N, "n pairs must count as n samples, not 2n");
assert_eq!(
acc.mean().unwrap(),
(P1 + P2) / 2.0,
"must average the pair"
);
assert_eq!(
acc.weight_sum(),
N as Real * W1,
"must accumulate the forward weight, not the antithetic weight"
);
}
#[test]
fn multipath_model_reproduces_the_forward_mean() {
const N: Size = 40_000;
const STEPS: Size = 4;
const T: Time = 1.0;
const S0: Real = 100.0;
const RR: Rate = 0.05;
const QQ: Rate = 0.02;
const SIG: Volatility = 0.20;
let array = StochasticProcessArray::new(
vec![gbs_1d(S0, RR, QQ, SIG), gbs_1d(S0, RR, QQ, SIG)],
&Matrix::from([[1.0, 0.0], [0.0, 1.0]]),
)
.unwrap();
let process = shared(array) as Shared<dyn StochasticProcess>;
let grid = TimeGrid::new(T, STEPS).unwrap();
let generator = PseudoRandom::make_sequence_generator(2 * STEPS, 42).unwrap();
let mpg = MultiPathGenerator::new(process, grid, generator, false).unwrap();
let mut m = MonteCarloModel::new(
mpg,
|mp: &MultiPath| mp[0].back(),
GeneralStatistics::new(),
false,
)
.unwrap();
m.add_samples(N).unwrap();
let mean = m.sample_accumulator().mean().unwrap();
let target = S0 * ((RR - QQ) * T).exp();
let var = S0 * S0 * (2.0 * (RR - QQ) * T).exp() * ((SIG * SIG * T).exp() - 1.0);
let se = (var / N as Real).sqrt();
assert!(
(mean - target).abs() < 5.0 * se,
"E[S0(T)] {mean} vs {target}: {:.2} se",
(mean - target).abs() / se
);
}
struct ArithmeticProcess {
a: Real,
b: Real,
observable: Shared<Observable>,
}
impl ArithmeticProcess {
fn new(a: Real, b: Real) -> Self {
ArithmeticProcess {
a,
b,
observable: shared(Observable::new()),
}
}
}
impl AsObservable for ArithmeticProcess {
fn observable(&self) -> &Observable {
&self.observable
}
}
impl StochasticProcess for ArithmeticProcess {
fn size(&self) -> Size {
2
}
fn factors(&self) -> Size {
2
}
fn initial_values(&self) -> QlResult<Array> {
Ok(Array::with_size(2))
}
fn drift(&self, _t: Time, _x: &Array) -> QlResult<Array> {
Ok(Array::from([self.a, self.a]))
}
fn diffusion(&self, _t: Time, _x: &Array) -> QlResult<Matrix> {
Ok(Matrix::from([[self.b, 0.0], [0.0, self.b]]))
}
fn evolve(&self, _t0: Time, x0: &Array, dt: Time, dw: &Array) -> QlResult<Array> {
let mut out = x0.clone();
for i in 0..2 {
out[i] = x0[i] + self.a * dt + self.b * dw[i];
}
Ok(out)
}
}
#[test]
fn antithetic_collapses_a_linear_pricer_variance() {
const N: Size = 2_000;
const STEPS: Size = 4;
const T: Time = 1.0;
let build = || {
let process = shared(ArithmeticProcess::new(0.3, 1.0)) as Shared<dyn StochasticProcess>;
let grid = TimeGrid::new(T, STEPS).unwrap();
let generator = PseudoRandom::make_sequence_generator(2 * STEPS, 7).unwrap();
MultiPathGenerator::new(process, grid, generator, false).unwrap()
};
let mut anti = MonteCarloModel::new(
build(),
|mp: &MultiPath| mp[0].back(),
GeneralStatistics::new(),
true,
)
.unwrap();
anti.add_samples(N).unwrap();
let se_anti = anti.sample_accumulator().error_estimate().unwrap();
let mut plain = MonteCarloModel::new(
build(),
|mp: &MultiPath| mp[0].back(),
GeneralStatistics::new(),
false,
)
.unwrap();
plain.add_samples(N).unwrap();
let se_plain = plain.sample_accumulator().error_estimate().unwrap();
assert!(
se_anti < 1e-9,
"antithetic linear estimator variance must collapse: se={se_anti}"
);
assert!(
se_plain > 0.01,
"non-antithetic run must retain material variance: se={se_plain}"
);
}
#[test]
fn a_non_finite_price_aborts_accumulation() {
let mut m = model(|_: &Path| Real::NAN, 4, 42);
assert!(m.add_samples(1).is_err());
}
}