roughprix 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
roughprix/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ from .heston import RoughHeston, RoughHestonParam
2
+ from . import heston
3
+ from . import fourier
4
+
5
+ __all__ = [
6
+ "RoughHeston",
7
+ "RoughHestonParam",
8
+ "heston",
9
+ "fourier",
10
+ ]
roughprix/fourier.py ADDED
@@ -0,0 +1,539 @@
1
+ """Generic Fourier quadrature pricing utilities.
2
+
3
+ Works with plain numpy/JAX arrays — no model objects.
4
+
5
+ General methodology
6
+ -------------------
7
+ Pricing is split into a model-independent quadrature stage and a
8
+ model-dependent evaluation stage. The characteristic function is
9
+ factored as
10
+
11
+ phi(u, T) = phi_0(u, T) * hN(u, T)
12
+
13
+ where phi_0 is the *base* characteristic function (the order-0 Riccati
14
+ solution, cheap to reuse across strikes and maturities) and hN is the
15
+ *expansion correction* (phi_N / phi_0, capturing the Taylor correction in H).
16
+ At expansion order 0 one has hN = 1 identically.
17
+
18
+ The separation is useful because the Chebyshev interpolation weights W
19
+ depend only on phi_0 and the payoff, while hN is evaluated on the coarse
20
+ node grid and multiplied in cheaply. For a rough-Heston model the
21
+ caller produces phi_0 and hN via ``model.expansion_state`` together with
22
+ ``model.log_charfun_from_field``; the high-level entry point
23
+ ``RoughHeston.eur_call_put_measure_quad`` handles this automatically.
24
+
25
+ Two contour paths are provided:
26
+
27
+ Damped-measure path (any real R):
28
+ Contour: u + iR, dampening factor exp(-R * log K) absorbed into the
29
+ payoff transform. Needs E[S_T^R] to be finite. The integral is the
30
+ price for R > 1 (vanilla calls), R > 0 (digital calls) or R < 0
31
+ (puts); on other contours step 4 adds the payoff poles in between.
32
+ RoughHeston prices digitals on R = 0.5 by default, which needs only
33
+ E[S_T^0.5].
34
+
35
+ 1. build_panel_quadrature — build Chebyshev–GL panel nodes once
36
+ 2. measure_weights_from_phi — W[k,j] = ∫ payoff_hat(K_k, u+iR) S^-i(u+iR) phi_0(u+iR) ℓ_j(u) du
37
+ 3. eur_call_put_from_weights — price[k] = (disc/π) Re(W[k,:] @ hN)
38
+ 4. damped_contour_residues — + the residues of the poles at R = 0 and R = 1
39
+
40
+ Lewis contour path (R = 0.5, calls only):
41
+ Contour: u - i/2. No dampening factor; numerically recommended.
42
+
43
+ 1. build_panel_quadrature — same quadrature setup
44
+ 2. lewis_weights_from_phi — W[k,j] = ∫ exp(iu log(S/K_k))/(u²+¼) phi_0(u) ℓ_j(u) du
45
+ 3. eur_call_from_lewis_weights — C_k = S - (disc/π) sqrt(S·K_k) Re(W[k,:] @ hN)
46
+ """
47
+ from __future__ import annotations
48
+
49
+ __all__ = [
50
+ "PanelQuadrature",
51
+ "build_panel_quadrature",
52
+ "fourier_payoff_call_put",
53
+ "measure_weights_from_phi",
54
+ "eur_call_put_from_weights",
55
+ "damped_contour_residues",
56
+ "lewis_weights_from_phi",
57
+ "eur_call_from_lewis_weights",
58
+ "otm_prices_to_ivs",
59
+ "adaptive_check_values",
60
+ "adaptive_converged",
61
+ ]
62
+
63
+ from dataclasses import dataclass, field
64
+ from functools import lru_cache
65
+
66
+ import numpy as np
67
+ import jax.numpy as jnp
68
+ from scipy.stats import norm
69
+
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # Black-Scholes / implied-vol helpers
73
+ # ---------------------------------------------------------------------------
74
+
75
+ def _bs_price(S_0, K, sigma, T, r=0., call=True):
76
+ d1 = (np.log(S_0 / K) + (r + sigma ** 2 / 2) * T) / (sigma * np.sqrt(T))
77
+ d2 = d1 - sigma * np.sqrt(T)
78
+ d1 = np.clip(d1, -30, 30)
79
+ d2 = np.clip(d2, -30, 30)
80
+ if call:
81
+ return S_0 * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
82
+ return -S_0 * norm.cdf(-d1) + K * np.exp(-r * T) * norm.cdf(-d2)
83
+
84
+
85
+ def _iv_bisect(price_fun, price, tol=1e-10, sl=1e-10, sr=10., max_iter=100):
86
+ """Vectorised bisection for the implied vol on the bracket ``[sl, sr]``.
87
+
88
+ ``price_fun(sigma)`` must be increasing in ``sigma``. The result is NaN
89
+ where ``price`` is not finite, where ``price_fun`` is not finite, and where
90
+ the implied vol is not strictly inside the bracket. The last case covers
91
+ prices outside the no-arbitrage bounds: at or below intrinsic value the
92
+ bisection collapses onto ``sl``, and at or above ``price_fun(sr)`` it
93
+ collapses onto ``sr``.
94
+
95
+ The loop runs at most ``max_iter`` times, so it always terminates. The
96
+ default bracket reaches ``tol`` after 37 halvings.
97
+ """
98
+ price = np.asarray(price, dtype=float)
99
+ finite = np.isfinite(price)
100
+ # A NaN price compares false against everything, which would freeze its
101
+ # bracket. Bisect a placeholder for every non-finite price and mask it at
102
+ # the end.
103
+ price = np.where(finite, price, 0.0)
104
+ lo, hi = sl, sr
105
+ sm = (sl + sr) / 2
106
+ for _ in range(max_iter):
107
+ if np.amax(sr - sl) <= tol:
108
+ break
109
+ em = price_fun(sm) - price
110
+ sl = np.where(em < 0, sm, sl)
111
+ sr = np.where(em >= 0, sm, sr)
112
+ sm = (sl + sr) / 2
113
+ # A NaN from price_fun also freezes the bracket, so it fails the width test.
114
+ ok = finite & (sr - sl <= tol) & (sm >= lo + tol) & (sm <= hi - tol)
115
+ return np.where(ok, sm, np.nan)
116
+
117
+
118
+ def _iv_eur(S_0, K, T, price, payoff, r=0.):
119
+ if isinstance(T, np.ndarray) and T.ndim == 1:
120
+ T = T[..., None]
121
+ call = payoff == "call"
122
+ def price_fun(s):
123
+ return _bs_price(S_0=S_0, K=K, sigma=s, T=T, r=r, call=call)
124
+ return _iv_bisect(price_fun, price)
125
+
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # Fourier payoff transform (JAX, returns complex128 array (N_K, N_u))
129
+ # ---------------------------------------------------------------------------
130
+
131
+ def fourier_payoff_call_put(K, u, call=True, digital=False, logarithmic=True):
132
+ """Fourier transform of call/put payoff evaluated at complex argument u.
133
+
134
+ Args:
135
+ K: strikes, real array (N_K,)
136
+ u: complex frequency array (N_u,); includes the dampening shift
137
+ call / digital / logarithmic: payoff variant flags
138
+
139
+ Returns:
140
+ complex JAX array (N_K, N_u)
141
+ """
142
+ K = jnp.asarray(K, dtype=jnp.float64).reshape(-1)
143
+ u = jnp.asarray(u, dtype=jnp.complex128)
144
+ iu = 1j * u # (N_u,)
145
+ log_K = jnp.log(K)[:, None] # (N_K, 1)
146
+ iu2d = iu[None, :] # (1, N_u)
147
+
148
+ if digital:
149
+ sign = -1.0 if call else 1.0
150
+ if logarithmic:
151
+ return sign * jnp.exp(log_K * iu2d) / iu2d
152
+ return sign * jnp.exp(K[:, None] * iu2d) / iu2d
153
+ if logarithmic:
154
+ return jnp.exp(log_K * (1 + iu2d)) / (iu2d * (1 + iu2d))
155
+ return jnp.exp(K[:, None] * iu2d) / iu2d ** 2
156
+
157
+
158
+ # ---------------------------------------------------------------------------
159
+ # Chebyshev / barycentric quadrature helpers (numpy, computed once)
160
+ # ---------------------------------------------------------------------------
161
+
162
+ def _cheb_lobatto_nodes(a: float, b: float, m: int) -> np.ndarray:
163
+ if m < 2:
164
+ raise ValueError("m must be >= 2")
165
+ j = np.arange(m)
166
+ x = np.cos(np.pi * j / (m - 1))
167
+ return np.sort(0.5 * (a + b) + 0.5 * (b - a) * x)
168
+
169
+
170
+ def _barycentric_weights(x: np.ndarray) -> np.ndarray:
171
+ x = np.asarray(x, dtype=float)
172
+ diff = x[:, None] - x[None, :] # (m, m), zero on diagonal
173
+ np.fill_diagonal(diff, 1.0)
174
+ return 1.0 / np.prod(diff, axis=1)
175
+
176
+
177
+ def _barycentric_interp_matrix(
178
+ x_eval: np.ndarray,
179
+ x_nodes: np.ndarray,
180
+ tol: float = 1e-14,
181
+ ) -> np.ndarray:
182
+ x_eval = np.asarray(x_eval, dtype=float).reshape(-1)
183
+ x_nodes = np.asarray(x_nodes, dtype=float).reshape(-1)
184
+ w = _barycentric_weights(x_nodes)
185
+ diff = x_eval[:, None] - x_nodes[None, :] # (N_eval, m)
186
+ near = np.abs(diff) < tol * np.maximum(1.0, np.abs(x_eval[:, None]))
187
+ safe_diff = np.where(near, 1.0, diff)
188
+ tmp = np.where(near, 0.0, w[None, :] / safe_diff)
189
+ row_sum = tmp.sum(axis=1, keepdims=True)
190
+ M = tmp / np.where(row_sum == 0.0, 1.0, row_sum)
191
+ near_rows = np.where(near.any(axis=1))[0]
192
+ for i in near_rows:
193
+ M[i, :] = 0.0
194
+ M[i, np.argmax(near[i])] = 1.0
195
+ return M
196
+
197
+
198
+ def _as_strict_panels(panels) -> np.ndarray:
199
+ panels = np.asarray(panels, dtype=float)
200
+ if panels[0] != 0.0:
201
+ raise ValueError("panels must start at 0.")
202
+ if not np.all(np.diff(panels) > 0):
203
+ raise ValueError("panels must be strictly increasing.")
204
+ return panels
205
+
206
+
207
+ # ---------------------------------------------------------------------------
208
+ # Panel quadrature data (built once, numpy arrays)
209
+ # ---------------------------------------------------------------------------
210
+
211
+ @dataclass
212
+ class PanelQuadrature:
213
+ """Pre-built panel quadrature for the Fourier u-axis integration.
214
+
215
+ u_nodes — Chebyshev interpolation nodes across all panels; shape (N_u,).
216
+ hN (the correction multiplier) is evaluated here.
217
+ u_quad — Gauss-Legendre quadrature points across all panels; shape (N_q,).
218
+ phi0 (the base char function) is evaluated here.
219
+ w_quad — GL weights (panel-scaled); shape (N_q,).
220
+ Lmat — block-diagonal barycentric interpolation matrix (N_q, N_u).
221
+ """
222
+ u_nodes: np.ndarray
223
+ u_quad: np.ndarray
224
+ w_quad: np.ndarray
225
+ Lmat: np.ndarray
226
+ panels: np.ndarray
227
+ panel_data: list = field(default_factory=list)
228
+
229
+
230
+ @lru_cache(maxsize=None)
231
+ def _gauss_legendre(q_weight: int) -> tuple[np.ndarray, np.ndarray]:
232
+ """Gauss-Legendre nodes and weights of order q_weight on [-1, 1], as read-only arrays.
233
+
234
+ numpy's leggauss finds the nodes as the eigenvalues of the symmetric companion matrix of
235
+ the Legendre polynomial P_q; this eigenvalue problem is solved once per order.
236
+ """
237
+ xg, wg = np.polynomial.legendre.leggauss(q_weight)
238
+ xg.flags.writeable = False
239
+ wg.flags.writeable = False
240
+ return xg, wg
241
+
242
+
243
+ def build_panel_quadrature(
244
+ panels: np.ndarray,
245
+ m_cheb: int,
246
+ q_weight: int,
247
+ ) -> PanelQuadrature:
248
+ """Build Chebyshev–Gauss-Legendre panel quadrature for the Fourier integral."""
249
+ panels = _as_strict_panels(np.asarray(panels, dtype=float))
250
+ xg, wg = _gauss_legendre(q_weight)
251
+
252
+ all_u_nodes, all_u_quad, all_w_quad, Lmat_blocks, panel_data = [], [], [], [], []
253
+
254
+ for p in range(len(panels) - 1):
255
+ a, b = float(panels[p]), float(panels[p + 1])
256
+ u_nodes = _cheb_lobatto_nodes(a, b, m_cheb)
257
+ u_q = 0.5 * (a + b) + 0.5 * (b - a) * xg
258
+ w_q = 0.5 * (b - a) * wg
259
+ all_u_nodes.append(u_nodes)
260
+ all_u_quad.append(u_q)
261
+ all_w_quad.append(w_q)
262
+ Lmat_blocks.append(_barycentric_interp_matrix(u_q, u_nodes))
263
+ panel_data.append({"a": a, "b": b, "u_nodes": u_nodes, "u_quad": u_q, "w_quad": w_q})
264
+
265
+ u_nodes_all = np.concatenate(all_u_nodes)
266
+ u_quad_all = np.concatenate(all_u_quad)
267
+ w_quad_all = np.concatenate(all_w_quad)
268
+
269
+ N_q = len(u_quad_all)
270
+ N_u = len(u_nodes_all)
271
+ Lmat = np.zeros((N_q, N_u))
272
+ q_off = u_off = 0
273
+ for Lb in Lmat_blocks:
274
+ nq, nu = Lb.shape
275
+ Lmat[q_off:q_off + nq, u_off:u_off + nu] = Lb
276
+ q_off += nq
277
+ u_off += nu
278
+
279
+ return PanelQuadrature(
280
+ u_nodes=u_nodes_all,
281
+ u_quad=u_quad_all,
282
+ w_quad=w_quad_all,
283
+ Lmat=Lmat,
284
+ panels=panels,
285
+ panel_data=panel_data,
286
+ )
287
+
288
+
289
+ # ---------------------------------------------------------------------------
290
+ # Phase 2: integrate phi0 * payoff_hat onto Chebyshev nodes (JAX)
291
+ # ---------------------------------------------------------------------------
292
+
293
+ def measure_weights_from_phi(
294
+ phi0: jnp.ndarray,
295
+ quad: PanelQuadrature,
296
+ K: np.ndarray,
297
+ R: float,
298
+ call: bool = True,
299
+ digital: bool = False,
300
+ S_0: float = 1.0,
301
+ ) -> jnp.ndarray:
302
+ """Compute interpolation weights W[k, j] = ∫ payoff_hat(K_k, u) S_0^(-iu) phi0(u) ell_j(u) du.
303
+
304
+ Args:
305
+ phi0: complex JAX array (N_q,) — base char function of log(S_T/S_0) at quad.u_quad
306
+ quad: PanelQuadrature from build_panel_quadrature
307
+ K: strike array (N_K,)
308
+ R: dampening shift (real)
309
+ call / digital: payoff variant
310
+ S_0: spot price
311
+
312
+ Returns:
313
+ complex JAX array (N_K, N_u)
314
+ """
315
+ phi0 = jnp.asarray(phi0, dtype=jnp.complex128)
316
+ w_quad = jnp.asarray(quad.w_quad, dtype=jnp.float64)
317
+ Lmat = jnp.asarray(quad.Lmat, dtype=jnp.float64)
318
+ u_c = jnp.asarray(quad.u_quad + 1j * R, dtype=jnp.complex128)
319
+
320
+ payoff_hat = fourier_payoff_call_put(K, u_c, call=call, digital=digital) # (N_K, N_q)
321
+ phi_S = jnp.exp(-1j * np.log(S_0) * u_c) * phi0 # CF of log S_T
322
+ B = payoff_hat * phi_S[None, :] # (N_K, N_q)
323
+ return (B * w_quad[None, :]) @ Lmat # (N_K, N_u)
324
+
325
+
326
+ # ---------------------------------------------------------------------------
327
+ # Phase 3: apply correction multiplier and price (JAX)
328
+ # ---------------------------------------------------------------------------
329
+
330
+ def eur_call_put_from_weights(
331
+ weights: jnp.ndarray,
332
+ hN: jnp.ndarray,
333
+ discount: float,
334
+ ) -> jnp.ndarray:
335
+ """Price European options from interpolation weights and correction multiplier.
336
+
337
+ price[k] = discount / pi * Re( weights[k, :] @ hN )
338
+
339
+ Args:
340
+ weights: complex JAX array (N_K, N_u)
341
+ hN: complex JAX array (N_u,) — correction multiplier at u_nodes
342
+ discount: exp(-r * T)
343
+
344
+ Returns:
345
+ real JAX array (N_K,)
346
+ """
347
+ return (discount / np.pi) * jnp.real(weights @ hN)
348
+
349
+
350
+ def damped_contour_residues(
351
+ K: np.ndarray,
352
+ R: float,
353
+ S_0: float,
354
+ discount: float,
355
+ call: bool = True,
356
+ digital: bool = False,
357
+ ) -> np.ndarray:
358
+ """Pole terms that turn eur_call_put_from_weights into the price on any contour.
359
+
360
+ The integral is the price itself when R lies in the payoff's strip: R > 1 for
361
+ vanilla calls, R > 0 for digital calls, R < 0 for puts. Each pole of the payoff
362
+ transform between R and that strip adds its residue, half of it when the contour
363
+ runs through the pole: S_0 at R = 1 (vanillas), and K * discount (vanillas) or
364
+ discount (digitals) at R = 0. For calls on R = 0.5 this is the Lewis formula,
365
+ C = S_0 + integral.
366
+
367
+ Args:
368
+ K: strike array (N_K,)
369
+ R: contour height, as passed to measure_weights_from_phi
370
+ S_0: spot price
371
+ discount: exp(-r * T)
372
+ call / digital: payoff variant
373
+
374
+ Returns:
375
+ real numpy array (N_K,)
376
+ """
377
+ K = np.asarray(K, dtype=float).reshape(-1)
378
+ # np.heaviside(x, 0.5): 1 above 0, 1/2 on the pole, 0 below
379
+ if digital:
380
+ return np.full(K.shape, discount * np.heaviside(-R if call else R, 0.5))
381
+ if call:
382
+ return S_0 * np.heaviside(1 - R, 0.5) - K * discount * np.heaviside(-R, 0.5)
383
+ return K * discount * np.heaviside(R, 0.5) - S_0 * np.heaviside(R - 1, 0.5)
384
+
385
+
386
+ # ---------------------------------------------------------------------------
387
+ # Lewis-contour pricing (R=0.5 path)
388
+ # ---------------------------------------------------------------------------
389
+
390
+ def lewis_weights_from_phi(
391
+ phi0: jnp.ndarray,
392
+ quad: PanelQuadrature,
393
+ K: np.ndarray,
394
+ S_0: float,
395
+ ) -> jnp.ndarray:
396
+ """Compute Lewis weights W[k, j] for the contour u - i/2.
397
+
398
+ W[k, j] = int_0^L exp(iu*log(S_0/K_k)) / (u^2+1/4) * phi0(u) * ell_j(u) du
399
+
400
+ Args:
401
+ phi0: complex JAX array (N_q,) — char function at quad.u_quad (real nodes)
402
+ quad: PanelQuadrature from build_panel_quadrature
403
+ K: strike array (N_K,)
404
+ S_0: spot price
405
+
406
+ Returns:
407
+ complex JAX array (N_K, N_u)
408
+ """
409
+ phi0 = jnp.asarray(phi0, dtype=jnp.complex128)
410
+ w_quad = jnp.asarray(quad.w_quad, dtype=jnp.float64)
411
+ Lmat = jnp.asarray(quad.Lmat, dtype=jnp.float64)
412
+ u_q = jnp.asarray(quad.u_quad, dtype=jnp.float64)
413
+ K_arr = jnp.asarray(np.asarray(K, dtype=float).reshape(-1), dtype=jnp.float64)
414
+
415
+ log_ratio = jnp.log(float(S_0) / K_arr)[:, None] # (N_K, 1)
416
+ phase = jnp.exp(1j * u_q[None, :] * log_ratio) # (N_K, N_q)
417
+ denom = u_q[None, :] ** 2 + 0.25 # (1, N_q)
418
+ B = (phase / denom) * phi0[None, :] # (N_K, N_q)
419
+ return (B * w_quad[None, :]) @ Lmat # (N_K, N_u)
420
+
421
+
422
+ def eur_call_from_lewis_weights(
423
+ weights: jnp.ndarray,
424
+ hN: jnp.ndarray,
425
+ S_0: float,
426
+ K: np.ndarray,
427
+ discount: float,
428
+ ) -> jnp.ndarray:
429
+ """Call price from Lewis weights and correction multiplier.
430
+
431
+ C_k = S_0 - discount * sqrt(S_0 * K_k) / pi * Re( weights[k, :] @ hN )
432
+
433
+ Args:
434
+ weights: complex JAX array (N_K, N_u)
435
+ hN: complex JAX array (N_u,) — correction multiplier at u_nodes
436
+ S_0: spot price
437
+ K: strike array (N_K,)
438
+ discount: exp(-r * T)
439
+
440
+ Returns:
441
+ real JAX array (N_K,)
442
+ """
443
+ K_arr = jnp.asarray(np.asarray(K, dtype=float).reshape(-1), dtype=jnp.float64)
444
+ sqrt_S0K = jnp.sqrt(float(S_0) * K_arr)
445
+ integral = jnp.real(weights @ jnp.asarray(hN, dtype=jnp.complex128))
446
+ return float(S_0) - (float(discount) / np.pi) * sqrt_S0K * integral
447
+
448
+
449
+ # ---------------------------------------------------------------------------
450
+ # Adaptive panel convergence utilities (model-agnostic)
451
+ # ---------------------------------------------------------------------------
452
+
453
+ def otm_prices_to_ivs(
454
+ raw_otm_prices: np.ndarray,
455
+ K: np.ndarray,
456
+ maturity: float,
457
+ S_0: float,
458
+ call_mask: np.ndarray,
459
+ put_mask: np.ndarray,
460
+ r: float = 0.0,
461
+ ) -> np.ndarray:
462
+ """Invert a vector of OTM option prices to Black-Scholes implied vols.
463
+
464
+ Strikes in ``call_mask`` are treated as calls, those in ``put_mask`` as puts.
465
+ The two masks must partition K (no overlap, no gap).
466
+ """
467
+ ivs = np.empty_like(raw_otm_prices, dtype=float)
468
+ if np.any(call_mask):
469
+ ivs[call_mask] = _iv_eur(
470
+ S_0=S_0, K=K[call_mask], T=maturity,
471
+ price=raw_otm_prices[call_mask], payoff="call", r=r,
472
+ )
473
+ if np.any(put_mask):
474
+ ivs[put_mask] = _iv_eur(
475
+ S_0=S_0, K=K[put_mask], T=maturity,
476
+ price=raw_otm_prices[put_mask], payoff="put", r=r,
477
+ )
478
+ return ivs
479
+
480
+
481
+ def adaptive_check_values(
482
+ result: np.ndarray,
483
+ raw_otm_prices: np.ndarray,
484
+ call_mask: np.ndarray,
485
+ put_mask: np.ndarray,
486
+ implied_vol: bool,
487
+ adaptive_metric: str,
488
+ K: np.ndarray,
489
+ maturity: float,
490
+ S_0: float,
491
+ r: float = 0.0,
492
+ scalar_H: bool = True,
493
+ ) -> np.ndarray:
494
+ """Extract a flat array of convergence-check values from a fixed-panel result.
495
+
496
+ Used to compare successive adaptive-panel iterations. Returns raw OTM prices
497
+ when ``adaptive_metric='price'``, and implied vols when ``adaptive_metric='iv'``
498
+ (inverting prices if the result is not already in IV space).
499
+
500
+ ``scalar_H=False`` signals that ``raw_otm_prices`` has shape ``(N_K, N_H)``
501
+ and each H-slice is inverted independently.
502
+ """
503
+ if adaptive_metric == "price":
504
+ return np.asarray(raw_otm_prices).ravel()
505
+ # metric == "iv"
506
+ if implied_vol:
507
+ return np.asarray(result).ravel()
508
+ if scalar_H:
509
+ return otm_prices_to_ivs(
510
+ raw_otm_prices, K, maturity, S_0, call_mask, put_mask, r
511
+ ).ravel()
512
+ n_K, n_H = raw_otm_prices.shape
513
+ ivs = np.empty_like(raw_otm_prices)
514
+ for j in range(n_H):
515
+ ivs[:, j] = otm_prices_to_ivs(
516
+ raw_otm_prices[:, j], K, maturity, S_0, call_mask, put_mask, r
517
+ )
518
+ return ivs.ravel()
519
+
520
+
521
+ def adaptive_converged(
522
+ prev_check: np.ndarray,
523
+ check: np.ndarray,
524
+ metric: str,
525
+ tol: float,
526
+ abs_tol: float,
527
+ rel_tol: float,
528
+ ) -> bool:
529
+ """Return True when successive adaptive-panel check values are within tolerance.
530
+
531
+ For ``metric='iv'`` a single absolute tolerance ``tol`` is applied.
532
+ For ``metric='price'`` a combined absolute + relative criterion is used.
533
+ """
534
+ err = float(np.max(np.abs(check - prev_check)))
535
+ if metric == "iv":
536
+ return err <= tol
537
+ scale = max(float(np.max(np.abs(check))), 1.0)
538
+ return err <= abs_tol + rel_tol * scale
539
+
@@ -0,0 +1,42 @@
1
+ from .rough_heston import (
2
+ RoughHeston,
3
+ RoughHestonParam,
4
+ HestonExpansionState,
5
+ )
6
+ from .pade import (
7
+ h_pade22,
8
+ h_pade33,
9
+ h_pade44,
10
+ rough_heston_pade_f0,
11
+ rough_heston_root_pade_f0,
12
+ )
13
+ from .integration import (
14
+ TimeGrid,
15
+ Field,
16
+ Integrate,
17
+ LinearVolterraSolver,
18
+ AdamsFractionalRiccatiSolver,
19
+ ProductIntegrationRiccatiSolver,
20
+ suggested_time_grid_power,
21
+ )
22
+
23
+ __all__ = [
24
+ # Model
25
+ "RoughHeston",
26
+ "RoughHestonParam",
27
+ "HestonExpansionState",
28
+ # Padé approximations
29
+ "h_pade22",
30
+ "h_pade33",
31
+ "h_pade44",
32
+ "rough_heston_pade_f0",
33
+ "rough_heston_root_pade_f0",
34
+ # Integration
35
+ "TimeGrid",
36
+ "Field",
37
+ "Integrate",
38
+ "LinearVolterraSolver",
39
+ "AdamsFractionalRiccatiSolver",
40
+ "ProductIntegrationRiccatiSolver",
41
+ "suggested_time_grid_power",
42
+ ]