feat(real-family): realistic strategy lengths over real data

The built-in family grid was calibrated for the synthetic demo streams (18/60
bars), where trend SMA 2-5 / MACD 2-4-3 are the only lengths that warm. Over real
M1 data those are noise — ~9000 exposure sign-flips per month, a strategy trading
the bid-ask wiggle, not a trend.

DataSource::strategy_lengths() makes the grid data-kind-dependent (like
wf_window_sizes): synthetic keeps {2,3}x{4,5} + MACD 2/4/3 (byte-unchanged); real
uses {50,100}x{200,400} trend SMAs + standard 12/26/9 MACD. Threaded through
sweep_family + sweep_over (the walk-forward in-sample sweep). Over real EURUSD the
monthly flip count drops from ~9000 to ~130-310 — a genuine trend cross.

This is a demo-strategy calibration patch (ledger C22 amendment notes it as such);
the real answer is project-authored strategies owning their own grid (C9), deferred
to the project-env work.

Verified: cargo test -p aura-cli (all green, synthetic byte-unchanged),
clippy -D warnings clean; real sweep probe shows ~130-310 flips/month.

refs #106
This commit is contained in:
2026-06-21 16:06:28 +02:00
parent 607e5487e9
commit 99fd32b1f9
2 changed files with 58 additions and 11 deletions
+49 -10
View File
@@ -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
+9 -1
View File
@@ -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,