10.Custom indicators — AST you write once, reuse everywhere
Author an indicator in the small DSL, save it to custom_indicators, evaluate it per symbol via POST /custom-indicators/eval/{sym}, and reference it by name from chart overlays, scanner filters, alert rules, and backtest strategies. Compiles to the same stryke bytecode the backtest engine runs.
The DSL — small on purpose
Custom indicators are pure functions over a bar series. The DSL has:
- Bar accessors —
open,high,low,close,volume,time. - Built-in TA —
sma(x, n),ema(x, n),rsi(x, n),atr(n),stdev(x, n),bb_upper(x, n, k),highest(x, n),lowest(x, n), plus the ~30 standard indicators. - Arithmetic, comparison, boolean operators.
- Time / calendar accessors —
hour,minute,day_of_week,is_market_open,is_first_15_min. - References to other custom indicators — by name. The compiler resolves the DAG and detects cycles.
- Plot hints —
@plot(line, color = "#05d9e8"),@plot(histogram),@plot(none). Controls how the chart overlay renders the result.
// vwap_distance_z — z-score of price distance from VWAP @plot(line, color = "#ff2a6d", subpane = true) let vw = vwap(session) let dist = close - vw let z = (dist - sma(dist, 200)) / stdev(dist, 200) return z
Indicators are stateless w.r.t. each other — every call sees the same input bar series. Look-back is implicit through the windowed TA functions; no manual indexing into "the bar N ago".
Authoring + saving
The Custom Indicators tile is a list + editor. Each indicator has:
- Name — unique per user. Used to reference the indicator from other places.
- Body — the DSL source.
- Default parameters — if the body references undeclared identifiers (e.g.
fast_len,slow_len), they're surfaced as parameters with defaults. - Tags — for organization.
The editor has live syntax highlighting + inline errors. POST /custom-indicators/eval/{sym}?from=&to=&tf= evaluates the indicator against the symbol over the date range and renders a preview chart in the editor before you save.
Where you can reference custom indicators
- Charts overlays. Add
my_indicatorfrom the overlay menu; it renders per its@plothint (line / histogram / none). - Screener filters. The screener (step 08) accepts custom-indicator expressions in its filter row. "
my_volume_spike > 3.0". - Alert rules. Alerts (step 11) take a boolean expression over indicators; custom indicators show up by name.
- Strategy alerts. AND/OR/NOT boolean composition over base alerts (step 11). Each base alert can reference a custom indicator.
- Backtest strategies. The entry / exit / sizing expressions in a backtest preset (step 09) can call any of your custom indicators by name.
The compiler resolves the call graph and rebuilds the bytecode whenever any indicator in the graph changes — backtests / alerts / overlays automatically pick up the new logic.
Evaluation modes
The same bytecode runs in three modes:
- Live — per incoming tick or per just-closed bar; result lands in the WebSocket alert / scanner pipelines.
- Historical — over a date range; renders chart overlays + backtest signals.
- One-shot — single evaluation at a specific bar; used by the screener and the per-symbol research dossier.
The compiler emits the same bytecode in all three modes. The only difference is how bars are streamed in; the indicator code itself is identical.
Performance & cost
A typical indicator costs sub-microsecond per bar after JIT warm-up. The screener evaluates indicators across a 3 000-symbol universe in the time it takes to fetch the bars. Backtest walk-forward sweeps that evaluate the same indicator 1 000 times against 5 years of intraday bars finish in seconds, not minutes.
The expensive piece is the bar fetch, not the indicator math. The bars cache (per-symbol, per-timeframe) is sized aggressively for exactly this reason.
sma(x, 500) over 1m bars = a lot of evaluation) or (b) a too-broad universe. Profile by halving each axis once.