Walk-forward + Monte-Carlo OOS validation for R candidates (not just the SMA sample) #139

Closed
opened 2026-06-25 09:15:12 +02:00 by Brummel · 5 comments
Owner

Summary

The edge research needs honest out-of-sample R-metrics for an arbitrary
candidate
strategy on real data. Today the validation machinery exists as a
clean library API but is only half-reachable from the CLI, and not at all for
R-based candidates:

  • aura walkforward is hardwired to the SMA sample blueprint with fixed
    90 / 30 / 30-day windows and pip metrics only — it cannot run stage1-r
    (or any new candidate) and reports no R.
  • aura mc is synthetic-only, 3 hardcoded seeds — useless on a real
    candidate.
  • The library is ready: walk_forward(WindowRoller, space, run_window) with
    WindowRoller::new(span, is_len, oos_len, step, RollMode::{Rolling,Anchored}),
    monte_carlo(base, seeds, run_one), summarize_r(record, cost).

What to build

A candidate-aware OOS validation path (CLI subcommand and/or extended
walkforward/mc) that, for a chosen strategy (stage1-r first; the griddable
flags already exist) on --real <SYM>:

  • Walk-forward: roll IS → OOS windows; on each IS window sweep the
    candidate's param grid and pick the best by an R metric (e.g.
    sqn_normalized or expectancy_r); measure that param set on the held-out
    OOS window; stitch the OOS trades. Configurable IS/OOS length + step + mode.
  • Monte-Carlo: on the stitched OOS per-trade R series, bootstrap
    (trade-shuffle and/or block) to get a distribution / confidence interval on
    E[R] — so a positive OOS edge is shown to be unlikely under reshuffling.
  • Report per-symbol and pooled OOS R-metrics.

Dependencies / notes

  • Depends on the memory BLOCKER: walk-forward runs a sweep per window, so
    it multiplies the per-member footprint that already nearly exhausts RAM. Fix
    that first.
  • Frictionless Stage-1 R (costs are Stage-2). Behaviour of the existing
    walkforward/mc SMA paths and their goldens must be preserved (add,
    don't break).

refs #137

## Summary The edge research needs honest **out-of-sample R-metrics for an arbitrary candidate** strategy on real data. Today the validation machinery exists as a clean library API but is only half-reachable from the CLI, and not at all for R-based candidates: - `aura walkforward` is hardwired to the **SMA sample blueprint** with fixed 90 / 30 / 30-day windows and **pip metrics only** — it cannot run stage1-r (or any new candidate) and reports no R. - `aura mc` is **synthetic-only**, 3 hardcoded seeds — useless on a real candidate. - The library is ready: `walk_forward(WindowRoller, space, run_window)` with `WindowRoller::new(span, is_len, oos_len, step, RollMode::{Rolling,Anchored})`, `monte_carlo(base, seeds, run_one)`, `summarize_r(record, cost)`. ## What to build A candidate-aware OOS validation path (CLI subcommand and/or extended `walkforward`/`mc`) that, for a chosen strategy (stage1-r first; the griddable flags already exist) on `--real <SYM>`: - **Walk-forward**: roll IS → OOS windows; on each IS window sweep the candidate's param grid and pick the best by an R metric (e.g. `sqn_normalized` or `expectancy_r`); measure that param set on the held-out OOS window; stitch the OOS trades. Configurable IS/OOS length + step + mode. - **Monte-Carlo**: on the stitched OOS per-trade R series, bootstrap (trade-shuffle and/or block) to get a distribution / confidence interval on `E[R]` — so a positive OOS edge is shown to be unlikely under reshuffling. - Report **per-symbol and pooled** OOS R-metrics. ## Dependencies / notes - **Depends on the memory BLOCKER**: walk-forward runs a sweep per window, so it multiplies the per-member footprint that already nearly exhausts RAM. Fix that first. - Frictionless Stage-1 R (costs are Stage-2). Behaviour of the existing `walkforward`/`mc` SMA paths and their goldens must be preserved (add, don't break). refs #137
Brummel added this to the Edge research: prove a real tradeable edge milestone 2026-06-25 09:15:12 +02:00
Brummel added the feature label 2026-06-25 09:15:12 +02:00
Author
Owner

Surviving the edge-research milestone wind-down (#137). This is a genuine engine-capability gap — the World layer's walk-forward + Monte-Carlo are locked to the SMA sample blueprint and carry no R; they should validate ANY harness. Independent of the abandoned edge hunt. Detached from the (now closed) milestone; kept open as standalone engine work.

Surviving the edge-research milestone wind-down (#137). This is a genuine engine-capability gap — the World layer's walk-forward + Monte-Carlo are locked to the SMA sample blueprint and carry no R; they should validate ANY harness. Independent of the abandoned edge hunt. Detached from the (now closed) milestone; kept open as standalone engine work.
Brummel removed this from the Edge research: prove a real tradeable edge milestone 2026-06-25 15:29:20 +02:00
Author
Owner

Empirical confirmation (2026-06-26, post-0074)

Ran the existing aura walkforward --real {USDJPY,GER40} (2023-01..2024-12,
90/30/30) to confirm the gap is live at HEAD:

  • Confirmed hardwired: walk-forward runs the SMA sample_blueprint, optimizes
    on total_pips, and the OOS report carries pip metrics only
    (total_pips/max_drawdown/bias_sign_flips, no r block). No
    --strategy flag, so stage1-r is unreachable here.
  • Result (pip-denominated, so per-instrument sign only — not comparable across
    symbols):
    USDJPY stitched −171.7 pips (8/21 OOS windows positive); GER40
    −271.1 pips (11/21). Param choice unstable window-to-window (USDJPY trend fast
    50↔100, slow 200↔400). Both negative OOS — the in-sample stage1-r best-of-grid
    looked +0.1..0.2 E[R], so this is exactly the overfit-exposure the candidate-
    aware OOS path is for.

Scope narrowing — the sweep half already works. aura sweep --strategy stage1-r --real <SYM> reports the R block (expectancy_r, sqn_normalized) and
is pip-agnostic across instruments (verified cross-instrument over Gold/USDJPY/
GER40). So the remaining work is specifically the walkforward + mc side:
make them strategy-selectable and R-reporting, reusing the existing stage1-r grid
(--fast/--slow/--stop-length/--stop-k) + summarize_r.

RAM note. This modest run (2y, default sample grid, no per-window candidate
grid) completed fast with no OOM. The cited RAM dependency likely bites at the
larger scale (per-window candidate grid × all symbols × MC bootstrap), not a
modest walk-forward — worth re-measuring before treating it as a hard blocker.

## Empirical confirmation (2026-06-26, post-0074) Ran the existing `aura walkforward --real {USDJPY,GER40}` (2023-01..2024-12, 90/30/30) to confirm the gap is live at HEAD: - **Confirmed hardwired**: walk-forward runs the SMA `sample_blueprint`, optimizes on `total_pips`, and the OOS report carries pip metrics only (`total_pips`/`max_drawdown`/`bias_sign_flips`, **no `r` block**). No `--strategy` flag, so stage1-r is unreachable here. - **Result (pip-denominated, so per-instrument sign only — not comparable across symbols):** USDJPY stitched −171.7 pips (8/21 OOS windows positive); GER40 −271.1 pips (11/21). Param choice unstable window-to-window (USDJPY trend fast 50↔100, slow 200↔400). Both negative OOS — the in-sample stage1-r best-of-grid looked +0.1..0.2 E[R], so this is exactly the overfit-exposure the candidate- aware OOS path is for. **Scope narrowing — the sweep half already works.** `aura sweep --strategy stage1-r --real <SYM>` reports the R block (`expectancy_r`, `sqn_normalized`) and is pip-agnostic across instruments (verified cross-instrument over Gold/USDJPY/ GER40). So the remaining work is specifically the **walkforward + mc** side: make them strategy-selectable and R-reporting, reusing the existing stage1-r grid (`--fast/--slow/--stop-length/--stop-k`) + `summarize_r`. **RAM note.** This modest run (2y, default sample grid, no per-window *candidate* grid) completed fast with no OOM. The cited RAM dependency likely bites at the larger scale (per-window candidate grid × all symbols × MC bootstrap), not a modest walk-forward — worth re-measuring before treating it as a hard blocker.
Author
Owner

/boss run — decision log (design settled in-context with the user, 2026-06-26)

Provenance: these forks were settled in a live design discussion with the user
before the /boss run started; the user was in the loop for each. Recorded
here as the run's reference-issue decision log. Entry path: specify (settled
prose design — no discovery fork left open), not brainstorm.

1. #139 is NOT blocked by #136. It builds correct today against the
already-generic walk_forward / monte_carlo / optimize / summarize_r with
concrete RunMetrics. The work arrow is #139#136: #136 later genericises that
orchestration surface over a metric type M and absorbs #139's added call
sites. This is a merge-collision on the sweep/mc/walkforward files, not a logical
dependency. Sequence #139 first (smaller, value-bearing).

2. The cited "memory BLOCKER" is discharged. It pointed at #138
(stage1-r sweep ~2 GiB/member), CLOSED in cycle 0070 with the explicit note
"unblocks #139"; per-member RAM dropped from O(cycles) to O(trades) via the
finalize lifecycle + folding sinks. Not a gate — a constraint: the new
per-window / per-MC runs MUST use the folded reduce-mode sinks (as the stage1-r
sweep already does), otherwise O(cycles) returns per window.

3. Per-trade R series placement: carry it inside the R-metric block
(RMetrics), #[serde(skip)].
summarize_r already computes the per-trade R
vector internally and drops it (report.rs:137) — stop dropping it. serde(skip)
keeps it in-memory and off the C18 wire (no runs.jsonl bloat). Adds no new
concrete series field to the engine's WindowRun (the R block already rides
through oos_report). Rationale is today-substantive (the natural home for the
distribution's own sample; the smallest change), explicitly not #136-driven —
chosen for clarity now, #136 relocates the whole RMetrics type later regardless.

4. MC bootstrap shape: block bootstrap with a configurable block length;
trade-shuffle is the block_len = 1 special case.
R-multiples from sequential
trades are not i.i.d. (winner/loser streaks in a trend follower), so a pure
trade-shuffle overstates the confidence on E[R]. One block-aware primitive
subsumes both. Build it as a new deterministic primitive (on SplitMix64) over a
Vec<f64> of R, golden-testable on fixed quantiles. This is the one genuinely new
library component; everything else is CLI wiring onto already-generic APIs.

5. Reuse map (substrate already green at HEAD). aura sweep --strategy stage1-r --real <SYM> is fully R-reporting today (R block via summarize_r, all
four grid flags --fast/--slow/--stop-length/--stop-k, pip-agnostic across
instruments). #139 reuses: parse_csv_list + Stage1RGrid + the Strategy enum
(arg parsing), stage1_r_sweep_family / stage1_r_graph (blueprint),
summarize_r (R), optimize(.., "sqn_normalized" | "expectancy_r") (IS
objective). Walk-forward half = wire this into the already-generic
walk_forward + WindowRoller; MC half = the new R-series bootstrap above.

6. Scope & invariants. Frictionless Stage-1 R (costs are Stage-2). Report
per-symbol and pooled OOS R-metrics. Preserve the existing SMA/total_pips
walkforward goldens and the synthetic-mc goldens — add, don't break. Rough
size: walk-forward strategy-selectable + R-reporting S–M (mostly CLI wiring +
small R-series exposure); MC real-candidate R-bootstrap M (the new primitive).

## `/boss` run — decision log (design settled in-context with the user, 2026-06-26) > Provenance: these forks were settled in a live design discussion with the user > *before* the `/boss` run started; the user was in the loop for each. Recorded > here as the run's reference-issue decision log. Entry path: **specify** (settled > prose design — no discovery fork left open), not brainstorm. **1. #139 is NOT blocked by #136.** It builds correct today against the already-generic `walk_forward` / `monte_carlo` / `optimize` / `summarize_r` with concrete `RunMetrics`. The work arrow is #139 → #136: #136 later genericises that orchestration surface over a metric type `M` and *absorbs* #139's added call sites. This is a merge-collision on the sweep/mc/walkforward files, not a logical dependency. Sequence #139 first (smaller, value-bearing). **2. The cited "memory BLOCKER" is discharged.** It pointed at #138 (`stage1-r sweep ~2 GiB/member`), CLOSED in cycle 0070 with the explicit note "unblocks #139"; per-member RAM dropped from O(cycles) to O(trades) via the `finalize` lifecycle + folding sinks. Not a gate — a constraint: the new per-window / per-MC runs MUST use the folded reduce-mode sinks (as the stage1-r sweep already does), otherwise O(cycles) returns per window. **3. Per-trade R series placement: carry it inside the R-metric block (`RMetrics`), `#[serde(skip)]`.** `summarize_r` already computes the per-trade R vector internally and drops it (`report.rs:137`) — stop dropping it. `serde(skip)` keeps it in-memory and off the C18 wire (no `runs.jsonl` bloat). Adds **no** new concrete series field to the engine's `WindowRun` (the R block already rides through `oos_report`). Rationale is today-substantive (the natural home for the distribution's own sample; the smallest change), explicitly **not** #136-driven — chosen for clarity now, #136 relocates the whole `RMetrics` type later regardless. **4. MC bootstrap shape: block bootstrap with a configurable block length; trade-shuffle is the `block_len = 1` special case.** R-multiples from sequential trades are not i.i.d. (winner/loser streaks in a trend follower), so a pure trade-shuffle overstates the confidence on E[R]. One block-aware primitive subsumes both. Build it as a new deterministic primitive (on `SplitMix64`) over a `Vec<f64>` of R, golden-testable on fixed quantiles. This is the one genuinely new library component; everything else is CLI wiring onto already-generic APIs. **5. Reuse map (substrate already green at HEAD).** `aura sweep --strategy stage1-r --real <SYM>` is fully R-reporting today (R block via `summarize_r`, all four grid flags `--fast/--slow/--stop-length/--stop-k`, pip-agnostic across instruments). #139 reuses: `parse_csv_list` + `Stage1RGrid` + the `Strategy` enum (arg parsing), `stage1_r_sweep_family` / `stage1_r_graph` (blueprint), `summarize_r` (R), `optimize(.., "sqn_normalized" | "expectancy_r")` (IS objective). Walk-forward half = wire this into the already-generic `walk_forward` + `WindowRoller`; MC half = the new R-series bootstrap above. **6. Scope & invariants.** Frictionless Stage-1 R (costs are Stage-2). Report per-symbol **and** pooled OOS R-metrics. Preserve the existing SMA/`total_pips` walkforward goldens and the synthetic-mc goldens — **add, don't break**. Rough size: walk-forward strategy-selectable + R-reporting **S–M** (mostly CLI wiring + small R-series exposure); MC real-candidate R-bootstrap **M** (the new primitive).
Author
Owner

/boss run — derived sub-fork decisions (specify, grounding read)

Four finer forks surfaced while reading the code to write spec 0075. All
derived (sources / code / consistency / risk), none a user-preference fork.

A. The OOS per-trade R series rides in RMetrics, excluded from BOTH serde
and equality.
walk_forward must stay unchanged, so the series can only
survive into result.windows via the already-carried oos_report → so it lives
on RMetrics (decision #3 in the prior comment). To keep the C18 wire shape and
every existing RunReport/RMetrics equality assertion byte- and value-unchanged
("add, don't break"), the new field participates in neither serde (skip) nor
PartialEq — it is a pure in-memory conduit for the bootstrap. Basis: derived —
forced by walk_forward-unchanged + C18/golden stability.

B. In-sample sweep folds (reduce-mode); the single OOS run uses raw recorders.
The RAM constraint bites on the per-window sweep (many members × O(cycles)) — so
the IS stage1-r sweep uses the folded reduce-mode sinks (GatedRecorder
O(trades)/member), optimised by an R metric. The one OOS run per window stays
non-reduce: stitching needs the full pip-equity curve, and one window's curve is
bounded. Basis: derived — the decision-log RAM constraint applied at the right
granularity; mirrors the existing SMA sweep_over/run_oos split.

C. aura mc --strategy ... --real ... runs the walk-forward internally, then
bootstraps its pooled OOS R series.
The MC bootstrap operates on "the stitched
OOS per-trade R series" — which the walk-forward produces. So the real-candidate
mc path reuses walkforward_family to obtain the pooled OOS R series, then
applies the new bootstrap — no new on-disk artifact, and aura mc stays a single
command. The synthetic seed-resweep mc_family is untouched (its goldens hold).
Basis: derived — reuse over a new persistence format; least surface.

D. Scope: single --real <SYM> per invocation; "pooled" = pooled across the
OOS windows of that symbol.
The issue's "per-symbol and pooled" is delivered
as: each invocation reports one symbol's OOS R-metrics over its stitched
(across-windows) OOS trade series. Cross-symbol pooling needs a multi-symbol CLI
grammar (RealWindowGrammar accepts one symbol today) — a separable expansion,
deferred to a follow-on, not built this cycle. Basis: derived scope narrowing;
flagged here so it is vetoable.

## `/boss` run — derived sub-fork decisions (specify, grounding read) Four finer forks surfaced while reading the code to write spec 0075. All **derived** (sources / code / consistency / risk), none a user-preference fork. **A. The OOS per-trade R series rides in `RMetrics`, excluded from BOTH serde *and* equality.** `walk_forward` must stay unchanged, so the series can only survive into `result.windows` via the already-carried `oos_report` → so it lives on `RMetrics` (decision #3 in the prior comment). To keep the C18 wire shape and every existing `RunReport`/`RMetrics` equality assertion byte- and value-unchanged ("add, don't break"), the new field participates in neither `serde` (`skip`) nor `PartialEq` — it is a pure in-memory conduit for the bootstrap. Basis: derived — forced by `walk_forward`-unchanged + C18/golden stability. **B. In-sample sweep folds (reduce-mode); the single OOS run uses raw recorders.** The RAM constraint bites on the *per-window sweep* (many members × O(cycles)) — so the IS stage1-r sweep uses the folded reduce-mode sinks (`GatedRecorder` → O(trades)/member), optimised by an R metric. The *one* OOS run per window stays non-reduce: stitching needs the full pip-equity curve, and one window's curve is bounded. Basis: derived — the decision-log RAM constraint applied at the right granularity; mirrors the existing SMA `sweep_over`/`run_oos` split. **C. `aura mc --strategy ... --real ...` runs the walk-forward internally, then bootstraps its pooled OOS R series.** The MC bootstrap operates on "the stitched OOS per-trade R series" — which the walk-forward produces. So the real-candidate `mc` path reuses `walkforward_family` to obtain the pooled OOS R series, then applies the new bootstrap — no new on-disk artifact, and `aura mc` stays a single command. The synthetic seed-resweep `mc_family` is untouched (its goldens hold). Basis: derived — reuse over a new persistence format; least surface. **D. Scope: single `--real <SYM>` per invocation; "pooled" = pooled across the OOS *windows* of that symbol.** The issue's "per-symbol and pooled" is delivered as: each invocation reports one symbol's OOS R-metrics over its stitched (across-windows) OOS trade series. Cross-*symbol* pooling needs a multi-symbol CLI grammar (`RealWindowGrammar` accepts one symbol today) — a separable expansion, **deferred to a follow-on**, not built this cycle. Basis: derived scope narrowing; flagged here so it is vetoable.
Author
Owner

/boss run — pooled oos_r reducer (planner, derived)

The spec left the pooled-oos_r mechanism as "a new reducer or an inline
fold". Decided: a new self-contained pub fn r_metrics_from_rs(rs: &[f64]) -> RMetrics in aura-engine/report.rs, not a refactor of summarize_r. It
copies summarize_r's rs-derived arithmetic verbatim (byte-pinned floats must
not be algebraically refactored), leaving summarize_r and its goldens untouched.
The two fields a flat R series cannot carry are set honestly: net_expectancy_r = expectancy_r (exact at the Stage-1 cost=0 invariant) and conviction_terciles_r = [0,0,0] (per-trade conviction is not pooled). So the pooled OOS block reports the
well-defined R-distribution metrics (E[R], SQN, n_trades, win-rate, profit-factor,
R-drawdown); the per-window r blocks keep full fidelity. Basis: derived — least
risk to pinned arithmetic; honest field semantics under frictionless Stage-1.

## `/boss` run — pooled `oos_r` reducer (planner, derived) The spec left the pooled-`oos_r` mechanism as "a new reducer **or** an inline fold". Decided: a new self-contained `pub fn r_metrics_from_rs(rs: &[f64]) -> RMetrics` in `aura-engine/report.rs`, **not** a refactor of `summarize_r`. It copies `summarize_r`'s rs-derived arithmetic *verbatim* (byte-pinned floats must not be algebraically refactored), leaving `summarize_r` and its goldens untouched. The two fields a flat R series cannot carry are set honestly: `net_expectancy_r = expectancy_r` (exact at the Stage-1 cost=0 invariant) and `conviction_terciles_r = [0,0,0]` (per-trade conviction is not pooled). So the pooled OOS block reports the well-defined R-distribution metrics (E[R], SQN, n_trades, win-rate, profit-factor, R-drawdown); the per-window `r` blocks keep full fidelity. Basis: derived — least risk to pinned arithmetic; honest field semantics under frictionless Stage-1.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#139