feat(aura-ingest): World-family demos prove the GER40 breakout is World-consumable (spec 0051)
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
This commit is contained in:
@@ -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<DataServer>,
|
||||
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<Cell> = 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<Box<dyn Source>> = 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:?}"),
|
||||
}
|
||||
}
|
||||
@@ -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<DataServer>, 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::<Vec<_>>());
|
||||
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<usize> = (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::<Vec<_>>(),
|
||||
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:?}"),
|
||||
}
|
||||
}
|
||||
@@ -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<DataServer>,
|
||||
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::<Vec<_>>(),
|
||||
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:?}"),
|
||||
}
|
||||
}
|
||||
@@ -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<DataServer>, 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::<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![],
|
||||
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<_>>(),
|
||||
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::<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",
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user