Skip to content
Single .md

Sandbox limits

The code runs in a hardened Python sandbox. The engine validates the source before executing, blocks unsafe modules and builtins, and enforces time and memory limits.

This document describes the applied limits and how to avoid them.

Available modules

Injected automatically as globals (no import required):

Name in the scriptTypeCommon use
npnumpynp.mean, np.std, np.array
pdpandaspd.DataFrame, pd.Series, pd.to_datetime
mathmath stdlibmath.sqrt, math.log, math.pi
jsonjson stdlibjson.dumps, json.loads
datetimedatetime moduledatetime.datetime.fromtimestamp
ta / pandas_tasafe optimized versionsubset of ta.ema, ta.rsi, etc.
talibsafe optimized versionlimited subset
Indicatornative indicator math (no import)Indicator.sma/ema/rsi/macd/bollinger over a bounded window
Signalsignal constantsbuilding order signals

There are no extra importable indicator modules: the native indicator math reaches your script through the injected Indicator global (no import) and through ta/pandas_ta. In particular, import tesstrade_indicators is not allowed in the editor — the validator rejects it (see Indicator and pandas_ta).

Example: the script below runs without error, with no import:

python
def on_bar_strategy(sdk, params):
    closes = np.array([c["close"] for c in sdk.candles])
    vol = np.std(closes[-20:])  # standard deviation of the last 20 closes
    ...

About ta / pandas_ta / talib

These are safe, optimized versions that expose a subset of the popular functions. For full control over behavior, implement indicators in pure Python or with np/pd. See implementing SMA/EMA.

About the Indicator global

The native indicator math is exposed to your script as the pre-injected Indicator global — no import. It provides the same Wilder/standard formulas as pandas_ta (Indicator.sma, .ema, .rsi, .macd, .bollinger), each returning a plain list aligned 1:1 with the input. The underlying tesstrade_indicators module is a native (PyO3, chart-side) implementation detail and is not importable from the strategy editor — the validator rejects import tesstrade_indicators. Full reference in Indicator and pandas_ta.

To stay O(1) per bar, call Indicator.* over a bounded sdk.candles[-N:] window (a fixed lookback comfortably larger than the period converges to the full-history value) rather than over the entire growing history. For a bit-exact, truly O(1) update, keep a recursive accumulator in sdk.state.

python
LOOKBACK = 300  # fixed window >> period → O(1) in history length

def on_bar_strategy(sdk, params):
    if len(sdk.candles) < 15:
        return
    rows = sdk.candles[-LOOKBACK:]          # bounded — never the whole history
    rsi = Indicator.rsi(rows, 14)[-1]       # injected global, no import
    if rsi is not None and rsi < 30:
        sdk.buy(action="buy_to_open", qty=1, order_type="market")

Forbidden modules

The import whitelist accepts only the modules above. Any other triggers SecurityError:

python
import os              # SecurityError
import sys             # SecurityError
import subprocess      # SecurityError
import requests        # SecurityError
from scipy import ...  # SecurityError
import random          # SecurityError - not in the whitelist

Why random is blocked

To guarantee determinism — the same input always produces the same output, which is what makes a backtest reproducible. If you need a value that varies per bar, derive it deterministically from the candle timestamp (for example, hash(int(sdk.candles[-1]["time"]))).

Available builtins

Full whitelist:

Types and constants: None, True, False, bool, int, float, str, list, tuple, dict, set, frozenset

Math: abs, min, max, sum, round, pow, divmod

Iteration: len, range, enumerate, zip, reversed, sorted, filter, map

Logic: all, any

Type checking: isinstance, issubclass, type, callable, hasattr, getattr

String: chr, ord, format, repr, ascii

Object: id, hash, iter, next, slice

Exceptions (for try/except): Exception, ValueError, TypeError, KeyError, IndexError, AttributeError, RuntimeError, ZeroDivisionError, OverflowError, StopIteration

Debug: print (the output is captured and sent to the logs)

Blocked builtins

python
open("file.txt")      # SecurityError - I/O
exec("code")          # SecurityError - dynamic execution
eval("expression")    # SecurityError
__import__("os")      # SecurityError
input()               # SecurityError - I/O
dir()                 # SecurityError - introspection
vars()                # SecurityError
globals()             # SecurityError
locals()              # SecurityError
exit(), quit()        # SecurityError - process control
breakpoint()          # SecurityError - debugging
setattr(x, "y", 1)    # SecurityError (removed)
delattr(x, "y")       # SecurityError
memoryview(x)         # SecurityError

Blocked builtins are RESERVED NAMES — even as plain variables

The validator bans the bare identifier of every blocked builtin in any position — assignment and read included — because a stored name could alias the builtin (f = dir; f()). This code never calls dir() and still fails validation before running:

python
dir = 0             # SecurityError: Forbidden function: dir
if dir != 0: ...    # same error — the NAME itself is what is banned

Never use these as variable names: dir, vars, input, exit, quit, help, file, locals, globals, eval, exec, compile, delattr, memoryview, breakpoint, reload. The natural trading-code traps and their renames:

Instead ofUse
dir (trade direction)direction, trade_dir
exit (exit price/bar)exit_price, exit_bar
input (user parameter)user_input, param
file / vars / helpfile_name / variables / help_text

Dict keys and strings are fine (c["open"], "exit"), and so are prefixed or suffixed names (exit_r, dir_up) — only the exact bare identifier is rejected.

Dunder attributes

__xxx__ attributes are not available to scripts (a small set such as __name__ is the exception). Trading logic never needs them — to check a type, use isinstance(x, T) instead of any form of introspection.

Lambdas

Lambdas are not allowed:

python
f = lambda x: x * 2       # SecurityError

Alternative: use a normal def.

python
def f(x):
    return x * 2

Other restricted constructs

ConstructWhy it is blockedAlternative
lambdaCan hide arbitrary codedef helper(...)
global / nonlocalMutates outer scopes implicitlyPass values via parameters or sdk.state
while True: (infinite loop)Cannot terminate within budgetfor ... in range(...) or a finite condition
eval / exec / compileDynamic code executionExpress logic as plain Python
del (statement)Removes protections from objectsRe-bind the variable to None instead

In short: write direct, explicit code. If a construct does not pass the validator, simplify the function. The whitelist intentionally favors predictable scripts over clever ones.

Resource limits

ResourceDefault limitRaised if exceeded
Time per bar800msTimeoutError
Memoryper-strategy ceiling enforced by the engineMemoryError
Source size~100KBrejected at load time (SecurityError)
Nesting depth50 levels of blocksrejected at load time
Code complexity~10000 syntax nodesrejected at load time (SecurityError: Code too complex)

About the 800ms time budget

For a typical strategy (indicators over 500 candles, simple logic), 800ms is generous headroom — usual execution time is between 5 and 50ms. Backtests can request a higher per-bar budget when running heavier strategies (ML inference, custom scientific computation); the backtest panel exposes the option when applicable.

A single bar that exceeds the budget produces a TimeoutError for that bar and the engine continues with subsequent bars. The backtest only aborts when transient failures cross 5% of the total bar count (and at least 5 bars failed) — single GC pauses or cold starts no longer kill the run.

⚠️ A persistently slow frame is not just a tolerated TimeoutError. When a bar overruns the per-bar budget, the worker's reader abandons the late response and the request/response protocol can desynchronize — the next read then consumes an out-of-order line and the backtest dies with ProtocolError: Failed to parse persistent strategy output JSON: data did not match any variant of untagged enum StrategyOutput. Unlike a TimeoutError, this ProtocolError is fatal and is not covered by the 5% tolerance — it aborts the whole run immediately. The only reliable cure is to keep every frame O(1) per bar (incremental indicators), not to rely on the tolerance.

If timeouts are persistent, check:

  • Building pd.DataFrame(sdk.candles) on every candle is expensive. Prefer collecting directly with a list comprehension.
  • Iteration over the entire sdk.candles. Use only the last N (sdk.candles[-period:]).
  • Nested loop over candles. Reduce complexity — it is usually possible to vectorize with np or call the injected Indicator over a bounded sdk.candles[-N:] window.
  • Recomputing the same indicator from scratch over the full history every bar. Compute it over a bounded sdk.candles[-N:] window with the injected Indicator global (no import), or keep an incremental accumulator in sdk.state.

About the memory limit

For trading logic the per-strategy memory ceiling is generous — strategies very rarely hit it organically. When MemoryError does appear, there is usually a list accumulating in sdk.state without bound:

python
# Unbounded growth - causes MemoryError.
sdk.state["all_closes"] = sdk.state.get("all_closes", []) + [c["close"] for c in sdk.candles]

Cap the size:

python
buf = sdk.state.setdefault("buffer", [])
buf.append(sdk.candles[-1]["close"])
if len(buf) > 1000:
    sdk.state["buffer"] = buf[-1000:]  # keep only the last 1000 (del is forbidden)

Other important restrictions

  • print output goes to the engine logs, not to the frontend console. Useful for debugging, but does not appear in real time.
  • The return value must be JSON-serializable. Dict, list, str, int, float, bool, or None. Objects, sets (convert to list), and NaN (use None) are not supported.
  • Writes to params are ignored. params is treated as read-only by the engine. To persist something, use sdk.state.
  • Writes to sdk.candles[i] may have non-deterministic effects. Do not modify them.

Diagnosing errors

When something fails, the engine categorizes the error. See error catalog for the full table:

ErrorCommon cause
SecurityErrorForbidden import, blocked builtin, lambda, dunder attribute
TimeoutErrorA single bar exceeded the time budget. Tolerated up to 5% of bars (min 5 absolute) — beyond that the run aborts.
MemoryErrorList/dict growing without bound
ProtocolErrorsdk.buy() without action, invalid signal, non-JSON return — or the request/response protocol desynced because a frame overran its time budget (produces did not match any variant of untagged enum StrategyOutput). Fatal: aborts the run immediately, unlike a tolerated TimeoutError.
RuntimeErrorClassic Python: IndexError, ValueError, ZeroDivisionError
WorkerPoolTimeoutEngine queue full; the request waited beyond its limit. Retry.

Next steps