diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 716270b..9457aad 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -604,6 +604,20 @@ impl DataSource { DataSource::Real { .. } => (WF_REAL_IS_NS, WF_REAL_OOS_NS, WF_REAL_STEP_NS), } } + + /// The built-in strategy's length grid, **per data kind**. Synthetic keeps the + /// short lengths that fit the 18/60-bar demo streams (a 200-bar MA would never + /// warm on a 60-bar stream); real uses realistic M1 lengths so the SMA-cross + + /// MACD signal is a real trend cross over tens of thousands of bars, not noise. + /// Returns `(trend_fast grid, trend_slow grid, (macd_fast, macd_slow, macd_signal))`; + /// only the two trend axes vary (a 2×2 sweep), the MACD lengths are pinned. + fn strategy_lengths(&self) -> ([i64; 2], [i64; 2], (i64, i64, i64)) { + match self { + DataSource::Synthetic => ([2, 3], [4, 5], (2, 4, 3)), + // 50/200-style intraday crosses on M1 (minutes), standard 12/26/9 MACD. + DataSource::Real { .. } => ([50, 100], [200, 400], (12, 26, 9)), + } + } } /// The SMA-cross signal as a named composite (price -> fast/slow SMA -> spread). @@ -702,12 +716,13 @@ fn sweep_family(trace: Option<&str>, data: &DataSource) -> SweepFamily { let window = data.full_window(); let bp = sample_blueprint_with_sinks(pip).0; let space = bp.param_space(); + let (tf, ts, (mf, ms, msig)) = data.strategy_lengths(); let binder = bp - .axis("signals.trend.fast.length", [2, 3]) - .axis("signals.trend.slow.length", [4, 5]) - .axis("signals.momentum.fast.length", [2]) - .axis("signals.momentum.slow.length", [4]) - .axis("signals.momentum.signal.length", [3]) + .axis("signals.trend.fast.length", tf) + .axis("signals.trend.slow.length", ts) + .axis("signals.momentum.fast.length", [mf]) + .axis("signals.momentum.slow.length", [ms]) + .axis("signals.momentum.signal.length", [msig]) .axis("signals.blend.weights[0]", [1.0]) .axis("signals.blend.weights[1]", [1.0]) .axis("exposure.scale", [0.5]); @@ -985,11 +1000,12 @@ fn sweep_over(from: Timestamp, to: Timestamp, data: &DataSource) -> SweepFamily let pip = data.pip_size(); let bp = sample_blueprint_with_sinks(pip).0; let space = bp.param_space(); - bp.axis("signals.trend.fast.length", [2, 3]) - .axis("signals.trend.slow.length", [4, 5]) - .axis("signals.momentum.fast.length", [2]) - .axis("signals.momentum.slow.length", [4]) - .axis("signals.momentum.signal.length", [3]) + let (tf, ts, (mf, ms, msig)) = data.strategy_lengths(); + bp.axis("signals.trend.fast.length", tf) + .axis("signals.trend.slow.length", ts) + .axis("signals.momentum.fast.length", [mf]) + .axis("signals.momentum.slow.length", [ms]) + .axis("signals.momentum.signal.length", [msig]) .axis("signals.blend.weights[0]", [1.0]) .axis("signals.blend.weights[1]", [1.0]) .axis("exposure.scale", [0.5]) @@ -1447,6 +1463,29 @@ mod tests { assert_eq!(WF_REAL_STEP_NS, 30 * day_ns); } + #[test] + fn strategy_lengths_are_short_for_synthetic_realistic_for_real() { + // Synthetic keeps the demo-stream lengths (byte-unchanged: the 18/60-bar + // built-in streams cannot warm a long MA). + assert_eq!(DataSource::Synthetic.strategy_lengths(), ([2, 3], [4, 5], (2, 4, 3))); + // Real uses realistic M1 lengths — no 2-5-bar noise over tens of thousands + // of bars. Constructing Real needs a server, but strategy_lengths matches on + // the variant only (no data access). + let real = DataSource::Real { + server: std::sync::Arc::new(data_server::DataServer::new(data_server::DEFAULT_DATA_PATH)), + symbol: "EURUSD".into(), + from_ms: None, + to_ms: None, + pip: 0.0001, + }; + let (tf, ts, macd) = real.strategy_lengths(); + assert_eq!((tf, ts, macd), ([50, 100], [200, 400], (12, 26, 9))); + // every trend-fast < every trend-slow (a valid SMA cross, both variants). + assert!(tf.iter().max().unwrap() < ts.iter().min().unwrap()); + let (stf, sts, _) = DataSource::Synthetic.strategy_lengths(); + assert!(stf.iter().max().unwrap() < sts.iter().min().unwrap()); + } + #[test] fn walkforward_report_is_deterministic() { // The built-in WFO render is byte-identical across two diff --git a/docs/design/INDEX.md b/docs/design/INDEX.md index 9ac2380..cac092c 100644 --- a/docs/design/INDEX.md +++ b/docs/design/INDEX.md @@ -1057,7 +1057,15 @@ the whole change is in `aura-cli`); the synthetic path is byte-unchanged. realization (C12 axis 4), which is undefined over real data's single realization — `aura mc --real` refuses with exit 2; a real MC needs a bootstrap-resampling axis (its own cycle). The real walk-forward member key `oos{from.0}` widens from a small -synthetic bar index to an epoch-ns integer (still portable, collision-free). +synthetic bar index to an epoch-ns integer (still portable, collision-free). The +built-in demo strategy's **length grid is likewise data-kind-dependent** +(`DataSource::strategy_lengths`): synthetic keeps the short lengths that fit the +18/60-bar demo streams (trend SMA `{2,3}×{4,5}`, MACD `2/4/3`), real uses realistic +M1 lengths (trend `{50,100}×{200,400}`, MACD `12/26/9`) so the cross is a genuine +trend signal over tens of thousands of bars, not noise. This — like the roller +sizes — is a *demo-strategy calibration* patch; the real answer is project-authored +strategies (C9: a project crate owns its own grid), deferred to the project-env +work. ### C23 — Graph compilation and behaviour-preserving optimisation **Guarantee.** The bootstrap (C19) is a **compilation**: it lowers a param-generic,