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:
+20
-19
@@ -2,7 +2,7 @@
|
||||
//! automation drive: author a node, run a sim/sweep, emit structured metrics).
|
||||
//!
|
||||
//! The walking skeleton's closing seam: `aura run` bootstraps a built-in sample
|
||||
//! signal-quality harness (synthetic source → SMA-cross → Exposure → SimBroker →
|
||||
//! signal-quality harness (synthetic source → SMA-cross → Bias → SimBroker →
|
||||
//! recording sinks), runs it deterministically (C1), and prints the run's
|
||||
//! metrics + manifest (#6) as canonical JSON to stdout (the headline C14 move).
|
||||
//!
|
||||
@@ -26,7 +26,7 @@ use aura_registry::{
|
||||
walkforward_member_reports, FamilyKind, FamilyMember, NameKind, Registry, RunTraces, TraceStore,
|
||||
WriteKind,
|
||||
};
|
||||
use aura_std::{Ema, Exposure, LinComb, LongOnly, Recorder, SimBroker, Sma, Sub};
|
||||
use aura_std::{Bias, Ema, LinComb, LongOnly, Recorder, SimBroker, Sma, Sub};
|
||||
use std::sync::mpsc::{self, Receiver};
|
||||
use std::collections::HashSet;
|
||||
|
||||
@@ -82,7 +82,7 @@ fn showcase_prices() -> Vec<(Timestamp, Scalar)> {
|
||||
}
|
||||
|
||||
/// Bootstrap the sample signal-quality harness with two recording sinks (equity
|
||||
/// tapped on the SimBroker, exposure tapped on the Exposure node). Rust-authored
|
||||
/// tapped on the SimBroker, exposure tapped on the Bias node). Rust-authored
|
||||
/// wiring (C17/C20) over the raw bootstrap API — no builder DSL this cycle. The
|
||||
/// price taps both SMAs and the broker's price slot (slot 1); exposure feeds the
|
||||
/// broker's slot 0 (slot order is load-bearing — both are f64).
|
||||
@@ -106,7 +106,7 @@ fn sample_harness(pip_size: f64) -> (
|
||||
Box::new(Sma::new(2)), // 0 fast SMA
|
||||
Box::new(Sma::new(4)), // 1 slow SMA
|
||||
Box::new(Sub::new()), // 2 spread
|
||||
Box::new(Exposure::new(0.5)), // 3 exposure
|
||||
Box::new(Bias::new(0.5)), // 3 bias
|
||||
Box::new(SimBroker::new(pip_size)), // 4 sim-optimal broker
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx_eq)), // 5 equity sink
|
||||
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx_ex)), // 6 exposure sink
|
||||
@@ -115,7 +115,7 @@ fn sample_harness(pip_size: f64) -> (
|
||||
Sma::builder().schema().clone(),
|
||||
Sma::builder().schema().clone(),
|
||||
Sub::builder().schema().clone(),
|
||||
Exposure::builder().schema().clone(),
|
||||
Bias::builder().schema().clone(),
|
||||
SimBroker::builder(pip_size).schema().clone(),
|
||||
f64_recorder_sig(),
|
||||
f64_recorder_sig(),
|
||||
@@ -960,16 +960,16 @@ fn sample_blueprint_with_sinks(pip_size: f64) -> (
|
||||
let (tx_ex, rx_ex) = mpsc::channel();
|
||||
let mut g = GraphBuilder::new("sample");
|
||||
let sig = g.add(signals("signals"));
|
||||
let exposure = g.add(Exposure::builder());
|
||||
let exposure = g.add(Bias::builder().named("exposure"));
|
||||
let broker = g.add(SimBroker::builder(pip_size));
|
||||
let eq = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq));
|
||||
let ex = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex));
|
||||
let price = g.source_role("price", ScalarKind::F64);
|
||||
g.feed(price, [sig.input("price"), broker.input("price")]);
|
||||
g.connect(sig.output("signal"), exposure.input("signal")); // blended signal -> Exposure
|
||||
g.connect(exposure.output("exposure"), broker.input("exposure")); // exposure -> broker slot 0
|
||||
g.connect(sig.output("signal"), exposure.input("signal")); // blended signal -> Bias
|
||||
g.connect(exposure.output("bias"), broker.input("exposure")); // bias -> broker slot 0
|
||||
g.connect(broker.output("equity"), eq.input("col[0]")); // equity -> sink
|
||||
g.connect(exposure.output("exposure"), ex.input("col[0]")); // exposure -> sink
|
||||
g.connect(exposure.output("bias"), ex.input("col[0]")); // bias -> sink
|
||||
let bp = g.build().expect("sample blueprint wiring resolves");
|
||||
(bp, rx_eq, rx_ex)
|
||||
}
|
||||
@@ -1031,8 +1031,9 @@ fn sweep_family(trace: Option<&str>, data: &DataSource) -> SweepFamily {
|
||||
}
|
||||
|
||||
/// The EMA-distance momentum demo strategy with its two recording sinks reachable.
|
||||
/// The signal is how far price sits above/below its own EMA (`price - ema`), sized
|
||||
/// by `Exposure`, then passed through the long-only `LongOnly` gate. Three swept
|
||||
/// The signal is how far price sits above/below its own EMA (`price - ema`), bounded
|
||||
/// into a directional (unsized) bias by `Bias`, then passed through the long-only
|
||||
/// `LongOnly` gate. Three swept
|
||||
/// knobs of three kinds — `ema.length` (i64), `exposure.scale` (f64),
|
||||
/// `longonly.enabled` (bool) — with nothing in common with the SMA-cross demo: the
|
||||
/// generic-sweep proof. The exposure sink taps the FINAL (gated) exposure so the
|
||||
@@ -1051,7 +1052,7 @@ fn momentum_blueprint_with_sinks(pip_size: f64) -> (
|
||||
// derivation (the member-key examples + the param-space test depend on these).
|
||||
let ema = g.add(Ema::builder().named("ema")); // ema.length
|
||||
let dist = g.add(Sub::builder()); // momentum = price - ema
|
||||
let expo = g.add(Exposure::builder().named("exposure")); // exposure.scale
|
||||
let expo = g.add(Bias::builder().named("exposure")); // exposure.scale
|
||||
let gate = g.add(LongOnly::builder().named("longonly")); // longonly.enabled (the bool param)
|
||||
let broker = g.add(SimBroker::builder(pip_size));
|
||||
let eq = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq));
|
||||
@@ -1060,7 +1061,7 @@ fn momentum_blueprint_with_sinks(pip_size: f64) -> (
|
||||
g.feed(price, [ema.input("series"), dist.input("lhs"), broker.input("price")]);
|
||||
g.connect(ema.output("value"), dist.input("rhs")); // momentum = price - ema
|
||||
g.connect(dist.output("value"), expo.input("signal"));
|
||||
g.connect(expo.output("exposure"), gate.input("exposure"));
|
||||
g.connect(expo.output("bias"), gate.input("exposure"));
|
||||
g.connect(gate.output("exposure"), broker.input("exposure")); // gated exposure -> broker
|
||||
g.connect(broker.output("equity"), eq.input("col[0]")); // equity -> sink
|
||||
g.connect(gate.output("exposure"), ex.input("col[0]")); // gated exposure -> sink
|
||||
@@ -1601,7 +1602,7 @@ fn macd(name: &str) -> Composite {
|
||||
g.build().expect("sample macd wiring resolves")
|
||||
}
|
||||
|
||||
/// The MACD strategy blueprint (value-empty): the `macd` histogram → `Exposure` →
|
||||
/// The MACD strategy blueprint (value-empty): the `macd` histogram → `Bias` →
|
||||
/// `SimBroker` → recording sinks. Channels are threaded so a run can drain the
|
||||
/// sinks; `macd_blueprint` drops the receivers for the structural render.
|
||||
fn macd_strategy_blueprint(
|
||||
@@ -1610,16 +1611,16 @@ fn macd_strategy_blueprint(
|
||||
) -> Composite {
|
||||
let mut g = GraphBuilder::new("macd_strategy");
|
||||
let macd_node = g.add(macd("macd"));
|
||||
let exposure = g.add(Exposure::builder());
|
||||
let exposure = g.add(Bias::builder().named("exposure"));
|
||||
let broker = g.add(SimBroker::builder(SYNTHETIC_PIP_SIZE));
|
||||
let eq = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq));
|
||||
let ex = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex));
|
||||
let price = g.source_role("price", ScalarKind::F64);
|
||||
g.feed(price, [macd_node.input("price"), broker.input("price")]);
|
||||
g.connect(macd_node.output("histogram"), exposure.input("signal")); // histogram → Exposure
|
||||
g.connect(exposure.output("exposure"), broker.input("exposure")); // exposure → broker slot 0
|
||||
g.connect(macd_node.output("histogram"), exposure.input("signal")); // histogram → Bias
|
||||
g.connect(exposure.output("bias"), broker.input("exposure")); // bias → broker slot 0
|
||||
g.connect(broker.output("equity"), eq.input("col[0]")); // equity → sink
|
||||
g.connect(exposure.output("exposure"), ex.input("col[0]")); // exposure → sink
|
||||
g.connect(exposure.output("bias"), ex.input("col[0]")); // bias → sink
|
||||
g.build().expect("macd_strategy wiring resolves")
|
||||
}
|
||||
|
||||
@@ -2303,7 +2304,7 @@ mod tests {
|
||||
let names: Vec<String> =
|
||||
macd_blueprint().param_space().into_iter().map(|p| p.name).collect();
|
||||
// three named composite-interior slots, in declared (fast, slow, signal)
|
||||
// order, then the strategy-level Exposure `scale` (a root-level leaf).
|
||||
// order, then the strategy-level Bias `scale` (a root-level leaf).
|
||||
assert_eq!(
|
||||
names,
|
||||
vec![
|
||||
|
||||
@@ -259,7 +259,7 @@ mod tests {
|
||||
// the deterministic model is injected as the viewer's data source
|
||||
assert!(html.contains("window.AURA_MODEL = {\"root\":"), "model JSON not injected");
|
||||
// a known node from the sample harness round-trips via the model
|
||||
assert!(html.contains("\"type\":\"Exposure\""), "sample model content missing");
|
||||
assert!(html.contains("\"type\":\"Bias\""), "sample model content missing");
|
||||
// the vendored Graphviz-WASM is present (its bootstrap global)
|
||||
assert!(html.contains("Viz.instance"), "viewer bootstrap (Viz.instance) missing");
|
||||
// C4: the four-scalar palette, no fifth colour
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"root":{"nodes":{"0":{"comp":"signals"},"1":{"prim":{"type":"Exposure","role":"node","params":[["scale","f64"]],"ins":[["f64","any","signal"]],"outs":[["exposure","f64"]]}},"2":{"prim":{"type":"SimBroker","role":"node","params":[],"ins":[["f64","any","exposure"],["f64","any","price"]],"outs":[["equity","f64"]]}},"3":{"prim":{"type":"Recorder","role":"sink","params":[],"ins":[["f64","any","col[0]"]],"outs":[]}},"4":{"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"],["2.o0","3.i0"],["1.o0","4.i0"],["src_price.o0","0.i0"],["src_price.o0","2.i1"]]},"composites":{"signals":{"inputs":[["f64","any","price"]],"outputs":[["signal","f64"]],"nodes":{"0":{"comp":"trend"},"1":{"comp":"momentum"},"2":{"prim":{"name":"blend","type":"LinComb","role":"node","params":[["weights[0]","f64"],["weights[1]","f64"]],"bound":[[2,"weights[2]","f64","0.5"]],"ins":[["f64","any","term[0]"],["f64","any","term[1]"],["f64","any","term[2]"]],"outs":[["value","f64"]]}}},"edges":[["0.o0","2.i0"],["1.o2","2.i1"],["1.o1","2.i2"],["@price","0.i0"],["@price","1.i0"],["2.o0","#0"]]},"trend":{"inputs":[["f64","any","price"]],"outputs":[["cross","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"]]},"momentum":{"inputs":[["f64","any","price"]],"outputs":[["macd","f64"],["signal","f64"],["histogram","f64"]],"nodes":{"0":{"prim":{"name":"fast","type":"EMA","role":"node","params":[["length","i64"]],"ins":[["f64","any","series"]],"outs":[["value","f64"]]}},"1":{"prim":{"name":"slow","type":"EMA","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"]]}},"3":{"prim":{"name":"signal","type":"EMA","role":"node","params":[["length","i64"]],"ins":[["f64","any","series"]],"outs":[["value","f64"]]}},"4":{"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"],["2.o0","3.i0"],["2.o0","4.i0"],["3.o0","4.i1"],["@price","0.i0"],["@price","1.i0"],["2.o0","#0"],["3.o0","#1"],["4.o0","#2"]]}}}
|
||||
{"root":{"nodes":{"0":{"comp":"signals"},"1":{"prim":{"type":"Bias","role":"node","params":[["scale","f64"]],"ins":[["f64","any","signal"]],"outs":[["bias","f64"]]}},"2":{"prim":{"type":"SimBroker","role":"node","params":[],"ins":[["f64","any","exposure"],["f64","any","price"]],"outs":[["equity","f64"]]}},"3":{"prim":{"type":"Recorder","role":"sink","params":[],"ins":[["f64","any","col[0]"]],"outs":[]}},"4":{"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"],["2.o0","3.i0"],["1.o0","4.i0"],["src_price.o0","0.i0"],["src_price.o0","2.i1"]]},"composites":{"signals":{"inputs":[["f64","any","price"]],"outputs":[["signal","f64"]],"nodes":{"0":{"comp":"trend"},"1":{"comp":"momentum"},"2":{"prim":{"name":"blend","type":"LinComb","role":"node","params":[["weights[0]","f64"],["weights[1]","f64"]],"bound":[[2,"weights[2]","f64","0.5"]],"ins":[["f64","any","term[0]"],["f64","any","term[1]"],["f64","any","term[2]"]],"outs":[["value","f64"]]}}},"edges":[["0.o0","2.i0"],["1.o2","2.i1"],["1.o1","2.i2"],["@price","0.i0"],["@price","1.i0"],["2.o0","#0"]]},"trend":{"inputs":[["f64","any","price"]],"outputs":[["cross","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"]]},"momentum":{"inputs":[["f64","any","price"]],"outputs":[["macd","f64"],["signal","f64"],["histogram","f64"]],"nodes":{"0":{"prim":{"name":"fast","type":"EMA","role":"node","params":[["length","i64"]],"ins":[["f64","any","series"]],"outs":[["value","f64"]]}},"1":{"prim":{"name":"slow","type":"EMA","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"]]}},"3":{"prim":{"name":"signal","type":"EMA","role":"node","params":[["length","i64"]],"ins":[["f64","any","series"]],"outs":[["value","f64"]]}},"4":{"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"],["2.o0","3.i0"],["2.o0","4.i0"],["3.o0","4.i1"],["@price","0.i0"],["@price","1.i0"],["2.o0","#0"],["3.o0","#1"],["4.o0","#2"]]}}}
|
||||
|
||||
Reference in New Issue
Block a user