Skip to main content

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 at write_idx % lookback, then write_idx increments:
The oldest data is automatically overwritten — no shifting, no copying, no reallocation.

append()

Every tick, the graph calls buffer.append(tick):
This is O(n_symbols) per append — just array assignment.

Reading Data

get_last(): Most Recent Tick

Returns the single most recent tick as a 1D TaggedArray:
Used when 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:
Since the buffer is circular, the raw array may be out of order. to_array() reorders using np.concatenate:

Per-Consumer Buffers

Every (consumer, source) pair gets its own RollingBuffer. This is crucial:
This creates two separate buffers:
Both receive the same FIELD data (appended by _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 needs lookback 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:
Timeframe ratio: If the system clock is "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:
This extends all 4 arrays by pad_count columns:
  • value: padded with NaN
  • exists, valid, updated: padded with False
The new symbol starts with no data. As ticks arrive, its data fills in naturally.

Lazy Resize

On the first append() call, the buffer checks if tick.value.shape matches n_symbols. If not, it resizes:
This handles the case where the axis size wasn’t known at buffer creation time.

Memory Efficiency

RollingBuffer is designed for minimal memory use:
Compare to storing the full history in a dataframe:
RollingBuffer uses 0.005% of the memory. It only stores what’s needed.

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. The Input.gaps parameter controls how each buffer handles missing rows:

Headroom for skip Mode

When gaps="skip", the buffer allocates extra capacity so that after filtering out non-existing rows, at least lookback valid rows remain:
This is transparent to operators — they always receive exactly lookback rows.

Trading Calendars

The calendar system marks which timestamps are valid trading periods. It generates an exists 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:
The system handles timezone alignment automatically:
  • Crypto (UTC, 24/7) → AlwaysOnCalendar, gaps="nan"
  • US Stocks (ET, weekdays) → USEquityCalendar, gaps="skip"
  • Macro (UTC, sparse) → MonthlyCalendar, gaps="ffill"
All data aligns to the system clock frequency. Lower-frequency sources (1d stock data on a 1h clock) are forward-filled or skip-filtered based on the 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