1use crate::models::control::{ControlProblem, StateDerivatives};
46use crate::numeric::finite_difference::discretization::{DimensionKind, Transport};
47use crate::numeric::finite_difference::pde::PdeProblem;
48
49#[derive(Clone, Copy, Debug)]
53pub struct Merton {
54 pub risk_free_rate: f64,
56 pub mu: f64,
58 pub sigma: f64,
60}
61
62impl Merton {
63 pub fn new(risk_free_rate: f64, mu: f64, sigma: f64) -> Self {
77 assert!(sigma > 0.0, "volatility must be positive");
78 Self {
79 risk_free_rate,
80 mu,
81 sigma,
82 }
83 }
84
85 pub fn exact_policy(&self) -> f64 {
95 (self.mu - self.risk_free_rate) / (self.sigma * self.sigma)
96 }
97
98 pub fn exact_value(&self, wealth: f64, tau: f64) -> f64 {
109 let u = self.exact_policy();
110 let growth = self.risk_free_rate + 0.5 * (self.mu - self.risk_free_rate) * u;
111 wealth.ln() + growth * tau
112 }
113}
114
115impl ControlProblem<1> for Merton {
116 type Control = f64;
117
118 fn optimize(&self, _t: f64, state: &[f64; 1], _derivs: &StateDerivatives<1>) -> Self::Control {
119 let _ = state;
123 self.exact_policy().max(0.0)
124 }
125
126 fn running_reward(&self, _t: f64, _state: &[f64; 1], _control: &Self::Control) -> f64 {
127 0.0
129 }
130
131 fn generator(
132 &self,
133 _t: f64,
134 state: &[f64; 1],
135 control: &Self::Control,
136 derivs: &StateDerivatives<1>,
137 ) -> f64 {
138 let x = state[0];
139 let vx = derivs.grad[0];
140 let vxx = derivs.hessian[0];
141 let u = *control;
142
143 x * (self.risk_free_rate + u * (self.mu - self.risk_free_rate)) * vx
144 + 0.5 * (x * u * self.sigma).powi(2) * vxx
145 }
146
147 fn terminal(&self, state: &[f64; 1]) -> f64 {
148 state[0].ln()
149 }
150
151 fn next_step(&self, _t: f64, state: &[f64; 1], dt: f64, noise: &[f64; 1]) -> [f64; 1] {
152 let x = state[0];
153 let u = self.exact_policy().max(0.0);
154 let drift = x * (self.risk_free_rate + u * (self.mu - self.risk_free_rate)) * dt;
155 let diffusion = x * u * self.sigma * dt.sqrt() * noise[0];
156 [x + drift + diffusion]
157 }
158
159 fn is_diffusion_dimension(&self, _dim: usize) -> bool {
160 true
161 }
162}
163
164impl PdeProblem<1> for Merton {
165 fn dimension_kind(&self, _dim: usize) -> DimensionKind {
166 DimensionKind::Diffusion
167 }
168
169 fn transport(
170 &self,
171 _t: f64,
172 state: &[f64; 1],
173 control: &Self::Control,
174 _derivs: &StateDerivatives<1>,
175 ) -> Transport<1> {
176 let x = state[0];
178 let u = *control;
179 let b = x * (self.risk_free_rate + u * (self.mu - self.risk_free_rate));
180 let d = 0.5 * (x * u * self.sigma).powi(2);
181 let h = x.max(1e-12);
182 Transport::new(
183 [d / (h * h) + b.max(0.0) / h],
184 [d / (h * h) + (-b).max(0.0) / h],
185 0.0,
186 )
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 fn model() -> Merton {
195 Merton::new(0.03, 0.08, 0.2)
196 }
197
198 #[test]
199 fn exact_policy_matches_closed_form() {
200 let m = model();
201 assert!((m.exact_policy() - 0.05 / 0.04).abs() < 1e-12);
202 }
203
204 #[test]
205 fn optimize_returns_exact_policy() {
206 let m = model();
207 let d = StateDerivatives::new([0.0], [0.0]);
208 let u = m.optimize(0.0, &[100.0], &d);
209 assert!((u - m.exact_policy()).abs() < 1e-12);
210 }
211
212 #[test]
213 fn terminal_is_log_wealth() {
214 let m = model();
215 assert!((m.terminal(&[100.0]) - 100.0f64.ln()).abs() < 1e-12);
216 }
217
218 #[test]
219 fn driver_matches_hjb_form_at_exact_derivatives() {
220 let m = model();
222 let x = 100.0;
223 let u = m.exact_policy();
224 let d = StateDerivatives::new([1.0 / x], [-1.0 / (x * x)]);
225
226 let expected =
227 u * (m.mu - m.risk_free_rate) + m.risk_free_rate - 0.5 * u * u * m.sigma * m.sigma;
228 assert!((m.driver(0.0, &[x], &u, &d) - expected).abs() < 1e-12);
229 }
230
231 #[test]
232 fn next_step_preserves_positive_wealth_for_small_dt() {
233 let m = model();
234 let s = m.next_step(0.0, &[100.0], 0.001, &[0.0]);
235 assert!(s[0] > 0.0);
236 }
237
238 #[test]
239 fn dimension_kind_is_diffusion() {
240 let m = model();
241 assert_eq!(PdeProblem::dimension_kind(&m, 0), DimensionKind::Diffusion);
242 }
243}