Core Design Principle
ClyptQ’s engine is built on the same principle that underpins every modern computer: the Von Neumann separation of stateless computation from stateful memory.- Operators are the CPU — stateless computation units. They receive inputs, compute, and return outputs. No internal state between ticks. Like an ALU that processes whatever data it receives, operators do not know or care where their inputs originate.
- The Graph is the memory — it manages buffers, routing, axis registration, and execution order via RollingBuffers. All state lives here, not in the operators.
StatefulGraph
StatefulGraph is the DAG (Directed Acyclic Graph) execution engine. It manages:
- Nodes: Operators registered with
add_node() - Edges: Dependencies declared through
Inputspecifications - Buffers: Pre-allocated
RollingBufferper (consumer, source) pair - Axes: Symbol dimensions with
AxisMeta - Execution order: Topological sort computed at construction time
Input: Dependency Declaration
Every operator input is anInput object (frozen dataclass):
Source types:
Timeframe Validation
Input parses timeframes into seconds using regex (\d+)([smhdw]):
Input.validate_against_source() ensures you can’t upsample — input timeframe must be >= source timeframe. You can request "1h" data from a "1m" source (downsample), but not "1m" from a "1h" source.
add_node(): Registration
When you calladd_node(), the graph does several things:
- Validates the operator implements the correct interface
- Extracts input specifications from the operator
- Creates RollingBuffers for each input (sized by
lookback) - Pre-computes consumer maps for FIELD/STATE sources
- Re-runs topological sort to update execution order
Buffer Setup
For eachInput, a dedicated RollingBuffer is created:
sma_20 and rsi_14 consume FIELD:binance:futures:ohlcv:close, they each get separate buffers with their own lookback sizes.
Consumer Map Pre-computation
FIELD and STATE sources are registered in_field_consumers:
on_tick(): Execution Loop
on_tick() is the main entry point called every tick:
Step 1: Distribute Source Inputs
FIELD and STATE data is appended to every consuming buffer:updated=False.
Step 2: Execute in Topological Order
Nodes execute in dependency order — guaranteed by Kahn’s algorithm:- Gather inputs from buffers (
buffer.to_array()for lookback > 1,buffer.get_last()for lookback = 1) - Call
operator.compute()with gathered inputs - Distribute output to all downstream consumers’ buffers
Step 3: Output Distribution
After an operator produces output, it’s appended to buffers of all downstream nodes:Topological Sort
The graph uses Kahn’s algorithm with modifications:- FIELD and STATE sources are excluded from the sort — they’re external data, not computed nodes
- Self-references are excluded (some operators reference their own previous output)
- Only real node-to-node dependencies count for in-degree
add_node() call. If a cycle is detected, a ValueError is raised.
Axis Management
The graph tracks the symbol dimension through axes:Dynamic Axis Expansion
If a new symbol appears at runtime (e.g., new listing), the axis can expand:_pad_all_buffers(pad_count) iterates through every buffer and adds NaN-filled columns. This is safe because:
- New symbols have
exists=Falseinitially - Operators check
existsbefore using values
Warmup Calculation
The graph automatically computes how many ticks are needed before the strategy can produce valid signals:sma_20 uses "1h" data but the system clock is "1m", 20 hours × 60 minutes = 1200 ticks.
STATE inputs do not contribute to warmup calculation. Portfolio state starts from initial conditions — there’s no historical state to warm up from.
Stateless Operators, Stateful Graph
This is the key architectural insight:
This is the Von Neumann principle in action: computation units (operators) are pure functions with no memory, while the storage system (the graph) manages all state. This architectural split — not any individual feature — is what makes code parity across modes possible.
Why This Matters
Reproducibility: Same inputs → same outputs. No hidden state in operators that could drift between backtest and live. Composability: Operators don’t know about each other. They connect through the graph’s buffer system. Parallelism: Independent operators (same topological level) could theoretically execute in parallel — they share no state.Self-Referencing Operators
Some operators need their own previous output as input — for example, EMA uses its previous value. ClyptQ handles self-references as a special case in the graph:- Topological sort excludes self-edges — Kahn’s algorithm ignores the
ema_12 → ema_12edge, so it’s not treated as a cycle - Buffer initialization — The self-reference buffer starts with
valid=Falseon the first tick - Output feedback — After each
compute(), the output is appended to the self-reference buffer for the next tick
Feedback Loop via STATE
The graph supports closed-loop feedback through STATE inputs. The execution engine updates STATE after fills, and STATE flows back into the graph on the next tick:- Reduce leverage during drawdowns
- Increase position size after winning streaks
- Switch strategy regimes based on equity curve
Common Pitfalls
Circular Dependencies
Missing Source
Timeframe Mismatch
Relationship to Other Concepts
- FIELD & STATE: FIELD and STATE sources are distributed to consumer buffers by the graph
- TaggedArray: All data flowing through the graph is TaggedArrays
- Lookback Buffers: The graph creates and manages RollingBuffers for each input
- TradingSpec:
TradingStrategySpec.graphholds the StatefulGraph instance - Operator Protocol: Defines the interface operators must implement

