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

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 recorded geometry sidecar — #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_backtest::{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 recorded geometry sidecar (#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 recorded geometry sidecar (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.bias_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.bias_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:?}"),
}
}