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

247 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `M1FieldSource`s 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_sweep` → `param_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=5` → 45.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` |