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 <name>: 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
This commit is contained in:
2026-06-24 10:56:39 +02:00
parent 92591e5dc0
commit 08d09df97d
+169 -1
View File
@@ -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 <name>`
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 <price-units>` 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 <name>` 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 <name>`,
`--macd`, `--real <SYMBOL>`, `--from <ms>`, `--to <ms>`, `--trace <name>` **in any
order**, each at most once. Semantics: `--harness <name>` 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 <name> --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 <name>` 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 <SYM>`) still parses
identically; the new combination `run --harness stage1-r --real <SYM> --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.