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
This commit is contained in:
2026-06-21 16:41:37 +02:00
parent 094d63bc47
commit e1ad0979de
7 changed files with 103 additions and 41 deletions
@@ -5,7 +5,8 @@
//! 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
//! engine has no pip registry yet — #22, filed).
//! 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
@@ -38,18 +39,17 @@ 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 (no engine
// registry, #22). (symbol, pip_size, human note)
const INSTRUMENTS: &[(&str, f64, &str)] = &[
("GER40", 1.0, "index point"),
("FRA40", 1.0, "index point"),
];
// 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` rides on the
/// blueprint's baked SimBroker (always 1.0 here; both indices). Returns the
/// report plus the entry count (0→1 transitions of held).
/// 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,
@@ -57,7 +57,9 @@ fn run_cell(
from: Timestamp,
to: Timestamp,
) -> Option<(RunMetrics, u32)> {
let (bp, taps) = ger40_breakout_blueprint(bar_period, SESSION_HOUR, SESSION_MINUTE, Berlin);
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()
@@ -117,7 +119,7 @@ fn main() {
"symbol", "pip-meaning", "sessions", "total_pips", "max_dd", "flips"
);
println!("{}", "-".repeat(62));
for &(symbol, _pip, note) in INSTRUMENTS {
for &(symbol, note) in INSTRUMENTS {
if !server.has_symbol(symbol) {
println!("{symbol:<8} (no local data — skip)");
continue;
@@ -58,7 +58,9 @@ fn main() {
// 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 (bp, taps) = ger40_breakout_blueprint(BAR_MINUTES, SESSION_HOUR, SESSION_MINUTE, Berlin);
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))
@@ -76,7 +78,7 @@ fn main() {
println!("=== GER40 15m session-breakout — REAL bars ===");
println!(
"symbol={SYMBOL} window={YEAR}-{MONTH:02} (UTC, inclusive) \
bars={} pip_size={PIP_SIZE}",
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");
@@ -35,7 +35,13 @@ const EXIT_BARS: &[i64] = &[4, 5, 6];
/// 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(BAR_MINUTES, SESSION_HOUR, SESSION_MINUTE, Berlin);
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");
@@ -76,9 +82,11 @@ fn main() {
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(BAR_MINUTES, SESSION_HOUR, SESSION_MINUTE, Berlin)
let space = ger40_breakout_blueprint(pip_size, BAR_MINUTES, SESSION_HOUR, SESSION_MINUTE, Berlin)
.0
.param_space();
let grid = GridSpace::new(
@@ -96,7 +104,7 @@ fn main() {
println!("=== GER40 session-breakout — World `sweep` over entry×exit ===");
println!(
"symbol={SYMBOL} window=2024 (UTC, full year) bars=15m pip_size={PIP_SIZE} \
"symbol={SYMBOL} window=2024 (UTC, full year) bars=15m pip_size={pip_size} \
grid={}×{} = {} points",
ENTRY_BARS.len(),
EXIT_BARS.len(),
@@ -45,7 +45,13 @@ fn run_point(
from: Timestamp,
to: Timestamp,
) -> (RunReport, Vec<(Timestamp, f64)>) {
let (bp, taps) = ger40_breakout_blueprint(BAR_MINUTES, SESSION_HOUR, SESSION_MINUTE, Berlin);
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("point kind-checked against param_space");
@@ -105,9 +111,15 @@ fn main() {
// The non-empty space the per-window in-sample sweep optimizes over: the two
// genuine tuning knobs (the #97 resolution — a real space to optimize).
let space = ger40_breakout_blueprint(BAR_MINUTES, SESSION_HOUR, SESSION_MINUTE, Berlin)
.0
.param_space();
let space = ger40_breakout_blueprint(
pip_size_of(SYMBOL),
BAR_MINUTES,
SESSION_HOUR,
SESSION_MINUTE,
Berlin,
)
.0
.param_space();
println!(
"optimizing param_space() per window: {:?} (in-sample grid {}×{})",
space.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(),
@@ -65,12 +65,19 @@ pub const REC_HELD: usize = 10; // tap B — Latch exposure (f64, 0.0/1.0)
pub const REC_BARS: usize = 11; // tap C — Session bars_since_open (i64)
pub const REC_BREAKOUT: usize = 12; // tap D — Gt breakout flag (bool)
/// The instrument. GER40 is an index, so pips are index points: `pip_size = 1.0`.
/// The instrument. GER40 is an index, so pips are index points (`pip_size = 1.0`).
pub const SYMBOL: &str = "GER40";
/// SimBroker pip size for GER40. An index quotes in points, so one pip is one
/// point — `1.0`, exactly as the synthetic capstone test uses.
pub const PIP_SIZE: f64 = 1.0;
/// The SimBroker pip divisor for `symbol`, sourced from the central vetted
/// instrument-metadata channel ([`aura_ingest::instrument_spec`], C7/C15) instead of a
/// baked literal — so these examples consume the same pip table the CLI `--real` path
/// does, and a cross-instrument comparison is honest by construction (#98). Panics on
/// an un-vetted symbol (the examples only ever run known instruments).
pub fn pip_size_of(symbol: &str) -> f64 {
aura_ingest::instrument_spec(symbol)
.unwrap_or_else(|| panic!("no vetted instrument spec for {symbol}"))
.pip_size
}
/// Session config: Frankfurt cash open 09:00 Europe/Berlin, 15m bars. The same
/// `Session::new(9, 0, Europe::Berlin, 15)` the synthetic test pins.
@@ -94,6 +101,7 @@ pub struct Taps {
pub fn build_harness() -> (Harness, Taps) {
use aura_core::Firing;
let pip_size = pip_size_of(SYMBOL);
let (tx_equity, rx_equity) = mpsc::channel();
let (tx_held, rx_held) = mpsc::channel();
let (tx_bars, rx_bars) = mpsc::channel();
@@ -111,7 +119,7 @@ pub fn build_harness() -> (Harness, Taps) {
EqConst::builder().schema().clone(), // 5
And::builder().schema().clone(), // 6
Latch::builder().schema().clone(), // 7
SimBroker::builder(PIP_SIZE).schema().clone(), // 8
SimBroker::builder(pip_size).schema().clone(), // 8
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_equity.clone())
.schema()
.clone(), // 9
@@ -135,7 +143,7 @@ pub fn build_harness() -> (Harness, Taps) {
Box::new(EqConst::new(5)), // 5
Box::new(And::new()), // 6
Box::new(Latch::new()), // 7
Box::new(SimBroker::new(PIP_SIZE)), // 8
Box::new(SimBroker::new(pip_size)), // 8
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx_equity)), // 9
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx_held)), // 10
Box::new(Recorder::new(&[ScalarKind::I64], Firing::Any, tx_bars)), // 11
@@ -220,6 +228,7 @@ pub fn build_harness() -> (Harness, Taps) {
/// (fresh nodes + channels) keeps two bootstraps disjoint for the C1 determinism
/// assertion, exactly as `build_harness` does.
pub fn ger40_breakout_blueprint(
pip_size: f64,
bar_period_minutes: i64,
open_hour: u32,
open_minute: u32,
@@ -249,7 +258,7 @@ pub fn ger40_breakout_blueprint(
let exit_bar = g.add(EqConst::builder().named("exit_bar"));
let and = g.add(And::builder());
let latch = g.add(Latch::builder());
let broker = g.add(SimBroker::builder(PIP_SIZE));
let broker = g.add(SimBroker::builder(pip_size));
// Recording sinks: equity (A), held (B), bars_since_open (C), breakout (D).
let rec_equity = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_equity));
@@ -37,7 +37,7 @@ const MONTH: u32 = 9;
/// be swept into a desync. No data needed — runs everywhere.
#[test]
fn param_space_is_exactly_entry_and_exit_bar_targets() {
let (bp, _taps) = ger40_breakout_blueprint(15, 9, 0, Berlin);
let (bp, _taps) = ger40_breakout_blueprint(pip_size_of(SYMBOL), 15, 9, 0, Berlin);
let space = bp.param_space();
let names: Vec<&str> = space.iter().map(|p| p.name.as_str()).collect();
@@ -64,8 +64,8 @@ fn param_space_is_exactly_entry_and_exit_bar_targets() {
/// `composite_matches_flatgraph_bit_identical`.)
#[test]
fn blueprint_param_space_is_construction_deterministic() {
let a = ger40_breakout_blueprint(15, 9, 0, Berlin).0.param_space();
let b = ger40_breakout_blueprint(15, 9, 0, Berlin).0.param_space();
let a = ger40_breakout_blueprint(pip_size_of(SYMBOL), 15, 9, 0, Berlin).0.param_space();
let b = ger40_breakout_blueprint(pip_size_of(SYMBOL), 15, 9, 0, Berlin).0.param_space();
assert_eq!(a, b, "blueprint param_space() is a deterministic function of its args");
}
@@ -74,7 +74,13 @@ fn blueprint_param_space_is_construction_deterministic() {
/// build per call (fresh nodes + channels) keeps two runs disjoint for the C1
/// determinism assertion.
fn run_blueprint(server: &Arc<DataServer>, from: Timestamp, to: Timestamp) -> Vec<(f64, f64)> {
let (bp, taps) = ger40_breakout_blueprint(BAR_MINUTES, SESSION_HOUR, SESSION_MINUTE, Berlin);
let (bp, taps) = ger40_breakout_blueprint(
pip_size_of(SYMBOL),
BAR_MINUTES,
SESSION_HOUR,
SESSION_MINUTE,
Berlin,
);
let mut h = bp
.with("entry_bar.target", Scalar::i64(3))
.with("exit_bar.target", Scalar::i64(5))
@@ -40,7 +40,13 @@ use breakout_real::*;
/// `run_point` do). 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(BAR_MINUTES, SESSION_HOUR, SESSION_MINUTE, Berlin);
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");
@@ -88,9 +94,15 @@ fn sweep_consumes_blueprint_over_named_params() {
}
let (from, to) = utc_month_window(2024, 9);
let space = ger40_breakout_blueprint(BAR_MINUTES, SESSION_HOUR, SESSION_MINUTE, Berlin)
.0
.param_space();
let space = ger40_breakout_blueprint(
pip_size_of(SYMBOL),
BAR_MINUTES,
SESSION_HOUR,
SESSION_MINUTE,
Berlin,
)
.0
.param_space();
// The grid is exactly over `param_space()`: entry ∈ {2,3,4}, exit ∈ {4,5,6}.
let grid = GridSpace::new(
&space,
@@ -170,9 +182,15 @@ fn walk_forward_consumes_blueprint_non_degenerate() {
// The non-empty space the in-sample sweep optimizes over per window: the two
// genuine tuning knobs. This is the #97 resolution — a real space, so the
// chosen params are populated, never an empty degenerate vec.
let space = ger40_breakout_blueprint(BAR_MINUTES, SESSION_HOUR, SESSION_MINUTE, Berlin)
.0
.param_space();
let space = ger40_breakout_blueprint(
pip_size_of(SYMBOL),
BAR_MINUTES,
SESSION_HOUR,
SESSION_MINUTE,
Berlin,
)
.0
.param_space();
let server_for_closure = Arc::clone(&server);
let result = walk_forward(roller, space.clone(), move |w: WindowBounds| -> WindowRun {
@@ -210,8 +228,13 @@ fn walk_forward_consumes_blueprint_non_degenerate() {
// Apply the chosen params on the OOS window.
let oos_report = run_point(&server_for_closure, &chosen, w.oos.0, w.oos.1);
// Re-run once on OOS to capture the recorded equity segment for stitching.
let (bp, taps) =
ger40_breakout_blueprint(BAR_MINUTES, SESSION_HOUR, SESSION_MINUTE, Berlin);
let (bp, taps) = ger40_breakout_blueprint(
pip_size_of(SYMBOL),
BAR_MINUTES,
SESSION_HOUR,
SESSION_MINUTE,
Berlin,
);
let mut h = bp.bootstrap_with_cells(&chosen).expect("chosen point kind-checked");
let sources = open_ohlc(&server_for_closure, SYMBOL, w.oos.0, w.oos.1)
.expect("OOS window overlaps data");