Files
Aura/docs/specs/fieldtest-research-breakout-deepdive.md
T
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

16 KiB
Raw Blame History

Fieldtest — research deep-dive on the GER40 session-breakout demo — 2026-06-17

Status: Draft — awaiting orchestrator triage Author: fieldtester (downstream-quant-researcher dispatch)

Scope

The just-shipped runnable demo crates/aura-ingest/examples/ger40_breakout_real.rs (+ examples/shared/breakout_real.rs, commit 5b5f034) backtests the 15m Frankfurt session-breakout over real GER40 M1 bars for one month. This dispatch pushes that strategy through the escalating sequence a serious researcher attempts next: (1) scale to a full year and a 7-year span; (2) walk-forward across real history; (3) sweep the structural params (entry/exit bar, bar period); (4) compare signal quality across instruments (GER40 vs FRA40). Worked ONLY from the public surface (design ledger, glossary, the example corpus, cargo doc); no engine crates/*/src was read. All runs are against the real /mnt/tickdata/Pepperstone archive. Binaries path-dep the engine crates (HEAD source), built --release.

The central, recurring tension: the demo ships the breakout as a hand-wired raw FlatGraph, but every World orchestration family (sweep / walk_forward / optimize) consumes a Composite blueprint with param_space() / bootstrap_with_cells. A FlatGraph has no param space, so to take this exact strategy into the World a researcher must first re-author it as a Composite — re-typing the 13-node wiring by hand. The demo provides no such blueprint.

Examples

All fixtures live under fieldtests/research-breakout-deepdive/ (a standalone C16-style consumer crate). Output transcripts are the sibling out_*.txt.

t1_scale.rs — scale the breakout over long horizons

  • Reuses the shipped shared/breakout_real.rs wiring verbatim (via #[path]), changing only the window: month → year → 7 years; adds an RSS probe + timer.
  • Fits scope: the first thing a researcher does with a one-month demo is run it over real history; the docs make a load-bearing O(one-chunk) residency claim.
  • Outcome: built, ran. 2024-09 45.0 pips / 5 sessions (matches the shipped demo). 2024 full year +203.4 / 62 sessions. 2018..2024 (7y) +520.2 / 401 sessions in 1.12s. Determinism held (rerun report JSON bit-identical). But RSS grew 8→127 MiB on the 7y run.

t1b_residency.rs / t1c_residency_scaling.rs — residency attribution

  • Isolates where the memory goes: drives 4 raw M1FieldSources with no harness/ sinks, then the full harness; and sweeps one source over 1mo→10yr windows.
  • Fits scope: pin down whether the O(one-chunk) claim holds at the process level.
  • Outcome: built, ran. resident_records ≤ 1024 (the docs' asserted predicate) holds at every window length, yet VmHWM scales linearly with window: 6 → 15 → 24 → 78 → 173 MiB across 1mo → 6mo → 1y → 4y → 10y of one symbol's Close.

t2_walkforward.rs — walk-forward the shipped breakout (2018..2024)

  • Rolls the breakout with walk_forward (12mo IS / 3mo OOS / 3mo step, Rolling), stitching the OOS pip curve.
  • Fits scope: the headline World promise on the headline strategy.
  • Outcome: built, ran. 24 windows; stitched OOS curve 131508 points ending +618.8 pips. But the walk-forward is degenerate — see finding below: the breakout being a raw FlatGraph means space = vec![], chosen_params = vec![], and there is NO in-sample optimization (the whole point of WFO).

t3_sweep.rs / t3b_period_desync.rs — sweep the structural params

  • Re-authors the breakout as a Composite, prints param_space(), runs a 3×3 entry_bar×exit_bar grid, and demonstrates the period-coupling failure.
  • Fits scope: the mission's explicit coupling question (Resample period == Session period; entry/exit as EqConst targets).
  • Outcome: built, ran. Grid works; entry_bar=3,exit_bar=5 reproduces the shipped 45.0 exactly (re-authoring fidelity). The coupling is not expressible, and violating it fails silently (period-desync run → 0.0 pips, no error).

t4_compare.rs — GER40 vs FRA40 signal quality

  • Reuses the Composite, loops over (symbol, pip_size), compares 2024.
  • Fits scope: the structural "which instrument" axis + the per-asset pip question.
  • Outcome: built, ran. GER40 +203.4 / 62 sessions vs FRA40 +83.7 / 63 sessions (both pip=1.0 index points). Comparison is honest only because the researcher hand-supplied matching pip sizes — there is no instrument registry.

Findings

[spec_gap] The shipped breakout is a raw FlatGraph, unreachable by the World API

  • Example(s): t2_walkforward, t3_sweep, t4_compare (all three had to re-author it).
  • What happened: sweep / walk_forward / optimize operate on a Composite (they need param_space() + bootstrap_with_cells). The demo's breakout is a hand-wired FlatGraph built by Harness::bootstrap, which has no param space. To do any sweep/optimize/WFO of the shipped strategy I re-typed all 13 nodes + 14 edges + 4 roles as a Composite (t3_sweep.rs breakout_composite).
  • Why spec_gap: C20/C21 promise the strategy blueprint is "reused across backtest, sweep, visual workspaces"; the milestone fixtures demonstrate this for SMA-cross. But the just-shipped, real-data flagship strategy ships in a form the World cannot consume, and the design ledger does not say which form a shippable strategy should take. A researcher cannot tell from the public surface whether the FlatGraph form is intentional or a gap. The re-reading I picked (re-author as Composite) is plausible; "the demo should have shipped a Composite" is equally plausible — the ledger is silent.
  • Recommended action: ship the breakout as a reusable Composite blueprint (alongside or instead of the FlatGraph), so the demo's strategy is World-ready; or document in the example that the FlatGraph is a substrate-level fixture and a blueprint is the consumer form.

[bug] Process residency scales with window length despite the O(one-chunk) claim

  • Example(s): t1_scale, t1b_residency, t1c_residency_scaling.
  • What happened: the docs (C12 cycle-0041 realization; tests/streaming_seam.rs residency_is_bounded_by_one_chunk_independent_of_window_length) claim "a 20-year window streams in the same memory as a one-day one". Empirically the aura-level predicate resident_records ≤ CHUNK_SIZE (1024) holds at every window length, but VmHWM (OS peak RSS) grows ~linearly: 6→15→24→78→173 MiB for 1mo→6mo→1y→4y→10y of ONE symbol's Close field; the 7-year 4-field breakout hits 127 MiB streaming, 210 MiB after drain_trace. The test's predicate measures only the aura M1FieldSource ring, not the underlying data-server chunk/file retention, so it passes while the process-level promise is false.
  • Why bug: a load-bearing, documented, test-asserted guarantee ("streaming residency is O(one chunk), independent of window length") is contradicted by the process's own VmHWM. A researcher who budgets RAM on the doc's promise (one chunk ≈ tens of KiB) will be off by 100-1000×; a real multi-symbol sweep over decades would OOM where the docs say it streams flat.
  • Repro: cargo run --release --manifest-path fieldtests/research-breakout-deepdive/Cargo.toml --bin t1c_residency_scaling → VmHWM column grows monotonically with the record count while the aura-resident<=1024? column stays true.
  • Recommended action: debug — either the data-server chunks/decoded buffers are retained across pulls (release them), or the residency claim must be narrowed in the docs/test to "aura-level ring residency" (which is all the predicate proves) and the true per-window process cost documented.

[spec_gap] walk_forward demands a param-space even with nothing to optimize

  • Example(s): t2_walkforward.
  • What happened: walk_forward(roller, space: Vec<ParamSpec>, F) with WindowRun { chosen_params: Vec<Cell>, .. }. For a fixed-param OOS roll of a param-free strategy I passed space = vec![] and chosen_params = vec![]. It ran, but the result is a walk-forward with no in-sample optimization — the OOS verdict of a strategy that was never tuned per window. The signature gives no hint whether the empty-space idiom is the intended way to express "OOS-only, no optimize", or whether WFO without per-window optimize is even a supported use.
  • Why spec_gap: I guessed the empty-space reading; "WFO requires a non-empty space and per-window optimize" is an equally plausible reading the ledger does not settle. Combined with the FlatGraph finding, the headline WFO demo on the headline strategy is unreachable in its optimizing form without re-authoring.
  • Recommended action: ratify the param-free OOS-roll idiom (document that empty space + empty chosen_params is a valid fixed-strategy walk-forward), or tighten walk_forward to make the optimize-per-window contract explicit.

[bug] Structural deformers (Delay.lag, Resample.period_minutes) leak into param_space

  • Example(s): t3_sweep (the grid panicked on the un-enumerated delay.lag slot).
  • What happened: the breakout Composite's param_space() is { resample.period_minutes, delay.lag, entry_bar.target, exit_bar.target }. delay.lag is the breakout's "previous bar's high" — a STRUCTURAL constant (varying it deforms the strategy, the exact C34 deform-vs-tune discriminator), yet Delay::builder() ships lag as a tunable param. So GridSpace::new REQUIRES the researcher to enumerate delay.lag (or it errors), and a naive random sweep would silently deform the strategy. C34's bind() can remove it, but the node pollutes the space by default.
  • Why bug: this is the precise failure C34 (cycle 0034) exists to prevent — a structural deformer presented as a tuning knob. The strategy author did not opt into it; the node's default builder() forces the pollution onto every consumer.
  • Repro: cargo run --release --manifest-path fieldtests/research-breakout-deepdive/Cargo.toml --bin t3_sweepparam_space() lists delay.lag; removing its grid axis makes GridSpace::new reject the grid.
  • Recommended action: debug / planner — decide whether Delay.lag (and Resample.period_minutes) are tuning params or structural constants; if structural, they should not appear in a default param_space() (or the demo should bind() them out and the example should model that for consumers).

[bug] The Resample/Session period coupling is unenforceable and fails silently

  • Example(s): t3_sweep, t3b_period_desync.
  • What happened: the breakout's bar period lives in TWO places — Resample::builder() exposes period_minutes as a tunable param, but Session::builder(open, tz, period) BAKES its period (its rustdoc: "NOT scalar params, the declared param list is empty"). So param_space() has resample.period_minutes but NO session period. Sweeping resample.period_minutes = 30 while Session stays baked at 15 bootstraps and runs with NO error, but the two clocks desync: entry_bar.target == 3 no longer means "3rd 30m bar". The desynced run produced 0.0 pips / no error; a coherent 30m strategy (+63.0 pips) was reachable ONLY by rebuilding the Composite with session_period = 30 (a different blueprint — topology, not a sweep).
  • Why bug: the public sweep API silently admits a parameter combination that is structurally meaningless, and the failure mode is a quiet 0.0-pip result rather than a diagnostic. The two periods are one logical knob split across a tunable param and a baked factory arg with no coupling mechanism on the public surface.
  • Repro: cargo run --release --manifest-path fieldtests/research-breakout-deepdive/Cargo.toml --bin t3b_period_desync → the resample=30/session=15 row prints total_pips=0.0 with no error.
  • Recommended action: debug / planner — either make Session's period a tunable param so a single swept value feeds both (the C8/C12 way), or provide a coupling/ derived-param mechanism, or have the demo bind the period as a structural constant so it cannot be swept into a desync. A silent 0.0 is the worst outcome.

[spec_gap] No per-instrument pip/metadata registry; cross-asset honesty is manual

  • Example(s): t4_compare.
  • What happened: SimBroker::new(pip_size) takes a bare f64. There is no instrument/pip-metadata registry on the public surface — aura-ingest exposes only M1 streaming, and Aura.toml (which the glossary says carries "instrument/pip metadata") does not exist (open architectural thread). So the researcher hard-codes pip_size per symbol from outside knowledge (GER40=1.0, FRA40=1.0, EURUSD=0.0001). The GER40-vs-FRA40 comparison is honest only because both happen to be index points and the researcher knew to pass 1.0 for each; nothing flags or prevents comparing GER40 index-pips to EURUSD fx-pips.
  • Why spec_gap: C10/C20 lean on "per-instrument pip metadata (reference data beside the hot path)" for the sim-optimal broker's currency-free yardstick, but the public surface offers no place to register or look up that metadata, so the yardstick's cross-asset honesty is entirely on the researcher. The ledger names the metadata; the surface does not realize it. (Acknowledged as an open thread, so this is a confirmation that the gap bites at the first real comparison.)
  • Recommended action: ratify / planner — sequence the Aura.toml instrument/pip schema (or a minimal pip-metadata source) so cross-asset comparison can be honest by construction, not by researcher discipline.

[working] Determinism, the streaming source, and the sweep/WFO cores are solid

  • Example(s): t1_scale, t1c, t2, t3, t4.
  • What happened: every long-horizon run was bit-identical on rerun (C1 held over 7 years / 148k bars). The lazy M1FieldSource streamed a 7-year, 4-field, 8.6M- record window in 1.12s with no errors. The Composite re-authoring reproduced the shipped FlatGraph's numbers EXACTLY (entry=3,exit=545.0; 2024 full year → +203.4 / 62 sessions, identical in t1 and t4) — strong evidence the bootstrap/compile and bootstrap_with_cells paths are behaviour-preserving (C23). sweep parallelized the 9-point grid and walk_forward produced 24 windows + a correctly stitched, timestamp-monotone OOS curve. The named-param surface (param_space, named_params, GridSpace::new validating axes against the space) is clean and self-describing where the strategy IS a Composite.
  • Why working: these are the load-bearing guarantees the whole World rests on, and they held under real multi-year load on the first or second try. Worth recording so they are protected from drift.
  • Recommended action: carry-on.

[friction] ns↔ms conversion is one-directional; residency probe not on the trait

  • Example(s): t2_walkforward, t1b_residency.
  • What happened: (a) WindowBounds/RunManifest.window are epoch-ns but M1FieldSource::open wants Unix-ms; aura-ingest exports unix_ms_to_epoch_ns but NOT its inverse, so every real-data walk-forward hand-divides ts.0 / 1_000_000 (the milestone mw_3 fixture hit the identical seam). (b) resident_records() is an inherent method on the concrete M1FieldSource, not on the public Source trait, so a consumer driving Box<dyn Source> (as run does) cannot read residency — I had to drop to the concrete type to probe it.
  • Why friction: both tasks completed, but the surface forced a verbose dance the ledger's "producer-supplied window" / "residency is O(one chunk)" framing implied would be cleaner.
  • Recommended action: plan — add epoch_ns_to_unix_ms to aura-ingest (mirror of the existing forward fn), and consider surfacing residency on the Source trait (or document that it is concrete-only).

Recommendation summary

Finding Action
spec_gap — breakout ships as FlatGraph, not a World-consumable Composite planner (ship a Composite blueprint)
bug — process residency scales with window despite O(one-chunk) claim debug
spec_gap — walk_forward empty-space idiom undefined for param-free strategy ratify
bug — Delay.lag / Resample.period_minutes leak into default param_space debug / planner
bug — Resample/Session period coupling unenforceable, desyncs silently to 0 pips debug / planner
spec_gap — no per-instrument pip/metadata registry; cross-asset honesty manual ratify / planner
working — C1 determinism, streaming source, sweep/WFO cores, re-author fidelity carry-on
friction — one-directional ns/ms conversion; residency not on Source trait plan