1use std::f64::consts::PI;
32
33use crate::core::trade::PutOrCall;
34use crate::equity::heston::Cpx;
35
36pub const CALIBRATION_TERMS: usize = 160;
39
40pub const DEFAULT_TERMS: usize = 2048;
44
45pub(crate) struct CosPricer {
49 a: f64,
50 b: f64,
51 weights: Vec<f64>,
53 df: f64,
54 forward: f64,
56}
57
58impl CosPricer {
59 pub(crate) fn new(cf: &dyn Fn(Cpx) -> Cpx, r: f64, t: f64, n: usize) -> Self {
62 let psi = |h: f64| cf(Cpx::real(h)).ln();
67 let rough = psi(1e-2);
68 let c2_rough = (-2.0 * rough.re / 1e-4).abs().max(1e-10);
69 let h = (0.05 / c2_rough.sqrt()).clamp(1e-4, 1e-1);
70 let p1 = psi(h);
71 let p2 = psi(2.0 * h);
72 let c1 = p1.im / h;
73 let c4 = (2.0 * (p2.re - 4.0 * p1.re) / h.powi(4)).max(0.0);
74 let c2 = (-2.0 * p1.re / (h * h) + c4 * h * h / 12.0).abs().max(1e-10);
75
76 let width = 10.0 * (c2 + c4.sqrt()).sqrt();
79 let (a, b) = (c1 - width, c1 + width);
80
81 let bma = b - a;
82 let weights = (0..n)
83 .map(|k| {
84 let u = k as f64 * PI / bma;
85 let e = Cpx::new((u * a).cos(), -(u * a).sin());
87 let w = cf(Cpx::real(u)).mul(e).re;
88 if k == 0 { 0.5 * w } else { w }
89 })
90 .collect();
91
92 let forward = cf(Cpx::new(0.0, -1.0)).re;
93 CosPricer { a, b, weights, df: (-r * t).exp(), forward }
94 }
95
96 pub(crate) fn call(&self, strike: f64) -> f64 {
101 if strike < self.forward {
102 return self.raw_put(strike) + self.df * (self.forward - strike);
103 }
104 self.raw_call(strike)
105 }
106
107 pub(crate) fn put(&self, strike: f64) -> f64 {
109 if strike > self.forward {
110 return self.raw_call(strike) + self.df * (strike - self.forward);
111 }
112 self.raw_put(strike)
113 }
114
115 fn raw_call(&self, strike: f64) -> f64 {
116 let lnk = strike.ln();
117 if lnk >= self.b {
118 return 0.0; }
120 self.sum(strike, lnk.max(self.a), self.b, true)
121 }
122
123 fn raw_put(&self, strike: f64) -> f64 {
124 let lnk = strike.ln();
125 if lnk <= self.a {
126 return 0.0;
127 }
128 self.sum(strike, self.a, lnk.min(self.b), false)
129 }
130
131 pub(crate) fn price(&self, strike: f64, put_or_call: PutOrCall) -> f64 {
132 match put_or_call {
133 PutOrCall::Call => self.call(strike),
134 PutOrCall::Put => self.put(strike),
135 }
136 }
137
138 fn sum(&self, strike: f64, c: f64, d: f64, call: bool) -> f64 {
143 let bma = self.b - self.a;
144 let (ec, ed) = (c.exp(), d.exp());
145 let (yc, yd) = (c - self.a, d - self.a);
146 let mut total = 0.0;
147 for (k, w) in self.weights.iter().enumerate() {
148 let omega = k as f64 * PI / bma;
149 let (sin_c, cos_c) = (omega * yc).sin_cos();
150 let (sin_d, cos_d) = (omega * yd).sin_cos();
151 let chi =
152 (cos_d * ed - cos_c * ec + omega * (sin_d * ed - sin_c * ec)) / (1.0 + omega * omega);
153 let psi = if k == 0 { d - c } else { (sin_d - sin_c) / omega };
154 let payoff_coeff = if call { chi - strike * psi } else { strike * psi - chi };
155 total += w * payoff_coeff;
156 }
157 self.df * total * 2.0 / bma
158 }
159}
160
161pub(crate) fn group_by_maturity(maturities: impl Iterator<Item = f64>) -> Vec<(f64, Vec<usize>)> {
165 let mut groups: Vec<(f64, Vec<usize>)> = Vec::new();
166 for (i, t) in maturities.enumerate() {
167 match groups.iter_mut().find(|(gt, _)| *gt == t) {
168 Some((_, idxs)) => idxs.push(i),
169 None => groups.push((t, vec![i])),
170 }
171 }
172 groups
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::equity::bates::{
179 bates_double_exp_price, bates_price, BatesDoubleExpParams, BatesParams, KouJumps,
180 MertonJumps,
181 };
182 use crate::equity::blackscholes::bs_price;
183 use crate::equity::heston::{heston_price, HestonParams};
184
185 const S: f64 = 100.0;
186 const R: f64 = 0.03;
187 const Q: f64 = 0.01;
188
189 fn gbm_cf(u: Cpx, sigma: f64, t: f64) -> Cpx {
191 let drift = S.ln() + (R - Q - 0.5 * sigma * sigma) * t;
192 crate::equity::heston::I
193 .mul(u)
194 .scale(drift)
195 .sub(u.mul(u).scale(0.5 * sigma * sigma * t))
196 .exp()
197 }
198
199 fn heston() -> HestonParams {
200 HestonParams { v0: 0.09, kappa: 2.0, theta: 0.09, vol_of_vol: 0.4, rho: -0.7 }
201 }
202
203 #[test]
204 fn cos_reproduces_black_scholes() {
205 for t in [0.1, 1.0, 3.0] {
206 for sigma in [0.1, 0.3, 0.6] {
207 let pricer = CosPricer::new(&|u| gbm_cf(u, sigma, t), R, t, DEFAULT_TERMS);
208 for k in [60.0, 90.0, 100.0, 110.0, 150.0] {
209 let call = bs_price(S, k, R, Q, sigma, t, PutOrCall::Call);
210 let put = bs_price(S, k, R, Q, sigma, t, PutOrCall::Put);
211 assert!(
212 (pricer.call(k) - call).abs() < 1e-8,
213 "call K={k} t={t} sigma={sigma}: cos {} bs {call}",
214 pricer.call(k)
215 );
216 assert!(
217 (pricer.put(k) - put).abs() < 1e-8,
218 "put K={k} t={t} sigma={sigma}: cos {} bs {put}",
219 pricer.put(k)
220 );
221 }
222 }
223 }
224 }
225
226 #[test]
227 fn cos_agrees_with_the_heston_integration_oracle() {
228 let params = [
229 heston(),
230 HestonParams { v0: 0.04, kappa: 1.0, theta: 0.04, vol_of_vol: 0.9, rho: -0.9 },
232 HestonParams { v0: 0.16, kappa: 3.0, theta: 0.09, vol_of_vol: 0.2, rho: 0.3 },
233 ];
234 for hp in ¶ms {
235 for t in [0.25, 1.0, 2.0] {
236 let pricer = CosPricer::new(
237 &|u| crate::equity::heston::characteristic_fn(u, S, R, Q, t, hp),
238 R,
239 t,
240 DEFAULT_TERMS,
241 );
242 for k in [70.0, 90.0, 100.0, 110.0, 140.0] {
243 let oracle = heston_price(S, k, R, Q, t, hp, PutOrCall::Call);
244 let cos = pricer.call(k);
245 assert!(
246 (cos - oracle).abs() < 5e-6,
247 "K={k} t={t} hp={hp:?}: cos {cos} oracle {oracle}"
248 );
249 }
250 }
251 }
252 }
253
254 #[test]
255 fn cos_agrees_with_both_bates_oracles() {
256 let merton = BatesParams {
257 heston: heston(),
258 jumps: MertonJumps { intensity: 0.5, mean_jump: -0.1, jump_vol: 0.2 },
259 };
260 let kou = BatesDoubleExpParams {
261 heston: heston(),
262 jumps: KouJumps { intensity: 0.7, p_up: 0.4, eta_up: 12.0, eta_down: 8.0 },
263 };
264 let t = 1.0;
265 let merton_pricer =
266 CosPricer::new(&|u| crate::equity::bates::ln_price_cf(u, S, R, Q, t, &merton), R, t, DEFAULT_TERMS);
267 let kou_pricer = CosPricer::new(
268 &|u| crate::equity::bates::ln_price_cf_double_exp(u, S, R, Q, t, &kou),
269 R,
270 t,
271 DEFAULT_TERMS,
272 );
273 for k in [80.0, 100.0, 120.0] {
274 let m_oracle = bates_price(S, k, R, Q, t, &merton, PutOrCall::Call);
275 let k_oracle = bates_double_exp_price(S, k, R, Q, t, &kou, PutOrCall::Call);
276 assert!(
277 (merton_pricer.call(k) - m_oracle).abs() < 5e-6,
278 "merton K={k}: cos {} oracle {m_oracle}",
279 merton_pricer.call(k)
280 );
281 assert!(
282 (kou_pricer.call(k) - k_oracle).abs() < 5e-6,
283 "kou K={k}: cos {} oracle {k_oracle}",
284 kou_pricer.call(k)
285 );
286 }
287 }
288
289 #[test]
290 fn put_call_parity_holds_to_high_precision() {
291 let t = 1.0;
292 let hp = heston();
293 let pricer = CosPricer::new(
294 &|u| crate::equity::heston::characteristic_fn(u, S, R, Q, t, &hp),
295 R,
296 t,
297 DEFAULT_TERMS,
298 );
299 let df = (-R * t).exp();
300 let forward = S * ((R - Q) * t).exp();
301 for k in [80.0, 100.0, 125.0] {
302 let parity = pricer.call(k) - pricer.put(k) - df * (forward - k);
303 assert!(parity.abs() < 1e-9, "parity violation {parity} at K={k}");
304 }
305 }
306
307
308
309 #[test]
310 fn maturity_grouping_preserves_indices() {
311 let groups = group_by_maturity([1.0, 0.5, 1.0, 2.0, 0.5].into_iter());
312 assert_eq!(
313 groups,
314 vec![(1.0, vec![0, 2]), (0.5, vec![1, 4]), (2.0, vec![3])]
315 );
316 }
317}