Skip to content

Commit abe41e1

Browse files
committed
fix: heston calibration derivates
1 parent 9ee2285 commit abe41e1

File tree

2 files changed

+147
-33
lines changed

2 files changed

+147
-33
lines changed

src/quant/calibration/heston.rs

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ mod tests {
222222
30.75, 25.88, 21.00, 16.50, 11.88, 7.69, 4.44, 2.10, 0.78, 0.25, 0.10, 0.10,
223223
];
224224

225-
let v0 = Array1::linspace(0.0, 0.01, 1);
225+
let v0 = Array1::linspace(0.0, 1.0, 10);
226226

227227
for v in v0.iter() {
228228
let calibrator = HestonCalibrator::new(
@@ -249,6 +249,58 @@ mod tests {
249249
Ok(())
250250
}
251251

252+
#[test]
253+
fn test_heston_calibrate_v2() -> Result<()> {
254+
let tau = 24.0 / 365.0;
255+
println!("Time to maturity: {}", tau);
256+
257+
let s = vec![
258+
8700.53, 8700.53, 8700.53, 8700.53, 8700.53, 8700.53, 8700.53, 8700.53, 8700.53, 8700.53,
259+
8700.53, 8700.53,
260+
];
261+
262+
let k = vec![
263+
5220.318, 6090.371, 6960.424, 7830.477, 8265.5035, 8483.01675, 8700.53, 8918.04325,
264+
9135.5565, 9570.583, 10440.636, 11310.689,
265+
];
266+
267+
let c_market = vec![
268+
3499.564, 2632.74, 1765.933, 901.846, 479.055, 279.313, 118.848, 28.79, 4.23, 0.143,
269+
1.799e-5, 1.259e-9,
270+
];
271+
272+
let r = 0.04;
273+
let q = 0.06;
274+
let t = 0.083;
275+
276+
let v0 = Array1::linspace(1e-5, 2.0, 5);
277+
278+
for v in v0.iter() {
279+
let calibrator = HestonCalibrator::new(
280+
Some(HestonParams {
281+
v0: *v,
282+
theta: 6.47e-5,
283+
rho: -1.98e-3,
284+
kappa: 6.57e-3,
285+
sigma: 5.09e-4,
286+
}),
287+
c_market.clone().into(),
288+
s.clone().into(),
289+
k.clone().into(),
290+
t,
291+
r,
292+
Some(q),
293+
OptionType::Call,
294+
);
295+
296+
let data = calibrator.calibrate()?;
297+
let heston_params = data.iter().map(|d| d.params.clone()).collect::<Vec<_>>();
298+
println!("Calibration data: {:?}", heston_params);
299+
}
300+
301+
Ok(())
302+
}
303+
252304
#[test]
253305
fn test_heston_calibrate_guess_params() -> Result<()> {
254306
let tau = 24.0 / 365.0;
@@ -283,7 +335,8 @@ mod tests {
283335
calibrator.set_initial_params(s.clone().into(), Array1::from_elem(s.len(), *v), 6.40e-4);
284336

285337
let data = calibrator.calibrate()?;
286-
println!("Calibration data: {:?}", data);
338+
let heston_params = data.iter().map(|d| d.params.clone()).collect::<Vec<_>>();
339+
println!("Calibration data: {:?}", heston_params);
287340
}
288341

289342
Ok(())

src/quant/pricing/heston.rs

Lines changed: 92 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ use std::f64::consts::FRAC_1_PI;
22

33
use impl_new_derive::ImplNew;
44
use implied_vol::implied_black_volatility;
5+
use nalgebra::Complex;
56
use num_complex::Complex64;
67
use quadrature::double_exponential;
8+
use rand_distr::num_traits::ConstOne;
79

810
use crate::quant::{
911
r#trait::{PricerExt, TimeExt},
@@ -58,11 +60,11 @@ impl PricerExt for HestonPricer {
5860
let tau = self.tau().unwrap_or(1.0);
5961

6062
vec![
61-
self.dC_dv0(tau),
62-
self.dC_dtheta(tau),
63-
self.dC_drho(tau),
64-
self.dC_dkappa(tau),
65-
self.dC_dsigma(tau),
63+
self.h1(tau),
64+
self.h2(tau),
65+
self.h3(tau),
66+
self.h4(tau),
67+
self.h5(tau),
6668
]
6769
}
6870

@@ -154,38 +156,108 @@ impl HestonPricer {
154156
/// https://www.sciencedirect.com/science/article/abs/pii/S0377221717304460
155157
156158
/// Partial derivative of the C function with respect to the v0 parameter
157-
pub(crate) fn dC_dv0(&self, tau: f64) -> f64 {
158-
(-self.A(tau) / self.v0).re
159+
pub(crate) fn h1(&self, tau: f64) -> f64 {
160+
-(self.A(tau) / self.v0).re
159161
}
160162

161163
/// Partial derivative of the C function with respect to the theta parameter
162-
pub(crate) fn dC_dtheta(&self, tau: f64) -> f64 {
164+
pub(crate) fn h2(&self, tau: f64) -> f64 {
163165
((2.0 * self.kappa / self.sigma.powi(2)) * self.D_(tau)
164166
- self.kappa * self.rho * tau * Complex64::i() * self.u(1) / self.sigma)
165167
.re
166168
}
167169

168170
/// Partial derivative of the C function with respect to the rho parameter
169-
pub(crate) fn dC_drho(&self, tau: f64) -> f64 {
170-
(-self.kappa * self.theta * tau * Complex64::i() * self.u(1) / self.sigma).re
171+
pub(crate) fn h3(&self, tau: f64) -> f64 {
172+
(-self.dA_drho(tau)
173+
+ ((2.0 * self.kappa * self.theta) / (self.sigma.powi(2) * self.d_()))
174+
* (self.dd__drho() - (self.d_() / self.A2(tau)) * self.dA2_drho(tau))
175+
- (self.kappa * self.theta * tau * Complex64::i() * self.u(1)) / self.sigma)
176+
.re
171177
}
172178

173179
/// Partial derivative of the C function with respect to the kappa parameter
174-
pub(crate) fn dC_dkappa(&self, tau: f64) -> f64 {
175-
(2.0 * self.theta * self.D_(tau) / self.sigma.powi(2)
176-
+ ((2.0 * self.kappa * self.theta) / self.sigma.powi(2) * self.B(tau)) * self.dB_dkappa(tau)
177-
- (self.theta * self.rho * tau * Complex64::i() * self.u(1) / self.sigma))
180+
pub(crate) fn h4(&self, tau: f64) -> f64 {
181+
((1.0 / (self.sigma * Complex64::i() * self.u(1))) * self.dA_drho(tau)
182+
+ ((2.0 * self.theta) / self.sigma.powi(2)) * self.D_(tau)
183+
+ ((2.0 * self.kappa * self.theta) / (self.sigma.powi(2) * self.B(tau)))
184+
* self.dB_dkappa(tau)
185+
- (self.theta * self.rho * tau * Complex64::i() * self.u(1)) / self.sigma)
178186
.re
179187
}
180188

181189
/// Partial derivative of the C function with respect to the sigma parameter
182-
pub(crate) fn dC_dsigma(&self, tau: f64) -> f64 {
183-
((-4.0 * self.kappa * self.theta / self.sigma.powi(3)) * self.D_(tau)
184-
+ ((2.0 * self.kappa * self.theta) / (self.sigma.powi(2) * self.d_())) * self.dd_dsigma()
185-
+ self.kappa * self.theta * self.rho * tau * Complex64::i() * self.u(1) / self.sigma.powi(2))
190+
pub(crate) fn h5(&self, tau: f64) -> f64 {
191+
(-self.dA_dsigma(tau) - ((4.0 * self.kappa * self.theta) / self.sigma.powi(3)) * self.D_(tau)
192+
+ ((2.0 * self.kappa * self.theta) / (self.sigma.powi(2) * self.d_()))
193+
* (self.dd__dsigma() - (self.d_() / self.A2(tau)) * self.dA2_dsigma(tau))
194+
+ (self.kappa * self.theta * self.rho * tau * Complex64::i() * self.u(1))
195+
/ self.sigma.powi(2))
186196
.re
187197
}
188198

199+
// helpers derivates
200+
pub(self) fn dxi_drho(&self) -> Complex64 {
201+
-self.sigma * Complex64::i() * self.u(1)
202+
}
203+
204+
pub(self) fn dd__drho(&self) -> Complex64 {
205+
-(self.xi() * self.sigma * Complex64::i() * self.u(1)) / self.d_()
206+
}
207+
208+
pub(self) fn dA1_drho(&self, tau: f64) -> Complex64 {
209+
-((Complex64::i()
210+
* self.u(1)
211+
* (self.u(1).powi(2) + Complex64::i() * self.u(1))
212+
* tau
213+
* self.xi()
214+
* self.sigma)
215+
/ (2.0 * self.d_())
216+
* (self.d_() * tau / 2.0).cosh())
217+
}
218+
219+
pub(self) fn dA2_drho(&self, tau: f64) -> Complex64 {
220+
((self.dxi_drho() * (2.0 + self.xi() * tau)) / (2.0 * self.d_() * self.v0))
221+
* (self.xi() * (self.d_() * tau / 2.0).cosh() + self.d_() * (self.d_() * tau / 2.0).sinh())
222+
}
223+
224+
pub(self) fn dA_drho(&self, tau: f64) -> Complex64 {
225+
(1.0 / self.A2(tau)) * self.dA1_drho(tau) - (self.A(tau) / self.A2(tau)) * self.dA2_drho(tau)
226+
}
227+
228+
pub(self) fn dd__dsigma(&self) -> Complex64 {
229+
(self.rho / self.sigma - 1.0 / self.xi()) * self.dd__drho()
230+
+ (self.sigma * self.u(1).powi(2) / self.d_())
231+
}
232+
233+
pub(self) fn dA1_dsigma(&self, tau: f64) -> Complex64 {
234+
(((self.u(1).powi(2) + Complex64::i() * self.u(1)) * tau) / 2.0)
235+
* self.dd__dsigma()
236+
* (self.d_() * tau / 2.0).cosh()
237+
}
238+
239+
pub(self) fn dA2_dsigma(&self, tau: f64) -> Complex64 {
240+
(self.rho / self.sigma) * self.dA2_drho(tau)
241+
- ((2.0 + tau * self.xi()) / (self.v0 * tau * self.xi() * Complex64::i() * self.u(1)))
242+
* self.dA1_drho(tau)
243+
+ (self.sigma * tau * self.A1(tau)) / (2.0 * self.v0)
244+
}
245+
246+
pub(self) fn dA_dsigma(&self, tau: f64) -> Complex64 {
247+
(1.0 / self.A2(tau)) * self.dA1_dsigma(tau)
248+
- (self.A(tau) / self.A2(tau)) * self.dA2_dsigma(tau)
249+
}
250+
251+
pub(self) fn dB_drho(&self, tau: f64) -> Complex64 {
252+
((self.kappa * tau / 2.0).exp() / self.v0)
253+
* ((1.0 / self.A2(tau)) * self.dd__drho()
254+
- (self.d_() / self.A2(tau).powi(2)) * self.dA2_drho(tau))
255+
}
256+
257+
pub(self) fn dB_dkappa(&self, tau: f64) -> Complex64 {
258+
(Complex64::i() / (self.sigma * self.u(1))) * self.dB_drho(tau) + (self.B(tau) * tau) / 2.0
259+
}
260+
189261
pub(self) fn xi(&self) -> Complex64 {
190262
self.kappa - self.sigma * self.rho * Complex64::i() * self.u(1)
191263
}
@@ -195,10 +267,6 @@ impl HestonPricer {
195267
.sqrt()
196268
}
197269

198-
pub(self) fn dd_dsigma(&self) -> Complex64 {
199-
(self.sigma * (self.u(1) + Complex64::i() * self.u(1))) / self.d_()
200-
}
201-
202270
pub(self) fn A1(&self, tau: f64) -> Complex64 {
203271
(self.u(1).powi(2) + Complex64::i() * self.u(1)) * (self.d_() * tau / 2.0).sinh()
204272
}
@@ -212,19 +280,12 @@ impl HestonPricer {
212280
self.A1(tau) / self.A2(tau)
213281
}
214282

215-
pub(self) fn D_(&self, tau: f64) -> Complex64 {
216-
(self.d_() / self.v0).ln() + (self.kappa - self.d_() / 2.0) * tau
217-
- (((self.d_() + self.xi()) / (2.0 * self.v0))
218-
+ ((self.d_() - self.xi()) / (2.0 * self.v0)) * (-self.d_() * tau).exp())
219-
.ln()
220-
}
221-
222283
pub(self) fn B(&self, tau: f64) -> Complex64 {
223284
(self.d_() * (self.kappa * tau / 2.0).exp()) / (self.v0 * self.A2(tau))
224285
}
225286

226-
pub(self) fn dB_dkappa(&self, tau: f64) -> Complex64 {
227-
(self.d_() * tau * (self.kappa * tau / 2.0).exp()) / (2.0 * self.v0 * self.A2(tau))
287+
pub(self) fn D_(&self, tau: f64) -> Complex64 {
288+
self.B(tau).ln()
228289
}
229290
}
230291

0 commit comments

Comments
 (0)