Skip to content

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

The following modules require an explicit import:

ModuleWhat it isWhen to use
tesstrade_indicatorsnative indicator library (optimized)hot path of strategies that recompute indicators every bar

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 tesstrade_indicators

A native indicator library available as an optional import. Provides the same Wilder/standard formulas as pandas_ta (RSI, EMA, ATR, MACD, WMA, SMA) plus state-preserving streaming classes that are O(1) per bar instead of O(n) — meaningful when the strategy recomputes the same indicator on every candle. Full reference in tesstrade_indicators.

python
import tesstrade_indicators as ti

_rsi = ti.Rsi(14)  # constructed once at module scope; state survives across bars

def on_bar_strategy(sdk, params):
    _rsi.update(sdk.candles[-1]["close"])
    if _rsi.is_ready() and _rsi.value() < 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

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.

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 move the hot path to tesstrade_indicators.
  • Recomputing the same indicator from scratch every bar. Cache in sdk.state or use a streaming class from tesstrade_indicators.

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:
    del buf[:len(buf) - 1000]  # keep only the last 1000

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
RuntimeErrorClassic Python: IndexError, ValueError, ZeroDivisionError
WorkerPoolTimeoutEngine queue full; the request waited beyond its limit. Retry.

Next steps