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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user