solver/models/
american_put.rs1use super::control::{ControlProblem, StateDerivatives};
2use crate::numeric::finite_difference::discretization::{DimensionKind, Transport};
3use crate::numeric::finite_difference::pde::PdeProblem;
4
5pub struct AmericanPut {
13 pub risk_free_rate: f64,
15 pub volatility: f64,
17 pub strike: f64,
19 pub dx: f64,
21}
22
23impl ControlProblem<1> for AmericanPut {
24 type Control = ();
25
26 fn optimize(&self, _t: f64, _state: &[f64; 1], _derivs: &StateDerivatives<1>) {}
27
28 fn running_reward(&self, _t: f64, _state: &[f64; 1], _control: &()) -> f64 {
29 0.0
32 }
33
34 fn generator(
35 &self,
36 _t: f64,
37 state: &[f64; 1],
38 _control: &(),
39 derivs: &StateDerivatives<1>,
40 ) -> f64 {
41 let s = state[0];
43 self.risk_free_rate * s * derivs.grad[0]
44 + 0.5 * self.volatility.powi(2) * s.powi(2) * derivs.hessian[0]
45 }
46
47 fn terminal(&self, state: &[f64; 1]) -> f64 {
48 let s = state[0];
49 (self.strike - s).max(0.0)
50 }
51
52 fn discount_rate(&self, _state: &[f64; 1]) -> f64 {
53 self.risk_free_rate
54 }
55
56 fn constant_discount_rate(&self) -> Option<f64> {
57 Some(self.risk_free_rate)
58 }
59
60 fn apply_constraint(&self, state: &[f64; 1], value: f64) -> f64 {
61 let s = state[0];
62 let intrinsic = (self.strike - s).max(0.0);
63 value.max(intrinsic)
64 }
65
66 fn next_step(&self, _t: f64, state: &[f64; 1], dt: f64, noise: &[f64; 1]) -> [f64; 1] {
67 let drift = (self.risk_free_rate - 0.5 * self.volatility.powi(2)) * dt;
69 let diffusion = self.volatility * dt.sqrt() * noise[0];
70 [state[0] * (drift + diffusion).exp()]
71 }
72
73 fn is_diffusion_dimension(&self, _dim: usize) -> bool {
74 true
75 }
76}
77
78impl PdeProblem<1> for AmericanPut {
79 fn dimension_kind(&self, _dim: usize) -> DimensionKind {
80 DimensionKind::Diffusion
81 }
82
83 fn transport(
84 &self,
85 _t: f64,
86 state: &[f64; 1],
87 _control: &(),
88 _derivs: &StateDerivatives<1>,
89 ) -> Transport<1> {
90 let s = state[0];
95 let h = self.dx.max(1e-12);
96 let b = self.risk_free_rate * s;
97 let d = 0.5 * self.volatility.powi(2) * s.powi(2);
98 Transport::new(
99 [d / (h * h) + b.max(0.0) / h],
100 [d / (h * h) + (-b).max(0.0) / h],
101 0.0,
102 )
103 }
104}
105
106impl AmericanPut {
107 pub fn new(risk_free_rate: f64, volatility: f64, strike: f64, dx: f64) -> Self {
117 Self {
118 risk_free_rate,
119 volatility,
120 strike,
121 dx,
122 }
123 }
124
125 pub fn set_dx(&mut self, dx: f64) {
137 self.dx = dx;
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use crate::models::control::ControlProblem;
145
146 fn default_model() -> AmericanPut {
147 AmericanPut::new(0.05, 0.2, 100.0, 1.0)
148 }
149
150 #[test]
151 fn terminal_is_payoff() {
152 let m = default_model();
153 assert_eq!(ControlProblem::terminal(&m, &[80.0]), 20.0);
154 assert_eq!(ControlProblem::terminal(&m, &[100.0]), 0.0);
155 assert_eq!(ControlProblem::terminal(&m, &[120.0]), 0.0);
156 }
157
158 #[test]
159 fn constraint_enforces_early_exercise() {
160 let m = default_model();
161 assert_eq!(ControlProblem::apply_constraint(&m, &[80.0], 15.0), 20.0);
162 assert_eq!(ControlProblem::apply_constraint(&m, &[80.0], 25.0), 25.0);
163 }
164
165 #[test]
166 fn discount_rate_is_risk_free() {
167 let m = default_model();
168 assert_eq!(ControlProblem::discount_rate(&m, &[100.0]), 0.05);
169 assert_eq!(ControlProblem::constant_discount_rate(&m), Some(0.05));
170 }
171
172 #[test]
173 fn control_problem_constraint_matches_model() {
174 let m = default_model();
175 let v = ControlProblem::apply_constraint(&m, &[80.0], 15.0);
176 assert_eq!(v, 20.0);
177 }
178
179 #[test]
180 fn control_problem_generator_is_black_scholes_operator() {
181 let m = default_model();
182 let d = StateDerivatives::new([1.0], [0.0]);
183 let g = ControlProblem::generator(&m, 0.0, &[100.0], &(), &d);
184 assert!((g - m.risk_free_rate * 100.0).abs() < 1e-12);
186 }
187
188 #[test]
189 fn control_problem_next_step_is_log_euler() {
190 let m = default_model();
191 let s = ControlProblem::next_step(&m, 0.0, &[100.0], 0.0, &[0.0]);
192 assert!((s[0] - 100.0).abs() < 1e-12);
193 }
194
195 #[test]
196 fn dimension_kind_is_diffusion() {
197 let m = default_model();
198 assert_eq!(PdeProblem::dimension_kind(&m, 0), DimensionKind::Diffusion);
199 }
200
201 #[test]
202 fn transport_is_black_scholes_upwind_stencil() {
203 let m = default_model();
204 let s: f64 = 100.0;
205 let d = 0.5 * m.volatility.powi(2) * s.powi(2);
206 let h = m.dx;
207 let t = PdeProblem::transport(&m, 0.0, &[s], &(), &StateDerivatives::new([0.0], [0.0]));
208 assert!((t.plus[0] - (d / (h * h) + m.risk_free_rate * s / h)).abs() < 1e-12);
209 assert!((t.minus[0] - d / (h * h)).abs() < 1e-12);
210 assert_eq!(t.source, 0.0);
211 }
212}