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 levels

highest/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.

  1. long_when(condition) — open/hold a long.
  2. short_when(condition) — open/hold a short.
  3. exit_when(condition) — close the open position.
  4. 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

  1. stlab.position.size(percent) — capital per trade (1–100, default 10).
  2. stlab.position.stop_loss(percent) — exit at this loss (default 5).
  3. stlab.position.take_profit(percent) — exit at this gain (default 10).
  4. 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 bar

Trading 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.

  1. stlab.cost.commission(bps) — per-trade fee.
  2. stlab.cost.slippage(bps) — execution slippage.
  3. 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

  1. 30-second execution limit; numpy only (no pandas, no network, no file access).
  2. Ichimoku is not in the Python DSL yet (Wizard/AI builder only).
  3. stlab.config (walk-forward split) is not implemented — use the Validation Lab on a saved backtest instead.
  4. Per-bar dynamic position sizing isn't supported yet; size() takes a fixed percent.
  5. 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.