Files
Aura/crates/aura-ingest/examples/ger40_breakout_sweep.rs
T
Brummel 0a06afed29 feat(0078): cross-instrument generalization as a validation step
Last piece of the Inferential-validation milestone (#146): a new
`aura generalize` subcommand grades how consistently a single candidate
holds across a set of instruments — the across-instrument axis of the
same anti-false-discovery concern as trials-deflation (#144, within one
family) and plateau-over-peak (#145, within one instrument's surface).

Aggregator/validator, not a third selector (user-ratified): it runs a
brought candidate (a single-cell stage1-r grid — --fast/--slow/--stop-
length/--stop-k pinned to single values) across an instrument list
(--real SYM1,SYM2,...), collects per-instrument R-metrics, and reduces
them to a worst-case floor (min over instruments) + a sign-agreement
count + the per-instrument breakdown. R-only (C10 — R is the only
account/instrument-agnostic unit); the worst-case min is the cross-
instrument sibling of PlateauMode::Worst and satisfies the non-
domination constraint by construction (a strong instrument cannot lift
a min). Rejected: a cross-instrument t-stat mean/std*sqrt(M), which
reintroduces the pooled mean the milestone warns against.

- RunManifest gains a first-class `instrument` lineage field (serde-
  widened, C14/C18 — legacy lines load as None, existing manifest bytes
  unchanged); the compat read-mirror threads it in lockstep.
- FamilyKind::CrossInstrument (C12 comparison axis); the M per-instrument
  runs persist as a CrossInstrument family, each member self-identifying
  via its stamped instrument. The aggregate is recomputable from the
  members (McAggregate precedent), printed live.
- The generalization reduction lives registry-side beside optimize_
  deflated/optimize_plateau (C9), reusing resolve_metric/metric_value/
  is_r_metric; check_r_metric is the data-free pre-check the CLI runs
  before evaluating any instrument.
- Additive: existing sweep/walkforward/mc/standalone paths stay byte-
  identical (C23 — instrument omitted when None via skip_serializing_if).

Verified by the orchestrator: cargo test --workspace green (0 failed —
the new generalization_*, check_r_metric, parse_generalize_*, and the
gated cross-instrument E2E all pass; the E2E ran the real GER40/USDJPY
path on this host, exit 0); cargo clippy --workspace --all-targets
-D warnings clean.

closes #146
2026-06-26 19:04:53 +02:00

174 lines
6.2 KiB
Rust
Raw 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.
//! Runnable World-family demo: **`sweep`** the GER40 session-breakout `Composite`
//! blueprint 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 aura_ingest::{default_data_server, open_ohlc, 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 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).
/// 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()
.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: (from, to),
seed: 0,
broker: "sim-optimal(pip_size=1)".to_string(),
selection: None,
instrument: None,
},
metrics: summarize(&equity, &exposure),
}
}
fn main() {
let server = default_data_server();
if !server.has_symbol(SYMBOL) {
println!("skip: no local data at {DEFAULT_DATA_PATH} (symbol {SYMBOL} absent).");
return;
}
// Full-year 2024.
let (from, _) = utc_month_window(2024, 1);
let (_, to) = utc_month_window(2024, 12);
let pip_size = pip_size_of(SYMBOL);
// 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(pip_size, 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, to)
});
// 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.bias_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:?}"),
}
}