From 0883998a8a9489868ee2a9fbb2270a3390320742 Mon Sep 17 00:00:00 2001 From: Luca Date: Sun, 3 May 2026 16:37:23 +0100 Subject: [PATCH 1/2] Remove time comparison --- docs/examples/pricing_method_comparison.py | 81 +-------------------- docs/tutorials/pricing_method_comparison.md | 22 ++---- 2 files changed, 6 insertions(+), 97 deletions(-) diff --git a/docs/examples/pricing_method_comparison.py b/docs/examples/pricing_method_comparison.py index 85d8f736..b2a50f77 100644 --- a/docs/examples/pricing_method_comparison.py +++ b/docs/examples/pricing_method_comparison.py @@ -1,6 +1,4 @@ -"""Compare Carr-Madan, Lewis and COS option pricing methods for accuracy and speed.""" - -import time +"""Compare Carr-Madan, Lewis and COS option pricing methods for accuracy.""" import numpy as np import plotly.graph_objects as go @@ -34,10 +32,6 @@ class PricingMethodComparison(BaseModel): default=(32, 64, 128, 256, 512), description="Discretization points to compare", ) - timing_reps: int = Field( - default=20, - description="Number of repetitions for timing measurements", - ) max_moneyness: float = Field( default=1.5, description="Maximum time-adjusted moneyness for option pricing", @@ -168,81 +162,8 @@ def run_ttm(self) -> None: height=800, ) - def run_speed(self) -> None: - # --- Speed: wall-clock time per maturity call vs n --- - fig_speed = go.Figure() - ms = self.model.marginal(1.0) - for method, props in self.charts.items(): - times = [] - for n in self.ns: - t0 = time.perf_counter() - for _ in range(self.timing_reps): - ms.call_option(n, max_log_strike=1.0, pricing_method=method) - times.append((time.perf_counter() - t0) / self.timing_reps * 1000) - fig_speed.add_trace( - go.Scatter( - x=self.ns, - y=times, - name=method.value, - mode="lines+markers", - line=dict(color=props.color, dash=props.dash), - ) - ) - fig_speed.update_layout( - title=( - f"Wall-clock time per pricing call" - f" (TTM=1.0, avg over {self.timing_reps} reps)" - ), - xaxis_title="N (discretization points)", - yaxis_title="time (ms)", - ) - fig_speed.write_image(assets_path("pricing_method_speed.png")) - - # --- Accuracy vs speed trade-off --- - tradeoff_ttm = self.ttms[1] - ms = self.model.marginal(tradeoff_ttm) - max_log_strike = self.max_moneyness * np.sqrt(tradeoff_ttm) - log_strikes = ms.option_support(self.ref_n + 1, max_log_strike=max_log_strike) - ref = ms.call_option(self.ref_n, max_log_strike=max_log_strike) - fig_tradeoff = go.Figure() - for method, props in self.charts.items(): - errors, times = [], [] - for n in self.ns: - t0 = time.perf_counter() - for _ in range(self.timing_reps): - r = ms.call_option( - n, max_log_strike=max_log_strike, pricing_method=method - ) - elapsed = (time.perf_counter() - t0) / self.timing_reps * 1000 - errors.append( - min( - self._iv_error(r, ref, log_strikes, tradeoff_ttm), - self.max_iv_error, - ) - ) - times.append(elapsed) - fig_tradeoff.add_trace( - go.Scatter( - x=times, - y=errors, - name=method.value, - mode="lines+markers", - text=[str(n) for n in self.ns], - textposition="top center", - line=dict(color=props.color, dash=props.dash), - ) - ) - fig_tradeoff.update_layout( - title=f"Accuracy vs speed trade-off (TTM={tradeoff_ttm})", - xaxis_title="time (ms)", - yaxis_title="max implied vol error", - yaxis_type="log", - ) - fig_tradeoff.write_image(assets_path("pricing_method_tradeoff.png")) - if __name__ == "__main__": pr = Heston.create(vol=0.5, kappa=2, sigma=0.8, rho=-0.2) comparison = PricingMethodComparison(model=pr) comparison.run_ttm() - comparison.run_speed() diff --git a/docs/tutorials/pricing_method_comparison.md b/docs/tutorials/pricing_method_comparison.md index 0af06ff1..59e14a16 100644 --- a/docs/tutorials/pricing_method_comparison.md +++ b/docs/tutorials/pricing_method_comparison.md @@ -44,30 +44,18 @@ can struggle with the auto-selected $\alpha$, while Lewis and COS remain stable: [![Accuracy TTM=0.02](../assets/examples/pricing_method_accuracy_ttm0_02.png)](../assets/examples/pricing_method_accuracy_ttm0_02.png){target="_blank"} -## Speed comparison +## Complexity | Method | Complexity | |---|---| | Carr-Madan | $O(N \log N)$ via Fractional Fourier Transform | | Lewis | $O(N \log N)$ via Fractional Fourier Transform | -| COS | $O(N^2)$ — dense $N \times N$ complex matrix-vector product | +| COS | $O(N^2)$ | -COS is slower because it evaluates $e^{-i j\pi(k+a)/(b-a)}$ for every combination of -strike $k$ and cosine index $j$, forming an explicit $N \times N$ matrix before -multiplying by the coefficient vector. Lewis and Carr-Madan avoid this by evaluating -the transform on a uniform grid and applying the FrFT in $O(N \log N)$. +Unlike the FRFT methods, COS can price a single strike at a time without computing the +full grid, making it well suited for lazy or on-demand strike evaluation. -Wall-clock time per pricing call as a function of $N$: - -![Speed](../assets/examples/pricing_method_speed.png) - -## Accuracy vs speed trade-off - -Each point is labelled with its $N$ value (TTM=0.25): - -![Trade-off](../assets/examples/pricing_method_tradeoff.png) - -## Example code +## Code for the above charts ```python --8<-- "docs/examples/pricing_method_comparison.py" From 6852a271ea0603f702fb2620d9023e63065d7a15 Mon Sep 17 00:00:00 2001 From: Luca Date: Sun, 3 May 2026 17:22:13 +0100 Subject: [PATCH 2/2] CARR_MADAN default --- docs/api/options/pricer.md | 2 -- docs/api/utils/marginal1d.md | 6 ++++++ docs/examples/pricing_method_comparison.py | 15 ++++++++++++--- docs/glossary.md | 8 ++++++-- quantflow/options/calibration.py | 1 + quantflow/options/pricer.py | 2 +- quantflow/utils/marginal.py | 5 +++-- 7 files changed, 29 insertions(+), 10 deletions(-) diff --git a/docs/api/options/pricer.md b/docs/api/options/pricer.md index 3b90ad1c..796736d8 100644 --- a/docs/api/options/pricer.md +++ b/docs/api/options/pricer.md @@ -5,8 +5,6 @@ different stochastic volatility models. ::: quantflow.options.pricer.OptionPricerBase -::: quantflow.options.pricer.OptionPricingMethod - ::: quantflow.options.pricer.OptionPricer ::: quantflow.options.pricer.MaturityPricer diff --git a/docs/api/utils/marginal1d.md b/docs/api/utils/marginal1d.md index 5dc8ab22..3220e7f9 100644 --- a/docs/api/utils/marginal1d.md +++ b/docs/api/utils/marginal1d.md @@ -1,3 +1,9 @@ # Marginal 1D +::: quantflow.utils.marginal.OptionPricingMethod + +::: quantflow.utils.marginal.OptionPricingResult + +::: quantflow.utils.marginal.OptionPricingCosResult + ::: quantflow.utils.marginal.Marginal1D diff --git a/docs/examples/pricing_method_comparison.py b/docs/examples/pricing_method_comparison.py index b2a50f77..345a1bed 100644 --- a/docs/examples/pricing_method_comparison.py +++ b/docs/examples/pricing_method_comparison.py @@ -9,7 +9,11 @@ from quantflow.options.bs import implied_black_volatility from quantflow.sp.base import StochasticProcess1D from quantflow.sp.heston import Heston -from quantflow.utils.marginal import OptionPricingMethod, OptionPricingResult +from quantflow.utils.marginal import ( + OptionPricingCosResult, + OptionPricingMethod, + OptionPricingResult, +) class ChartProps(BaseModel): @@ -120,8 +124,13 @@ def run_ttm(self) -> None: max_log_strike=max_log_strike, pricing_method=method, ) + ks = ( + log_strikes + if isinstance(r, OptionPricingCosResult) + else ms.option_support(n + 1, max_log_strike=max_log_strike) + ) errors.append( - min(self._iv_error(r, ref, log_strikes, ttm), self.max_iv_error) + min(self._iv_error(r, ref, ks, ttm), self.max_iv_error) ) if n == self.ns[-1]: fig.add_trace( @@ -153,7 +162,7 @@ def run_ttm(self) -> None: fig.update_yaxes(title_text="implied volatility", row=1, col=1) fig.update_xaxes(title_text="N (discretization points)", row=1, col=2) fig.update_yaxes( - title_text="max implied vol error", type="log", row=1, col=2 + title_text="max implied vol error (log scale)", type="log", row=1, col=2 ) fig.update_layout(title=ttm_label) fig.write_image( diff --git a/docs/glossary.md b/docs/glossary.md index f3d7aed8..e53ff156 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -49,11 +49,15 @@ The Feller condition is a parameter constraint on a square-root diffusion proces (such as [CIR][quantflow.sp.cir.CIR]) that ensures the process remains strictly positive. For a process of the form -$$dx_t = \kappa(\theta - x_t)\,dt + \sigma\sqrt{x_t}\,dw_t$$ +\begin{equation} +dx_t = \kappa(\theta - x_t)\,dt + \sigma\sqrt{x_t}\,dw_t +\end{equation} the condition is -$$2\kappa\theta \geq \sigma^2$$ +\begin{equation} +2\kappa\theta \geq \sigma^2 +\end{equation} where $\kappa$ is the mean reversion speed, $\theta$ is the long-run mean, and $\sigma$ is the diffusion coefficient. When the condition holds, the origin is an inaccessible diff --git a/quantflow/options/calibration.py b/quantflow/options/calibration.py index 1819d14b..9a222800 100644 --- a/quantflow/options/calibration.py +++ b/quantflow/options/calibration.py @@ -254,6 +254,7 @@ def plot_maturities( model = model.max_moneyness_ttm( max_moneyness=max_moneyness_ttm, support=support ) + plot.plot_vol_surface( pd.DataFrame([d.info_dict() for d in options]), model=model.df, diff --git a/quantflow/options/pricer.py b/quantflow/options/pricer.py index 67e8aa96..a333c8c1 100644 --- a/quantflow/options/pricer.py +++ b/quantflow/options/pricer.py @@ -350,7 +350,7 @@ class OptionPricer(OptionPricerBase, Generic[M]): description="Number of discretization points for the marginal distribution", ) method: OptionPricingMethod = Field( - default=OptionPricingMethod.LEWIS, + default=OptionPricingMethod.CARR_MADAN, description="Method to use for option pricing", ) max_moneyness_ttm: float = Field( diff --git a/quantflow/utils/marginal.py b/quantflow/utils/marginal.py index 14b79d2c..d68c4e36 100644 --- a/quantflow/utils/marginal.py +++ b/quantflow/utils/marginal.py @@ -253,8 +253,9 @@ def call_option_cos( normalised call payoff $(e^y - 1)^+$ integrated over $[0, b]$. The $e^k$ factor converts from strike-normalised to forward-space pricing. - Returns an [OptionPricingCosResult][.OptionPricingCosResult] with the - precomputed coefficient vector. Use + Returns an + [OptionPricingCosResult][quantflow.utils.marginal.OptionPricingCosResult] + with the precomputed coefficient vector. Use [call_at][quantflow.utils.marginal.OptionPricingCosResult.call_at] to evaluate at arbitrary log-strikes in $O(N)$ per strike. """