From 044d8b03f6cc88a696e17a5e7c3964b3e1a07514 Mon Sep 17 00:00:00 2001 From: Matt Spinola Date: Sun, 2 Aug 2026 23:22:16 -0400 Subject: [PATCH] Release 0.2.0 (deployment monitoring; the orchestrate decay stack) 0.1.0 published 2026-07-23 and main has been 15 commits ahead ever since, carrying the entire half of the framework that runs AFTER a promotion. Nothing was installable from PyPI, which is the same shape as the crucible 0.4.0 problem: real work on main, a changelog that reads as if it shipped, and no tag. It also makes today's floor fix mean something. The pin was raised to crucible>=0.6.0 so a PyPI install could not resolve a crucible without Thresholds.monitor_arl0_years, but PyPI's crucible-stack did not contain the code that needs it. The built wheel now carries Requires-Dist: crucible>=0.6.0. Contents: orchestrate.decay + EdgeDecayTrigger, the EdgeBaseline frozen into the DeploymentLedger beside the DriftEnvelope, trade_r / trade_dates through the trigger seam, the --edge-decay and --arl0-years flags, docs/api-stability.md, and the public-API surface pins. Three changelog problems fixed while consolidating rather than published as they were: * Two separate ### Added blocks, because three PRs each prepended one, so the entries were filed under whichever heading happened to be nearest. * A ### Changed entry still warning that the crucible>=0.5.0 floor was "not yet true" and "must not be released before crucible 0.5.0 is on PyPI". Both conditions have since been met and the floor has moved twice more. The three steps are now collapsed into one entry that is true as written. * No ## [0.1.0] heading existed at all, so the published release had no record. Added with its upload date. __version__ said 0.1.0.dev0 against a published 0.1.0; now 0.2.0, matching pyproject. Verified the way the release workflow will: python -m build then twine check both artifacts PASSED, and the wheel contains orchestrate/decay.py and orchestrate/__main__.py. 407 tests pass, ruff clean. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 97 +++++++++++++++++++++++++------------- crucible_stack/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 66 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a14254..75a3cb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,25 @@ breaking change are governed by [docs/api-stability.md](docs/api-stability.md). ## [Unreleased] +## [0.2.0] - 2026-08-02 + +The deployment-monitoring release. 0.1.0 shipped the define / search / size / deploy +framework; this adds the half that runs *after* a promotion, and the API-stability +policy that says what any of it promises. + +`drift` already watched the equity **path**. This adds the per-trade **parameter**: +`orchestrate.decay` over crucible's edge monitor, `EdgeDecayTrigger`, an `EdgeBaseline` +frozen into the `DeploymentLedger` beside the `DriftEnvelope`, and the `--edge-decay` / +`--arl0-years` flags that drive them. The two monitors come apart in both directions, so +running only one leaves a real failure invisible. + +**`--edge-decay` is off by default, and the order matters.** `EdgeDecayTrigger` fails +open, so enabling it before a promotion has written a baseline makes every cycle fire and +re-optimize, and every promotion freezes a *new* envelope. The drift envelope would then +be discarded on every tick and never allowed to mean anything, which is the re-baselining +trap both monitors are built to refuse, arriving through the back door. Let one promotion +write a baseline first. + ### Added - **`--arl0-years`**, exposing the edge-decay CUSUM's false-alarm budget on the CLI. `EdgeDecayTrigger` has always taken a `Thresholds`, but `__main__` built it with none, @@ -26,28 +45,6 @@ breaking change are governed by [docs/api-stability.md](docs/api-stability.md). crucible falls back to `monitor_arl0_trades` and the flag does nothing). A non-positive budget raises, since it is a span of calendar time. -### Fixed -- **The edge monitor's firing-rate channel was dead through the orchestrator.** - `check_decay` built its `TradeLog` from bare floats (`TradeLog.from_arrays(trade_r)`), - while crucible derives the live firing rate from the log's `entry_date`. So - `live_trades_per_year` and `frequency_ratio` came back `None` on every cycle, for every - book, however carefully the baseline's own rate had been frozen at promotion. Two of the - monitor's three channels ran; the third reported itself off and nothing said why. - - It is the channel that matters most on its own, because neither of the others can see - the failure it covers: a signal that stops firing while the trades it still takes keep - their per-trade edge. Expectancy is unchanged, so the CUSUM stays quiet and the rolling - window reads full size, and annual R falls anyway because the opportunity set shrank. - - `check_decay` now takes `trade_dates`, `TriggerContext` carries them (refusing a length - mismatch against `trade_r`, which would mis-date the rate rather than fail), `run_cycle` - forwards them, and `EdgeDecayTrigger` passes them through. The CLI sources them from an - optional `trade_dates_since(since, params)` on the book. Absence stays legal and is now - *reported* rather than silent, since a book that cannot date its trades is still worth - monitoring on expectancy. No crucible change was needed: `TradeLog.from_arrays` has - accepted `entry_date` all along. - -### Added - **`crucible_stack.orchestrate.decay` and `EdgeDecayTrigger`**: the parameter-space counterpart to `drift`. `drift` watches the equity **path** (cumulative R and drawdown against the frozen envelope); this watches the per-trade **parameter** (expectancy, and @@ -101,14 +98,48 @@ breaking change are governed by [docs/api-stability.md](docs/api-stability.md). `_max_drawdown` remains as a deprecated alias. ### Changed -- **The `crucible` floor is raised to `>=0.5.0`** for the monitor API above. Unlike the - 0.3.0 constraint, this one is **not yet true**: the monitor is in crucible's - `[Unreleased]` and the newest published crucible is 0.4.0. This must not be released - before crucible 0.5.0 is on PyPI, or `import crucible_stack.orchestrate` raises for - anyone installing from PyPI. `tests/test_crucible_compat.py` gains two checks that name - the cause, including one asserting `edge_monitor` still has no parameter from which a - baseline could be rebuilt, since a future crucible relaxing that would silently make - `EdgeDecayTrigger` incapable of firing while every other test kept passing. -- `crucible>=0.3.0` (was `>=0.2.0`). The honest-N API this package depends on landed after - crucible's v0.2.0 tag, so the old constraint was satisfiable by a version that could not - actually satisfy it. The CI workaround that installed crucible from git is gone. +- **The `crucible` floor is `>=0.6.0`** (was `>=0.2.0` at 0.1.0). It moved three times + across this release and the intermediate notes are collapsed here rather than left + looking live: `>=0.3.0` for the honest-N API, `>=0.5.0` for `validation.monitor` + (`EdgeBaseline` / `edge_monitor`), and `>=0.6.0` for + `Thresholds.monitor_arl0_years`, which `--arl0-years` constructs. All three are true + as of this release; crucible 0.6.0 published 2026-08-02. + + The `>=0.5.0` step carried a warning that it was not yet satisfiable, which is why + the CI workaround that installed crucible from git is gone. The `>=0.6.0` step was + briefly *wrong* rather than aspirational, and that is the more useful lesson: the + flag merged while the pin still said `0.5.0`, whose `Thresholds` has no such field. + A floor is only ever exercised by the version you do NOT have, so with every local + checkout on 0.6.0 nothing failed. `tests/test_crucible_compat.py` now reads the + declared floor out of `pyproject.toml` and fails when it falls behind the APIs the + package uses, alongside its existing checks that the monitor surface is importable + and that `edge_monitor` still has no parameter from which a baseline could be + rebuilt (a future crucible relaxing that would silently make `EdgeDecayTrigger` + incapable of firing while every other test kept passing). + +### Fixed +- **The edge monitor's firing-rate channel was dead through the orchestrator.** + `check_decay` built its `TradeLog` from bare floats (`TradeLog.from_arrays(trade_r)`), + while crucible derives the live firing rate from the log's `entry_date`. So + `live_trades_per_year` and `frequency_ratio` came back `None` on every cycle, for every + book, however carefully the baseline's own rate had been frozen at promotion. Two of the + monitor's three channels ran; the third reported itself off and nothing said why. + + It is the channel that matters most on its own, because neither of the others can see + the failure it covers: a signal that stops firing while the trades it still takes keep + their per-trade edge. Expectancy is unchanged, so the CUSUM stays quiet and the rolling + window reads full size, and annual R falls anyway because the opportunity set shrank. + + `check_decay` now takes `trade_dates`, `TriggerContext` carries them (refusing a length + mismatch against `trade_r`, which would mis-date the rate rather than fail), `run_cycle` + forwards them, and `EdgeDecayTrigger` passes them through. The CLI sources them from an + optional `trade_dates_since(since, params)` on the book. Absence stays legal and is now + *reported* rather than silent, since a book that cannot date its trades is still worth + monitoring on expectancy. No crucible change was needed: `TradeLog.from_arrays` has + accepted `entry_date` all along. + +## [0.1.0] - 2026-07-23 + +First release. The define / search / size / deploy framework, strategies not +included. No changelog section was written at the time; this heading exists so the +published version is on the record. diff --git a/crucible_stack/__init__.py b/crucible_stack/__init__.py index 5ee2276..0b6de55 100644 --- a/crucible_stack/__init__.py +++ b/crucible_stack/__init__.py @@ -16,4 +16,4 @@ See ADR-0004 for the reasoning, and ADR-0003 for the deployment loop's design. """ -__version__ = "0.1.0.dev0" +__version__ = "0.2.0" diff --git a/pyproject.toml b/pyproject.toml index 1072b4f..1c84291 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "crucible-stack" -version = "0.1.0" +version = "0.2.0" description = "A framework for defining, searching, sizing and deploying trading strategies. Strategies not included." readme = "README.md" requires-python = ">=3.11"