Files
Brummel 90c1de841d fieldtest: GER40 session-breakout research deep-dive — 7 bins, 8 findings
Public-API research deep-dive driving the just-shipped GER40 real-data
session-breakout demo (5b5f034) through the escalating sequence a serious
researcher attempts next: scale to multi-year, walk-forward, structural-param
sweep, multi-instrument compare. Standalone consumer crate (path-deps only;
public interface = ledger + glossary + example corpus + cargo doc; no
crates/*/src read). All runs against the real /mnt/tickdata/Pepperstone archive.

Findings: 3 bugs, 1 friction, 3 spec_gap, 1 working. Triaged to the tracker:
- spec_gap: the flagship breakout ships as a raw FlatGraph the World API cannot
  consume (sweep / walk_forward / compare all forced a hand re-author to a
  Composite, which reproduced the FlatGraph numbers EXACTLY) — refs #94.
- bug: process residency is O(window) (6->173 MiB across 1mo->10y) despite the
  documented O(one-chunk) claim; the aura ring stays <=1024 but data-server
  retention scales, and streaming_seam.rs only measures the ring — refs #95.
- bug: the bar-period is one knob split across Resample (tunable param) and
  Session (baked) and desyncs silently to 0.0 pips; Delay.lag leaks into the
  default param_space (the C34 deform-vs-tune discriminator) — refs #96.
- spec_gap: the walk_forward empty-space idiom for param-free strategies is
  undefined — refs #97.
- spec_gap (no per-instrument pip registry) and friction (one-directional
  ns->ms conversion) re-confirm the known #22 and #80.
- working: C1 determinism over 7y / 148k bars, the lazy streaming source, and
  the sweep/WFO cores held under real multi-year load; the Composite re-author
  was behaviour-preserving (C23).

Scratch consumer crate + run transcripts committed under fieldtests/ per the
fieldtest convention (matching cycle-0049). The crate path-deps the engine and
reads the real archive; its bins are the repros cited in the issues above.
2026-06-17 18:55:04 +02:00

134 lines
5.6 KiB
Rust

// Task 2 — Walk-forward the SHIPPED breakout across real GER40 history.
//
// The researcher's task: "the engine advertises a World walk-forward family.
// Roll my GER40 breakout across 2018..2024 with in-sample/out-of-sample windows
// and stitch the OOS pip curve." The breakout I want to roll is the SHIPPED one
// (crates/aura-ingest/examples/shared/breakout_real.rs) — a hand-wired FlatGraph.
//
// This file records the discovery journey verbatim. The public walk_forward is:
// walk_forward(roller: WindowRoller, space: Vec<ParamSpec>,
// run_window: Fn(WindowBounds) -> WindowRun) -> WalkForwardResult
// with WindowRun { chosen_params: Vec<Cell>, oos_equity, oos_report }.
//
// SEAM 1 (recorded, not worked around): the breakout has NO swept params in this
// scenario (a fixed-param OOS roll), yet walk_forward DEMANDS a space:
// Vec<ParamSpec> and each WindowRun a chosen_params: Vec<Cell>. With a raw
// FlatGraph there is no param_space() to hand it — the FlatGraph is built by
// Harness::bootstrap, which takes no params. So I pass an EMPTY space (vec![]) and
// EMPTY chosen_params (vec![]). Whether that is the intended idiom for a
// param-free strategy is a spec question — recorded.
//
// SEAM 2 (recorded): the breakout source is FOUR M1FieldSources (OHLC) opened by
// Unix-ms, but WindowBounds.{is,oos} are epoch-ns. aura-ingest exports
// unix_ms_to_epoch_ns but NOT its inverse, so I hand-divide ns->ms (the milestone
// fixture's mw_3 hit the same seam).
//
// PUBLIC SURFACE ONLY: walk_forward signature from cargo doc; the breakout wiring
// reused verbatim from the shipped example's shared module.
use std::sync::Arc;
use chrono::TimeZone;
use data_server::{DataServer, DEFAULT_DATA_PATH};
use aura_core::Timestamp;
use aura_engine::{
walk_forward, RollMode, RunReport, WindowBounds, WindowRoller, WindowRun,
};
#[path = "../../crates/aura-ingest/examples/shared/breakout_real.rs"]
mod breakout_real;
use breakout_real::*;
// SEAM 2: ns -> ms (aura-ingest exports only the forward direction).
fn ns_to_ms(ts: Timestamp) -> i64 {
ts.0 / 1_000_000
}
// Run the SHIPPED breakout over one [from, to] epoch-ns window of real GER40 M1
// OHLC, returning the OOS pip-equity stream + the folded RunReport.
fn run_breakout_window(
server: &Arc<DataServer>,
from: Timestamp,
to: Timestamp,
) -> (Vec<(Timestamp, f64)>, RunReport) {
let from_ms = ns_to_ms(from);
let to_ms = ns_to_ms(to);
// Open the four OHLC field sources directly (the shipped open_ohlc_sources
// helper hard-codes Some(from)/Some(to) and returns Option — reuse it).
let sources = open_ohlc_sources(server, from_ms, to_ms).expect("window overlaps GER40 data");
let (mut h, taps) = build_harness();
h.run(sources);
let trace = drain_trace(&taps);
let report = report_from_trace(&trace, from_ms, to_ms);
let oos_equity: Vec<(Timestamp, f64)> = trace.iter().map(|b| (b.ts, b.equity)).collect();
(oos_equity, report)
}
fn main() {
let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH));
if !server.has_symbol(SYMBOL) {
println!("skip: no local {SYMBOL} data");
return;
}
println!("=== Task 2: walk-forward the shipped GER40 breakout (2018..2024) ===\n");
// The roll span: 2018-01 .. 2024-12 in epoch-ns. is=12mo, oos=3mo, step=3mo.
let day_ns: i64 = 86_400_000 * 1_000_000;
let month_ns: i64 = 30 * day_ns;
let origin = aura_ingest::unix_ms_to_epoch_ns(
chrono::Utc.with_ymd_and_hms(2018, 1, 1, 0, 0, 0).unwrap().timestamp_millis(),
);
let end = aura_ingest::unix_ms_to_epoch_ns(
chrono::Utc.with_ymd_and_hms(2024, 12, 31, 23, 59, 59).unwrap().timestamp_millis(),
);
let roller = WindowRoller::new(
(origin, end),
12 * month_ns, // in-sample length
3 * month_ns, // out-of-sample length
3 * month_ns, // step
RollMode::Rolling,
)
.expect("span fits >= 1 window");
let server_for_closure = Arc::clone(&server);
// SEAM 1: the breakout is param-free here, so space = vec![] and each
// WindowRun carries chosen_params = vec![]. There is no in-sample optimize to
// do (no params to tune with a raw FlatGraph), so each window just runs the
// fixed strategy on its OOS slice. (Tuning the breakout's structural params is
// Task 3 — and the seam there is that they are NOT a FlatGraph param_space.)
let result = walk_forward(roller, vec![], move |w: WindowBounds| -> WindowRun {
let (oos_equity, oos_report) =
run_breakout_window(&server_for_closure, w.oos.0, w.oos.1);
WindowRun { chosen_params: vec![], oos_equity, oos_report }
});
println!("walk-forward produced {} windows\n", result.windows.len());
for (k, w) in result.windows.iter().enumerate() {
let b = w.bounds;
println!(
" window {k}: is=[{}..{}] oos=[{}..{}] oos_pips={:>8.1} oos_dd={:>7.1}",
ns_to_ms(b.is.0),
ns_to_ms(b.is.1),
ns_to_ms(b.oos.0),
ns_to_ms(b.oos.1),
w.run.oos_report.metrics.total_pips,
w.run.oos_report.metrics.max_drawdown,
);
}
let stitched = &result.stitched_oos_equity;
println!(
"\nstitched OOS curve: {} points, final cumulative = {:.1} pips",
stitched.len(),
stitched.last().map(|(_, v)| *v).unwrap_or(0.0),
);
// The stitched OOS curve is the breakout's out-of-sample pip total over the
// concatenated OOS quarters (no in-sample optimization happened — same fixed
// FlatGraph strategy everywhere, since a raw FlatGraph has no param_space()).
println!("\nOK: fixed-param OOS walk-forward over the shipped breakout FlatGraph.");
}