| name | quaq-extend |
| description | Use this skill when the user wants to add custom indicators, create new node types, write custom formulas, extend the quaq engine with new blocks, or add custom logic that doesn't exist in the built-in node catalog. Triggers include: "add custom indicator", "create new node", "custom formula", "extend engine", "add block", "custom logic", "new indicator", "write a block", or any request to add functionality not covered by existing 150+ built-in nodes.
|
Extending Quaq
Decision Tree: Formula vs. Zig Block
Before writing any Zig code, determine if the user's need can be solved with existing formula nodes.
Use logic.formula when:
- Arithmetic on existing signals (composite scores, normalization, weighted combinations)
- Simple transformations that don't need state across bars
- Any expression using
+, -, *, /, parentheses, and float literals
- Variables
a through d map to input ports
Use logic.logical_formula when:
- Custom boolean conditions combining multiple signals
- Complex entry/exit logic with
AND, OR, NOT
- Variables
a through d map to boolean input ports (f64 > 0.5 = true)
- Literals:
TRUE, FALSE
Use logic.constant when:
- Fixed threshold values (e.g., RSI overbought level = 70)
- Configuration constants that might be parameter-swept
Write a Zig block when:
- Needs state across bars (ring buffer, running averages, counters)
- Needs special initialization (allocator for dynamic memory)
- Fundamentally new mathematical computation not expressible as arithmetic
- Performance-critical hot path (formula has stack-VM overhead)
Formula Block Quick Reference
logic.formula -- Arithmetic expressions
Syntax: +, -, *, /, unary -, parentheses, float literals, variables a-d
Schema: 4 optional f64 inputs (a, b, c, d), 1 f64 output (y), required string param expr
NaN handling: IEEE 754 propagation -- only variables actually used in the expression can introduce NaN.
[[nodes]]
id = "composite"
kind = "logic.formula"
[nodes.params]
expr = "a * 0.6 + b * 0.4"
logic.logical_formula -- Boolean expressions
Syntax: AND, OR, NOT, parentheses, variables a-d, TRUE, FALSE
Schema: 4 optional bool inputs (a, b, c, d), 1 bool output (y), required string param expr
NaN handling: NaN inputs treated as false.
[[nodes]]
id = "entry_logic"
kind = "logic.logical_formula"
[nodes.params]
expr = "a AND (b OR NOT c)"
logic.constant -- Fixed value
Schema: no inputs, 1 f64 output (y), required float param value
[[nodes]]
id = "threshold"
kind = "logic.constant"
[nodes.params]
value = 70.0
Wiring Pattern: Indicators -> Formula -> Comparison -> Trade
[[nodes]]
id = "rsi1"
kind = "ind.rsi"
[nodes.params]
period = 14
[[nodes]]
id = "ema1"
kind = "ind.ema"
[nodes.params]
period = 20
[[nodes]]
id = "score"
kind = "logic.formula"
[nodes.params]
expr = "(a - 50) / 50 * 0.6 + b * 0.4"
[[nodes]]
id = "trigger"
kind = "op.gt"
[nodes.params]
b_const = 0.3
[[edges]]
from = "bars.close"
to = "rsi1.x"
[[edges]]
from = "bars.close"
to = "ema1.x"
[[edges]]
from = "rsi1.y"
to = "score.a"
[[edges]]
from = "ema1.y"
to = "score.b"
[[edges]]
from = "score.y"
to = "trigger.a"
[[edges]]
from = "trigger.y"
to = "trader.enter_long"
Common Formula Patterns
| Pattern | Expression | Use case |
|---|
| Normalized RSI | "(a - 50) / 50" | RSI centered around 0 |
| Weighted composite | "a * 0.4 + b * 0.3 + c * 0.3" | Multi-signal score |
| Spread | "a - b" | Pairs trading basis |
| Pct change proxy | "(a - b) / b" | Current vs reference |
| Inverse | "1 / a" | Reciprocal scaling |
| Offset | "a + 100" | Shift to positive range |
| Complex condition | "a AND (b OR NOT c)" | Multi-signal entry gate |
| Triple confirm | "a AND b AND c" | All three must be true |
Writing a Custom Zig Block
For blocks that need state across bars (ring buffers, running averages, EMA accumulators), you need to write a Zig block and integrate it into the engine.
See references/zig-block-guide.md for the complete step-by-step guide with real code patterns.
Files to modify (7 total):
core/src/blocks/ind_myblock.zig -- New block file with State struct, init(), step(), deinit(), inline tests
core/src/blocks/registry.zig -- Add BlockKind enum variant + fromString + toString + isStateful + needsAllocator + inputCount
core/src/schema.zig -- Add BlockSpec with PortSpec inputs/outputs and ParamSpec params, add to getBlockSpec()
core/src/compile.zig -- Add param extraction in compileStrategy() if block has custom params
core/src/runtime.zig -- Add NodeState union variant, initNodeState() case, stepNode() dispatch case
core/src/root.zig -- Add pub const ind_myblock = @import("blocks/ind_myblock.zig");
- Tests -- Inline tests in the block file + optional integration test in
tests/integration_tests.zig
Quick reference -- block categories by init pattern:
| Category | isStateful | needsAllocator | Init pattern | Examples |
|---|
| Stateless op | false | false | op_stateless = {} | op.add, op.gt, data.close |
| EMA-style | true | false | State.init(period) | ind.ema, ind.rsi, ind.trix |
| Ring buffer | true | true | State.init(allocator, period) | ind.sma, ind.wma, ind.roc, ind.bb_* |
| Cumulative | true | false | State.init() | ind.obv, ind.vwap, ind.adl |
| Multi-param | true | false | State.init(p1, p2, p3) | ind.macd_signal, ind.psar |
| Compiled expr | true | true | compile(allocator, expr) | logic.formula, logic.logical_formula |