Applies the principle the user named: a node is a primitive only if it is NOT
DAG-expressible from other primitives; a function that needs a missing primitive
gets the primitive added, not fused away.
- Remove the fused VolStop node (it was pure feed-forward arithmetic).
- The volatility stop is now a composition of primitives, vol_stop(length, k):
k * Sqrt(Ema(Mul(d,d), length)), d = Sub(price, Delay(price,1)) -- a rolling
EWMA standard deviation. Built with GraphBuilder; proven end-to-end
(bootstraps + runs + emits k*sigma) in tests/vol_stop_composite.rs.
- Migrate the one VolStop caller (stage1_r_e2e.rs) off it: the R-is-stop-defined
test now contrasts a tight vs a wide FixedStop (two constant distances still
fold to different R, the property under test).
Uses the Mul + Sqrt primitives added in the prior commit. FixedStop stays the
only stop-rule primitive (a triggered constant). Full suite + clippy green.
refs #117#119
Iteration 1 of the Stage-1 "R-based signal quality" cycle (spec 0065): a
strategy's signal quality, measured in R on its unsized bias stream, feed-forward.
New discrete-trade machinery, NOT a SimBroker extension.
What lands:
- exposure -> bias (refs #126): the Exposure node/type/file/output-field renamed to
Bias (computation unchanged: clamp(signal/scale,-1,+1); the SEMANTICS change — the
output is the unsized strategy bias, sizing leaves the strategy). Scoped to the
semantic core; behaviour-preserving cosmetics are deferred (see below).
- stop-rule nodes (refs #119): VolStop = k*EMA(|price - prev_price|) (one fused node;
the volatility that defines 1R, close-to-close — true-range ATR deferred, needs
OHLC) + FixedStop (constant, the test fixture / fixed-vs-vol structural-axis sibling).
- PositionManagement (refs #127): the stateful heart. Latches the entry-cycle stop
distance as the FROZEN R-denominator (never re-read), marks/exits no-look-ahead like
SimBroker (a position earns from the next cycle; an exit realises against the cycle's
close), and emits ONE dense per-cycle R-record (C8). Stop-outs are NOT capped at -1R
(a gap through the stop realises R < -1 — the honest loss tail); R is computed
size-invariantly (size = (exit-entry)*dir / latched_dist cancels). The trade ledger
is the rows where closed_this_cycle; the R-equity is cum_realized + unrealized; a
position open at window end is the last row (open=true) — explicit, never silent MtM.
- summarize_r + RMetrics (refs #129): the post-run fold (NOT an in-graph node —
recorders drain post-run). E[R], win-rate, profit-factor, avg win/loss R, by-trade
max R-drawdown, n_open_at_end (window-end force-close). SQN / conviction terciles /
net-of-cost / RunMetrics.r are iteration 2.
Design adversarially hardened before implementation (12-juror refute panel; decisions
on #117). Ratified implementer deviations from the plan snippet, verified by hand:
- col-4 `direction` tracks the OPEN position at cycle end (window-end synthesis; names
the reopened leg on a reversal), falling back to the closed trade's dir only when
flat. summarize_r does not read col 4; the R-metrics are unaffected.
- .named("exposure") kept on the 4 previously-unnamed Bias nodes so the auto-derived
param-path stays exposure.scale (no manifest/dir-name drift) — the unnamed nodes
would otherwise flip to bias.scale.
- intra-doc links [`Exposure`] -> [`Bias`] in sim_broker/latch + the sample-model test
pin (cosmetic, behaviour-neutral; broken intra-doc links fail cargo doc).
- the E2E drives a full bootstrapped Harness (stronger than the plan's direct-node
drive — exercises the real cross-crate producer->consumer seam).
Deferred (behaviour-preserving, separate cosmetic pass — NOT this iteration):
RunMetrics.exposure_sign_flips / Metric::ExposureSignFlips, SimBroker/LongOnly
"exposure" input ports, the "exposure" tap/trace names, and the .named("exposure")
instance labels. Iteration 2: the Sizer seam, the RiskExecutor composite (+ the Veto
documented-seam), summarize_r enrichment, RunMetrics.r, and the CLI/recording surface.
Verification (orchestrator-run, not agent-claimed):
- cargo build --workspace: clean.
- cargo test --workspace: all green, 0 failed (incl. the new bias/stop_rule/
position_management unit tests, the summarize_r arithmetic tests, and the
stage1_r_e2e capstone + layout guard).
- cargo clippy --workspace --all-targets -- -D warnings: clean (exit 0).
- Keystone RED tests pass: no_lookahead_bias_exit_realises_the_held_move,
stop_out_is_not_capped_at_minus_one_r, no_gap_stop_is_exactly_minus_one_r,
reversal_closes_one_leg_and_reopens, open_at_window_end_is_carried_on_the_last_row.
refs #117#119#126#127#129