Why Lookback Buffers?
Most technical indicators need historical data. An SMA(20) needs 20 ticks of history. An RSI(14) needs 15 ticks. In vectorized frameworks, this means slicing into a large dataframe — which introduces lookahead risk and wastes memory. ClyptQ takes a different approach: each operator input gets a pre-allocated circular buffer of exactly the size it needs. No more, no less.RollingBuffer
RollingBuffer is a fixed-size circular buffer that stores TaggedArray history:
Pre-allocated Arrays
On creation, 4 numpy arrays are allocated:
No dynamic allocation during execution. The memory footprint is fixed at
lookback × n_symbols × (8 + 3) bytes (float64 + 3 bools).
Circular Write
New data is written atwrite_idx % lookback, then write_idx increments:
append()
Every tick, the graph callsbuffer.append(tick):
Reading Data
get_last(): Most Recent Tick
Returns the single most recent tick as a 1D TaggedArray:lookback=1 — the operator only needs the current value.
to_array(): Full Buffer as 2D
Returns the entire buffer as a 2D TaggedArray, ordered oldest-to-newest:to_array() reorders using np.concatenate:
Per-Consumer Buffers
Every (consumer, source) pair gets its own RollingBuffer. This is crucial:_distribute_source_inputs), but each maintains its own circular buffer with its own lookback size. SMA sees 20 ticks, RSI sees 15.
Warmup: When Buffers Are Ready
A buffer needslookback ticks before it’s full. The graph tracks this automatically:
Warmup Calculation Algorithm
The graph traces backwards from every node to its FIELD sources, accumulating lookback requirements:"1m" but an operator uses "1h" data with lookback=5, that’s 5 × 60 = 300 clock ticks.
During warmup, operators may return valid=False — not enough data yet. The graph continues ticking, filling buffers, until all paths have enough data.
Dynamic Axis Expansion
When new symbols are added at runtime, buffers expand:pad_count columns:
value: padded withNaNexists,valid,updated: padded withFalse
Lazy Resize
On the firstappend() call, the buffer checks if tick.value.shape matches n_symbols. If not, it resizes:
Memory Efficiency
RollingBuffer is designed for minimal memory use:Buffer Lifecycle
Gap Handling: skip / ffill / nan
Different asset classes have different trading schedules. Crypto trades 24/7, US stocks trade weekdays 9:30-16:00 ET, and macro data releases monthly. When these data sources are combined in a single graph, gaps appear — timestamps where some sources have data and others don’t. TheInput.gaps parameter controls how each buffer handles missing rows:
Headroom for skip Mode
Whengaps="skip", the buffer allocates extra capacity so that after filtering out non-existing rows, at least lookback valid rows remain:
lookback rows.
Trading Calendars
The calendar system marks which timestamps are valid trading periods. It generates anexists mask that the data provider applies before the data enters the graph.
Calendars are set at the observation spec level, not per operator. The graph combines multiple calendars when a strategy uses cross-asset data.
Cross-Asset & Cross-Timezone Strategies
ClyptQ’s tagged tensor architecture natively supports strategies that combine data from different asset classes, exchanges, and timezones:- Crypto (UTC, 24/7) →
AlwaysOnCalendar,gaps="nan" - US Stocks (ET, weekdays) →
USEquityCalendar,gaps="skip" - Macro (UTC, sparse) →
MonthlyCalendar,gaps="ffill"
gaps parameter. The exists mask ensures operators know which data points are real observations vs. filled values.
The calendar system and
gaps="skip" mode are fully implemented and production-ready, but the cross-asset use case (crypto + stocks) is not yet exercised in production because US stock data collection has not been deployed. Once stock data is onboarded, cross-asset strategies will work without any engine changes — only the observation specs and gaps parameters need to be configured.Relationship to Other Concepts
- TaggedArray: RollingBuffer stores and delivers TaggedArrays
- StatefulGraph: The graph creates and manages all RollingBuffers
- FIELD & STATE: FIELD and STATE data are distributed to consumer buffers via the same mechanism
- Operator Protocol: Operators receive buffered data from their Input specifications

