Files
Aura/crates/aura-ingest/examples/ger40_breakout_real.rs
T
Brummel e1ad0979de refactor(ger40-examples): source pip from instrument_spec, drop the baked literal
The runnable GER40/FRA40 examples baked `const PIP_SIZE = 1.0` and a dead pip
column in the compare INSTRUMENTS table — a second source of pip truth parallel
to the vetted `aura_ingest::instrument_spec` channel the CLI `--real` path already
uses (#22). Route them all through one `pip_size_of(symbol)` helper: build_harness
sources GER40 internally, ger40_breakout_blueprint gains a `pip_size` first arg fed
from the lookup, and compare's run_cell sources each instrument's pip per cell.

Value-identical (GER40/FRA40 both resolve to 1.0) and behaviour-preserving: the
gated determinism / blueprint param-space / world tests stay green unchanged, and
the frozen `sim-optimal(pip_size=1)` broker-label pins are untouched.

closes #98
2026-06-21 16:41:37 +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!(" exposure_sign_flips = {}", metrics.exposure_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),
}
}