Files
Aura/docs/specs/0050-session-breakout-node-vocabulary.md
T
Brummel 16adb2b949 docs(specs): 0050 — session-breakout node vocabulary (milestone spec)
Milestone 'Strategy node vocabulary I — temporal, logic, resample & session':
the 7 aura-std nodes (EqConst, Gt, And, Delay, Latch, Resample, Session) plus a
synthetic e2e fixture, driven by the GER40 15m session-breakout. Records the
design pass's cross-cutting decisions: close-instant timestamp convention,
Latch emits f64 (Exposure dropped from the DAG), the EqConst i64->bool gate,
the Resample-only Barrier(0) firing regime, and the build order. refs #84-#91.
2026-06-17 16:41:31 +02:00

145 lines
8.8 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.
# 0050 — Session-breakout node vocabulary (aura-std)
Status: draft (milestone spec)
Driver: the GER40 / DAX 15-minute session-breakout strategy (below) as a
forcing function for the stateful / temporal / logic / resampling vocabulary
aura-std is still missing.
## 1. Goal & non-goal
aura today ships eight nodes (`Sma, Ema, Add, Sub, LinComb, Exposure,
SimBroker, Recorder`). That set composes cross / linear-combination signals but
has **no** previous-bar lag (C5 register), **no** comparison producing `bool`,
**no** boolean logic, **no** resampler (C2 complete-bar), and **no** session /
calendar awareness. The GER40 session breakout needs all of them, so it is the
ideal driver to grow that vocabulary.
This milestone is **engine-side, Phase 1**: the reusable nodes in `aura-std`
plus one synthetic end-to-end fixture that pins the whole strategy on
hand-built bars. The **real** GER40 strategy — wired against real GER40 M1 data
with the correct pip value — is **Phase 2**, a *separate consumer crate* in its
own repo (C9), out of this milestone's scope (see §7).
## 2. The strategy (the driver)
- Resample raw M1 OHLC → 15-minute OHLC bars. Frankfurt session opens 09:00
Europe/Berlin (DST-aware).
- Bars indexed from the open: bar1 = 09:0009:15, bar2 = 09:1509:30,
bar3 = 09:3009:45, bar4, bar5 …
- **Entry (long):** when bar3 *closes* above the *high of bar2* (the previous
15m bar): `close(bar3) > high(bar2)`.
- **Exit:** flat when bar5 *closes*.
- Exposure is therefore `+1` held from bar3-close through bar5-close, then `0`.
Long-only (`0` or `+1`).
## 3. Dataflow (the DAG)
```
M1 OHLC: open/high/low/close as 4 separate f64 sources [aura-ingest M1FieldSource]
└─ Resample[15m] ⇒ record{open15,high15,low15,close15} [NEW, C2 emit-on-rollover]
close15 ─────────────────────────────────────────┐ ┐ ┐
high15 ─→ Delay[1] ─→ prevHigh15 ─┐ │ │ │
▼ │ │ │
Gt(close15 > prevHigh15) ⇒ breakout:bool [NEW]
close15 ─→ Session[FRA 09:00,15m] ⇒ bars_since_open:i64 [NEW]
├─ EqConst(·,3) ⇒ isBar3:bool [NEW]
└─ EqConst(·,5) ⇒ isBar5Close:bool [NEW]
And(breakout, isBar3) ⇒ entry:bool [NEW]
Latch(set=entry, reset=isBar5Close) ⇒ held:f64 (0.0/1.0) [NEW]
held ─→ SimBroker(exposure=held, price=close15) ⇒ equity(pips) ─→ Recorder [exists]
```
Note vs. the first sketch: **`Exposure` is dropped** — `Latch` emits `f64`
directly, so an on/off latch needs no clamp/scale. **`EqConst` is added** — it is
the `i64 → bool` gate that turns `Session`'s `bars_since_open` into the per-bar
booleans; without it the `i64 → bool` seam is unbridged.
## 4. Cross-cutting decisions (settled by the design pass)
1. **Timestamp = close-instant, everywhere.** A resampled bar carries only its
OHLC `f64` *values* (no timestamp on the hot path, C4); its actionable cycle
timestamp is the close-instant the engine stamps on the forwarded edge
(`harness.rs:427`). `Resample` emits bucket *B* exactly when `ctx.now()`
first lands in bucket *B+1*, so the emission cycle's ts **is** the close
instant of *B* — C2-correct (a bar is actionable only once complete).
`SimBroker` marks the held exposure against `close15` at that instant;
`Session` indexes off the *same* close-instant. **Off-by-one:** the prose
"bar3" is 1-indexed by ordinal; under close-count indexing,
`bars_since_open == 3` is the close of the 09:3009:45 bar. Pin this once in
`Session`'s RED test and reuse the constant in the `EqConst` bindings.
2. **`bool → f64` via `Latch` output, no cast node.** `SimBroker` slot 0
(`exposure`) is `f64` and reads a cold leg as `0.0`; a held position *is*
exposure `+1`, flat *is* `0.0`. Every other `bool` in the DAG (`breakout`,
`isBar3`, `isBar5Close`, `entry`) stays `bool` on the hot path (a first-class
streamed scalar). The only `bool → f64` transition lives inside `Latch.eval`.
3. **Firing.** The *only* `Barrier(0)` group is `Resample`'s 4 OHLC inputs (four
separate M1 sources at the same M1 timestamp — the `Ohlcv` fixture
precedent). Everything downstream is `Firing::Any` and co-fresh because it all
hangs off the single `Resample` emission cycle. `Latch.set`/`reset` are `Any`
(disjoint pulses on different cycles). This holds **provided** `Session` is
triggered off a `Resample` field (see 4).
4. **`Session` trigger is `f64` Any, wired from `close15`** (the trigger *value*
is unused — `Session` reads only `ctx.now()` + config). This makes `Session`
fire on exactly the bar-close cycle (the once-per-bar contract) without a new
`Resample` field. Pin in `Session`'s RED test that the trigger value is
ignored.
5. **Dependency:** `Session` needs DST-correct wall-clock math →
`chrono` / `chrono-tz` enters `aura-std` (a vetted standard crate; do not
hand-roll timezone/DST). `chrono` is already transitive via aura-ingest's
data-server.
## 5. Node contracts
| Node | in | out | params | state / notes |
|---|---|---|---|---|
| `EqConst` | `value:i64 Any` | `bool` | `target:i64` | stateless; `out = (value == target)`. Operator is topology (separate type), only `target` is a knob (C11/C12). The `i64→bool` analogue of `Gt`. |
| `Gt` | `a:f64 Any, b:f64 Any` | `bool` | — | stateless; `out = (a > b)`. First bool-emitting node. Relational family (`Lt/Ge/…`) are separate types, not a swept op param. |
| `And` | `a:bool Any, b:bool Any` | `bool` | — | stateless; `out = a && b`. Logic family (`Or/Not`) are separate types. |
| `Delay` | `series:f64 Any` | `value:f64` | `lag:i64` | C5 register; node-owned ring of length `lag`; warm-up **skip-emit** (`None` until `lag+1` samples). Output depends only on pre-cycle state (the half that lets a future engine close a feedback loop). |
| `Latch` | `set:bool Any, reset:bool Any` | `held:f64` | — | C5 SR register; `held``1.0` on `set`, → `0.0` on `reset`; initial `0.0`; define set&reset-same-cycle priority (cannot coincide here, but specify). Feeds `SimBroker.slot0` directly. |
| `Resample` | `open,high,low,close: 4×f64 Barrier(0)` | record `open15,high15,low15,close15: f64` | `period_minutes:i64` | accumulator (`open=first, high=max, low=min, close=last`); emit-on-rollover via `ctx.now()` bucket (C2); **drops** the partial last bar. |
| `Session` | `trigger:f64 Any` (value ignored) | `bars_since_open:i64` | `open_time, tz, period_minutes` (+ indices via `EqConst` downstream) | derives Frankfurt local time from `ctx.now()` (chrono-tz, DST); close-count indexing; emits `0`/none outside the session. |
## 6. Build order & deliverable
Dependency-ordered (tiny independent primitives first), each a RED-first TDD
unit per the skills pipeline:
1. `EqConst` — unblocks the `Session` gate seam (biggest gap).
2. `Gt``Gt.rhs` depends on `Delay`; build the comparator early.
3. `And`.
4. `Delay`.
5. `Latch`.
6. `Resample` (heaviest internal accumulator).
7. `Session` (heaviest external surface — admits `chrono-tz`).
8. **Synthetic end-to-end fixture** — the Phase-1 C9 deliverable.
### E2E fixture (deliverable 8)
`crates/aura-engine` test `ger40_session_breakout_holds_exposure_bar3_to_bar5`,
a hand-wired `FlatGraph` (no composite/strategy-builder API is in scope — that
is a later milestone). One Frankfurt session, 4 synthetic OHLC `VecSource`
streams declared in fixed source order (k-way merge tie-breaks by source index).
Asserts:
- equity flat `0.0` through bar1..bar3-close, then integrates `+1·Δprice` per
price cycle bar3-close → bar5-close, then flat after exit;
- a second `Recorder` on `Latch.held`: `0.0` before bar3-close, `1.0` through
the hold, `0.0` after;
- a no-entry control session (`bar3 close == bar2 high`, strict-`>` false) stays
flat throughout — pins the strict-greater semantic;
- run twice → byte-identical (C1 determinism).
**Two traps the fixture must avoid:** (a) include an M1 tick in *bar6*'s window
so bar5 actually closes (the partial last bar is dropped) — else exposure leaks
to EOF; (b) supply ≥2 bars before the entry so `Delay` is warm.
## 7. Phase 2 (out of scope here)
The real GER40 strategy lives in a *separate consumer crate* (its own repo),
depending on `aura-engine` + `aura-std` cargo-natively (C9). It is not built via
`aura new` / cdylib hosting (a later milestone) — just a hand-wired Rust
bin/test running `bootstrap → run` on real GER40 M1 bars. Related existing
issues it will need: #22 (SimBroker pip value is per-asset — GER40 is an index,
`pip_size = 1.0`, not FX `0.0001`), #80 / #81 (real-data ingestion seams).