Files
Aura/crates/aura-ingest/examples/ger40_breakout_sweep.rs
T
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

178 lines
6.3 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_backtest::{summarize, RunReport};
use aura_engine::{sweep, GridSpace, RunManifest};
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![],
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 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:?}"),
}
}