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:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user