Skip to content

Alpha Signal input paths — two shapes, one timing rule


status: ACTIVE tier: 2 (design reference) last-verified: 2026-11-06 verified-against: master @ b7af9e70 (Alpha Signal v3.14.3, Python backtest.py v1.7.1) owner-initiative: Projects/Levels-Gated PA Evolution


Purpose. The Alpha Signal core receives market/level facts through two different transports, not one. This page records the two shapes, why they stay two (the owner ruled out merging them — "one input layer" deliberation, 2026-11-05), and the single timing rule that governs both. Citations are file:line at the verified commit.

The two input shapes

Shape Transport What it carries Where
Per-candle record (fixed-size scalars, pushed) Built once per closed bar, then handed to the core on OnClosedBar VWAP, value area (VAH/VAL/POC), Demand Index — the per-bar scalar series Alpha Signal.cs:5134-5136, BuildAlphaBarRecord :2643-2660; AlphaSignalCore.cs:690
On-demand level lists (variable-length sets, pulled) Read from TJ Levels through the host seam, at anchor bar a = bar − rm (a closed, past bar) Naked levels, UA levels, swing zones, bands Alpha Signal.cs:2990-2991; AlphaSignalCore.cs:1365-1398 (via IAlphaSignalHost)

The split is dictated by the data's shape, not by accident:

  • Scalars are pushed. They are a fixed, small per-bar struct; pushing them avoids a lookup per field and keeps the record the goldens feed.
  • Level lists are pulled. They are variable-length and already stored per-bar inside TJ Levels (flat arrays + per-bar offset/count — Levels.cs:6292-6331 naked, :6356-6399 UA, :6424-6431 bands). Pushing them would copy that history into the core — the duplication the owner ruled out. TJLevelSet is a readonly struct, so the extra GetLevelsForBar read costs a copy, not an allocation (Levels.cs:76).
  • Zones are a filter, not a series. CopyActive*Zones(dest, beforeBar) filters the live work-set under lock (Levels.cs:3283-3316); it is not per-bar history. The parent bounds it by driving WalkZoneStep up to bar immediately before detection (Alpha Signal.cs:5121-5128). The result mixes an origin cutoff at the anchor with live IsActive state through N−1 — correct only because Python mirrors it exactly.

The same facts reach the core by both paths (VWAP/VA via the record for the legacy filter at AlphaSignalCore.cs:1010-1050, and again via GetLevelsForBar for the gate at :1365-1398). They agree because both read the same per-bar series — but at different bars: the record is read at the signal bar N, the gate at the anchor N−rm.

The timing rule (one owner)

Values are final at the candle close. The rule, and the mechanism that enforces it:

  • ATAS re-drives each bar at its close. When bar N+1 forms, ProcessBar(N) is re-driven with N < CurrentBar−1, rewriting series[N] to through-N before Alpha reads it. The live and Ticks-replay probes both observed this rewrite, so the value Alpha snapshots for a closed bar is the final (through-N) value.
  • History walk is trivially final. On a chart load/recalc, the whole chart is pre-written in order (RebuildSessionNow, Levels.cs:4046-4089; each ProcessBar(b) writes only series[b]) before the parent reads.
  • Load-end candle (the one real bug, fixed in v3.14.3). The old load-end branch advanced _finalizedBar to the still-forming last loaded bar and blocked its true-close re-record, leaving that bar's VA/POC snapshot stale by one bar. v3.14.3 replaces it with one pure decision (DecideTransitionFinalization) that returns −1 for the load-end transition, so the last loaded bar is recorded at its real close like every other live bar.
  • One live-field trap. RthVwapStdDev is a live-only scalar, not a per-bar series (Levels.cs:3252 sets it from the live _rthVwapStdDev). It is marked do-not-use in Levels.cs; consumers that need a per-bar σ must derive it as RthVwapDev1Upper − RthVwap (AlphaSignalCore.cs:1388-1390).

Deliberation outcome

The owner approved the "one input layer" brief (2026-11-06) with the recommendation do not refactor now. The two shapes stay; the single timing owner is the rule above. The refactor ("one doorway") is on the waiting list, to be revisited after the demo. See Projects/Levels-Gated PA Evolution/deliberation/2026-11-05 One input layer/.