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

8.8 KiB
Raw Blame History

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 droppedLatch 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; held1.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. GtGt.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).