diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index f62d5e9..b21390f 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -31,6 +31,7 @@ use aura_std::{ Bias, Ema, LinComb, LongOnly, Recorder, SimBroker, Sma, Sub, PM_FIELD_NAMES, PM_RECORD_KINDS, }; use std::sync::mpsc::{self, Receiver}; +use std::sync::LazyLock; use std::collections::HashSet; /// The pip size the built-in *synthetic* harnesses run at: a 5-decimal FX major @@ -1719,6 +1720,13 @@ fn stage1_r_prices() -> Vec<(Timestamp, Scalar)> { .collect() } +/// Interned `col[i]` recorder-port names, built once. The `GraphBuilder::input` API +/// wants `&'static str`; interning here (instead of `format!(...).leak()` per field) +/// means reusing the stage1-r harness in a sweep / Monte-Carlo loop reuses these +/// strings rather than leaking 14 fresh ones per build (#132). +static COL_PORTS: LazyLock> = + LazyLock::new(|| (0..PM_FIELD_NAMES.len()).map(|i| format!("col[{i}]")).collect()); + /// The Stage-1 R harness: the SMA-cross → `Bias` signal fanned into BOTH a `SimBroker` /// (pip equity, the existing pip yardstick) AND the `risk_executor` (the dense R-record, /// the new R yardstick), so one run scores the same signal in pips and in R. Four taps: @@ -1770,8 +1778,7 @@ fn stage1_r_blueprint( g.connect(broker.output("equity"), eq.input("col[0]")); // tap the dense R-record (all PM fields) for summarize_r. for (i, field) in PM_FIELD_NAMES.iter().enumerate() { - let col: &'static str = format!("col[{i}]").leak(); - g.connect(exec.output(field), rrec.input(col)); + g.connect(exec.output(field), rrec.input(COL_PORTS[i].as_str())); } // r_equity sum + tap. g.connect(exec.output("cum_realized_r"), r_equity.input("term[0]")); @@ -1820,7 +1827,7 @@ fn run_stage1_r(data: RunData, trace: Option<&str>) -> RunReport { let r_rows: Vec<(Timestamp, Vec)> = rx_r.try_iter().collect(); let req_rows: Vec<(Timestamp, Vec)> = rx_req.try_iter().collect(); - let manifest = sim_optimal_manifest( + let mut manifest = sim_optimal_manifest( vec![ ("sma_fast".to_string(), Scalar::i64(2)), ("sma_slow".to_string(), Scalar::i64(4)), @@ -1832,6 +1839,9 @@ fn run_stage1_r(data: RunData, trace: Option<&str>) -> RunReport { 0, pip_size, ); + // #132: the dual-tap harness runs a RiskExecutor branch alongside the SimBroker, so + // the plain "sim-optimal" label would under-report it — record both honestly. + manifest.broker = format!("sim-optimal+risk-executor(pip_size={pip_size})"); if let Some(name) = trace { persist_traces_r(name, &manifest, &eq_rows, &ex_rows, &req_rows); } @@ -2943,6 +2953,13 @@ mod tests { assert!(r.n_trades >= 1, "expected >= 1 trade, got {}", r.n_trades); assert!(r.sqn.is_finite(), "SQN must be finite, got {}", r.sqn); assert!(r.expectancy_r.is_finite(), "E[R] must be finite, got {}", r.expectancy_r); + // #132: the dual-tap harness runs a RiskExecutor branch alongside the + // SimBroker, so its manifest carries a dedicated broker label. + assert!( + report.manifest.broker.contains("risk-executor"), + "stage1-r manifest should carry a dedicated broker label, got: {}", + report.manifest.broker + ); } /// Property: a bare `aura run` parses to the default harness/data — `--harness sma`