walk-forward: space on the family, tag-free chosen_params, schema-checked param_stability #75

Closed
opened 2026-06-16 18:26:03 +02:00 by Brummel · 0 comments
Owner

Problem

Last cycle (Scalar-as-enum + RunManifest.params lift) kept WindowRun.chosen_params
as Vec<Scalar> — an explicit out-of-scope decision ("Fork A"), justified by "Scalar is
now serializable, so a WindowRun is a self-describing record."

That defense does not hold: WalkForwardResult and WindowRun carry no serde derives
(walkforward.rs:123, 147). They are never serialized — only the embedded oos_report
(a RunReport, already typed via the lift) is. So chosen_params is a pure in-memory
substrate
for param_stability, and Vec<Scalar> makes it C7-impure: it carries the
kind N×M times at the value, where C11 makes the kind per-slot constant. The lone
reader (param_stability, :243) then re-asks "is this slot numeric?" once per
window-value via a per-value unreachable!.

SweepFamily already shows the C7-clean form in the same crate: kinds live once on
the family (space: Vec<ParamSpec>), points are tag-free Vec<Cell>, names recombined on
demand via zip_params. Walk-forward is the lone asymmetry.

Change (three coupled parts)

  • β1 — data shape. WalkForwardResult gains space: Vec<ParamSpec>;
    WindowRun.chosen_params: Vec<Scalar>Vec<Cell>. Shares the
    (space, tag-free Cell points) idiom with SweepFamily (+ zip_params for the named
    view) — distinct types, not a unified container (Sweep's axis is the param-space, an
    input coordinate; WF's axis is time, chosen_params is the inner optimizer's output,
    and WF additionally carries bounds + oos_equity + stitched_oos_equity).
  • [A] — the walk_forward seam. walk_forward(roller, space, run_window); the
    Fn(WindowBounds) -> WindowRun + Sync generic is untouched — space is schema
    metadata passed beside the closure (by value, moved onto the result), never captured, so
    run_indexed is untouched. The caller computes the space once (C11: window-invariant)
    via param_space(); the closure simplifies to chosen_params: best.params (drops the
    from_cell reconstruction).
  • [C]/[D] — schema-checked reduction. param_stability builds a per-slot coercer
    Vec<fn(Cell) -> f64> from result.space before a type-blind reduction loop.
    Bool -> 0/1 keeps the coercer total over the knob kinds; Timestamp is never a knob
    (C20) -> a single schema-level assert. scalar_as_f64 + its per-value unreachable! are
    deleted. Public signature unchanged (reads result.space).

Notes

  • Overturns Fork A. Same argument-form that retired the {kind, cell} Scalar struct last
    cycle: "it works" does not beat "it fits structurally."
  • Behaviour-preserving (C1): param_stability yields identical MetricStats over the
    existing fixtures (I64->f64/F64 coercions are value-identical; the Bool arm is new
    and unexercised by built-in grids).
  • Path: specify -> planner -> implement -> audit (design settled — no fork).
## Problem Last cycle (Scalar-as-enum + `RunManifest.params` lift) kept `WindowRun.chosen_params` as `Vec<Scalar>` — an explicit out-of-scope decision ("Fork A"), justified by "Scalar is now serializable, so a `WindowRun` is a self-describing record." That defense does not hold: `WalkForwardResult` and `WindowRun` carry **no serde derives** (`walkforward.rs:123, 147`). They are never serialized — only the embedded `oos_report` (a `RunReport`, already typed via the lift) is. So `chosen_params` is a **pure in-memory substrate** for `param_stability`, and `Vec<Scalar>` makes it C7-impure: it carries the kind N×M times *at the value*, where C11 makes the kind per-slot **constant**. The lone reader (`param_stability`, `:243`) then re-asks "is this slot numeric?" once per window-value via a per-value `unreachable!`. `SweepFamily` already shows the C7-clean form in the same crate: kinds live **once** on the family (`space: Vec<ParamSpec>`), points are tag-free `Vec<Cell>`, names recombined on demand via `zip_params`. Walk-forward is the lone asymmetry. ## Change (three coupled parts) - **β1 — data shape.** `WalkForwardResult` gains `space: Vec<ParamSpec>`; `WindowRun.chosen_params`: `Vec<Scalar>` → `Vec<Cell>`. Shares the `(space, tag-free Cell points)` idiom with `SweepFamily` (+ `zip_params` for the named view) — distinct types, not a unified container (Sweep's axis *is* the param-space, an input coordinate; WF's axis is time, `chosen_params` is the inner optimizer's output, and WF additionally carries `bounds` + `oos_equity` + `stitched_oos_equity`). - **[A] — the `walk_forward` seam.** `walk_forward(roller, space, run_window)`; the `Fn(WindowBounds) -> WindowRun + Sync` generic is **untouched** — space is schema metadata passed beside the closure (by value, moved onto the result), never captured, so `run_indexed` is untouched. The caller computes the space **once** (C11: window-invariant) via `param_space()`; the closure simplifies to `chosen_params: best.params` (drops the `from_cell` reconstruction). - **[C]/[D] — schema-checked reduction.** `param_stability` builds a per-slot coercer `Vec<fn(Cell) -> f64>` from `result.space` **before** a type-blind reduction loop. `Bool -> 0/1` keeps the coercer total over the knob kinds; `Timestamp` is never a knob (C20) -> a single schema-level assert. `scalar_as_f64` + its per-value `unreachable!` are deleted. Public signature unchanged (reads `result.space`). ## Notes - Overturns Fork A. Same argument-form that retired the `{kind, cell}` Scalar struct last cycle: "it works" does not beat "it fits structurally." - Behaviour-preserving (C1): `param_stability` yields identical `MetricStats` over the existing fixtures (`I64->f64`/`F64` coercions are value-identical; the `Bool` arm is new and unexercised by built-in grids). - Path: `specify` -> `planner` -> `implement` -> `audit` (design settled — no fork).
Brummel added the feature label 2026-06-16 18:26:03 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#75