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

129 lines
4.8 KiB
Rust

// Task 1 — Scale / long horizon.
//
// The downstream researcher's task: "the shipped GER40 breakout demo runs ONE
// month. I want to know if the strategy holds up over a full year (2024) and a
// multi-year span (2018-01 .. 2024-12). Does it stay deterministic? Does memory
// stay bounded (the docs claim streaming residency is O(one chunk) — does opening
// 4 independent OHLC sources break that)? How slow is it?"
//
// PUBLIC INTERFACE ONLY. I start from the shipped example's shared wiring module
// (crates/aura-ingest/examples/shared/breakout_real.rs), included by #[path] —
// exactly as the shipped example and its test do — so I re-author NOTHING about
// the proven 13-node FlatGraph; I only change the WINDOW (a month -> a year ->
// 7 years) and add a self-RSS probe + a wall-clock timer. No crates/*/src read.
use std::sync::Arc;
use std::time::Instant;
use chrono::TimeZone;
use data_server::{DataServer, DEFAULT_DATA_PATH};
// Reuse the SHIPPED breakout wiring verbatim, the same way the shipped example
// does. The researcher does not re-type 13 nodes — the example exports them.
#[path = "../../crates/aura-ingest/examples/shared/breakout_real.rs"]
mod breakout_real;
use breakout_real::*;
/// Read this process's resident set size (KiB) from /proc/self/statm * page size.
/// A coarse but honest peak-RSS probe — no allocator hooks, just the OS view.
fn rss_kib() -> u64 {
let statm = std::fs::read_to_string("/proc/self/statm").unwrap_or_default();
let resident_pages: u64 = statm
.split_whitespace()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(0);
resident_pages * 4 // page size 4 KiB on this box
}
/// Build the inclusive Unix-ms window spanning whole calendar months
/// [from (y0,m0) .. through end of (y1,m1)] in UTC.
fn utc_span_ms(y0: i32, m0: u32, y1: i32, m1: u32) -> (i64, i64) {
let from = chrono::Utc.with_ymd_and_hms(y0, m0, 1, 0, 0, 0).unwrap().timestamp_millis();
let (ny, nm) = if m1 == 12 { (y1 + 1, 1) } else { (y1, m1 + 1) };
let next = chrono::Utc.with_ymd_and_hms(ny, nm, 1, 0, 0, 0).unwrap().timestamp_millis();
(from, next - 1)
}
fn run_span(server: &Arc<DataServer>, label: &str, from_ms: i64, to_ms: i64) {
let Some(sources) = open_ohlc_sources(server, from_ms, to_ms) else {
println!("{label}: no overlapping {SYMBOL} files — skip");
return;
};
let peak_before = rss_kib();
let t0 = Instant::now();
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 elapsed = t0.elapsed();
let peak_after = rss_kib();
// count sessions (0->1 transitions)
let mut entries = 0u32;
let mut prev = 0.0;
for b in &trace {
if b.held == 1.0 && prev == 0.0 {
entries += 1;
}
prev = b.held;
}
let m = &report.metrics;
println!(
"{label:<22} bars={:<7} sessions={:<4} total_pips={:>9.1} max_dd={:>8.1} flips={:<4} \
| {:>6.2}s RSS {}->{} MiB",
trace.len(),
entries,
m.total_pips,
m.max_drawdown,
m.exposure_sign_flips,
elapsed.as_secs_f64(),
peak_before / 1024,
peak_after / 1024,
);
}
fn main() {
let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH));
if !server.has_symbol(SYMBOL) {
println!("skip: no local {SYMBOL} data at {DEFAULT_DATA_PATH}");
return;
}
println!("=== Task 1: scale the GER40 breakout over long horizons ===");
println!("startup RSS = {} MiB\n", rss_kib() / 1024);
// 1. The shipped one-month baseline, for reference.
let (f, t) = utc_span_ms(2024, 9, 2024, 9);
run_span(&server, "2024-09 (baseline)", f, t);
// 2. A full year of GER40 (2024).
let (f, t) = utc_span_ms(2024, 1, 2024, 12);
run_span(&server, "2024 full year", f, t);
// 3. A multi-year span: 2018-01 .. 2024-12 (7 years of M1 across 4 sources).
let (f, t) = utc_span_ms(2018, 1, 2024, 12);
run_span(&server, "2018..2024 (7y)", f, t);
// 4. Determinism: rerun the full-year span and assert bit-identical report.
println!("\n--- determinism (C1): rerun 2024 full year, compare report JSON ---");
let (f, t) = utc_span_ms(2024, 1, 2024, 12);
let rep = |srv: &Arc<DataServer>| {
let sources = open_ohlc_sources(srv, f, t).expect("year sources");
let (mut h, taps) = build_harness();
h.run(sources);
let trace = drain_trace(&taps);
report_from_trace(&trace, f, t)
};
let r1 = rep(&server);
let r2 = rep(&server);
let j1 = r1.to_json();
let j2 = r2.to_json();
println!("rerun report JSON bit-identical: {}", j1 == j2);
println!(" total_pips r1={:.1} r2={:.1}", r1.metrics.total_pips, r2.metrics.total_pips);
}