Files
Aura/crates/aura-ingest/tests/ger40_breakout_world.rs
claude a56ab7859d refactor: cut the engine's backtest-metrics edge via RunReport<M>
C28 phase 2 (Stratification); realizes item 1 of the deferred #147. The
engine's production surface no longer names a backtest-metric type:

- RunReport becomes generic over its metric payload M; sweep/mc/walkforward/
  blueprint thread the parameter (SweepPoint<M>, SweepFamily<M>, WindowRun<M>,
  WalkForwardResult<M>). RunManifest stays concrete and engine-owned (its
  selection: Option<FamilySelection> embeds the foundation-grade analysis type).
- summarize and the MC assembly (McDraw/McFamily/McAggregate/RBootstrap/
  r_bootstrap/monte_carlo) move to aura-backtest - McAggregate::from_draws reads
  RunMetrics fields by name, so generifying it is the phase-6 metric-vocabulary
  abstraction (#147 item 2), still deferred; wholesale relocation is the honest
  cut. The concrete instantiation lives in aura-backtest as
  `type RunReport = aura_engine::RunReport<RunMetrics>` + sibling aliases.
- the statistics kernel (MetricStats/quantile/resample_block/SplitMix64) moves
  to the aura-analysis foundation; the engine re-imports it (inner->foundation,
  legal) and re-exports it so existing consumers stay source-compatible.

Dependency inversion in one commit: aura-engine drops aura-backtest from
[dependencies] (back to dev-deps for its SimBroker/RunMetrics test fixtures);
aura-backtest gains aura-engine. Cycle-free for lib targets - the cycle closes
only through the engine's dev-dep edge, the pattern aura-vocabulary already
uses. aura-backtest reaches the kernel transitively through the engine
re-export, so no aura-backtest -> aura-analysis edge exists (the C28 ladder
permits backtest -> {core, engine} only). run_indexed / SplitMix64::next_f64
widened pub(crate) -> pub for cross-crate use.

Consumers (registry/campaign/cli/composites/ingest/bench) rewired by import
path only, no call-site logic changed. The c28_layering structural test extends
to the full ladder: aura-analysis (no aura-* deps), aura-engine ⊆ {core,
analysis}, aura-backtest ⊆ {core, engine}.

Behaviour-preserving: 1448/0 tests, clippy -D warnings clean, serde shapes
byte-identical (C18 - RunReport<M> keeps field order manifest,metrics; the
CLI pre-serialized-splice contract unchanged), moved code traceable via git
rename detection. Cycle-introduced broken intra-doc links fixed.

closes #292
2026-07-20 13:25:07 +02:00

279 lines
11 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Gated integration test: the GER40 session-breakout `Composite` blueprint is
//! driven by the World *orchestration families* — `sweep` and
//! `walk_forward` — on REAL GER40 M1 bars, with NO re-authoring of the strategy.
//! This is the milestone-completing executable proof that the #94 friction is
//! gone: the same `ger40_breakout_blueprint` the single backtest bootstraps is
//! consumed verbatim by the World machinery.
//!
//! Mirrors `tests/ger40_breakout_real.rs` / `real_bars.rs`: skips with a note
//! where the local Pepperstone archive is absent, so `cargo test --workspace`
//! stays green anywhere; exercises the real path where the files exist.
//!
//! Two properties, both over the shipped blueprint:
//! - `sweep` returns a ranked grid over the two named params
//! (`entry_bar.target` × `exit_bar.target`), each point's `total_pips` finite —
//! the `param_space()` is driven directly.
//! - `walk_forward` returns a NON-EMPTY roll, every window carrying a NON-EMPTY
//! `chosen_params` (the executable #97 non-degenerate proof: a real, non-empty
//! space is optimized per window — contrast the fieldtest's empty-space
//! degenerate roll over the raw FlatGraph).
use std::sync::Arc;
use aura_core::{Cell, Scalar, Timestamp};
use aura_backtest::{summarize, RunReport, WindowRun};
use aura_engine::{
sweep, walk_forward, GridSpace, RollMode, RunManifest, WindowBounds,
WindowRoller,
};
use chrono::TimeZone;
use chrono_tz::Europe::Berlin;
use aura_ingest::{default_data_server, open_ohlc, DataServer, DEFAULT_DATA_PATH};
#[path = "../examples/shared/breakout_real.rs"]
mod breakout_real;
use breakout_real::*;
/// Bootstrap the shipped blueprint at one `{entry_bar, exit_bar}` point, run it
/// over the epoch-ns [`Timestamp`] window `[from, to]` of real GER40 OHLC, and
/// fold the recorded equity/held taps into a `RunReport` (`summarize` over the
/// drained channels — exactly as the SMA-cross sweep fixture and the fieldtest's
/// `run_point` do). A fresh blueprint per call (fresh nodes + channels) keeps the
/// disjoint sweep points independent (C1).
fn run_point(server: &Arc<DataServer>, point: &[Cell], from: Timestamp, to: Timestamp) -> RunReport {
let (bp, taps) = ger40_breakout_blueprint(
pip_size_of(SYMBOL),
BAR_MINUTES,
SESSION_HOUR,
SESSION_MINUTE,
Berlin,
);
let mut h = bp
.bootstrap_with_cells(point)
.expect("sweep point kind-checked against param_space");
let sources = open_ohlc(server, SYMBOL, from, to).expect("window overlaps GER40 data");
h.run(sources);
drop(h);
let equity: Vec<(Timestamp, f64)> = taps.equity.try_iter().collect::<Vec<_>>()
.iter()
.map(|(t, r)| (*t, scalar_f64(&r[0])))
.collect();
let exposure: Vec<(Timestamp, f64)> = taps.held.try_iter().collect::<Vec<_>>()
.iter()
.map(|(t, r)| (*t, scalar_f64(&r[0])))
.collect();
RunReport {
manifest: RunManifest {
commit: "ger40-breakout-world".to_string(),
params: vec![],
defaults: vec![],
window: (from, to),
seed: 0,
broker: "sim-optimal(pip_size=1)".to_string(),
selection: None,
instrument: None,
topology_hash: None,
project: None,
},
metrics: summarize(&equity, &exposure),
}
}
fn scalar_f64(s: &Scalar) -> f64 {
match s {
Scalar::F64(v) => *v,
other => panic!("expected f64, got {other:?}"),
}
}
/// Property: the World `sweep` family CONSUMES the shipped breakout blueprint
/// over its two named params on real GER40 — it returns a full ranked grid (no
/// re-authoring), and every grid point's `total_pips` is finite (a backtest
/// actually ran at each point). The grid is built from the blueprint's
/// `param_space()` directly, so the two `EqConst` targets are the swept axes.
#[test]
fn sweep_consumes_blueprint_over_named_params() {
let server = default_data_server();
if !server.has_symbol(SYMBOL) {
eprintln!("skip: no local data at {DEFAULT_DATA_PATH} (symbol {SYMBOL} absent)");
return;
}
let (from, to) = utc_month_window(2024, 9);
let space = ger40_breakout_blueprint(
pip_size_of(SYMBOL),
BAR_MINUTES,
SESSION_HOUR,
SESSION_MINUTE,
Berlin,
)
.0
.param_space();
// The grid is exactly over `param_space()`: entry ∈ {2,3,4}, exit ∈ {4,5,6}.
let grid = GridSpace::new(
&space,
space
.iter()
.map(|ps| match ps.name.as_str() {
"entry_bar.target" => vec![Scalar::i64(2), Scalar::i64(3), Scalar::i64(4)],
"exit_bar.target" => vec![Scalar::i64(4), Scalar::i64(5), Scalar::i64(6)],
other => panic!("unexpected param slot {other}"),
})
.collect(),
)
.expect("grid well-formed for the breakout param_space");
let server_for_closure = Arc::clone(&server);
let family = sweep(&grid, |pt: &[Cell]| {
run_point(&server_for_closure, pt, from, to)
});
// A full 3×3 grid was enumerated over the two named params (no re-authoring).
assert_eq!(family.points.len(), 9, "the 3×3 entry×exit grid must enumerate 9 points");
assert_eq!(
family.space.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(),
vec!["entry_bar.target", "exit_bar.target"],
"the swept space is exactly the two genuine tuning knobs",
);
// Every point's metrics are finite — a real backtest ran at each grid point.
for (i, p) in family.points.iter().enumerate() {
assert!(
p.report.metrics.total_pips.is_finite(),
"point {i} {:?} produced a non-finite total_pips",
family.named_params(i),
);
}
// The best point (max total_pips) is finite — the ranked grid has a real head.
let best = family
.points
.iter()
.map(|p| p.report.metrics.total_pips)
.fold(f64::NEG_INFINITY, f64::max);
assert!(best.is_finite(), "the best grid point's total_pips must be finite");
}
/// Property: the World `walk_forward` family CONSUMES the shipped breakout
/// blueprint NON-DEGENERATELY (the executable #97 resolution). The roll over
/// multi-year GER40 yields a NON-EMPTY set of windows, and EACH window carries a
/// NON-EMPTY `chosen_params` — a real `{entry_bar, exit_bar}` point was selected
/// by an in-sample sweep per window (contrast the fieldtest's empty-space roll
/// over the raw FlatGraph, where `chosen_params == vec![]` everywhere).
#[test]
fn walk_forward_consumes_blueprint_non_degenerate() {
let server = default_data_server();
if !server.has_symbol(SYMBOL) {
eprintln!("skip: no local data at {DEFAULT_DATA_PATH} (symbol {SYMBOL} absent)");
return;
}
// A short 2-window roll over 2023..2024 keeps the gated test cheap while still
// exercising the per-window in-sample optimize. is=6mo, oos=3mo, step=3mo.
let day_ns: i64 = 86_400_000 * 1_000_000;
let month_ns: i64 = 30 * day_ns;
let origin = aura_ingest::unix_ms_to_epoch_ns(
chrono::Utc.with_ymd_and_hms(2023, 1, 1, 0, 0, 0).unwrap().timestamp_millis(),
);
let end = aura_ingest::unix_ms_to_epoch_ns(
chrono::Utc.with_ymd_and_hms(2024, 6, 30, 23, 59, 59).unwrap().timestamp_millis(),
);
let roller = WindowRoller::new(
(origin, end),
6 * month_ns,
3 * month_ns,
3 * month_ns,
RollMode::Rolling,
)
.expect("span fits >= 1 window");
// The non-empty space the in-sample sweep optimizes over per window: the two
// genuine tuning knobs. This is the #97 resolution — a real space, so the
// chosen params are populated, never an empty degenerate vec.
let space = ger40_breakout_blueprint(
pip_size_of(SYMBOL),
BAR_MINUTES,
SESSION_HOUR,
SESSION_MINUTE,
Berlin,
)
.0
.param_space();
let server_for_closure = Arc::clone(&server);
let result = walk_forward(roller, space.clone(), move |w: WindowBounds| -> WindowRun {
// In-sample optimize: a tiny entry×exit grid over the IS window, choose the
// point with the best in-sample total_pips. The blueprint is consumed
// directly via its param_space() — no re-authoring.
let is_grid = GridSpace::new(
&space,
space
.iter()
.map(|ps| match ps.name.as_str() {
"entry_bar.target" => vec![Scalar::i64(2), Scalar::i64(3)],
"exit_bar.target" => vec![Scalar::i64(5), Scalar::i64(6)],
other => panic!("unexpected param slot {other}"),
})
.collect(),
)
.expect("IS grid well-formed");
let is_family = sweep(&is_grid, |pt: &[Cell]| {
run_point(&server_for_closure, pt, w.is.0, w.is.1)
});
let best = is_family
.points
.iter()
.max_by(|a, b| {
a.report
.metrics
.total_pips
.partial_cmp(&b.report.metrics.total_pips)
.unwrap_or(std::cmp::Ordering::Equal)
})
.expect("the IS grid is non-empty");
let chosen = best.params.clone();
// Apply the chosen params on the OOS window.
let oos_report = run_point(&server_for_closure, &chosen, w.oos.0, w.oos.1);
// Re-run once on OOS to capture the recorded equity segment for stitching.
let (bp, taps) = ger40_breakout_blueprint(
pip_size_of(SYMBOL),
BAR_MINUTES,
SESSION_HOUR,
SESSION_MINUTE,
Berlin,
);
let mut h = bp.bootstrap_with_cells(&chosen).expect("chosen point kind-checked");
let sources = open_ohlc(&server_for_closure, SYMBOL, w.oos.0, w.oos.1)
.expect("OOS window overlaps data");
h.run(sources);
drop(h);
let oos_equity: Vec<(Timestamp, f64)> = taps
.equity
.try_iter()
.collect::<Vec<_>>()
.iter()
.map(|(t, r)| (*t, scalar_f64(&r[0])))
.collect();
WindowRun { chosen_params: chosen, oos_equity, oos_report }
});
// Non-degenerate: a non-empty roll, EACH window carrying a non-empty chosen
// point (the #97 proof — a real space was optimized per window).
assert!(!result.windows.is_empty(), "walk_forward must yield >= 1 window");
for (k, w) in result.windows.iter().enumerate() {
assert!(
!w.run.chosen_params.is_empty(),
"window {k}: chosen_params must be NON-empty (the #97 non-degenerate proof)",
);
assert_eq!(
w.run.chosen_params.len(),
2,
"window {k}: chosen_params spans the two-param space",
);
assert!(
w.run.oos_report.metrics.total_pips.is_finite(),
"window {k}: OOS total_pips must be finite",
);
}
}