1ebb94c1b8
RunManifest gains defaults: Vec<(String, Scalar)> — the wrap-prefixed bound_param_space() of the signal, read after axis reopening, so a bound param an axis overrode has already left the space and flows through params instead (disjoint by construction; verified end to end: a sweep member's overridden fast.length sits in params while slow.length/bias.scale sit in defaults). params keeps its "what varied" semantics and stays the reproduce input. One-directional serde widening (#[serde(default)]) per the selection/instrument/topology_hash idiom — old records deserialize with an empty defaults; unlike the Option fields it always serializes, mirroring params. ~20 struct-literal sites across five crates gained the field (compile-mandated breadth, no behaviour change at those sites). The C14 ledger records the underlying decision (2026-07-13): generated outcome records spend redundancy on direct readability (single writer, cannot drift); authored intent artifacts admit none (every redundancy is a drift site) — so the fix lands in the manifest, never the blueprint. Verification: RED test run_manifest_stamps_untouched_bound_defaults green; cargo build --workspace; cargo test --workspace green; clippy -D warnings on the touched crates; binary-level sweep exclusivity check.
177 lines
6.3 KiB
Rust
177 lines
6.3 KiB
Rust
//! 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![],
|
||
defaults: vec![],
|
||
window: (from, to),
|
||
seed: 0,
|
||
broker: "sim-optimal(pip_size=1)".to_string(),
|
||
selection: None,
|
||
instrument: None,
|
||
topology_hash: None,
|
||
project: None,
|
||
},
|
||
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:?}"),
|
||
}
|
||
}
|