From 08d09df97df20ab1121154fa0c836d32acbdee9e Mon Sep 17 00:00:00 2001 From: Brummel Date: Wed, 24 Jun 2026 10:56:39 +0200 Subject: [PATCH] spec: 0065 stage1-r iter-3 CLI surface (boss-signed) Amend the cycle-0065 spec with the "Iteration 3 - the CLI surface" design: make the Stage-1 R layer operable from the command line (the research loop's primary verb, ergonomic for Claude to drive), splitting the CLI/recording surface out of the originally-lumped iter-2. Design (forks derived under /boss, user-delegated "du entscheidest das Design", recorded on #117): - Promote vol_stop + risk_executor from test fixtures to public aura-engine composite-builders; the embedded stop generalizes to a StopRule{Fixed,Vol} enum (a C11 structural axis), vol_stop the volatility-defined default. - aura run --harness : a compile-time selector (sma|macd|stage1-r) over Rust-authored built-ins - running, not wiring (C9/C17 intact: no runtime registry, no DSL). Replaces the run literal-slice arms with a parse_run_args tokenizer so --harness/--real/--trace compose orthogonally; --macd kept as an alias. - The stage1-r harness fans one bias into both SimBroker (pip) and the RiskExecutor (R), so one RunReport carries both yardsticks honestly; the run path folds summarize_r -> RunMetrics.r (additive, skip_serializing_if keeps pip-only/legacy JSON byte-unchanged), round_trip_cost=0.0 (Stage-1 is frictionless; a --cost knob is a follow-on). - An r_equity tap (LinComb of cum_realized_r+unrealized_r) persists via an extended persist_traces and renders through the existing aura chart --tap - no new viewer code. Scope line: "operable from the CLI" = run+inspect a Rust-authored harness (the #109 (a) reading the contracts settle), not authoring topology in the CLI; the broad #109 meta-level stays a separate future milestone. Deferred follow-ons: R-sweep families + rank-by-sqn, rich R-record charting, the bias_sign_flips metric rename (never carried out in iter-0; field is still exposure_sign_flips). Verified before signing: grounding-check PASS (all substrate + post-patch current-code claims ratified by green tests) - the autonomous /boss signature; plus an advisory 5-lens spec-skeptic panel (criterion/invariants/scope SOUND; ambiguity + plan-readiness BLOCKs resolved inline: the argv tokenizer, the 3-tap persist change, the round_trip_cost value, the dual-tap arg provenance), re-grounded after the patch. refs #129 #117 --- docs/specs/0065-stage1-r-signal-quality.md | 170 ++++++++++++++++++++- 1 file changed, 169 insertions(+), 1 deletion(-) diff --git a/docs/specs/0065-stage1-r-signal-quality.md b/docs/specs/0065-stage1-r-signal-quality.md index 17e4cb9..33e0cc0 100644 --- a/docs/specs/0065-stage1-r-signal-quality.md +++ b/docs/specs/0065-stage1-r-signal-quality.md @@ -364,4 +364,172 @@ RED-first where behaviour is test-specifiable. Mandatory tests: `summarize_r`). **R-invariance is the load-bearing property pinned here.** - **Iteration 2:** the `Sizer` seam + the `RiskExecutor` composite (#128, Veto documented-not-built) + `summarize_r` enrichment (SQN, conviction terciles, - net-of-cost) + `RunMetrics.r` + the CLI/recording surface (#129) + tests 8, 10. + net-of-cost) + `RunMetrics.r` + tests 8, 10. (The `RiskExecutor`/`vol_stop` + composites land here as **integration-test fixtures**; the CLI/recording surface + originally lumped here is split to Iteration 3.) +- **Iteration 3 — the CLI surface (#129):** promote `vol_stop` + `risk_executor` + to public `aura-engine` composite-builders; add an `aura run --harness ` + selector and a `stage1-r` dual-tap harness; fold `summarize_r → RunMetrics.r` in + the run path; tap the R-equity series for `aura chart`. Full design below. + +## Iteration 3 — the CLI surface (#129) + +Iteration 2 shipped the node + metric layer (the `Sizer`, the +`RiskExecutor`/`vol_stop` composites as **integration-test fixtures**, +`summarize_r`, `RunMetrics.r`). Iteration 3 makes that layer **operable from the +command line** — the research loop's primary verb, ergonomic to drive from a shell +(decision log: #117, user-delegated). Authoring stays in Rust (C9/C17/C22): the CLI +*runs and inspects* a Rust-authored harness — it does **not** wire topology. The +broad meta-level CLI (#109: project-as-crate, composable orchestration) is a +separate future milestone, deliberately out of scope here. + +### The user-facing program (the acceptance evidence) + +What Claude (or a trader) types to score a strategy's signal quality in R: + +```console +$ aura run --harness stage1-r --real GER40 --from 1700000000000 --trace q1 +{"manifest":{ ... ,"broker":"risk-executor"}, + "metrics":{"total_pips":123.4,"max_drawdown": ... ,"exposure_sign_flips":7, + "r":{"expectancy_r":0.42,"sqn":1.85,"n_trades":31,"win_rate":0.55, + "avg_win_r":1.9,"avg_loss_r":-0.8,"profit_factor":1.7, + "max_r_drawdown":3.2,"n_open_at_end":1,"net_expectancy_r":0.42, + "conviction_terciles_r":[0.1,0.4,0.7]}}} + +$ aura chart q1 --tap r_equity > r_curve.html # by-trade R-equity curve, existing renderer +``` + +The R block rides in the existing `RunReport` JSON via `RunMetrics.r` +(`#[serde(skip_serializing_if = "Option::is_none")]`), so it appears exactly when +an R-producing harness ran and is absent otherwise — old `runs.jsonl` and pip-only +runs stay byte-unchanged. JSON (not a pretty text block) is the surface: it is the +most machine-ergonomic for Claude and is consumed unchanged by `runs family` and +`chart`. `--real` / `--trace` compose orthogonally with `--harness`. + +Two example details pinned: the pip key is `exposure_sign_flips` — the secondary +change (a) renaming it to `bias_sign_flips` was **not** carried out (the field is +still `exposure_sign_flips` in `report.rs`; the rename is deferred as a follow-on, +out of iter-3 scope), so the example uses the as-built key. And +`net_expectancy_r == expectancy_r` here because the run path folds with +`round_trip_cost = 0.0` — Stage-1 R is frictionless signal quality (costs are the +Stage-2 realistic-broker concern, C10 A-side); a `--cost ` knob that +threads a nonzero cost into the existing `summarize_r` param is a deliberate +follow-on, not iter-3. + +The `stage1-r` harness wires the **same bias stream into both** a `SimBroker` (the +existing pip curve) **and** the `RiskExecutor` (the new R outcomes), so one report +carries both yardsticks honestly — a direct pip-vs-R comparison of one signal, no +meaningless zeros — and the diff from the shipped pip `sample_harness` is exactly +the added risk branch. + +### Before → after: the load-bearing changes (secondary) + +**(f) `aura run --harness ` selector (`aura-cli/src/main.rs`).** Today `run` +is dispatched by an **exhaustive literal-slice match** (`["run"]`, +`["run","--macd"]`, `["run","--trace",n]`, `["run","--macd","--trace",n]`, +`["run","--real", rest@..]`) — there is no `run` tail parser, so the orthogonal +flag composition the worked example needs (`--harness` + `--real` + `--from` + +`--trace` in one call) is unreachable as-is. iter-3 therefore **replaces the `run` +literal arms with a `parse_run_args` tokenizer** (the same shape as the existing +`parse_real_args` / `parse_chart_args` helpers): it accepts `--harness `, +`--macd`, `--real `, `--from `, `--to `, `--trace ` **in any +order**, each at most once. Semantics: `--harness ` resolves a +**compile-time** `match name { "sma" => …, "macd" => …, "stage1-r" => … }` over +Rust-authored built-ins — a fixed enumeration, **not** a runtime node registry and +**not** a DSL (C9/C17); `--harness sma` is the default (today's bare `run` +behaviour); `--macd` is a back-compat alias for `--harness macd` (mutually +exclusive with `--harness`); `--from`/`--to` are legal only with `--real`. Every +invocation the current literal arms accept must still parse identically (the +existing `cli_run` tests stay green); the tokenizer only *adds* the +`--harness`×`--real`×`--trace` combinations. + +**(g) Promote `vol_stop` + `risk_executor` to shippable composite-builders +(`aura-engine`).** Both exist today only as integration-test fixtures +(`tests/vol_stop_composite.rs`, `tests/risk_executor.rs`); iter-3 lifts them to +public `aura-engine` functions (composites need `GraphBuilder`, which `aura-std` +lacks) so the CLI harness can wire them. Behaviour preserved: each public fn IS the +fixture's body verbatim (`vol_stop` as-is; `risk_executor`'s internal +`stop-rule → Sizer → PositionManagement` shape kept, the embedded stop generalized +from a hardcoded `FixedStop` to the `StopRule` match); the two test fixtures each +**delete their local `fn`** and import + call the `aura_engine::` public fn (the +`ConstLongBias` producer stays local to the test). A new `aura-engine` module +(e.g. `src/composites.rs`, re-exported from `lib.rs`) is their home. + +```rust +// aura-engine — new public API, promoted from the test fixtures: +pub fn vol_stop(length: i64, k: f64) -> Composite; // k·√EMA(Δ²); role: price → stop_distance +pub enum StopRule { Fixed(f64), Vol { length: i64, k: f64 } } // the stop axis (C11 structural) +pub fn risk_executor(stop: StopRule, risk_budget: f64) -> Composite; +// roles: bias + price; embeds the chosen stop-rule (FixedStop and vol_stop BOTH expose +// price → stop_distance, so the `match` arm is the only difference); price fans to the +// stop-rule + position-management; exposes the dense PM R-record. The fixture's +// `risk_executor(d, b)` becomes `risk_executor(StopRule::Fixed(d), b)` — its tests carry +// over unchanged; the `stage1-r` harness passes `StopRule::Vol { length: 20, k: 2.0 }`, +// the volatility-defined default. +``` + +**(h) The `stage1-r` harness + the R fold in the run path (`aura-cli`).** A +`stage1_r_harness()` builder reuses the SMA-cross→`Bias` signal and fans the **one** +in-graph `bias` output to **three** consumers: `SimBroker` (pip equity), the +`risk_executor` (the dense R-record), and the existing exposure tap. **Pip metrics +are unchanged** — `summarize(equity, exposure)` reads the `SimBroker` equity tap and +the `Bias` exposure tap exactly as today's `sample_harness` does (this is the +already-tested output fan-out in `macd_strategy_blueprint`). **R metrics are the new +path** — the run handler drains the `risk_executor`'s dense R-record tap (a third, +independent tap), folds `summarize_r(&ledger, /*round_trip_cost*/ 0.0)` → `RMetrics`, +and sets `RunMetrics.r = Some(..)`. The fold runs only for an R-producing harness; +`--harness sma`/`macd` leave `RunMetrics.r = None` (the `skip_serializing_if` keeps +their JSON byte-unchanged). + +**(i) The R-equity tap + its persistence.** The harness sums the executor's +`cum_realized_r` + `unrealized_r` outputs through a `LinComb([1,1])` into an +`r_equity` series and taps it. Today `persist_traces(name, manifest, equity, exposure)` +is hardwired to exactly **two** taps; iter-3 extends it to thread the **third** +`r_equity` tap (a signature + drain-plumbing change — the one place this is *not* +free). The decimation needs no new arm: `reduce_for_tap` already defaults every +non-`exposure` tap to MinMax, which suits an equity-like curve (the same reducer +`equity` uses). The chart **viewer** (uPlot/HTML) is genuinely unchanged: `aura +chart --tap r_equity` resolves the named tap through the existing +`filter_to_tap` path. So "no new code" holds for the *renderer*; the persist helper +is the bounded plumbing change. + +### Components (iter-3) + +| Component | Crate / file | +|-----------|--------------| +| `vol_stop`, `risk_executor` + `StopRule` promoted to public composite-builders | `aura-engine` (a new `composites` module, re-exported from `lib.rs`; the two fixtures delete their local `fn` and call these) | +| `parse_run_args` tokenizer (replaces the `run` literal-slice arms) + `--harness ` selector + `stage1_r_harness()` + the R fold in the run path | `aura-cli/src/main.rs` | +| `r_equity` tap wiring + `persist_traces` extended to a third tap | `aura-cli/src/main.rs` (the harness builder + the persist helper) | + +### Testing strategy (iter-3) + +- **CLI smoke:** `aura run --harness stage1-r` (synthetic) emits a `RunReport` + whose `metrics.r` is `Some` with a finite `sqn` / `expectancy_r` and + `n_trades ≥ 1` — a `cli_run`-suite integration test (the existing home for CLI + tests). +- **Selector:** `--harness sma` ≡ today's default output; `--harness macd` ≡ the + `--macd` output (the alias holds); an unknown `--harness x` exits non-zero with a + usage error. +- **Flag composition (the new tokenizer):** every invocation the old literal arms + accepted (`run`, `run --macd`, `run --trace t`, `run --real `) still parses + identically; the new combination `run --harness stage1-r --real --trace t` + (which no literal arm could express) parses and runs; `--macd` together with + `--harness` is rejected, as is `--from` without `--real`. +- **Additive back-compat:** a pip run (`--harness sma`) emits **no** `r` key + (`skip_serializing_if`); the existing legacy-`runs.jsonl` deserialise test stays + green. +- **R-equity round-trip:** `aura run --harness stage1-r --trace t` then + `aura chart t --tap r_equity` produces non-empty HTML over the R-equity series. +- **Promotion non-regression:** the `vol_stop_composite` and `risk_executor` + fixture tests stay green after they are rewired to call the promoted public fns. + +### Acceptance criteria (iter-3) + +- Claude can run a Stage-1 R backtest and read its signal-quality metrics in **one + shell call** (`aura run --harness stage1-r [--real …]`) — the R block in the + `RunReport` JSON. +- The CLI **runs** a Rust-authored harness; it does not author or wire topology + (C9/C17/C22 intact — compile-time selector, no registry, no DSL). +- Pip-only and legacy runs are byte-unchanged on disk (`r` absent when no + R-record); determinism (C1) untouched. +- `cargo test --workspace` green; `cargo clippy --workspace --all-targets -- -D warnings` clean.