spec(0051): strategy as a World-consumable Composite blueprint

Milestone spec: ship the GER40 session-breakout as a Composite blueprint so the
same strategy runs across backtest / sweep / walk_forward / compare without
re-authoring (the #94 fieldtest friction). Records the design decision that the
bar period is a structural axis (a construction arg binding both Resample and
Session, so a sweep cannot desync them — #96) and that the canonical shippable
strategy form is a Composite (FlatGraph is its compiled substrate). Authoring-
side; no engine-core change (the existing bind() + GraphBuilder + param_space()
layer already suffices).

refs #94, #96, #97
This commit is contained in:
2026-06-17 19:06:11 +02:00
parent 90c1de841d
commit 98be64ebdf
@@ -0,0 +1,138 @@
# 0051 — Strategy as a World-consumable Composite blueprint (GER40 breakout)
Status: draft (milestone spec)
Driver: the GER40 research deep-dive fieldtest
(`docs/specs/fieldtest-research-breakout-deepdive.md`) found that the shipped
real-data session-breakout (commit `5b5f034`) ships as a hand-wired raw
`FlatGraph`, which the World families (`sweep` / `walk_forward` / `optimize` /
compare) **cannot consume** — they need a `Composite` with `param_space()` +
`bootstrap_with_cells`. Every World task forced a researcher to re-author the
13-node graph by hand. This milestone makes the flagship strategy World-ready.
Tracker: closes #94, #96, #97 (milestone "GER40 breakout as a World-consumable
Composite blueprint").
## 1. Goal & non-goal
**Goal.** Ship the GER40 session-breakout as a reusable **`Composite` blueprint**
so the *same* strategy runs across single backtest, parameter sweep,
walk-forward and multi-instrument/timeframe comparison **without re-authoring**.
Establish (and record in the ledger) that the canonical *shippable* form of a
strategy is a `Composite` blueprint; the `FlatGraph` is its compiled substrate
(C11/C23), not the authoring or shipping form.
**Authoring-side only — no engine-core change.** The recon
(`crates/aura-engine/src/blueprint.rs`, `builder.rs`, `node.rs`) confirms the
authoring + binding layer already supports everything this needs:
`GraphBuilder` / `Composite::new`, `param_space()` derivation, and
`PrimitiveBuilder::bind(slot, value)` (removes a param from the space). No new
engine mechanism is built here.
**Non-goal.**
- The **generic param fan-out** feature (one blueprint param → multiple node
slots; recon §3 verdict: does not exist) is **explicitly not built**. The
bar-period is a structural axis (see §2), so fan-out is unnecessary for this
strategy. The capability stays an open idea for a future, genuinely-coupled
*sweep* param.
- The **process-residency bug** (#95) is a separate debug thread (likely
data-server chunk retention), not this milestone.
- **Per-instrument pip registry** (#22) and the **ns→ms inverse** (#80) stay as
filed; the compare demo hand-supplies `pip_size = 1.0` for both index
instruments (honest by construction — both quote in index points).
- No `aura new` / cdylib / deploy (C9 Part-B still deferred). No new nodes.
## 2. The design decision: bar-period is a structural axis, not a sweep param
The bar period lives in two nodes — `Resample` exposes `period_minutes` as a
tunable param; `Session` bakes its period (`session.rs:66-76`, `params: vec![]`).
Sweeping one while the other stays baked silently desyncs to 0.0 pips (#96).
**Decision:** the blueprint takes `bar_period_minutes` as a **construction
argument** that binds *both* nodes at authoring time —
`Resample::builder().bind("period_minutes", bar_period)` **and**
`Session::new(open_hour, open_minute, tz, bar_period)` — so the two periods are
**locked equal by construction** and a desync is structurally impossible.
Rationale (substance, not effort):
- **C34 (deform vs tune).** A 15m-breakout and a 30m-breakout are *different
strategies*, not one strategy tuned — the bar period deforms, it does not
tune. Deformers do not belong in `param_space()`.
- **C11 (structural axes = experiment matrix; tuning params = sweep).** The
timeframe is a matrix axis (instantiate the blueprint per period and compare),
the entry/exit bar indices are the sweep *within* each.
That this also avoids the net-new fan-out feature is a tiebreaker, not the
reason.
`delay.lag` (the "previous bar's high" register — a structural constant, the
exact C34 case) is likewise **bound out**: `Delay::builder().bind("lag", 1)`,
removing it from the space.
**Result:** `param_space() == { entry_bar.target, exit_bar.target }` — exactly
the two genuine tuning knobs (the `EqConst` targets, named `entry_bar` /
`exit_bar`). This resolves #96 (no desync, no `delay.lag` leak) and makes #97's
`walk_forward` non-degenerate (a real, non-empty space to optimize per window).
## 3. The blueprint
A factory in the example's shared module (C9: pure composition of shipped
`aura-std` nodes → lives in `aura/examples`, not a separate crate):
```
ger40_breakout_blueprint(bar_period_minutes, open_hour, open_minute, tz)
-> Composite
```
- Internally the 13-node breakout (the proven wiring), authored via
`GraphBuilder` (the fluent, name-addressed form; canonical template
`crates/aura-engine/src/test_fixtures.rs` SMA-cross).
- `bar_period_minutes` binds Resample's period and constructs Session (§2).
- `delay.lag` bound to 1 (§2).
- Four OHLC **source roles** (open, high, low, close) in the C4 merge order,
fanning into Resample's `Barrier(0)` slots.
- `param_space() = { entry_bar.target, exit_bar.target }`.
- One exposed output (the SimBroker equity) + the recorder taps as today.
## 4. Deliverables
1. The blueprint factory (above), replacing the hand-wired `FlatGraph` as the
authoring form in `crates/aura-ingest/examples/shared/`.
2. **Single backtest** — the existing `ger40_breakout_real` example bootstraps
the blueprint (`blueprint.with("entry_bar.target", 3).with("exit_bar.target",
5).bootstrap()`), reproducing the shipped FlatGraph's numbers **exactly**
(C23 behaviour-preserving: 2024-09 → 45.0; 2024 → +203.4 / 62 sessions).
3. **World-family demos on real GER40** (new runnable examples):
- `sweep` over the `{entry_bar, exit_bar}` grid, ranked by `total_pips`.
- `walk_forward` (IS/OOS roll) — non-degenerate, optimizing entry/exit per
window.
- `compare` across instruments (GER40 vs FRA40) **and** across the bar-period
structural axis (a 15m blueprint vs a 30m blueprint).
4. **Gated test** (mirrors `real_bars.rs` skip-guard): asserts
(a) C23 — Composite-bootstrap == FlatGraph-bootstrap on the same window,
bit-identical; (b) `param_space()` is exactly `{entry_bar.target,
exit_bar.target}` (no `delay.lag`, no period); (c) determinism.
5. A short **ledger note**: the canonical shippable strategy form is a
`Composite` blueprint; `FlatGraph` is its compiled substrate.
## 5. Acceptance criteria
- The breakout ships as a `Composite`; `sweep` / `walk_forward` / compare all run
on it with **no re-authoring** (the #94 friction is gone).
- `param_space()` is exactly `{ entry_bar.target, exit_bar.target }` (#96).
- The period **cannot** be swept into a desync (it is not in the space) (#96).
- `walk_forward` optimizes a non-empty space per window (#97).
- C23: the blueprint reproduces the shipped FlatGraph numbers exactly.
- `cargo test --workspace` green; `cargo clippy --workspace --all-targets -D
warnings` clean. No change under `crates/*/src` except the optional ledger
note (authoring-side milestone).
## 6. Issue resolution
| Issue | Resolution |
|---|---|
| #94 form (FlatGraph not World-consumable) | ship the Composite blueprint (§3, §4.1) |
| #96 period desync + `delay.lag` leak | bar-period structural-bound, `delay.lag` bound out (§2) |
| #97 walk_forward empty-space | non-degenerate via the `{entry,exit}` space (§2, §4.3) |
| #3 param fan-out | **not needed** — stays filed (non-goal §1) |
| #95 residency | separate debug thread (non-goal §1) |
| #22 pip / #80 ns→ms | adjacent, not in scope (non-goal §1) |