Files
Aura/crates/aura-ingest/examples/ger40_breakout_sweep.rs
T
Brummel c6c1d3bb10 feat(stage1-r): rename the exposure_sign_flips metric to bias_sign_flips (#126)
Complete the typed-field half of the exposure->bias rename - the one the cycle-0065
close audit flagged as aging worst (a serde-stable on-disk key).

- RunMetrics.exposure_sign_flips -> bias_sign_flips with
  #[serde(alias = "exposure_sign_flips")], so legacy runs.jsonl still deserialises
  (pinned by the legacy-read tests, which keep the old key); new output serialises
  bias_sign_flips (the byte-pin tests updated to the new key).
- The registry rank metric: Metric::ExposureSignFlips -> BiasSignFlips, the rank
  string accepts BOTH "bias_sign_flips" and "exposure_sign_flips" (CLI back-compat),
  the error/listing updated; the rank tests exercise both names.
- The MC aggregate field renamed too (McAggregate is hand-rendered to JSON, not
  serde-derived - no alias needed).

Scope held to the typed metric. The strategy-output PARAM NAMESPACE - the
.named("exposure") Bias instance, its exposure.scale knob path (Composite::param_space
derives the knob from the node name), and the exposure_scale manifest label - is a
coherent ~30-site, sweep-pinned surface (the implement loop correctly flagged renaming
the knob as an unscoped expansion touching walkforward + the cli_run byte-pins). Kept
uniformly as "exposure" and deferred to a follow-on so the cluster renames as one
consistent unit. SimBroker's pre-reframe exposure concept and the on-disk "exposure"
tap label stay by the #117 fork decision.

cargo test --workspace green; clippy --workspace --all-targets -D warnings clean; a
live `aura run --harness stage1-r` emits "bias_sign_flips".

closes #126
refs #117
2026-06-24 13:11:46 +02:00

172 lines
6.1 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(),
},
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:?}"),
}
}