diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 625080f..7ec3f14 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -45,25 +45,3 @@ applyTo: '/**' * 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 * mkdocs documentation pages live in `docs/api/options/` — do not mix these two locations - -## Releasing - -The release procedure is fully driven by `make release` and the `release.yml` workflow: - -1. Bump `version` in `pyproject.toml` to the new release version. -2. Add a `## vX.Y.Z` section to `docs/release-notes.md` with the notes for the release. The header text is matched verbatim by the workflow's `awk` extractor, so it must be `## vX.Y.Z` exactly (no trailing dash, no title after the version). -3. Commit and merge to `main`. -4. From `main`, run `make release` — it reads the version from `pyproject.toml`, prompts for confirmation, then creates an annotated `vX.Y.Z` tag and pushes it. -5. The tag push triggers `.github/workflows/release.yml`, which runs lint and tests, publishes the package to PyPI (`make publish`), and posts the extracted `## vX.Y.Z` section as the GitHub Release body. - -Do not publish to PyPI manually or via the old `head_commit.message == 'release'` flow — the tag-triggered workflow is the only supported path. - -### Release-notes conventions - -The `## vX.Y.Z` section in `docs/release-notes.md` must follow these conventions, since the same content is rendered both on the docs site and as the GitHub Release body: - -* Open with a one-paragraph summary describing the theme of the release. If the release contains breaking changes, point readers to the **Breaking changes** section in that paragraph. -* Group entries under H3 subsections in this order: `### Breaking changes`, `### New features`, `### Improvements and fixes`, `### Documentation and assets`. Omit any subsection that has no entries. -* Every PR reference must be a markdown link of the form `[#NN](https://github.com/quantmind/quantflow/pull/NN)`. Never write a bare `(#NN)` — both readers (docs and GitHub) benefit from the explicit URL, and GitHub's auto-linking only works in some contexts. When a single entry references multiple PRs, list them comma-separated inside one set of parentheses, each as its own markdown link. -* Build the PR list by running `git log vPREV..HEAD --oneline` against the previous release tag and following each squashed-merge commit back to its PR. Cross-check with `gh pr list --state merged --base main` for any PRs merged since the previous tag. -* End the section with a `[Full changelog](https://github.com/quantmind/quantflow/compare/vPREV...vX.Y.Z)` link comparing the new tag against the previous one. diff --git a/.github/instructions/release.instructions.md b/.github/instructions/release.instructions.md new file mode 100644 index 0000000..1843cd6 --- /dev/null +++ b/.github/instructions/release.instructions.md @@ -0,0 +1,48 @@ +# Release Instructions + +Releases are driven by `v*` git tags. Pushing a tag triggers +`.github/workflows/release.yml`, which runs lint and the test suite, publishes +the package to PyPI (`make publish`), then extracts the matching `## vX.Y.Z` +section from `docs/release-notes.md` and publishes it as the GitHub Release +body. + +## Cutting a release + +1. Bump `version` in `pyproject.toml` to the new release version. +2. Add a `## vX.Y.Z` section at the top of `docs/release-notes.md` with the + notes for the release. The header text is matched verbatim by the + workflow's `awk` extractor, so it must be `## vX.Y.Z` exactly (no trailing + dash, no title after the version). The release workflow fails if this + section is missing. +3. Commit and merge to `main`; let the `build` workflow pass. +4. From `main`, run `make release` — it reads the version from + `pyproject.toml`, asks for confirmation, then creates an annotated `vX.Y.Z` + tag and pushes it. The `release` workflow takes it from there. + +Do not publish to PyPI manually, and do not revive the old +`head_commit.message == 'release'` flow — the tag-triggered workflow is the +only supported path. + +## Release-notes conventions + +The `## vX.Y.Z` section in `docs/release-notes.md` is rendered both on the +docs site and as the GitHub Release body, so it must follow these conventions: + +- Open with a one-paragraph summary describing the theme of the release. If + the release contains breaking changes, point readers to the **Breaking + changes** section in that paragraph. +- Group entries under H3 subsections in this order: `### Breaking changes`, + `### New features`, `### Improvements and fixes`, + `### Documentation and assets`. Omit any subsection that has no entries. +- Every PR reference must be a markdown link of the form + `[#NN](https://github.com/quantmind/quantflow/pull/NN)` — never a bare + `(#NN)`. GitHub's auto-linking only works in some contexts, and the explicit + URL works everywhere. When one entry references multiple PRs, list them + comma-separated inside one set of parentheses, each as its own link. +- Build the PR list by running `git log vPREV..HEAD --oneline` against the + previous release tag and following each squashed-merge commit back to its + PR. Cross-check with `gh pr list --state merged --base main` for any PRs + merged since the previous tag. +- End the section with a + `[Full changelog](https://github.com/quantmind/quantflow/compare/vPREV...vX.Y.Z)` + link comparing the new tag against the previous one. diff --git a/CLAUDE.md b/CLAUDE.md index e3d1a55..663e229 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,5 @@ @readme.md @.github/copilot-instructions.md @.github/instructions/makefile.instructions.md +@.github/instructions/release.instructions.md @.github/instructions/tutorial.instructions.md diff --git a/docs/release-notes.md b/docs/release-notes.md index 31f85d7..07129dd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,94 @@ below maps to a tagged release on pushed, the matching section is extracted by `.github/workflows/release.yml` and published as the GitHub Release body. +## v0.9.0 + +Pricing-engine and calibration overhaul. `MaturityPricer` now evaluates call +prices and Greeks lazily at arbitrary log-strikes instead of carrying a +precomputed grid, Fourier pricers take a moneyness-based truncation parameter, +and the volatility-surface calibration can fit Black implied vols directly. +This release contains several API changes: see **Breaking changes** below. + +### Breaking changes + +**`MaturityPricer` reworked.** ([#59](https://github.com/quantmind/quantflow/pull/59)) + +- The precomputed `std`, `log_strike` and `call` arrays are gone. A + `MaturityPricer` now holds a single `pricing` field (an + `OptionPricingResult`) that evaluates call prices and Greeks on demand at + any log-strike. +- `moneyness` is now a method, `moneyness(log_strikes)`, not a cached array + property. The `time_value` and `intrinsic_value` array properties and the + `interp(...)` helper were removed; use `prices(log_strikes)` to get a + DataFrame of prices and implied vols on a chosen log-strike grid. + +**Fourier pricing truncation: `max_log_strike` → moneyness parameters.** +([#59](https://github.com/quantmind/quantflow/pull/59)) + +- `Marginal1D.call_option`, `call_option_carr_madan` and `call_option_lewis` + take `max_moneyness` (a multiple of the marginal standard deviation) + instead of `max_log_strike`. The COS path takes + `cos_moneyness_std_precision` instead. +- `OptionPricingResult.call_at(...)` is renamed `call_price(...)`, the + `method` field is removed, and a new abstract `call_greeks(log_strike)` + returns a `Greeks` namedtuple `(price, delta, gamma)`. + +**`OptionPricerBase.call_price` → `call_prices`.** +([#59](https://github.com/quantmind/quantflow/pull/59)) The method is now +vectorised: it takes arrays of times-to-maturity and log-strikes and prices +them in a single maturity-grouped call. + +**`DIVFMPricer` no longer builds a fixed moneyness grid.** +([#59](https://github.com/quantmind/quantflow/pull/59)) The +`max_moneyness_ttm` and `n` fields are removed; the fitted IV surface is +evaluated on demand through `OptionPricingResultDIVFM`. + +### New features + +- **Implied-vol calibration residuals.** New `ResidualKind` enum and a + `residual_kind` field on `VolModelCalibration`: set it to `ResidualKind.IV` + to fit the model to Black implied vols (recovered by inverting the model + price) rather than to forward-space prices. The IV residual is naturally + well-scaled across moneyness, so `moneyness_weight` is not applied in that + mode. ([#59](https://github.com/quantmind/quantflow/pull/59)) +- **Greeks from the pricing result.** `OptionPricingCosResult.call_greeks` + returns closed-form price, delta and gamma from the COS expansion; the + transform-based result derives delta and gamma by differentiating the call + grid; DIVFM uses finite differences on the fitted surface. + ([#59](https://github.com/quantmind/quantflow/pull/59)) +- **COS truncation control on `OptionPricer`.** New + `cos_moneyness_std_precision` field (default 12) sets the width of the COS + integration interval in standard deviations. + ([#59](https://github.com/quantmind/quantflow/pull/59)) + +### Improvements and fixes + +- Calibration residuals are now computed in a single vectorised pricing call. + Deep-wing strikes where the model price falls outside the no-arbitrage band + (so Newton fails to invert it) are masked out instead of poisoning the fit, + and a parameter set that fails to invert on more than half the options is + rejected with a large penalty. + ([#59](https://github.com/quantmind/quantflow/pull/59)) +- Calibration plots now evaluate the model on a fresh moneyness grid; + `plot(max_moneyness=...)` no longer accepts `None`. + ([#59](https://github.com/quantmind/quantflow/pull/59)) +- `OptionEntry.mid_price()` no longer caches through a private attribute. + ([#59](https://github.com/quantmind/quantflow/pull/59)) +- Stale Jupytext notebook mirrors under `notebooks/` removed. + ([#59](https://github.com/quantmind/quantflow/pull/59)) + +### Documentation and assets + +- New GitHub social-preview banner under `docs/assets/logos/png/`. + ([#59](https://github.com/quantmind/quantflow/pull/59)) +- `docs/api/options/black.md` and the volatility-surface calibration examples + updated for the new pricer API. + ([#59](https://github.com/quantmind/quantflow/pull/59)) +- The release procedure moved out of `.github/copilot-instructions.md` into + its own `.github/instructions/release.instructions.md`. + +[Full changelog](https://github.com/quantmind/quantflow/compare/v0.8.0...v0.9.0) + ## v0.8.0 Volatility-surface calibration overhaul. This release adds a two-factor BNS diff --git a/pyproject.toml b/pyproject.toml index b726e2c..3ad9c49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "quantflow" -version = "0.8.0" +version = "0.9.0" description = "quantitative analysis" authors = [ { name = "Luca Sbardella", email = "luca@quantmind.com" } ] license = "BSD-3-Clause" diff --git a/uv.lock b/uv.lock index d6a430d..34ba55a 100644 --- a/uv.lock +++ b/uv.lock @@ -712,7 +712,7 @@ name = "cuda-bindings" version = "12.9.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder" }, + { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/1f/a5/e9d37c10f6c27c9c65d53c6cd6d9763e1df99c004780585fc2ad9041fbe3/cuda_bindings-12.9.6-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2662f59db67d9aeaf8959c593c91f600792c2970cf02cae2814387fc687b115a", size = 7090971, upload-time = "2026-03-11T14:47:29.526Z" }, @@ -747,37 +747,37 @@ wheels = [ [package.optional-dependencies] cublas = [ - { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux'" }, ] cudart = [ - { name = "nvidia-cuda-runtime-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "sys_platform == 'linux'" }, ] cufft = [ - { name = "nvidia-cufft-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cufft-cu12", marker = "sys_platform == 'linux'" }, ] cufile = [ { name = "nvidia-cufile-cu12", marker = "sys_platform == 'linux'" }, ] cupti = [ - { name = "nvidia-cuda-cupti-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "sys_platform == 'linux'" }, ] curand = [ - { name = "nvidia-curand-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-curand-cu12", marker = "sys_platform == 'linux'" }, ] cusolver = [ - { name = "nvidia-cusolver-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cusolver-cu12", marker = "sys_platform == 'linux'" }, ] cusparse = [ - { name = "nvidia-cusparse-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cusparse-cu12", marker = "sys_platform == 'linux'" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "sys_platform == 'linux'" }, ] nvtx = [ - { name = "nvidia-nvtx-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-nvtx-cu12", marker = "sys_platform == 'linux'" }, ] [[package]] @@ -2321,7 +2321,7 @@ name = "nvidia-cudnn-cu12" version = "9.10.2.21" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cublas-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/fa/41/e79269ce215c857c935fd86bcfe91a451a584dfc27f1e068f568b9ad1ab7/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c9132cc3f8958447b4910a1720036d9eff5928cc3179b0a51fb6d167c6cc87d8", size = 705026878, upload-time = "2025-06-06T21:52:51.348Z" }, @@ -2333,7 +2333,7 @@ name = "nvidia-cufft-cu12" version = "11.3.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/1f/37/c50d2b2f2c07e146776389e3080f4faf70bcc4fa6e19d65bb54ca174ebc3/nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d16079550df460376455cba121db6564089176d9bac9e4f360493ca4741b22a6", size = 200164144, upload-time = "2024-11-20T17:40:58.288Z" }, @@ -2367,9 +2367,9 @@ name = "nvidia-cusolver-cu12" version = "11.7.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12" }, - { name = "nvidia-cusparse-cu12" }, - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-cublas-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cusparse-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/93/17/dbe1aa865e4fdc7b6d4d0dd308fdd5aaab60f939abfc0ea1954eac4fb113/nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0ce237ef60acde1efc457335a2ddadfd7610b892d94efee7b776c64bb1cac9e0", size = 157833628, upload-time = "2024-10-01T17:05:05.591Z" }, @@ -2383,7 +2383,7 @@ name = "nvidia-cusparse-cu12" version = "12.5.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/eb/eb/6681efd0aa7df96b4f8067b3ce7246833dd36830bb4cec8896182773db7d/nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d25b62fb18751758fe3c93a4a08eff08effedfe4edf1c6bb5afd0890fe88f887", size = 216451147, upload-time = "2024-11-20T17:44:18.055Z" }, @@ -3428,7 +3428,7 @@ wheels = [ [[package]] name = "quantflow" -version = "0.8.0" +version = "0.9.0" source = { editable = "." } dependencies = [ { name = "ccy" },