Skip to content
Single .md

Error catalog

Exceptions the engine may raise, with typical cause and fix. Search for the error string in the log and locate it in the table.

Quick table

CategoryWhen it firesSeverity
SecurityErrorForbidden import, builtin, or constructHigh - code outside the contract
TimeoutErrorExecution exceeded the time limitMedium - slow logic
MemoryErrorExceeded the memory limitMedium - uncontrolled accumulation
ProtocolErrorProtocol violated (missing action, invalid JSON) or output desync from an over-budget frameHigh - fatal, aborts the run
ProcessErrorEngine execution environment failedHigh - infra
RuntimeErrorClassic Python error (IndexError, ZeroDiv, etc.)Medium - bug in the script
WorkerPoolTimeoutQueue full; request did not acquire a permitLow - retry
UnknownErrorUncategorized errorInvestigate

SecurityError

The engine rejected the code before execution. The script did not run.

Cause 1 - Forbidden import

SecurityError: Import not allowed: os

Fix: the editor's import allowlist is exactly numpy, pandas, pandas_ta, talib, math, json, datetime. Indicator helpers do not need an import — Indicator, Signal, and ta are pre-injected globals. tesstrade_indicators is not importable from the editor; using it raises this error. See sandbox limits.

Cause 2 - Blocked builtin

SecurityError: Forbidden function call: eval
SecurityError: Forbidden function: dir

Fix: banned builtins include open, exec, eval, __import__, input, exit, dir, vars, globals, locals. No substitute; remove from the logic.

The second form (Forbidden function: <name>, no "call") fires on the bare identifier — these builtins are reserved names even as plain variables, because a stored name could alias the builtin. dir = 0 fails exactly like dir() does. Rename the variable: dirdirection, exitexit_price, inputuser_input, filefile_name, varsvariables. Dict keys and strings (c["open"], "exit") and prefixed names (exit_r, dir_up) are fine — only the exact bare identifier is rejected. See sandbox limits.

Cause 3 - Dunder attribute

SecurityError: Forbidden attribute: <__dunder__>

Fix: __xxx__ attributes are not available to scripts. Trading logic never needs them — to check a type, use isinstance(x, T).

Cause 4 - Lambda

SecurityError: Lambda functions are forbidden

Fix: replace with a def function:

python
# Incorrect
sorted_items = sorted(items, key=lambda x: x[1])

# Correct
def _key(x):
    return x[1]
sorted_items = sorted(items, key=_key)

Cause 5 - Code too large, too complex, or too nested

SecurityError: Code too large: <N> bytes (max 102400)
SecurityError: Code too complex: <N> AST nodes (max 10000)
SecurityError: Code nesting too deep (max 50)

Fix: split into smaller functions. The configured limits are hard to exceed in reasonable scripts.

Cause 6 - Restricted construct (global, nonlocal, while True, del)

SecurityError: Global statement forbidden
SecurityError: Nonlocal statement forbidden
SecurityError: Delete statement forbidden
SecurityError: Infinite loop forbidden: while True

Fix: rewrite the function without the construct.

  • global / nonlocal: pass values explicitly or persist through sdk.state.
  • while True: use a finite for ... in range(...) or guard the loop with a counter.
  • del statement: re-bind the variable to None.

TimeoutError

TimeoutError: Execution exceeded the time limit

The engine enforces a per-bar time budget (default 800ms). A single bar exceeding the budget produces a TimeoutError for that bar; the backtest continues and only aborts when transient failures cross 5% of total bars (with at least 5 absolute failures). If the run finishes with a log line X/Y bar callbacks failed transiently, the strategy is within tolerance.

⚠️ 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.

Cause 1 - Heavy loop

python
# Incorrect: O(n^2) on a list of 10000 candles
for i in range(len(sdk.candles)):
    for j in range(len(sdk.candles)):
        ...

Fix: use only the last N bars (sdk.candles[-period:]). Vectorize with numpy when possible.

Cause 2 - pd.DataFrame(sdk.candles) on every call

Building a DataFrame is costly. Fix: use a list comprehension: closes = [c["close"] for c in sdk.candles].

Cause 3 - Indicator recomputed from scratch over the full history on every bar

Fix: compute over a bounded sdk.candles[-N:] window with the injected Indicator global (no import) — a fixed lookback keeps each bar O(1) in history length and converges to the full-history value — or keep an incremental accumulator in sdk.state (see persistent state) that updates from only the newest close. Never call an indicator over the whole growing sdk.candles each bar.

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

def on_bar_strategy(sdk, params):
    period = int(params.get("period", 14))
    if len(sdk.candles) < period + 2:
        return
    rsi = Indicator.rsi(sdk.candles[-LOOKBACK:], period)[-1]  # bounded, no import
    if rsi is None:
        return
    # ... trading logic

MemoryError

MemoryError: Memory limit exceeded

Cause - Lists growing without a limit in sdk.state

python
# Incorrect: memory leak
sdk.state["all_closes"] = sdk.state.get("all_closes", []) + [...]

Fix: bound the size:

python
buf = sdk.state.setdefault("closes", [])
buf.append(new_value)
if len(buf) > 500:
    buf[:] = buf[-500:]

ProtocolError

The SDK contract was violated.

Cause 1 - sdk.buy() without action

ProtocolError: Strict Mode: buy() / sell() exige action explícita

The runtime emits this message in Portuguese ("requires explicit action"); search the log for the exact string above. Fix: always pass action=:

python
sdk.buy(action="buy_to_open", qty=1, order_type="market")

Cause 2 - No entry point defined

ProtocolError: Strict Mode. Your strategy must define a function
'main(df=None, sdk=None, params={})', 'on_bar(sdk)', ...

Fix: define main() at the root level of the file. See The main() dispatcher.

Cause 3 - Non-serializable return

ProtocolError: Unable to serialize return value

Fix: return only dict, list, str, int, float, bool, None. Convert numpy arrays with .tolist(). Convert NaN to None.

Cause 4 - Output shape does not match the study contract

ProtocolError: Failed to parse PyO3 study output: invalid type: sequence, expected a string

The returned dict has a field of the wrong TYPE for the study contract. The classic case is per-bar plot coloring: colorSeries in a plot declaration is a string key into the result's top-level colors map — putting the color array itself inside the plot triggers exactly this error ("sequence" = the array, "expected a string" = the key).

Fix: declare "colorSeries": "<series_name>" in the plot and return "colors": {"<series_name>": [per-bar colors]} at the top level of the result. See plots & series and declaration.

Cause 5 - Persistent output desync (over-budget frame)

ProtocolError: Failed to parse persistent strategy output JSON: data did not match any variant of untagged enum StrategyOutput

ProtocolError: Failed to parse persistent strategy output JSON: data did not match any variant of untagged enum StrategyOutput — The persistent request/response protocol desynchronized, almost always because a frame overran the per-bar time budget (typically an O(n²) indicator recomputed over the full sdk.candles history on every bar). Unlike TimeoutError, this error is fatal: it is not per-bar-recoverable and aborts the backtest immediately, bypassing the 5% tolerance. Fix: make every frame O(1) — compute over a bounded sdk.candles[-N:] window with the injected Indicator global (no import), or keep an incremental accumulator in sdk.state; never rescan the whole sdk.candles each bar.

ProcessError

ProcessError: <message>

Infrastructure problem: the engine's execution environment failed unexpectedly. Not caused by user code.

Fix: report to support. Occurs rarely.

RuntimeError

Classic Python exception not handled by the script:

RuntimeError: IndexError: list index out of range
RuntimeError: ValueError: could not convert string to float
RuntimeError: ZeroDivisionError: division by zero
RuntimeError: KeyError: 'close'

Cause - False assumption about data

python
# Incorrect: assumes candles has at least 20 items
sma = sum(sdk.candles[-20:][i]["close"] for i in range(20)) / 20

If sdk.candles has 5 items, an index out of range occurs.

Fix: always validate:

python
if len(sdk.candles) < 20:
    return
sma = sum(c["close"] for c in sdk.candles[-20:]) / 20

Cause - Division by zero

python
# Incorrect: when avg_loss == 0, breaks
rs = avg_gain / avg_loss

Fix:

python
if avg_loss == 0:
    return 100.0
rs = avg_gain / avg_loss

Cause - Key missing

python
# Incorrect: if the candle does not have 'volume'
vol = sdk.candles[-1]["volume"]

Fix:

python
vol = sdk.candles[-1].get("volume", 0.0)

WorkerPoolTimeout

WorkerPoolTimeout: timed out waiting for Python worker

The sandbox has a finite pool of Python workers. When many requests arrive, the queue fills and new requests wait. If the wait exceeds the configured timeout, the request fails.

Fix: this is transient. Retry after a few seconds. If persistent, the backend is overloaded; wait for capacity to free up.

UnknownError

UnknownError: <message>

Fallback when the engine has not categorized the exception. It usually coincides with a Python RuntimeError.

Fix: inspect the message. If it contains a Python traceback, treat it as RuntimeError.

Silent rendering issues (no exception)

Some misconfigurations do not raise an error — the script runs, the engine accepts the return value, but the chart is empty or wrong. The most common:

SymptomLikely causeFix
Legend chip appears, line is invisibleOscillator declared with pane: "overlay" (default). On a high-priced asset, the line collapses against y=0Add "pane": "new" and "scale": "right" to the DECLARATION
Width or color of a plot ignoredField lineWidth (legacy) or 8-digit hex (#RRGGBBAA)Use "width": 2 and 6-digit hex ("#RRGGBB"); area transparency is automatic
Reference levels (0, 70, 30) don't render with the right baselineConstant series used instead of levelsMove the constants into DECLARATION["levels"]
Plot drawn but pane field of declaration looks ignoredStyle override saved with the indicator (legacy style.pane) is fighting the declarationRe-save the indicator with the current declaration; the editor strips legacy style pane on save

Quick diagnostic table

MessageLikely categoryFirst step
"Import not allowed"SecurityErrorRemove the import
"Forbidden function call"SecurityErrorRemove the use
"Forbidden function: X" (no "call")SecurityErrorX used as a variable name — the identifier is reserved; rename it (dirdirection)
"Lambda"SecurityErrorReplace with def
"Strict Mode ... must define a function"ProtocolErrorAdd main()
"exige action explícita"ProtocolErrorAdd action=
"did not match any variant of untagged enum StrategyOutput"ProtocolError (fatal)Make every frame O(1) — see Cause 4
"execution exceeded time limit"TimeoutErrorOptimize the loop
"memory limit exceeded"MemoryErrorBound lists in state
"IndexError" / "list out of range"RuntimeErrorCheck len(sdk.candles)
"ZeroDivisionError"RuntimeErrorCheck divisor != 0
"KeyError"RuntimeErrorUse .get() with a default
"timed out waiting for worker"WorkerPoolTimeoutRetry

Debugging in the live editor

When seeing an error in the "Errors" tab:

  1. Read the full message, including traceback if present.
  2. Locate the line indicated by the traceback.
  3. Test hypotheses with print():
    python
    print(f"DEBUG: sdk.position={sdk.position}, len(candles)={len(sdk.candles)}")
  4. Re-run and inspect the logs.

Next steps