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
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ applyTo: '/**'
* Math notation convention: use $\Phi$ for the characteristic function and $\phi$ for the characteristic exponent, where $\Phi = e^{-\phi}$.
* Glossary entries in `docs/glossary.md` must be kept in alphabetical order.
* Do not repeat concept definitions inline in tutorials or docstrings — link to the glossary instead using a relative markdown link (e.g. `[moneyness](../glossary.md#moneyness)`).
* Prefer mkdocstrings relative cross-references whenever the target is visible from the current scope: write `[label][.member]` (same class) or `[label][..Sibling]` (same module) instead of repeating the fully-qualified path. Use the full path only when the target lives in a different module than the current docstring.
* To rebuild doc examples run `uv run ./dev/build-examples` — runs all scripts in `docs/examples/` and writes their output to `docs/examples_output/`

## Pydantic models
Expand Down
1 change: 1 addition & 0 deletions docs/api/data/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pip install quantflow[data]
| [Financial Modeling Prep](fmp.md) | Equity prices, company profiles, and sector data |
| [FRED](fred.md) | US macroeconomic time series from the St. Louis Fed |
| [Federal Reserve](fed.md) | Federal Reserve H.15 selected interest rate data |
| [Yahoo](yahoo.md) | Equity option chains from Yahoo Finance |

## Usage

Expand Down
21 changes: 21 additions & 0 deletions docs/api/data/yahoo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Yahoo

Fetch equity option chains from [Yahoo Finance](https://finance.yahoo.com/).

The client is intentionally minimal: it fetches a full option chain via the
public `v7/finance/options` endpoint and exposes a helper to build a
[VolSurfaceLoader][quantflow.options.surface.VolSurfaceLoader] from it.

You can import the module via

```python
from quantflow.data.yahoo import Yahoo
```

## Authentication

Yahoo Finance requires a session cookie and a `crumb` token for the options
endpoint. The client fetches both on the first request and caches the crumb
for the lifetime of the instance.

::: quantflow.data.yahoo.Yahoo
60 changes: 60 additions & 0 deletions docs/examples/spx_vol_surface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import gzip
import json
from pathlib import Path

from docs.examples._utils import assets_path, print_model
from quantflow.data.yahoo import Yahoo
from quantflow.options.calibration import BNS2Calibration
from quantflow.options.calibration.base import ResidualKind
from quantflow.options.pricer import OptionPricer, OptionPricingMethod
from quantflow.sp.bns import BNS, BNS2

FIXTURE = (
Path(__file__).resolve().parents[2]
/ "quantflow_tests"
/ "fixtures"
/ "yahoo_spx.json.gz"
)

chain = json.loads(gzip.decompress(FIXTURE.read_bytes()))
loader = Yahoo.loader_from_chain(chain, exclude_volume=1)
surface = loader.surface()
surface.bs()
surface.disable_outliers()

fig = surface.plot3d()
fig.update_traces(marker=dict(size=3))
fig.update_layout(
title="SPX implied volatility surface",
scene=dict(
xaxis_title="moneyness",
yaxis_title="time to maturity (log)",
zaxis_title="implied volatility",
yaxis=dict(type="log"),
camera=dict(eye=dict(x=0.6, y=-2.2, z=0.8)),
),
)
fig.write_image(assets_path("spx_vol_surface.png"), width=1200, height=800)

# Calibrate a two-factor BNS model to the SPX surface. A fast factor absorbs
# the steep short-dated equity skew; a slow factor anchors the long end.
pricer = OptionPricer(
model=BNS2(
bns1=BNS.create(vol=0.2, kappa=20.0, decay=10.0, rho=-0.6),
bns2=BNS.create(vol=0.2, kappa=0.3, decay=10.0, rho=-0.3),
weight=0.5,
),
method=OptionPricingMethod.COS,
)
calibration: BNS2Calibration[BNS2] = BNS2Calibration(
pricer=pricer,
vol_surface=surface,
residual_kind=ResidualKind.IV,
)
result = calibration.fit()
print(result.message)
print_model(calibration.model)

smile = calibration.plot_maturities(max_moneyness=0.5, support=101)
smile.update_layout(title="SPX BNS2 Calibrated Smiles")
smile.write_image(assets_path("spx_vol_surface_bns2.png"), width=1200)
33 changes: 33 additions & 0 deletions docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,39 @@ condition holds. The
`feller_enforce` flag (default `True`) that imposes this as a hard inequality constraint
during optimisation.

## Forward Space

Forward space is the unit-free convention in which option prices are
normalised by the forward price.

For a call $C$ and put $P$ with strike $K$, maturity $T$, and forward $F$,
the forward-space prices are

\begin{equation}
c = \frac{C}{F}, \qquad p = \frac{P}{F}
\end{equation}

Forward-space prices are dimensionless and depend only on the
[log-strike](#log-strike) $k = \log(K/F)$, the implied volatility,
and the time to maturity. They are the natural output of Fourier-based
pricers and of [Black pricing](api/options/black.md).

The conversion to quote-currency prices is a single multiplication by $F$:

\begin{equation}
C = c\, F, \qquad P = p\, F
\end{equation}

Quantflow uses forward space everywhere downstream of the input layer.
The `inverse` flag on [OptionPrice][quantflow.options.surface.OptionPrice]
only controls how the *input* `price` field is stored: for inverse
options (option premium paid in the underlying) it already is in forward
space; for non-inverse options (premium paid in the quote currency) it
is the absolute quote-currency price and must be divided by $F$ to enter
forward space. The
[price_in_forward_space][quantflow.options.surface.OptionPrice.price_in_forward_space]
property handles both cases uniformly.

## Hurst Exponent

The Hurst exponent is a measure of the long-term memory of time series. The Hurst exponent is a measure of the relative tendency of a time series either to regress strongly to the mean or to cluster in a direction.
Expand Down
1 change: 1 addition & 0 deletions docs/tutorials/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ Step-by-step guides for common quantflow workflows.
|---|---|
| [Option Pricing](option_pricing.md) | Price a European option with the Black-Scholes and Heston-jump-diffusion models |
| [Volatility Surface](volatility_surface.md) | Fetch live option data, build an implied volatility surface, and calibrate Heston and jump-diffusion models |
| [SPX Volatility Surface](spx_vol_surface.md) | Build a 3D implied volatility surface for the S&P 500 from a Yahoo Finance option chain |
| [BNS Volatility Model](bns_calibration.md) | Calibrate the Barndorff-Nielsen and Shephard stochastic-volatility model to an implied volatility surface |
75 changes: 75 additions & 0 deletions docs/tutorials/spx_vol_surface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# SPX Volatility Surface

Build an implied volatility surface for the S&P 500 index from a Yahoo Finance
option chain, then calibrate a two-factor BNS model to it.

The [Yahoo][quantflow.data.yahoo.Yahoo] client fetches the full chain for a
ticker. To keep this tutorial offline and reproducible we load a snapshot from
a gzipped JSON fixture, but the code is identical to what you would run
against the live endpoint.

## Loading the chain

[Yahoo.loader_from_chain][quantflow.data.yahoo.Yahoo.loader_from_chain] turns
the raw chain dictionary into a
[VolSurfaceLoader][quantflow.options.surface.VolSurfaceLoader]. SPX options
are non-inverse (quoted in USD) and Yahoo does not provide forwards, so each
maturity's forward is recovered from put-call parity inside the loader.

Once the loader has the data, [surface()][quantflow.options.surface.GenericVolSurfaceLoader.surface]
builds the [VolSurface][quantflow.options.surface.VolSurface],
[bs()][quantflow.options.surface.VolSurface.bs] inverts each bid and ask
through Black-Scholes, and
[disable_outliers()][quantflow.options.surface.VolSurface.disable_outliers]
drops strikes with unrealistic implied vols.

## 3D surface

[plot3d()][quantflow.options.surface.VolSurface.plot3d] renders the
converged implied vols against moneyness and time to maturity.

[![SPX implied volatility surface](../assets/examples/spx_vol_surface.png)](../assets/examples/spx_vol_surface.png){target="_blank"}

## BNS2 calibration

A single-factor diffusive Heston struggles on SPX because the short-dated
skew is too steep to absorb with a single mean-reversion timescale.
[BNS2][quantflow.sp.bns.BNS2] adds a second Gamma-OU variance factor and
injects jumps directly into the variance process, with the leverage parameter
mirroring those jumps into the log-price.

[BNS2Calibration][quantflow.options.calibration.bns.BNS2Calibration] fits
nine parameters with both factors sharing the same Gamma stationary marginal,
following the BNS superposition-of-OU construction. See the
[BNS tutorial](bns_calibration.md) for the full parameterisation and the
rationale behind tying $(\theta, \beta)$.

The initial parameters seed a fast factor ($\kappa = 20$) and a slow factor
($\kappa = 0.3$). Both leverages start negative to reflect the persistent
equity-style downside skew across the term structure. Residuals are scored in
implied-vol space ([ResidualKind.IV][quantflow.options.calibration.base.ResidualKind])
to weight the wings comparably to the ATM region.

### Calibrated parameters

--8<-- "docs/examples/output/spx_vol_surface.out"

[![SPX BNS2 calibrated smile](../assets/examples/spx_vol_surface_bns2.png)](../assets/examples/spx_vol_surface_bns2.png){target="_blank"}

The weight collapses almost entirely onto the fast factor, so $v_0$ for bns1
sits close to the ATM variance read off the 3D surface and bns2 contributes
only marginally to the variance level. The slow factor instead carries the
stronger negative leverage ($\rho \approx -0.84$ against the fast factor's
$\rho \approx -0.43$): its low $\kappa$ keeps jumps persistent in the
log-price, which is what shapes the long-dated downside skew. Both factors
share the same BDLP intensity and jump decay by construction.

The remaining short-maturity gap is structural to BNS, as discussed in the
[BNS tutorial](bns_calibration.md): jumps live in the variance process, so
the log-price wings are bounded by the variance jumps scaled by $|\rho_i|$.

## Code

```python
--8<-- "docs/examples/spx_vol_surface.py"
```
1 change: 0 additions & 1 deletion docs/tutorials/volatility_surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,4 +248,3 @@ The calibrated parameter vector for the jump-diffusion model is:
| `jump intensity` | Jump arrival rate (jumps per year) |
| `jump variance` | Variance of a single jump |
| `jump asymmetry` | Asymmetry of the jump distribution ([DoubleExponential][quantflow.utils.distributions.DoubleExponential]) |

2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ nav:
- Federal Reserve: api/data/fed.md
- Financial Modeling Prep: api/data/fmp.md
- FRED: api/data/fred.md
- Yahoo: api/data/yahoo.md
- Options:
- api/options/index.md
- Black-Scholes: api/options/black.md
Expand Down Expand Up @@ -112,6 +113,7 @@ nav:
- CIR Process: tutorials/cir.md
- Option Pricing: tutorials/option_pricing.md
- Pricing Method Comparison: tutorials/pricing_method_comparison.md
- SPX Volatility Surface: tutorials/spx_vol_surface.md
- Volatility Surface: tutorials/volatility_surface.md
- Theory:
- theory/index.md
Expand Down
Loading
Loading