feat(stage1-r): bias rename + stop-rule + position-management + summarize_r (iter 1)
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
This commit is contained in:
@@ -274,7 +274,7 @@ mod tests {
|
||||
use aura_core::{
|
||||
Cell, FieldSpec, Node, NodeSchema, PortSpec, PrimitiveBuilder, Scalar,
|
||||
};
|
||||
use aura_std::{Exposure, Recorder, Sma, Sub};
|
||||
use aura_std::{Bias, Recorder, Sma, Sub};
|
||||
use std::sync::mpsc;
|
||||
|
||||
/// A bare node whose only purpose is to back a `PrimitiveBuilder` in a test.
|
||||
@@ -331,7 +331,7 @@ mod tests {
|
||||
|
||||
/// A small, stable root harness exercising the four model elements: a bound
|
||||
/// source role (`price`, → a synthetic source node), a composite reference
|
||||
/// (`sma_cross`, → a `composites` entry), a plain node (`Exposure`), and a
|
||||
/// (`sma_cross`, → a `composites` entry), a plain node (`Bias`), and a
|
||||
/// sink (`Recorder`, `output: vec![]`). A deliberately minimal serializer
|
||||
/// fixture, independent of the CLI's built-in sample (`build_sample` in
|
||||
/// aura-cli — a richer nested `signals` graph since cycle 0036), kept small so
|
||||
@@ -343,11 +343,11 @@ mod tests {
|
||||
"sample",
|
||||
vec![
|
||||
BlueprintNode::Composite(sma_cross()),
|
||||
Exposure::builder().into(),
|
||||
Bias::builder().into(),
|
||||
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx).into(),
|
||||
],
|
||||
vec![
|
||||
Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // spread -> Exposure
|
||||
Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // spread -> Bias
|
||||
Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // exposure -> sink
|
||||
],
|
||||
vec![Role {
|
||||
@@ -375,7 +375,7 @@ mod tests {
|
||||
}
|
||||
|
||||
/// `sample_root` with one interior edge re-targeted to a different valid node:
|
||||
/// the `exposure -> sink` edge instead feeds the `Exposure` node's own input
|
||||
/// the `exposure -> sink` edge instead feeds the `Bias` node's own input
|
||||
/// slot (`to: 1`). Same node set, distinct wiring — the model must differ.
|
||||
fn miswired_root() -> Composite {
|
||||
let (tx, _rx) = mpsc::channel();
|
||||
@@ -383,7 +383,7 @@ mod tests {
|
||||
"sample",
|
||||
vec![
|
||||
BlueprintNode::Composite(sma_cross()),
|
||||
Exposure::builder().into(),
|
||||
Bias::builder().into(),
|
||||
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx).into(),
|
||||
],
|
||||
vec![
|
||||
@@ -432,7 +432,7 @@ mod tests {
|
||||
assert!(bound.contains(r#""params":[]"#), "{bound}");
|
||||
|
||||
// a bound f64 value renders canonically (shortest round-trip), e.g. 0.5
|
||||
let f = prim_record(&Exposure::builder().bind("scale", Scalar::f64(0.5)));
|
||||
let f = prim_record(&Bias::builder().bind("scale", Scalar::f64(0.5)));
|
||||
assert!(f.contains(r#""bound":[[0,"scale","f64","0.5"]]"#), "{f}");
|
||||
|
||||
// an unbound builder → no "bound" field at all
|
||||
@@ -494,7 +494,7 @@ mod tests {
|
||||
// Re-capture if an intended model-shape change lands: temporarily add a
|
||||
// `println!("{}", model_to_json(&sample_root()))` test, run with
|
||||
// `-- --nocapture`, and paste the fresh bytes below.
|
||||
let expected = r##"{"root":{"nodes":{"0":{"comp":"sma_cross"},"1":{"prim":{"type":"Exposure","role":"node","params":[["scale","f64"]],"ins":[["f64","any","signal"]],"outs":[["exposure","f64"]]}},"2":{"prim":{"type":"Recorder","role":"sink","params":[],"ins":[["f64","any","col[0]"]],"outs":[]}},"src_price":{"prim":{"type":"price","role":"source","params":[],"ins":[],"outs":[["price","f64"]]}}},"edges":[["0.o0","1.i0"],["1.o0","2.i0"],["src_price.o0","0.i0"]]},"composites":{"sma_cross":{"inputs":[["f64","any","price"]],"outputs":[["out","f64"]],"nodes":{"0":{"prim":{"name":"fast","type":"SMA","role":"node","params":[["length","i64"]],"ins":[["f64","any","series"]],"outs":[["value","f64"]]}},"1":{"prim":{"name":"slow","type":"SMA","role":"node","params":[["length","i64"]],"ins":[["f64","any","series"]],"outs":[["value","f64"]]}},"2":{"prim":{"type":"Sub","role":"node","params":[],"ins":[["f64","any","lhs"],["f64","any","rhs"]],"outs":[["value","f64"]]}}},"edges":[["0.o0","2.i0"],["1.o0","2.i1"],["@price","0.i0"],["@price","1.i0"],["2.o0","#0"]]}}}"##;
|
||||
let expected = r##"{"root":{"nodes":{"0":{"comp":"sma_cross"},"1":{"prim":{"type":"Bias","role":"node","params":[["scale","f64"]],"ins":[["f64","any","signal"]],"outs":[["bias","f64"]]}},"2":{"prim":{"type":"Recorder","role":"sink","params":[],"ins":[["f64","any","col[0]"]],"outs":[]}},"src_price":{"prim":{"type":"price","role":"source","params":[],"ins":[],"outs":[["price","f64"]]}}},"edges":[["0.o0","1.i0"],["1.o0","2.i0"],["src_price.o0","0.i0"]]},"composites":{"sma_cross":{"inputs":[["f64","any","price"]],"outputs":[["out","f64"]],"nodes":{"0":{"prim":{"name":"fast","type":"SMA","role":"node","params":[["length","i64"]],"ins":[["f64","any","series"]],"outs":[["value","f64"]]}},"1":{"prim":{"name":"slow","type":"SMA","role":"node","params":[["length","i64"]],"ins":[["f64","any","series"]],"outs":[["value","f64"]]}},"2":{"prim":{"type":"Sub","role":"node","params":[],"ins":[["f64","any","lhs"],["f64","any","rhs"]],"outs":[["value","f64"]]}}},"edges":[["0.o0","2.i0"],["1.o0","2.i1"],["@price","0.i0"],["@price","1.i0"],["2.o0","#0"]]}}}"##;
|
||||
assert_eq!(model_to_json(&sample_root()), expected);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user