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

171 lines
6.6 KiB
Rust

//! Runnable World-family demo: **`compare`** the session-breakout `Composite`
//! blueprint across two structural axes (C11/C20 experiment matrix),
//! on REAL data, with NO re-authoring:
//!
//! 1. **Instrument** — GER40 vs FRA40 (both `pip_size = 1.0`, index points), the
//! SAME blueprint bootstrapped at the same `{entry=3, exit=5}` point over each
//! symbol. The pip honesty is by construction (both quote in index points; the
//! pip is sourced per cell from the central `instrument_spec` channel — #22
//! shipped).
//! 2. **Bar period** — a 15m blueprint vs a 30m blueprint. The bar period is a
//! STRUCTURAL axis (C34): `ger40_breakout_blueprint` is instantiated TWICE,
//! at 15 and at 30, each a *different strategy* (the period is bound at
//! construction, locking Resample and Session equal — it is NOT a sweep param).
//!
//! A "comparison" here is a plain Rust loop over the matrix (C20): one blueprint
//! per matrix cell, each bootstrapped + run + summarized, the reports tabulated.
//!
//! Run:
//! ```text
//! cargo run -p aura-ingest --example ger40_breakout_compare
//! ```
//! Prints both comparison tables. Skips a row cleanly where a symbol is absent.
use std::sync::Arc;
use aura_core::{Cell, Scalar, Timestamp};
use aura_engine::{summarize, RunMetrics};
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 baseline tuning point both axes hold fixed (so only the structural axis
// varies): entry bar 3, exit bar 5 — the shipped baseline.
const ENTRY_BAR: i64 = 3;
const EXIT_BAR: i64 = 5;
// GER40 and FRA40 both quote in index points, so pip_size = 1.0 for both — the
// only reason a cross-instrument pip comparison is honest here. The pip is sourced
// per cell from the central vetted `instrument_spec` channel (#22 shipped), not a
// literal carried here. (symbol, human note)
const INSTRUMENTS: &[(&str, &str)] = &[("GER40", "index point"), ("FRA40", "index point")];
/// Bootstrap the blueprint (at `bar_period`) at `{entry=3, exit=5}`, run it over
/// `[from, to]` (epoch-ns) of real `symbol` OHLC, and summarize — or `None` if the
/// symbol has no file overlapping the window. The `pip_size` is sourced per cell
/// from the central vetted `instrument_spec` channel (always 1.0 here; both
/// indices) and rides on the blueprint's baked SimBroker. Returns the report plus
/// the entry count (0→1 transitions of held).
fn run_cell(
server: &Arc<DataServer>,
symbol: &str,
bar_period: i64,
from: Timestamp,
to: Timestamp,
) -> Option<(RunMetrics, u32)> {
let pip_size = pip_size_of(symbol);
let (bp, taps) =
ger40_breakout_blueprint(pip_size, bar_period, SESSION_HOUR, SESSION_MINUTE, Berlin);
let point: Vec<Cell> = bp
.param_space()
.iter()
.map(|ps| match ps.name.as_str() {
"entry_bar.target" => Scalar::i64(ENTRY_BAR).cell(),
"exit_bar.target" => Scalar::i64(EXIT_BAR).cell(),
other => panic!("unexpected param slot {other}"),
})
.collect();
let mut h = bp.bootstrap_with_cells(&point).expect("point kind-checked");
let srcs = open_ohlc(server, symbol, from, to)?;
h.run(srcs);
drop(h);
let equity: Vec<(Timestamp, f64)> = taps
.equity
.try_iter()
.map(|(t, r)| (t, as_f64(&r[0])))
.collect();
let held: Vec<(Timestamp, f64)> = taps
.held
.try_iter()
.map(|(t, r)| (t, as_f64(&r[0])))
.collect();
// Count entries (0 -> 1 transitions of held).
let mut entries = 0u32;
let mut prev = 0.0_f64;
for (_, v) in &held {
if *v == 1.0 && prev == 0.0 {
entries += 1;
}
prev = *v;
}
Some((summarize(&equity, &held), entries))
}
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 (the same UTC span, via the shared Timestamp helper).
let (from, _) = utc_month_window(2024, 1);
let (_, to) = utc_month_window(2024, 12);
println!("=== GER40 session-breakout — World `compare` (structural matrix) ===");
println!("window=2024 (UTC, full year), fixed tuning point entry={ENTRY_BAR}, exit={EXIT_BAR}\n");
// --- Axis 1: instrument (GER40 vs FRA40), 15m blueprint. -----------------
println!("--- axis 1: instrument (same 15m blueprint, GER40 vs FRA40) ---");
println!(
"{:<8} {:<14} {:>9} {:>11} {:>10} {:>7}",
"symbol", "pip-meaning", "sessions", "total_pips", "max_dd", "flips"
);
println!("{}", "-".repeat(62));
for &(symbol, note) in INSTRUMENTS {
if !server.has_symbol(symbol) {
println!("{symbol:<8} (no local data — skip)");
continue;
}
match run_cell(&server, symbol, BAR_MINUTES, from, to) {
Some((m, entries)) => println!(
"{symbol:<8} {note:<14} {entries:>9} {:>11.1} {:>10.1} {:>7}",
m.total_pips, m.max_drawdown, m.exposure_sign_flips
),
None => println!("{symbol:<8} (no overlapping OHLC files — skip)"),
}
}
println!();
// --- Axis 2: bar period (15m vs 30m blueprint), GER40. -------------------
// The STRUCTURAL axis: ger40_breakout_blueprint instantiated at 15 AND at 30
// — two *different strategies* (the period binds Resample AND Session equal at
// construction; it is not a sweep param). C34 / C11.
println!("--- axis 2: bar period (GER40, 15m blueprint vs 30m blueprint) ---");
println!(
"{:<10} {:>9} {:>11} {:>10} {:>7}",
"bar-period", "sessions", "total_pips", "max_dd", "flips"
);
println!("{}", "-".repeat(52));
for &period in &[15_i64, 30] {
match run_cell(&server, SYMBOL, period, from, to) {
Some((m, entries)) => println!(
"{:<10} {entries:>9} {:>11.1} {:>10.1} {:>7}",
format!("{period}m"),
m.total_pips,
m.max_drawdown,
m.exposure_sign_flips,
),
None => println!("{period}m (no data — skip)"),
}
}
println!("\nOK: one blueprint, two structural axes (instrument + bar period) compared");
println!(" with no re-authoring — the bar period is a construction arg (15m and 30m");
println!(" are different strategies, C34), the instrument a plain matrix loop (C20).");
}
fn as_f64(s: &Scalar) -> f64 {
match s {
Scalar::F64(v) => *v,
other => panic!("expected f64, got {other:?}"),
}
}