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
3 changes: 2 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ applyTo: '/**'

* The documentation for quantflow is available at `https://quantflow.quantmid.com`
* Documentation is built using [mkdocs](https://www.mkdocs.org/) and stored in the `docs/` directory. The documentation source files are written in markdown format.
* Do not use em dashes (—) in documentation files or docstrings. Use colons, parentheses, or restructure the sentence instead.
* Do not use dashes (em dashes, en dashes, or hyphens used as dashes) in documentation files or docstrings. Use colons, parentheses, or restructure the sentence instead.
* Math in documentation and docstrings: always use `\begin{equation}...\end{equation}` for any formula or equation. Use `$...$` only for brief inline references to variables (e.g. $F$, $K$). Do not use `$$...$$`, `` `...` ``, or RST syntax (`.. math::`, `:math:`).
* 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)`).
* To rebuild doc examples run `uv run ./dev/build-examples` — runs all scripts in `docs/examples/` and writes their output to `docs/examples_output/`
Expand Down
96 changes: 96 additions & 0 deletions .github/instructions/tutorial.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
name: quantflow-tutorial-instructions
description: 'Instructions for tutorial in quantflow'
applyTo: '/docs/tutorials/**,/docs/examples/**'
---



# Tutorial Instructions

## File locations

- Tutorial pages: `docs/tutorials/<name>.md`
- Example scripts: `docs/examples/<name>.py`
- Generated images: `docs/assets/examples/<name>.png`
- Script stdout captured to: `docs/examples/output/<name>.out`
- Every new tutorial must be added to the `nav` section of `mkdocs.yml` under `Tutorials`.
- Update `docs/tutorials/index.md` with a row in the summary table.

## Building

- Build a single example: `uv run python docs/examples/<name>.py`
- Build all examples and capture output: `make docs-examples`
- Preview the docs locally: `uv run mkdocs serve`

## Tutorial page structure

Each tutorial markdown file should follow this order:

1. **H1 title** — the subject, not "Tutorial on X".
2. **One-paragraph introduction** — what the tutorial demonstrates and why it is useful.
Link to the relevant API classes using `[ClassName][fully.qualified.path]`.
3. **Sections** (H2) — cover the concept first, then show usage, then show results.
Use H3 subsections for variants (different parameter regimes, maturities, etc.).
4. **Code section** — always the last H2. Embed the full example script with:
````
```python
--8<-- "docs/examples/<name>.py"
```
````
If the script prints structured output, embed it too:
````
```
--8<-- "docs/examples/output/<name>.out"
```
````

## Example scripts

- Each script must be self-contained and runnable with `uv run python docs/examples/<name>.py`.
- Place shared helpers in `docs/examples/_utils.py` — do not duplicate utility code.
- Use `assets_path(filename)` from `_utils.py` to get the correct path when saving images.
- Use `plotly` for all charts. Save to PNG with `fig.write_image(assets_path(...), width=900, height=500)`.
Use `width=1600, height=800` for side-by-side subplot layouts.
- When overlaying an analytical curve with a numerical result (e.g. PDF from characteristic
function), plot the analytical result as a solid line and the numerical result as circle
markers (`mode="markers", marker=dict(symbol="circle")`). This makes the discretization
points visible and the two series easy to distinguish.
- Do not `print` raw numbers — emit only what belongs in the captured `.out` file.
- Scripts must produce no warnings when run cleanly (fix the root cause, e.g. avoid `x=0`
in domains where the PDF is singular).
- The `build_examples` helper in `_utils.py` runs every non-underscore script and captures
stdout; keep scripts idempotent and deterministic.

## Charts

- Embed images with a clickable link that opens full-size in a new tab:
```markdown
[![Alt text](../assets/examples/<name>.png)](../assets/examples/<name>.png){target="_blank"}
```
- Write a one-sentence caption above each image explaining what the reader should observe.

## Math

Follow the math conventions in `copilot-instructions.md`:

- Use `\begin{equation}...\end{equation}` for standalone formulas.
- Use `\begin{equation}\begin{aligned}...\end{aligned}\end{equation}` for multi-line systems.
- Use `$...$` only for brief inline variable references.
- Use `\Phi` for the characteristic function and `\phi` for the characteristic exponent.

## Cross-references

- Link API symbols with `[ClassName][fully.qualified.module.ClassName]`.
- Link to theory pages with relative markdown links: `[Option Pricing](../theory/option_pricing.md)`.
- Link to the glossary for concept definitions rather than re-defining them inline.
- Link to the bibliography for external references: `[Carr-Madan](../bibliography.md#carr_madan)`.

## What not to include

- Do not explain implementation details that belong in docstrings.
- Do not reproduce equations already in the API reference — link to them instead.
- Do not add a summary or "next steps" section unless the tutorial is part of a series.
- Do not use math notation (`$...$`, `\begin{equation}`, etc.) in any heading (H1–H4).
Math does not render in the table of contents — write headings in plain English instead
(e.g. "Short horizon" not "Short horizon ($t = 0.5$)").
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@readme.md
@.github/copilot-instructions.md
@.github/instructions/makefile.instructions.md
@.github/instructions/tutorial.instructions.md
100 changes: 100 additions & 0 deletions docs/examples/cir_pdf_comparison.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""CIR process: compare analytical PDF with PDF from characteristic function."""

import numpy as np
import plotly.graph_objects as go

from docs.examples._utils import assets_path
from quantflow.sp.cir import CIR


def make_figure(cir: CIR, t: float, n: int = 128) -> go.Figure:
m = cir.marginal(t)
x = np.linspace(1e-6, m.mean() + 4 * float(m.std()), 300)

pdf_analytical = cir.analytical_pdf(t, x)
pdf_cf = m.pdf_from_characteristic(n, simpson_rule=True)

fig = go.Figure()
fig.add_trace(
go.Scatter(
x=x,
y=pdf_analytical,
mode="lines",
name="Analytical PDF",
line=dict(color="#1f77b4", width=2),
)
)
fig.add_trace(
go.Scatter(
x=pdf_cf.x,
y=pdf_cf.y,
mode="markers",
name="PDF from characteristic function",
marker=dict(color="#ff7f0e", size=6, symbol="circle"),
)
)
fig.update_layout(
title=(
f"CIR PDF at t={t}"
f" (κ={cir.kappa}, θ={cir.theta}, σ={cir.sigma}, x₀={cir.rate})"
),
xaxis_title="x",
yaxis_title="probability density",
legend=dict(x=0.6, y=0.95),
)
return fig


def make_cf_figure(cir: CIR, t: float, n: int = 512) -> go.Figure:
m = cir.marginal(t)
max_frequency = float(np.asarray(m.frequency_range().ub).flat[0])
u = np.linspace(0, max_frequency, n)
cf = cir.characteristic(t, u)

fig = go.Figure()
fig.add_trace(
go.Scatter(
x=u,
y=np.abs(cf),
mode="lines",
name="|Φ(u)|",
line=dict(color="#1f77b4", width=2),
)
)
fig.add_trace(
go.Scatter(
x=u,
y=cf.real,
mode="lines",
name="Re[Φ(u)]",
line=dict(color="#ff7f0e", width=2, dash="dash"),
)
)
fig.add_vline(
x=max_frequency,
line=dict(color="red", dash="dot", width=1),
annotation_text="max_frequency",
annotation_position="top left",
)
fig.update_layout(
title=(
f"CIR characteristic function at t={t}"
f" (κ={cir.kappa}, θ={cir.theta}, σ={cir.sigma}, x₀={cir.rate})"
),
xaxis_title="u",
yaxis_title="Φ(u)",
)
return fig


if __name__ == "__main__":
cir = CIR(kappa=1.0, theta=0.5, sigma=0.8, rate=3.0)

fig1 = make_figure(cir, t=0.5)
fig1.write_image(assets_path("cir_pdf_t05.png"), width=900, height=500)

fig2 = make_figure(cir, t=2.0)
fig2.write_image(assets_path("cir_pdf_t20.png"), width=900, height=500)

fig3 = make_cf_figure(cir, t=2.0)
fig3.write_image(assets_path("cir_cf_t20.png"), width=900, height=500)
9 changes: 6 additions & 3 deletions docs/examples/vol_surface_heston_calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from docs.examples._utils import assets_path, print_model
from quantflow.options.heston_calibration import HestonCalibration
from quantflow.options.pricer import OptionPricer
from quantflow.options.pricer import OptionPricer, OptionPricingMethod
from quantflow.options.surface import VolSurfaceInputs, surface_from_inputs
from quantflow.sp.heston import Heston

Expand All @@ -14,7 +14,10 @@
surface.disable_outliers()

# Create a Heston pricer with initial parameters
pricer = OptionPricer(model=Heston.create(vol=0.5, kappa=1, sigma=0.8, rho=0))
pricer = OptionPricer(
model=Heston.create(vol=0.5, kappa=1, sigma=0.8, rho=0),
method=OptionPricingMethod.COS,
)

# Set up the calibration, dropping the first (very short) maturity
calibration: HestonCalibration[Heston] = HestonCalibration(
Expand All @@ -27,6 +30,6 @@
print_model(calibration.model)

# Plot the calibrated smile for all maturities and save as PNG
fig = calibration.plot_maturities(max_moneyness_ttm=1.5, support=101)
fig = calibration.plot_maturities(max_moneyness=1.5, support=101)
fig.update_layout(title="Heston Calibrated Smiles")
fig.write_image(assets_path("heston_calibrated_smile.png"), width=1200)
2 changes: 1 addition & 1 deletion docs/examples/vol_surface_hestonj_calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@
print_model(calibration.model)

# Plot the calibrated smile for all maturities and save as PNG
fig = calibration.plot_maturities(max_moneyness_ttm=1.5, support=101)
fig = calibration.plot_maturities(max_moneyness=1.5, support=101)
fig.update_layout(title="HestonJ Calibrated Smiles")
fig.write_image(assets_path("hestonj_calibrated_smile.png"), width=1200)
6 changes: 6 additions & 0 deletions docs/javascripts/mathjax.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ window.MathJax = {
processHtmlClass: "arithmatex"
}
};

document$.subscribe(() => {
MathJax.startup.promise.then(() => {
MathJax.typesetPromise();
});
});
95 changes: 95 additions & 0 deletions docs/tutorials/cir.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# CIR Process

This tutorial shows how to use the
[CIR][quantflow.sp.cir.CIR] (Cox-Ingersoll-Ross) model and validates
the analytical [marginal PDF][quantflow.sp.cir.CIR.analytical_pdf] against
the PDF recovered from the [characteristic function][quantflow.sp.cir.CIR.characteristic_exponent].

## The model

The CIR process is a mean-reverting square-root diffusion with three parameters:

| Parameter | Description |
|---|---|
| `kappa` | Mean-reversion speed |
| `theta` | Long-run mean |
| `sigma` | Volatility of volatility |
| `rate` | Initial value $x_0$ |

```python
from quantflow.sp.cir import CIR

cir = CIR(kappa=2.0, theta=0.5, sigma=0.8, rate=1.0)
print(cir.feller_condition) # positive: process stays strictly positive
print(cir.is_positive)
```

The process stays strictly positive when the Feller condition holds:

\begin{equation}
2\kappa\theta \geq \sigma^2
\end{equation}

## Analytical moments

The marginal distribution at time $t$ has closed-form mean and variance,
accessible via the [marginal][quantflow.sp.cir.CIR.marginal]:

```python
m = cir.marginal(1.0)
print(m.mean()) # analytical mean
print(m.variance()) # analytical variance
```

## PDF comparison

The marginal PDF has two independent routes to the same result:

* **Analytical**: the [scaled non-central chi-squared][quantflow.sp.cir.CIR.analytical_pdf]
transition density in closed form.
* **Characteristic function**: numerical inversion of $\Phi = e^{-\phi}$ via
[pdf_from_characteristic][quantflow.utils.marginal.Marginal1D.pdf_from_characteristic].

The charts below overlay both for a CIR process with
$\kappa=1$, $\theta=0.5$, $\sigma=0.8$, $x_0=3$, starting well above the long-run mean
to make the mean-reversion clearly visible across time horizons.

### Short horizon

At $t = 0.5$ the distribution is still centred near the initial value $x_0$:

[![CIR PDF t=0.5](../assets/examples/cir_pdf_t05.png)](../assets/examples/cir_pdf_t05.png){target="_blank"}

### Long horizon

At $t = 2.0$ the distribution has mean-reverted toward $\theta = 0.5$ and the
inversion shows visible oscillations:

[![CIR PDF t=2.0](../assets/examples/cir_pdf_t20.png)](../assets/examples/cir_pdf_t20.png){target="_blank"}

The oscillations are a Gibbs phenomenon. The CIR density has a cusp at the
origin: near $x = 0$ it grows as $x^{q/2}$ where $q = 2\kappa\theta/\sigma^2 - 1$.
When $q < 1$ the characteristic function decays algebraically as $u^{-(1+q/2)}$
rather than exponentially. For these parameters $q \approx 0.56$, so the
integral is still non-negligible when it gets truncated.

At $t = 0.5$ the mean is nearly three standard deviations from zero, so the cusp
is invisible and the inversion is accurate. By $t = 2$ the process has drifted
to within 1.4 standard deviations of the origin and the cusp affects the result.

For CIR with $q < 1$ the analytical PDF is the right tool. The inversion is
confirmed by the characteristic function plot below.

## Characteristic function

The plot below shows $|\Phi(u)|$ and $\text{Re}[\Phi(u)]$ at $t=2$. The
magnitude is still around $0.05$ at the truncation point, confirming that the
integral is cut off before it decays to zero:

[![CIR characteristic function t=2.0](../assets/examples/cir_cf_t20.png)](../assets/examples/cir_cf_t20.png){target="_blank"}

## Code

```python
--8<-- "docs/examples/cir_pdf_comparison.py"
```
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ nav:
- Types: api/utils/types.md
- Tutorials:
- tutorials/index.md
- CIR Process: tutorials/cir.md
- Option Pricing: tutorials/option_pricing.md
- Pricing Method Comparison: tutorials/pricing_method_comparison.md
- Volatility Surface: tutorials/volatility_surface.md
Expand Down
Loading
Loading