90c1de841d
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.
247 lines
16 KiB
Markdown
247 lines
16 KiB
Markdown
# 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` |
|