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

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.

Forbidden modules

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

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

Why random is blocked

To guarantee determinism - same input, same output. When randomness is required, use a seed combined with a hash:

python
seed = int(sdk.candles[-1]["time"]) % 10_000
pseudo_random = (seed * 1103515245 + 12345) % 2_147_483_648

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

Forbidden dunder attributes

Access to __xxx__ attributes (except a few whitelisted ones) is blocked to prevent sandbox escape:

python
obj.__class__             # SecurityError
obj.__dict__              # SecurityError
obj.__bases__             # SecurityError
obj.__subclasses__()      # SecurityError

Lambdas

Lambdas are not allowed:

python
f = lambda x: x * 2       # SecurityError

Alternative: use a normal def.

python
def f(x):
    return x * 2

Resource limits

ResourceDefault limitRaised if exceeded
Time per call200msTimeoutError
Memory64MBMemoryError
Source size~100KBrejected at load time (SecurityError)
Nesting depth20 levels of blocksrejected at load time

About the 200ms limit

For a typical strategy (indicators over 500 candles, simple logic), 200ms is adequate headroom. The usual execution time is between 5 and 20ms. If the limit is being exceeded, 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.

About the 64MB limit

For trading logic, 64MB is sufficient. If the limit is being exceeded, 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
TimeoutErrorLoop too slow or infinite
MemoryErrorList/dict growing without bound
ProtocolErrorsdk.buy() without action, invalid signal, non-JSON return
RuntimeErrorClassic Python: IndexError, ValueError, ZeroDivisionError
WorkerPoolTimeoutPool full; the strategy waited in the queue. Retry.

Next steps