Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions docs/api/options/pricer.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ different stochastic volatility models.

::: quantflow.options.pricer.OptionPricerBase

::: quantflow.options.pricer.OptionPricingMethod

::: quantflow.options.pricer.OptionPricer

::: quantflow.options.pricer.MaturityPricer
Expand Down
6 changes: 6 additions & 0 deletions docs/api/utils/marginal1d.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# Marginal 1D

::: quantflow.utils.marginal.OptionPricingMethod

::: quantflow.utils.marginal.OptionPricingResult

::: quantflow.utils.marginal.OptionPricingCosResult

::: quantflow.utils.marginal.Marginal1D
96 changes: 13 additions & 83 deletions docs/examples/pricing_method_comparison.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,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):
Expand All @@ -34,10 +36,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",
Expand Down Expand Up @@ -126,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(
Expand Down Expand Up @@ -159,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(
Expand All @@ -168,81 +171,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()
8 changes: 6 additions & 2 deletions docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 5 additions & 17 deletions docs/tutorials/pricing_method_comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions quantflow/options/calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion quantflow/options/pricer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
5 changes: 3 additions & 2 deletions quantflow/utils/marginal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand Down
Loading