1use crate::models::merton_jump::MertonJump;
39
40const GH_NODES: [f64; 32] = [
42 -1.007_742_267_422_946_5e1,
43 -9.064_399_210_702_405,
44 -8.219_728_765_382_245,
45 -7.460_755_754_121_519,
46 -6.755_930_830_540_705,
47 -6.088_964_309_076_987,
48 -5.450_033_273_623_428,
49 -4.832_604_613_244_489,
50 -4.232_021_109_995_41,
51 -3.644_781_249_880_833,
52 -3.068_135_169_013_121_6,
53 -2.499_840_415_187_395_4,
54 -1.938_004_905_925_717_4,
55 -1.380_980_199_272_144_2,
56 -8.272_849_037_797_652e-1,
57 -2.755_464_192_302_758_4e-1,
58 2.755_464_192_302_758_4e-1,
59 8.272_849_037_797_652e-1,
60 1.380_980_199_272_144_2,
61 1.938_004_905_925_717_4,
62 2.499_840_415_187_395_4,
63 3.068_135_169_013_121_6,
64 3.644_781_249_880_833,
65 4.232_021_109_995_41,
66 4.832_604_613_244_489,
67 5.450_033_273_623_428,
68 6.088_964_309_076_987,
69 6.755_930_830_540_705,
70 7.460_755_754_121_519,
71 8.219_728_765_382_245,
72 9.064_399_210_702_405,
73 1.007_742_267_422_946_5e1,
74];
75
76const GH_WEIGHTS: [f64; 32] = [
78 4.124_607_489_018_283_5e-23,
79 5.208_449_591_960_913e-19,
80 6.755_290_223_670_204e-16,
81 2.378_064_855_777_808_6e-13,
82 3.347_501_239_801_196e-11,
83 2.312_518_412_074_239_3e-9,
84 8.881_290_713_105_89e-8,
85 2.059_622_103_953_427_5e-6,
86 3.055_980_306_089_633_6e-5,
87 3.025_570_258_170_628_5e-4,
88 2.062_051_051_307_880_8e-3,
89 9.903_461_702_320_593e-3,
90 3.410_984_772_609_207e-2,
91 8.534_480_827_208_074e-2,
92 1.565_389_937_575_985e-1,
93 2.117_055_698_804_792_8e-1,
94 2.117_055_698_804_792_8e-1,
95 1.565_389_937_575_985e-1,
96 8.534_480_827_208_074e-2,
97 3.410_984_772_609_207e-2,
98 9.903_461_702_320_593e-3,
99 2.062_051_051_307_880_8e-3,
100 3.025_570_258_170_628_5e-4,
101 3.055_980_306_089_633_6e-5,
102 2.059_622_103_953_427_5e-6,
103 8.881_290_713_105_89e-8,
104 2.312_518_412_074_239_3e-9,
105 3.347_501_239_801_196e-11,
106 2.378_064_855_777_808_6e-13,
107 6.755_290_223_670_204e-16,
108 5.208_449_591_960_913e-19,
109 4.124_607_489_018_283_5e-23,
110];
111
112#[derive(Clone, Copy, Debug)]
117pub struct MertonJumpLogNormal {
118 pub risk_free_rate: f64,
120 pub mu: f64,
122 pub sigma: f64,
124 pub jump_rate: f64,
126 pub jump_mean: f64,
128 pub jump_vol: f64,
130}
131
132impl MertonJumpLogNormal {
133 pub fn new(
149 risk_free_rate: f64,
150 mu: f64,
151 sigma: f64,
152 jump_rate: f64,
153 jump_mean: f64,
154 jump_vol: f64,
155 ) -> Self {
156 assert!(sigma > 0.0, "volatility must be positive");
157 assert!(jump_rate >= 0.0, "jump rate must be non-negative");
158 assert!(jump_vol >= 0.0, "jump volatility must be non-negative");
159 let model = Self {
160 risk_free_rate,
161 mu,
162 sigma,
163 jump_rate,
164 jump_mean,
165 jump_vol,
166 };
167 let f0 = model.first_order_condition(0.0);
168 let f1 = model.first_order_condition(1.0);
169 assert!(
170 f0 > 0.0 && f1 < 0.0,
171 "parameters do not yield an interior optimum on [0, 1]"
172 );
173 model
174 }
175
176 fn expected_jump_fraction(&self, u: f64) -> f64 {
178 let mut acc = 0.0;
179 for i in 0..32 {
180 let y = (self.jump_mean + self.jump_vol * GH_NODES[i]).exp();
181 acc += GH_WEIGHTS[i] * (y - 1.0) / (1.0 + u * (y - 1.0));
182 }
183 acc
184 }
185
186 fn expected_log_jump(&self, u: f64) -> f64 {
188 let mut acc = 0.0;
189 for i in 0..32 {
190 let y = (self.jump_mean + self.jump_vol * GH_NODES[i]).exp();
191 acc += GH_WEIGHTS[i] * (1.0 + u * (y - 1.0)).ln();
192 }
193 acc
194 }
195
196 fn first_order_condition(&self, u: f64) -> f64 {
198 let a = self.mu - self.risk_free_rate;
199 a - self.sigma * self.sigma * u + self.jump_rate * self.expected_jump_fraction(u)
200 }
201
202 pub fn exact_policy(&self) -> f64 {
213 let mut lo = 0.0;
214 let mut hi = 1.0;
215 let mut f_lo = self.first_order_condition(lo);
216 for _ in 0..80 {
217 let mid = 0.5 * (lo + hi);
218 let f_mid = self.first_order_condition(mid);
219 if f_lo * f_mid <= 0.0 {
220 hi = mid;
221 } else {
222 lo = mid;
223 f_lo = f_mid;
224 }
225 }
226 0.5 * (lo + hi)
227 }
228
229 pub fn exact_value(&self, wealth: f64, tau: f64) -> f64 {
240 let u = self.exact_policy();
241 let a = self.mu - self.risk_free_rate;
242 let growth = self.risk_free_rate + u * a - 0.5 * (u * self.sigma).powi(2)
243 + self.jump_rate * self.expected_log_jump(u);
244 wealth.ln() + growth * tau
245 }
246}
247
248impl From<&MertonJumpLogNormal> for MertonJump {
251 fn from(model: &MertonJumpLogNormal) -> Self {
252 MertonJump::new(
253 model.risk_free_rate,
254 model.mu,
255 model.sigma,
256 model.jump_rate,
257 model.jump_mean.exp(),
258 )
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 fn model() -> MertonJumpLogNormal {
267 MertonJumpLogNormal::new(0.03, 0.08, 0.3, 1.0, -0.02, 0.05)
268 }
269
270 #[test]
271 fn policy_is_interior() {
272 let m = model();
273 let u = m.exact_policy();
274 assert!(u > 0.0 && u < 1.0);
275 }
276
277 #[test]
278 fn policy_satisfies_first_order_condition() {
279 let m = model();
280 let u = m.exact_policy();
281 assert!(m.first_order_condition(u).abs() < 1e-10);
282 }
283
284 #[test]
285 fn zero_volatility_reduces_to_deterministic_jump() {
286 let m = MertonJumpLogNormal::new(0.03, 0.08, 0.3, 1.0, -0.02, 0.0);
287 let det = MertonJump::new(0.03, 0.08, 0.3, 1.0, (-0.02f64).exp());
288 assert!((m.exact_policy() - det.exact_policy()).abs() < 1e-10);
289 assert!((m.exact_value(100.0, 1.0) - det.exact_value(100.0, 1.0)).abs() < 1e-10);
290 }
291
292 #[test]
293 fn jump_reduces_exposure_relative_to_no_jump() {
294 let m = model();
295 let no_jump = crate::models::merton::Merton::new(0.03, 0.08, 0.3);
296 assert!(m.exact_policy() < no_jump.exact_policy());
297 }
298}