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
6 changes: 6 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,14 @@ applyTo: '/**'
* Do not use em 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:`).
* 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/`

## Pydantic models

* Always document Pydantic fields with `Field(description=...)` — never use a docstring below a field assignment
* Split long description strings across lines using implicit string concatenation rather than shortening the text

## Package structure

* Strategy runtime markdown descriptions (read by `load_description()` at runtime) live inside the package at `quantflow/options/strategies/docs/` — they must be inside the package to be accessible when the library is installed
Expand Down
5 changes: 5 additions & 0 deletions .github/instructions/makefile.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Makefile Conventions

- Keep all targets sorted alphabetically.
- targets should be separated by a one blank line only.
- Each target should have a one-line description, starting with `##`, that describes what the target does. This description is used by the `help` target to generate documentation for all targets.
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ jobs:
- uses: actions/checkout@v4
- name: install rops
uses: quantmind/rops/.github/actions/setup-rops@main
env:
GITHUB_TOKEN: ${{ secrets.TOKEN_DEPLOYMENT }}
- name: install taplo
run: rops tools update taplo
- name: Set up Python ${{ matrix.python-version }}
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/docker-multiarch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ jobs:
password: ${{ github.token }}
- name: install rops
uses: quantmind/rops/.github/actions/setup-rops@main
env:
GITHUB_TOKEN: ${{ secrets.TOKEN_DEPLOYMENT }}
- name: build amd64
run: rops docker build ${{ inputs.image-name }}
- name: push amd64
Expand All @@ -44,6 +46,8 @@ jobs:
password: ${{ github.token }}
- name: install rops
uses: quantmind/rops/.github/actions/setup-rops@main
env:
GITHUB_TOKEN: ${{ secrets.TOKEN_DEPLOYMENT }}
- name: build arm64
run: rops docker build ${{ inputs.image-name }}
- name: push arm64
Expand All @@ -64,5 +68,7 @@ jobs:
password: ${{ github.token }}
- name: install rops
uses: quantmind/rops/.github/actions/setup-rops@main
env:
GITHUB_TOKEN: ${{ secrets.TOKEN_DEPLOYMENT }}
- name: create manifest
run: rops docker manifest ${{ inputs.image-name }}
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
@readme.md
@.github/copilot-instructions.md
@.github/instructions/makefile.instructions.md
44 changes: 20 additions & 24 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,39 @@ help:
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'
@echo ================================================================================

.PHONY: docs
docs: ## build documentation
@cp docs/index.md readme.md
@uv run ./dev/build-examples
@uv run mkdocs build

.PHONY: docs-examples
docs-examples: ## Regenerate docs examples
@uv run ./dev/build-examples

.PHONY: docs-serve
docs-serve: ## serve documentation
@uv run mkdocs serve --livereload --watch quantflow --watch docs

.PHONY: install-dev
install-dev: ## Install development dependencies
@./dev/install

.PHONY: lint
lint: ## Lint and fix
@uv run ./dev/lint fix


.PHONY: lint-check
lint-check: ## Lint check only
@uv run ./dev/lint


.PHONY: install-dev
install-dev: ## Install development dependencies
@./dev/install

.PHONY: marimo
marimo: ## Run marimo for editing notebooks
@./dev/marimo edit

.PHONY: docs-png
docs-png: ## Regenerate PNG assets in docs/assets/ (requires Chrome via kaleido)
@for f in docs/examples_png/*.py; do uv run python $$f; done

.PHONY: docs
docs: ## build documentation
@cp docs/index.md readme.md
@uv run ./dev/build-examples
@uv run mkdocs build

.PHONY: docs-serve
docs-serve: ## serve documentation
@uv run mkdocs serve --livereload --watch quantflow --watch docs
.PHONY: outdated
outdated: ## Show outdated packages
uv tree --outdated

.PHONY: publish
publish: ## Release to pypi
Expand All @@ -47,11 +48,6 @@ publish: ## Release to pypi
tests: ## Unit tests
@./dev/test


.PHONY: outdated
outdated: ## Show outdated packages
uv tree --outdated

.PHONY: upgrade
upgrade: ## Upgrade dependencies
uv lock --upgrade
22 changes: 2 additions & 20 deletions dev/build-examples
Original file line number Diff line number Diff line change
@@ -1,28 +1,10 @@
#!/usr/bin/env python
"""Run all example scripts in docs/examples/ and capture their stdout to .out files."""
import subprocess
import sys
from pathlib import Path

out_dir = Path("docs/examples_output")
out_dir.mkdir(exist_ok=True)
from docs.examples._utils import build_examples

examples = sorted(Path("docs/examples").glob("*.py"))
failed = []

for script in examples:
out_file = out_dir / script.with_suffix(".out").name
print(f"running {script} -> {out_file}")
result = subprocess.run(
[sys.executable, str(script)],
capture_output=True,
text=True,
)
if result.returncode != 0:
print(f"FAILED: {script}\n{result.stderr}", file=sys.stderr)
failed.append(script)
else:
out_file.write_text(result.stdout)
failed = build_examples()

if failed:
sys.exit(1)
11 changes: 5 additions & 6 deletions dev/lint
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ ISORT_ARGS="-c"
BLACK_ARG="--check"
RUFF_ARG=""
TAPLO_ARG="format --check"
PACKAGES="quantflow quantflow_tests docs/examples"

if [ "$1" = "fix" ] ; then
ISORT_ARGS=""
Expand All @@ -15,12 +16,10 @@ fi

taplo ${TAPLO_ARG}
echo isort
uv run isort quantflow quantflow_tests ${ISORT_ARGS}
uv run isort ${PACKAGES} ${ISORT_ARGS}
echo black
uv run black quantflow quantflow_tests ${BLACK_ARG}
uv run black ${PACKAGES} ${BLACK_ARG}
echo ruff
uv run ruff check quantflow quantflow_tests ${RUFF_ARG}
uv run ruff check ${PACKAGES} ${RUFF_ARG}
echo mypy
uv run mypy quantflow
echo mypy tests
uv run mypy quantflow_tests --explicit-package-bases
uv run mypy ${PACKAGES}
Empty file added docs/__init__.py
Empty file.
6 changes: 5 additions & 1 deletion docs/api/options/calibration.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Vol Model Calibration

::: quantflow.options.calibration.OptionEntry

::: quantflow.options.calibration.VolModelCalibration

::: quantflow.options.calibration.HestonCalibration
::: quantflow.options.heston_calibration.HestonCalibration

::: quantflow.options.heston_calibration.HestonJCalibration
Binary file modified docs/assets/heston_calibrated_smile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/hestonj_calibrated_smile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file added docs/examples/__init__.py
Empty file.
36 changes: 36 additions & 0 deletions docs/examples/_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import subprocess
import sys
from pathlib import Path

from pydantic import BaseModel

EXAMPLE_DIR = Path(__file__).parent
OUT_DIR = EXAMPLE_DIR.parent / "examples_output"


def print_model(model: BaseModel) -> None:
"""Helper function to print a Pydantic model as pretty JSON"""
text = model.model_dump_json(indent=2)
text_data = ["", "```json", text, "```"]
print("\n".join(text_data))


def build_examples() -> list[Path]:
failed = []
OUT_DIR.mkdir(exist_ok=True)
for script in sorted(EXAMPLE_DIR.glob("*.py")):
if script.stem.startswith("_"):
continue
out_file = OUT_DIR / script.with_suffix(".out").name
print(f"running {script} -> {out_file}")
result = subprocess.run(
[sys.executable, str(script)],
capture_output=True,
text=True,
)
if result.returncode != 0:
print(f"FAILED: {script}\n{result.stderr}", file=sys.stderr)
failed.append(script)
else:
out_file.write_text(result.stdout)
return failed
32 changes: 17 additions & 15 deletions docs/examples/vol_surface_heston_calibration.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,35 @@
import json
from pathlib import Path

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

# Load a saved volatility surface snapshot and build the surface
with open("docs/examples/volsurface.json") as fp:
surface: VolSurface = surface_from_inputs(VolSurfaceInputs(**json.load(fp)))
surface = surface_from_inputs(VolSurfaceInputs(**json.load(fp)))

surface.bs()
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))

# Set up the calibration, dropping the first (very short) maturity and high-vol wings
calibration = HestonCalibration(
# Set up the calibration, dropping the first (very short) maturity
calibration: HestonCalibration[Heston] = HestonCalibration(
pricer=pricer,
vol_surface=surface.trim(len(surface.maturities) - 1),
moneyness_weight=1.0,
).remove_implied_above(quantile=0.95)
vol_surface=surface,
)

result = calibration.fit()
print(result.message)
params = calibration.get_params()
model = calibration.model
print(f"vol: {model.variance_process.rate**0.5:.4f}")
print(f"theta: {model.variance_process.theta**0.5:.4f}")
print(f"kappa: {model.variance_process.kappa:.4f}")
print(f"sigma: {model.variance_process.sigma:.4f}")
print(f"rho: {model.rho:.4f}")
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.update_layout(title="Heston Calibrated Smiles")

out_path = Path("docs/assets/heston_calibrated_smile.png")
fig.write_image(str(out_path), width=1200)
31 changes: 16 additions & 15 deletions docs/examples/vol_surface_hestonj_calibration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import json
from pathlib import Path

from quantflow.options.calibration import HestonJCalibration
from docs.examples._utils import print_model
from quantflow.options.heston_calibration import HestonJCalibration
from quantflow.options.pricer import OptionPricer
from quantflow.options.surface import VolSurface, VolSurfaceInputs, surface_from_inputs
from quantflow.sp.heston import HestonJ
Expand All @@ -26,21 +28,20 @@
)
)

# Set up the calibration, dropping the first (very short) maturity and high-vol wings
calibration = HestonJCalibration(
# Set up the calibration, dropping the first (very short) maturity
calibration: HestonJCalibration[DoubleExponential] = HestonJCalibration(
pricer=pricer,
vol_surface=surface.trim(len(surface.maturities) - 1),
moneyness_weight=1.0,
).remove_implied_above(quantile=0.95)
vol_surface=surface,
moneyness_weight=0.5,
)

result = calibration.fit()
print(result.message)
model = calibration.model
print(f"vol: {model.variance_process.rate**0.5:.4f}")
print(f"theta: {model.variance_process.theta**0.5:.4f}")
print(f"kappa: {model.variance_process.kappa:.4f}")
print(f"sigma: {model.variance_process.sigma:.4f}")
print(f"rho: {model.rho:.4f}")
print(f"jump intensity: {model.jumps.intensity:.4f}")
print(f"jump variance: {model.jumps.jumps.variance():.6f}")
print(f"jump asymmetry: {model.jumps.jumps.asymmetry():.4f}")
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.update_layout(title="HestonJ Calibrated Smiles")

out_path = Path("docs/assets/hestonj_calibrated_smile.png")
fig.write_image(str(out_path), width=1200)
6 changes: 5 additions & 1 deletion docs/examples/vol_surface_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@
inputs = surface.inputs(converged=True)
option_inputs = [i for i in inputs.inputs if isinstance(i, OptionInput)]
df = pd.DataFrame([i.model_dump() for i in option_inputs])
print(df[["maturity", "strike", "option_type", "bid", "ask", "iv_bid", "iv_ask"]].head(10).to_string(index=False))
print(
df[["maturity", "strike", "option_type", "bid", "ask", "iv_bid", "iv_ask"]]
.head(10)
.to_string(index=False)
)
Loading
Loading