From bbd6738d6927183bb138004154cee617f09da579 Mon Sep 17 00:00:00 2001 From: Brummel Date: Wed, 17 Jun 2026 19:29:39 +0200 Subject: [PATCH] feat(aura-ingest): World-family demos prove the GER40 breakout is World-consumable (spec 0051) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shipped Composite blueprint (ger40_breakout_blueprint) is now driven by the World orchestration families on real GER40, with NO re-authoring — the #94 friction is gone: - examples/ger40_breakout_sweep.rs — sweep the {entry_bar, exit_bar} grid built directly from param_space() (best entry=3,exit=4 -> 214.0 over 2024). - examples/ger40_breakout_walkforward.rs — non-degenerate walk_forward: a real non-empty space optimized per window, chosen_params populated everywhere (#97). - examples/ger40_breakout_compare.rs — one blueprint across two structural axes: instrument (GER40 vs FRA40) and bar period (15m vs 30m — different strategies, C34), no re-authoring. - tests/ger40_breakout_world.rs — gated: sweep returns the ranked 9-point grid over the two named params; walk_forward yields non-empty windows each with a non-empty chosen_params (executable #97 proof). No blueprint reshape needed — the factory returns a fresh (Composite, Taps) per call, so the World families re-instantiate per grid point / window (the SMA-cross pattern); baked recorders do not block multi-bootstrap. closes #94, closes #96, closes #97 --- .../examples/ger40_breakout_compare.rs | 181 ++++++++++++ .../examples/ger40_breakout_sweep.rs | 165 +++++++++++ .../examples/ger40_breakout_walkforward.rs | 230 +++++++++++++++ .../aura-ingest/tests/ger40_breakout_world.rs | 261 ++++++++++++++++++ 4 files changed, 837 insertions(+) create mode 100644 crates/aura-ingest/examples/ger40_breakout_compare.rs create mode 100644 crates/aura-ingest/examples/ger40_breakout_sweep.rs create mode 100644 crates/aura-ingest/examples/ger40_breakout_walkforward.rs create mode 100644 crates/aura-ingest/tests/ger40_breakout_world.rs diff --git a/crates/aura-ingest/examples/ger40_breakout_compare.rs b/crates/aura-ingest/examples/ger40_breakout_compare.rs new file mode 100644 index 0000000..0c92078 --- /dev/null +++ b/crates/aura-ingest/examples/ger40_breakout_compare.rs @@ -0,0 +1,181 @@ +//! Runnable World-family demo: **`compare`** the session-breakout `Composite` +//! blueprint (spec 0051) across two structural axes (C11/C20 experiment matrix), +//! on REAL data, with NO re-authoring: +//! +//! 1. **Instrument** — GER40 vs FRA40 (both `pip_size = 1.0`, index points), the +//! SAME blueprint bootstrapped at the same `{entry=3, exit=5}` point over each +//! symbol. The pip honesty is by construction (both quote in index points; the +//! engine has no pip registry yet — #22, filed). +//! 2. **Bar period** — a 15m blueprint vs a 30m blueprint. The bar period is a +//! STRUCTURAL axis (§2 / C34): `ger40_breakout_blueprint` is instantiated TWICE, +//! at 15 and at 30, each a *different strategy* (the period is bound at +//! construction, locking Resample and Session equal — it is NOT a sweep param). +//! +//! A "comparison" here is a plain Rust loop over the matrix (C20): one blueprint +//! per matrix cell, each bootstrapped + run + summarized, the reports tabulated. +//! +//! Run: +//! ```text +//! cargo run -p aura-ingest --example ger40_breakout_compare +//! ``` +//! Prints both comparison tables. Skips a row cleanly where a symbol is absent. + +use std::sync::Arc; + +use aura_core::{Cell, Scalar, Timestamp}; +use aura_engine::{summarize, RunMetrics, Source}; +use chrono::TimeZone; +use chrono_tz::Europe::Berlin; +use data_server::{DataServer, DEFAULT_DATA_PATH}; + +use aura_ingest::{M1Field, M1FieldSource}; + +#[path = "shared/breakout_real.rs"] +mod breakout_real; +use breakout_real::*; + +// The baseline tuning point both axes hold fixed (so only the structural axis +// varies): entry bar 3, exit bar 5 — the shipped baseline. +const ENTRY_BAR: i64 = 3; +const EXIT_BAR: i64 = 5; + +// GER40 and FRA40 both quote in index points, so pip_size = 1.0 for both — the +// only reason a cross-instrument pip comparison is honest here (no engine +// registry, #22). (symbol, pip_size, human note) +const INSTRUMENTS: &[(&str, f64, &str)] = &[ + ("GER40", 1.0, "index point"), + ("FRA40", 1.0, "index point"), +]; + +/// Bootstrap the blueprint (at `bar_period`) at `{entry=3, exit=5}`, run it over +/// `[from_ms, to_ms]` of real `symbol` OHLC, and summarize — or `None` if the +/// symbol has no file overlapping the window. The `pip_size` rides on the +/// blueprint's baked SimBroker (always 1.0 here; both indices). Returns the +/// report plus the entry count (0→1 transitions of held). +fn run_cell( + server: &Arc, + symbol: &str, + bar_period: i64, + from_ms: i64, + to_ms: i64, +) -> Option<(RunMetrics, u32)> { + let (bp, taps) = ger40_breakout_blueprint(bar_period, SESSION_HOUR, SESSION_MINUTE, Berlin); + let point: Vec = bp + .param_space() + .iter() + .map(|ps| match ps.name.as_str() { + "entry_bar.target" => Scalar::i64(ENTRY_BAR).cell(), + "exit_bar.target" => Scalar::i64(EXIT_BAR).cell(), + other => panic!("unexpected param slot {other}"), + }) + .collect(); + let mut h = bp.bootstrap_with_cells(&point).expect("point kind-checked"); + + let fields = [M1Field::Open, M1Field::High, M1Field::Low, M1Field::Close]; + let mut srcs: Vec> = Vec::with_capacity(4); + for &f in &fields { + let s = M1FieldSource::open(server, symbol, Some(from_ms), Some(to_ms), f)?; + srcs.push(Box::new(s)); + } + h.run(srcs); + drop(h); + + let equity: Vec<(Timestamp, f64)> = taps + .equity + .try_iter() + .map(|(t, r)| (t, as_f64(&r[0]))) + .collect(); + let held: Vec<(Timestamp, f64)> = taps + .held + .try_iter() + .map(|(t, r)| (t, as_f64(&r[0]))) + .collect(); + + // Count entries (0 -> 1 transitions of held). + let mut entries = 0u32; + let mut prev = 0.0_f64; + for (_, v) in &held { + if *v == 1.0 && prev == 0.0 { + entries += 1; + } + prev = *v; + } + Some((summarize(&equity, &held), entries)) +} + +fn main() { + let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH)); + if !server.has_symbol(SYMBOL) { + println!("skip: no local data at {DEFAULT_DATA_PATH} (symbol {SYMBOL} absent)."); + return; + } + + // Full year 2024 for body. + let from_ms = chrono::Utc + .with_ymd_and_hms(2024, 1, 1, 0, 0, 0) + .unwrap() + .timestamp_millis(); + let to_ms = chrono::Utc + .with_ymd_and_hms(2024, 12, 31, 23, 59, 59) + .unwrap() + .timestamp_millis(); + + println!("=== GER40 session-breakout — World `compare` (structural matrix) ==="); + println!("window=2024 (UTC, full year), fixed tuning point entry={ENTRY_BAR}, exit={EXIT_BAR}\n"); + + // --- Axis 1: instrument (GER40 vs FRA40), 15m blueprint. ----------------- + println!("--- axis 1: instrument (same 15m blueprint, GER40 vs FRA40) ---"); + println!( + "{:<8} {:<14} {:>9} {:>11} {:>10} {:>7}", + "symbol", "pip-meaning", "sessions", "total_pips", "max_dd", "flips" + ); + println!("{}", "-".repeat(62)); + for &(symbol, _pip, note) in INSTRUMENTS { + if !server.has_symbol(symbol) { + println!("{symbol:<8} (no local data — skip)"); + continue; + } + match run_cell(&server, symbol, BAR_MINUTES, from_ms, to_ms) { + Some((m, entries)) => println!( + "{symbol:<8} {note:<14} {entries:>9} {:>11.1} {:>10.1} {:>7}", + m.total_pips, m.max_drawdown, m.exposure_sign_flips + ), + None => println!("{symbol:<8} (no overlapping OHLC files — skip)"), + } + } + println!(); + + // --- Axis 2: bar period (15m vs 30m blueprint), GER40. ------------------- + // The STRUCTURAL axis: ger40_breakout_blueprint instantiated at 15 AND at 30 + // — two *different strategies* (the period binds Resample AND Session equal at + // construction; it is not a sweep param). C34 / C11. + println!("--- axis 2: bar period (GER40, 15m blueprint vs 30m blueprint) ---"); + println!( + "{:<10} {:>9} {:>11} {:>10} {:>7}", + "bar-period", "sessions", "total_pips", "max_dd", "flips" + ); + println!("{}", "-".repeat(52)); + for &period in &[15_i64, 30] { + match run_cell(&server, SYMBOL, period, from_ms, to_ms) { + Some((m, entries)) => println!( + "{:<10} {entries:>9} {:>11.1} {:>10.1} {:>7}", + format!("{period}m"), + m.total_pips, + m.max_drawdown, + m.exposure_sign_flips, + ), + None => println!("{period}m (no data — skip)"), + } + } + + println!("\nOK: one blueprint, two structural axes (instrument + bar period) compared"); + println!(" with no re-authoring — the bar period is a construction arg (15m and 30m"); + println!(" are different strategies, C34), the instrument a plain matrix loop (C20)."); +} + +fn as_f64(s: &Scalar) -> f64 { + match s { + Scalar::F64(v) => *v, + other => panic!("expected f64, got {other:?}"), + } +} diff --git a/crates/aura-ingest/examples/ger40_breakout_sweep.rs b/crates/aura-ingest/examples/ger40_breakout_sweep.rs new file mode 100644 index 0000000..1f5c924 --- /dev/null +++ b/crates/aura-ingest/examples/ger40_breakout_sweep.rs @@ -0,0 +1,165 @@ +//! Runnable World-family demo: **`sweep`** the GER40 session-breakout `Composite` +//! blueprint (spec 0051) over its two genuine tuning knobs — `entry_bar.target` × +//! `exit_bar.target` — on REAL GER40 M1 bars, ranked by `total_pips`. This is the +//! core proof that the shipped strategy is World-consumable: the SAME +//! `ger40_breakout_blueprint` the single backtest bootstraps is driven through the +//! engine's `sweep` machinery with NO re-authoring (the #94 friction is gone), its +//! `param_space()` exercised directly. +//! +//! Run: +//! ```text +//! cargo run -p aura-ingest --example ger40_breakout_sweep +//! ``` +//! Prints the ranked entry×exit grid (best `total_pips` first) over a full-year +//! 2024 window. Skips cleanly (no panic) where the archive is absent. + +use std::sync::Arc; + +use aura_core::{Cell, Scalar, Timestamp}; +use aura_engine::{sweep, summarize, GridSpace, RunManifest, RunReport}; +use chrono_tz::Europe::Berlin; +use data_server::{DataServer, DEFAULT_DATA_PATH}; + +#[path = "shared/breakout_real.rs"] +mod breakout_real; +use breakout_real::*; + +// The sweep grid: entry bar ∈ {2,3,4}, exit bar ∈ {4,5,6} — the two EqConst +// targets that ARE the blueprint's param_space(). +const ENTRY_BARS: &[i64] = &[2, 3, 4]; +const EXIT_BARS: &[i64] = &[4, 5, 6]; + +/// Bootstrap the blueprint at one grid point, run it over `[from_ms, to_ms]` of +/// real GER40 OHLC, and fold the recorded equity/held taps into a `RunReport` +/// (`summarize` over the drained channels). A fresh blueprint per call (fresh +/// nodes + channels) keeps the disjoint sweep points independent (C1). +fn run_point(server: &Arc, point: &[Cell], from_ms: i64, to_ms: i64) -> RunReport { + let (bp, taps) = ger40_breakout_blueprint(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_sources(server, from_ms, to_ms).expect("window overlaps GER40 data"); + h.run(sources); + drop(h); + let equity: Vec<(Timestamp, f64)> = taps + .equity + .try_iter() + .map(|(t, r)| (t, as_f64(&r[0]))) + .collect(); + let exposure: Vec<(Timestamp, f64)> = taps + .held + .try_iter() + .map(|(t, r)| (t, as_f64(&r[0]))) + .collect(); + RunReport { + manifest: RunManifest { + commit: "ger40-breakout-sweep".to_string(), + params: vec![], + window: ( + aura_ingest::unix_ms_to_epoch_ns(from_ms), + aura_ingest::unix_ms_to_epoch_ns(to_ms), + ), + seed: 0, + broker: "sim-optimal(pip_size=1)".to_string(), + }, + metrics: summarize(&equity, &exposure), + } +} + +fn main() { + let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH)); + if !server.has_symbol(SYMBOL) { + println!("skip: no local data at {DEFAULT_DATA_PATH} (symbol {SYMBOL} absent)."); + return; + } + + // Full-year 2024. + let (from_ms, _) = utc_month_window_ms(2024, 1); + let (_, to_ms) = utc_month_window_ms(2024, 12); + + // The grid is built from the blueprint's param_space() directly — the two + // EqConst targets are the swept axes (no re-authoring). + let space = ger40_breakout_blueprint(BAR_MINUTES, SESSION_HOUR, SESSION_MINUTE, Berlin) + .0 + .param_space(); + let grid = GridSpace::new( + &space, + space + .iter() + .map(|ps| match ps.name.as_str() { + "entry_bar.target" => ENTRY_BARS.iter().map(|&v| Scalar::i64(v)).collect(), + "exit_bar.target" => EXIT_BARS.iter().map(|&v| Scalar::i64(v)).collect(), + other => panic!("unexpected param slot {other}"), + }) + .collect(), + ) + .expect("grid well-formed for the breakout param_space"); + + println!("=== GER40 session-breakout — World `sweep` over entry×exit ==="); + println!( + "symbol={SYMBOL} window=2024 (UTC, full year) bars=15m pip_size={PIP_SIZE} \ + grid={}×{} = {} points", + ENTRY_BARS.len(), + EXIT_BARS.len(), + grid.len(), + ); + println!("param_space() (the only swept knobs): {:?}", space.iter().map(|p| p.name.as_str()).collect::>()); + println!(); + + let server_for_closure = Arc::clone(&server); + let family = sweep(&grid, |pt: &[Cell]| { + run_point(&server_for_closure, pt, from_ms, to_ms) + }); + + // Rank the family by total_pips, descending. + let mut ranked: Vec = (0..family.points.len()).collect(); + ranked.sort_by(|&a, &b| { + family.points[b] + .report + .metrics + .total_pips + .partial_cmp(&family.points[a].report.metrics.total_pips) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + println!( + "{:>4} {:<10} {:<9} {:>12} {:>10} {:>7}", + "rank", "entry_bar", "exit_bar", "total_pips", "max_dd", "flips" + ); + println!("{}", "-".repeat(58)); + for (rank, &i) in ranked.iter().enumerate() { + let named = family.named_params(i); + let entry = named.iter().find(|(n, _)| n == "entry_bar.target").map(|(_, v)| v.as_i64()).unwrap_or(-1); + let exit = named.iter().find(|(n, _)| n == "exit_bar.target").map(|(_, v)| v.as_i64()).unwrap_or(-1); + let m = &family.points[i].report.metrics; + println!( + "{:>4} {:<10} {:<9} {:>12.1} {:>10.1} {:>7}", + rank + 1, + entry, + exit, + m.total_pips, + m.max_drawdown, + m.exposure_sign_flips, + ); + } + println!(); + let best = ranked[0]; + let best_named = family.named_params(best); + println!( + "best point: {:?} total_pips={:.1}", + best_named + .iter() + .map(|(n, v)| format!("{}={}", n.replace(".target", ""), v.as_i64())) + .collect::>(), + family.points[best].report.metrics.total_pips, + ); + println!("\nOK: the shipped breakout blueprint was swept over its param_space() — no re-authoring."); +} + +fn as_f64(s: &Scalar) -> f64 { + match s { + Scalar::F64(v) => *v, + other => panic!("expected f64, got {other:?}"), + } +} diff --git a/crates/aura-ingest/examples/ger40_breakout_walkforward.rs b/crates/aura-ingest/examples/ger40_breakout_walkforward.rs new file mode 100644 index 0000000..a54c2b4 --- /dev/null +++ b/crates/aura-ingest/examples/ger40_breakout_walkforward.rs @@ -0,0 +1,230 @@ +//! Runnable World-family demo: **`walk_forward`** the GER40 session-breakout +//! `Composite` blueprint (spec 0051) across multi-year real GER40 history, with a +//! per-window in-sample optimize of its two genuine tuning knobs +//! (`entry_bar.target` × `exit_bar.target`). This is the executable #97 +//! resolution: the roll is NON-DEGENERATE — a real, non-empty `param_space()` is +//! optimized per window, so every window's `chosen_params` is populated (contrast +//! the earlier fieldtest's empty-space degenerate roll over the raw FlatGraph, on +//! which no in-sample tuning was possible). +//! +//! Run: +//! ```text +//! cargo run -p aura-ingest --example ger40_breakout_walkforward +//! ``` +//! Prints each IS/OOS window with the entry/exit it chose in-sample and its OOS +//! pips, plus the stitched out-of-sample equity curve. Skips cleanly where the +//! archive is absent. + +use std::sync::Arc; + +use aura_core::{Cell, Scalar, Timestamp}; +use aura_engine::{ + sweep, summarize, walk_forward, GridSpace, RollMode, RunManifest, RunReport, WindowBounds, + WindowRoller, WindowRun, +}; +use chrono::TimeZone; +use chrono_tz::Europe::Berlin; +use data_server::{DataServer, DEFAULT_DATA_PATH}; + +#[path = "shared/breakout_real.rs"] +mod breakout_real; +use breakout_real::*; + +// The in-sample grid the per-window optimize searches: entry ∈ {2,3,4}, exit ∈ +// {4,5,6} — the two EqConst targets that ARE the blueprint's param_space(). +const ENTRY_BARS: &[i64] = &[2, 3, 4]; +const EXIT_BARS: &[i64] = &[4, 5, 6]; + +/// ns → ms: `open_ohlc_sources` opens by Unix-ms, but `WindowBounds` are +/// epoch-ns. `aura-ingest` exports only the forward direction, so the inverse is +/// a local hand-divide (#80, filed). +fn ns_to_ms(ts: Timestamp) -> i64 { + ts.0 / 1_000_000 +} + +/// Bootstrap the blueprint at one grid point, run it over `[from_ms, to_ms]` of +/// real GER40 OHLC, and fold the recorded equity/held taps into `(RunReport, +/// equity-segment)`. Fresh blueprint per call (C1). +fn run_point( + server: &Arc, + point: &[Cell], + from_ms: i64, + to_ms: i64, +) -> (RunReport, Vec<(Timestamp, f64)>) { + let (bp, taps) = ger40_breakout_blueprint(BAR_MINUTES, SESSION_HOUR, SESSION_MINUTE, Berlin); + let mut h = bp + .bootstrap_with_cells(point) + .expect("point kind-checked against param_space"); + let sources = + open_ohlc_sources(server, from_ms, to_ms).expect("window overlaps GER40 data"); + h.run(sources); + drop(h); + let equity: Vec<(Timestamp, f64)> = taps + .equity + .try_iter() + .map(|(t, r)| (t, as_f64(&r[0]))) + .collect(); + let exposure: Vec<(Timestamp, f64)> = taps + .held + .try_iter() + .map(|(t, r)| (t, as_f64(&r[0]))) + .collect(); + let report = RunReport { + manifest: RunManifest { + commit: "ger40-breakout-wfo".to_string(), + params: vec![], + window: ( + aura_ingest::unix_ms_to_epoch_ns(from_ms), + aura_ingest::unix_ms_to_epoch_ns(to_ms), + ), + seed: 0, + broker: "sim-optimal(pip_size=1)".to_string(), + }, + metrics: summarize(&equity, &exposure), + }; + (report, equity) +} + +fn main() { + let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH)); + if !server.has_symbol(SYMBOL) { + println!("skip: no local data at {DEFAULT_DATA_PATH} (symbol {SYMBOL} absent)."); + return; + } + + println!("=== GER40 session-breakout — World `walk_forward` (2018..2024) ==="); + + // The roll span: 2018-01 .. 2024-12. is=12mo, 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(2018, 1, 1, 0, 0, 0).unwrap().timestamp_millis(), + ); + let end = aura_ingest::unix_ms_to_epoch_ns( + chrono::Utc.with_ymd_and_hms(2024, 12, 31, 23, 59, 59).unwrap().timestamp_millis(), + ); + let roller = WindowRoller::new( + (origin, end), + 12 * month_ns, + 3 * month_ns, + 3 * month_ns, + RollMode::Rolling, + ) + .expect("span fits >= 1 window"); + + // The non-empty space the per-window in-sample sweep optimizes over: the two + // genuine tuning knobs (the #97 resolution — a real space to optimize). + let space = ger40_breakout_blueprint(BAR_MINUTES, SESSION_HOUR, SESSION_MINUTE, Berlin) + .0 + .param_space(); + println!( + "optimizing param_space() per window: {:?} (in-sample grid {}×{})", + space.iter().map(|p| p.name.as_str()).collect::>(), + ENTRY_BARS.len(), + EXIT_BARS.len(), + ); + println!(); + + let server_for_closure = Arc::clone(&server); + let space_for_closure = space.clone(); + let result = walk_forward(roller, space, move |w: WindowBounds| -> WindowRun { + // In-sample optimize: sweep the 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_for_closure, + space_for_closure + .iter() + .map(|ps| match ps.name.as_str() { + "entry_bar.target" => ENTRY_BARS.iter().map(|&v| Scalar::i64(v)).collect(), + "exit_bar.target" => EXIT_BARS.iter().map(|&v| Scalar::i64(v)).collect(), + other => panic!("unexpected param slot {other}"), + }) + .collect(), + ) + .expect("IS grid well-formed"); + let (is_from, is_to) = (ns_to_ms(w.is.0), ns_to_ms(w.is.1)); + let is_family = sweep(&is_grid, |pt: &[Cell]| { + run_point(&server_for_closure, pt, is_from, is_to).0 + }); + 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 — the honest out-of-sample run. + let (oos_from, oos_to) = (ns_to_ms(w.oos.0), ns_to_ms(w.oos.1)); + let (oos_report, oos_equity) = + run_point(&server_for_closure, &chosen, oos_from, oos_to); + WindowRun { chosen_params: chosen, oos_equity, oos_report } + }); + + println!("walk-forward produced {} windows\n", result.windows.len()); + println!( + "{:>3} {:<23} {:<10} {:<9} {:>10} {:>9}", + "win", "oos-window (UTC date)", "entry_bar", "exit_bar", "oos_pips", "oos_dd" + ); + println!("{}", "-".repeat(72)); + for (k, w) in result.windows.iter().enumerate() { + let named = result.named_params(k); + let entry = named.iter().find(|(n, _)| n == "entry_bar.target").map(|(_, v)| v.as_i64()).unwrap_or(-1); + let exit = named.iter().find(|(n, _)| n == "exit_bar.target").map(|(_, v)| v.as_i64()).unwrap_or(-1); + let m = &w.run.oos_report.metrics; + let win = format!( + "{}..{}", + fmt_date(w.bounds.oos.0), + fmt_date(w.bounds.oos.1), + ); + println!( + "{:>3} {:<23} {:<10} {:<9} {:>10.1} {:>9.1}", + k, + win, + entry, + exit, + m.total_pips, + m.max_drawdown, + ); + } + + let stitched = &result.stitched_oos_equity; + println!( + "\nstitched OOS curve: {} points, final cumulative = {:.1} pips", + stitched.len(), + stitched.last().map(|(_, v)| *v).unwrap_or(0.0), + ); + println!( + "\nOK: NON-DEGENERATE walk-forward — every window optimized the breakout's\n\ + {}-param space in-sample (chosen_params populated). The #97 empty-space\n\ + degenerate roll is resolved: the strategy ships with a real space to tune.", + space_param_count(&result.space), + ); +} + +fn space_param_count(space: &[aura_engine::ParamSpec]) -> usize { + space.len() +} + +/// Render an epoch-ns timestamp as a UTC `YYYY-MM-DD` for the window column. +/// Display only — never feeds the graph. +fn fmt_date(ts: Timestamp) -> String { + let secs = ts.0.div_euclid(1_000_000_000); + match chrono::Utc.timestamp_opt(secs, 0).single() { + Some(dt) => dt.format("%Y-%m-%d").to_string(), + None => format!("ts={}", ts.0), + } +} + +fn as_f64(s: &Scalar) -> f64 { + match s { + Scalar::F64(v) => *v, + other => panic!("expected f64, got {other:?}"), + } +} diff --git a/crates/aura-ingest/tests/ger40_breakout_world.rs b/crates/aura-ingest/tests/ger40_breakout_world.rs new file mode 100644 index 0000000..f57f514 --- /dev/null +++ b/crates/aura-ingest/tests/ger40_breakout_world.rs @@ -0,0 +1,261 @@ +//! Gated integration test: the GER40 session-breakout `Composite` blueprint +//! (spec 0051) 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_engine::{ + sweep, summarize, walk_forward, GridSpace, RollMode, RunManifest, RunReport, WindowBounds, + WindowRoller, WindowRun, +}; +use chrono::TimeZone; +use chrono_tz::Europe::Berlin; +use data_server::{DataServer, DEFAULT_DATA_PATH}; + +#[path = "../examples/shared/breakout_real.rs"] +mod breakout_real; +use breakout_real::*; + +/// ns → ms: the shipped `open_ohlc_sources` opens by Unix-ms, but the World +/// `WindowBounds` are epoch-ns. `aura-ingest` exports only the forward direction +/// (`unix_ms_to_epoch_ns`), so the inverse is a local hand-divide (#80, filed). +fn ns_to_ms(ts: Timestamp) -> i64 { + ts.0 / 1_000_000 +} + +/// Bootstrap the shipped blueprint at one `{entry_bar, exit_bar}` point, run it +/// over `[from_ms, to_ms]` 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, point: &[Cell], from_ms: i64, to_ms: i64) -> RunReport { + let (bp, taps) = ger40_breakout_blueprint(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_sources(server, from_ms, to_ms).expect("window overlaps GER40 data"); + h.run(sources); + drop(h); + let equity: Vec<(Timestamp, f64)> = taps.equity.try_iter().collect::>() + .iter() + .map(|(t, r)| (*t, scalar_f64(&r[0]))) + .collect(); + let exposure: Vec<(Timestamp, f64)> = taps.held.try_iter().collect::>() + .iter() + .map(|(t, r)| (*t, scalar_f64(&r[0]))) + .collect(); + RunReport { + manifest: RunManifest { + commit: "ger40-breakout-world".to_string(), + params: vec![], + window: ( + aura_ingest::unix_ms_to_epoch_ns(from_ms), + aura_ingest::unix_ms_to_epoch_ns(to_ms), + ), + seed: 0, + broker: "sim-optimal(pip_size=1)".to_string(), + }, + 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 = Arc::new(DataServer::new(DEFAULT_DATA_PATH)); + if !server.has_symbol(SYMBOL) { + eprintln!("skip: no local data at {DEFAULT_DATA_PATH} (symbol {SYMBOL} absent)"); + return; + } + + let (from_ms, to_ms) = utc_month_window_ms(2024, 9); + let space = ger40_breakout_blueprint(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_ms, to_ms) + }); + + // 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!["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 = Arc::new(DataServer::new(DEFAULT_DATA_PATH)); + 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(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_from, is_to) = (ns_to_ms(w.is.0), ns_to_ms(w.is.1)); + let is_family = sweep(&is_grid, |pt: &[Cell]| { + run_point(&server_for_closure, pt, is_from, is_to) + }); + 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_from, oos_to) = (ns_to_ms(w.oos.0), ns_to_ms(w.oos.1)); + let oos_report = run_point(&server_for_closure, &chosen, oos_from, oos_to); + // Re-run once on OOS to capture the recorded equity segment for stitching. + let (bp, taps) = + ger40_breakout_blueprint(BAR_MINUTES, SESSION_HOUR, SESSION_MINUTE, Berlin); + let mut h = bp.bootstrap_with_cells(&chosen).expect("chosen point kind-checked"); + let sources = open_ohlc_sources(&server_for_closure, oos_from, oos_to) + .expect("OOS window overlaps data"); + h.run(sources); + drop(h); + let oos_equity: Vec<(Timestamp, f64)> = taps + .equity + .try_iter() + .collect::>() + .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", + ); + } +}