Python DSL (stlab) reference
The stlab library you write in the Code editor — indicators, signals, position, costs, and multi-timeframe.
The Code editor runs Python strategies with the stlab library on a Cloudflare Worker (Pyodide, numpy). Each backtest gets a clean namespace and a 30-second time limit. This is the same library the AI Strategy tab emits, so anything generated there is editable here.
Strategy structure
A valid strategy imports stlab, declares at least one entry signal (long_when or short_when), and at least one exit (exit_when or close_all_when). Position, costs, and multi-timeframe are optional.
import stlab
# Indicators return numpy arrays aligned to the chart's candles.
rsi = stlab.indicators.rsi(stlab.candles.close, period=14)
# Entry / exit rules — plain boolean expressions.
stlab.signal.long_when(rsi < 30)
stlab.signal.exit_when(rsi > 70)
# Risk (optional — these are the defaults).
stlab.position.size(percent=10)
stlab.position.stop_loss(percent=5)
stlab.position.take_profit(percent=10)Candles
stlab.candles holds the chart's OHLCV as numpy arrays: .open, .high, .low, .close, .volume (floats) and .time (ISO-8601 strings). Every array — and every indicator result — has a .shift(n) method that returns the value n bars ago (earlier bars become NaN; look-ahead is not allowed).
# Golden-cross style crossover with .shift()
fast = stlab.indicators.ema(stlab.candles.close, period=12)
slow = stlab.indicators.ema(stlab.candles.close, period=26)
crossed_up = (fast.shift(1) < slow.shift(1)) & (fast >= slow)
stlab.signal.long_when(crossed_up)Indicators
Available under stlab.indicators. Functions returning several lines (macd, bb, stoch, adx) return a dict of numpy arrays; the rest return a single array. Warm-up bars at the start are NaN, which fold to False in signals.
sma(data, period=20) -> array
ema(data, period=20) -> array
rsi(close, period=14) -> array # 0-100
macd(close, fast=12, slow=26, signal=9) -> {macd, signal, histogram}
bb(close, period=20, std_dev=2.0) -> {upper, middle, lower}
atr(high, low, close, period=14) -> array
stoch(high, low, close,
k_period=14, d_period=3, smooth_k=3) -> {k, d}
adx(high, low, close, period=14) -> {adx, plus_di, minus_di}
obv(close, volume) -> array
vwap(high, low, close, volume) -> array # session-anchored
volume_sma(volume, period=20) -> array
willr(high, low, close, period=14) -> array # Williams %R, -100..0
mfi(high, low, close, volume, period=14) -> array # Money Flow Index, 0..100 (volume-weighted RSI)
highest(data, period=20) -> array # rolling max
lowest(data, period=20) -> array # rolling min
donchian(high, low, period=20) -> {upper, middle, lower} # prior-N-bar channel
fib(high, low, period=30) -> {f236, f382, f500, f618, f786} # same window, sliced at the ratios
volume_profile(high, low, close, volume, period=30, bins=24, value_area=70.0) -> {poc, vah, val} # where the window's volume traded
heikin_ashi(open, high, low, close) -> {ha_open, ha_close} # smoothed candle body (NOT fillable prices)
atrp(high, low, close, period=14) -> array # ATR as % of price
supertrend(high, low, close, period=10, multiplier=3.0) -> {supertrend, trend} # ATR trailing stop
vwap_bands(high, low, close, volume, multiplier=2.0) -> {upper, middle, lower} # VWAP +/- volume-weighted sigma
cci(high, low, close, period=20) -> array # unbounded; +/-100 are conventional lines
roc(close, period=12) -> array # percent change from N bars ago
stoch_rsi(close, rsi_period=14, stoch_period=14, smooth_k=3, smooth_d=3) -> {k, d} # Stochastic of RSI, 0..100
aroon(high, low, period=25) -> {up, down} # 0..100, how recent each extreme is
keltner(high, low, close, period=20, atr_period=10, multiplier=2.0) -> {upper, middle, lower} # EMA with ATR rails
psar(high, low, close, step=0.02, max_step=0.2) -> {psar, trend} # accelerating trailing stop
wma(close, period=20) -> array # linear weights, newest bar heaviest
hma(close, period=16) -> array # Hull MA, least laggy of the four averages
cmf(high, low, close, volume, period=20) -> array # Chaikin Money Flow, -1..1
trix(close, period=15, signal=9) -> {trix, signal} # percent change of a triple-smoothed EMA
uo(high, low, close, short=7, medium=14, long=28) -> array # Ultimate Oscillator, 0..100
ichimoku(high, low, close, conversion=9, base=26, span_b=52, displacement=26) -> {tenkan, kijun, senkou_a, senkou_b} # no chikou: it reads the future
pivot(high, low, close, anchor="session") -> {p, r1, r2, r3, s1, s2, s3} # previous session/week/month levelshighest/lowest are the building block for "% off the recent high" drawdown entries — e.g. close <= highest(close, 180) * 0.80 buys 20% below the 180-bar high. There is no pandas: .rolling() does not exist.
Signals
Declare when to enter and exit. Each takes a boolean array (or a single bool). Calling the same function more than once AND-combines the conditions. Exits take priority over entries on the same bar.
- long_when(condition) — open/hold a long.
- short_when(condition) — open/hold a short.
- exit_when(condition) — close the open position.
- close_all_when(condition) — force flat: closes any open position and blocks new entries on that bar. Never reverses, even in reversal mode.
Combine conditions with & (and), | (or), and ~ (not) on arrays — wrap each comparison in parentheses, e.g. (rsi < 30) & (close > ema200).
Position & risk
- stlab.position.size(percent) — capital per trade (1–100, default 10).
- stlab.position.stop_loss(percent) — exit at this loss (default 5).
- stlab.position.take_profit(percent) — exit at this gain (default 10).
- stlab.position.max_holding_bars(n) — hard time cap: a position opened at bar i closes at bar i+n at the latest (exit reason "max_holding"). Default: unlimited.
Direction & reversal
By default a strategy trades one side (long_when or short_when). To trade both from a single position, enable reversal with stlab.position.reversal(on_opposite=...):
stlab.position.reversal(on_opposite="close") # default: close to flat, wait
stlab.position.reversal(on_opposite="reverse") # stop-and-reverse: flip on the same barTrading costs
Optional, in basis points (1 bps = 0.01%), default 0. Modeling realistic costs is the honest thing to do — a Reality-check flag warns when they're absent.
- stlab.cost.commission(bps) — per-trade fee.
- stlab.cost.slippage(bps) — execution slippage.
- stlab.cost.spread(bps) — bid-ask spread; the engine applies half of it to each side of a round trip.
Multi-timeframe
Reference other timeframes with stlab.candles_1m / _5m / _15m / _30m / _1h / _4h / _1d / _1w. Indicators compute on the source timeframe's own history and are auto-aligned back to the chart's bars (no manual resampling), with no look-ahead. A common pattern is a higher-timeframe trend filter on lower-timeframe entries.
import stlab
# Daily trend filter + 4h RSI entry (run the chart on 4h)
ema200_d = stlab.indicators.ema(stlab.candles_1d.close, period=200)
uptrend = stlab.candles_1d.close > ema200_d
rsi = stlab.indicators.rsi(stlab.candles.close, period=14)
stlab.signal.long_when(uptrend & (rsi < 30))
stlab.signal.exit_when(rsi > 70)Limits
- 30-second execution limit; numpy only (no pandas, no network, no file access).
- Ichimoku is not in the Python DSL yet (Wizard/AI builder only).
- stlab.config (walk-forward split) is not implemented — use the Validation Lab on a saved backtest instead.
- Per-bar dynamic position sizing isn't supported yet; size() takes a fixed percent.
- Strategies using close_all_when, max_holding_bars, or multi-timeframe candles run on the Python engine — the Validation Lab can't replay them window-by-window yet.