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

140 lines
5.7 KiB
Rust

//! Runnable demonstration: the GER40 15m session-breakout strategy — the
//! Phase-1 `aura-std` node vocabulary, proven synthetically in
//! `aura-engine/tests/ger40_breakout.rs` — driven over REAL GER40 M1 bars from
//! the local data-server archive (one calendar month).
//!
//! This is a *pure composition* of SHIPPED `aura-std` nodes: no project-specific
//! signal code, so it lives in the engine repo's `examples/` carveout (C9), not
//! a separate project crate. The strategy is authored as a World-consumable
//! `Composite` blueprint (`ger40_breakout_blueprint`) and bootstrapped
//! here by name (`entry_bar=3`, `exit_bar=5`); the four real `M1FieldSource`s
//! feed its four OHLC source roles. The blueprint reproduces the shipped
//! hand-wired `FlatGraph` EXACTLY (C23), so this prints the same report + trace.
//!
//! Run:
//! ```text
//! cargo run -p aura-ingest --example ger40_breakout_real
//! ```
//! Prints the [`RunReport`] metrics (total pips / drawdown / sign-flips) and a
//! compact entry/exit trace — each 0→1 entry and 1→0 exit with its timestamp —
//! so the strategy's behaviour over the ~20 sessions in the month is visibly
//! inspectable. Skips cleanly (no panic) where the archive is absent.
use chrono::TimeZone;
use chrono_tz::Europe::Berlin;
use aura_ingest::{default_data_server, open_ohlc, DEFAULT_DATA_PATH};
// The breakout wiring is defined once in the shared module and included by both
// this example and the gated determinism test — never duplicated.
#[path = "shared/breakout_real.rs"]
mod breakout_real;
use breakout_real::*;
// --- The window: September 2024 (inclusive), trivially changeable. -----------
const YEAR: i32 = 2024;
const MONTH: u32 = 9;
fn main() {
let server = default_data_server();
if !server.has_symbol(SYMBOL) {
println!(
"skip: no local data at {DEFAULT_DATA_PATH} (symbol {SYMBOL} absent) — \
nothing to demonstrate here."
);
return;
}
let (from, to) = utc_month_window(YEAR, MONTH);
let Some(sources) = open_ohlc(&server, SYMBOL, from, to) else {
println!(
"skip: {SYMBOL} has no M1 file overlapping {YEAR}-{MONTH:02}\
nothing to demonstrate here."
);
return;
};
// Author the breakout as a Composite blueprint and bootstrap it by name with
// the two genuine tuning knobs (entry bar 3, exit bar 5). The bar-period (15m),
// session open (09:00 Berlin) and delay.lag are structural — bound at
// construction, not exposed as params.
let pip_size = pip_size_of(SYMBOL);
let (bp, taps) =
ger40_breakout_blueprint(pip_size, BAR_MINUTES, SESSION_HOUR, SESSION_MINUTE, Berlin);
let mut h = bp
.with("entry_bar.target", aura_core::Scalar::i64(3))
.with("exit_bar.target", aura_core::Scalar::i64(5))
.bootstrap()
.expect("breakout blueprint bootstraps with entry=3, exit=5");
h.run(sources);
// Drain the per-bar trace first (it consumes the equity/held/bars/breakout
// channels), then fold the report from that same trace so the mpsc channels
// are drained exactly once (shared with the determinism test).
let trace = drain_trace(&taps);
let report = report_from_trace(&trace, from, to);
let metrics = &report.metrics;
println!("=== GER40 15m session-breakout — REAL bars ===");
println!(
"symbol={SYMBOL} window={YEAR}-{MONTH:02} (UTC, inclusive) \
bars={} pip_size={pip_size}",
trace.len()
);
println!("session: {SESSION_HOUR:02}:{SESSION_MINUTE:02} Europe/Berlin, {BAR_MINUTES}m bars; entry bar 3, exit bar 5");
println!();
println!("--- metrics (sim-optimal broker, pips = index points) ---");
println!(" total_pips = {:.1}", metrics.total_pips);
println!(" max_drawdown = {:.1}", metrics.max_drawdown);
println!(" bias_sign_flips = {}", metrics.bias_sign_flips);
println!();
// --- 2. The compact entry/exit trace. -----------------------------------
// Each 0→1 is an entry (a strict breakout on session bar 3), each 1→0 is the
// bar-5 exit. Printing only the transitions keeps the ~20 sessions readable.
println!("--- entry / exit transitions (held 0↔1) ---");
let mut prev_held = 0.0_f64;
let mut entries = 0u32;
let mut exits = 0u32;
let mut entry_equity = 0.0_f64;
for b in &trace {
if b.held == 1.0 && prev_held == 0.0 {
entries += 1;
entry_equity = b.equity;
println!(
" ENTER {} bar={} breakout={} equity={:+.1}",
fmt_ts(b.ts),
b.bars_since_open,
b.breakout,
b.equity
);
} else if b.held == 0.0 && prev_held == 1.0 {
exits += 1;
println!(
" EXIT {} bar={} equity={:+.1} (session pnl {:+.1})",
fmt_ts(b.ts),
b.bars_since_open,
b.equity,
b.equity - entry_equity
);
}
prev_held = b.held;
}
if prev_held == 1.0 {
println!(" (note: still held at end of window — a partial last session)");
}
println!();
println!("sessions entered = {entries}, exited = {exits}");
}
/// Render an epoch-ns [`Timestamp`](aura_core::Timestamp) as a UTC wall-clock
/// `YYYY-MM-DD HH:MM` for the trace. Display only — never feeds the graph.
fn fmt_ts(ts: aura_core::Timestamp) -> String {
let secs = ts.0.div_euclid(1_000_000_000);
let nanos = ts.0.rem_euclid(1_000_000_000) as u32;
match chrono::Utc.timestamp_opt(secs, nanos).single() {
Some(dt) => dt.format("%Y-%m-%d %H:%M UTC").to_string(),
None => format!("ts={}", ts.0),
}
}