Skip to main content

When a strategy does not work

Every validation error message with its cause and fix, common runtime problems (no trades, rejected orders, works in backtest but fails live, lost state), language rules, limits and known limitations.

Validation messages

The app shows validation issues as Line N: message. The messages you are most likely to see, and the fix for each:

Message

Cause

Fix

strategy must define a Run() function

No def Run():

Add one; put per-tick logic inside it

Run() function must not have parameters

def Run(x):

def Run():

top-level function calls are not allowed

A call such as log.info(...) or spot.place_order(...) outside any function

Move it into Run() or a helper. x = f() at the top level is fine

if statement not within a function (also for)

Control flow at the top level

Move it into a function

DecimalSeries cannot be used in binary operations directly; index it first (e.g., series[0])

rsi > 50, closes * 2, if ema_fast > ema_slow

Index: rsi[0] > Decimal("50"); for crosses use ta.cross_above(a, b)

unknown trading symbol: BTC/USDT

The symbol is not in the supported catalogue

Use the exchange's format. On Hyperliquid BTC/USDC (spot) or BTC/USDC:USDC (perpetual)

unsupported timeframe: "2h"

Timeframe outside the allowed set

One of 1m 5m 15m 30m 1h 4h 1d

type 'Market' has no attribute 'OPTION' / type 'Connection' has no attribute 'OKX'

Unsupported instrument or exchange constant

Instruments: SPOT, LINEAR_PERPETUAL, INVERSE_PERPETUAL, LINEAR_FUTURE, INVERSE_FUTURE. Exchanges: HYPERLIQUID, BINANCE, BYBIT, COINBASE

type 'Order' has no attribute 'error_code'

Reading order.error_code / order.error_message

Not readable from a script yet. Branch on order.status instead

'TA.RSI' is V1 API; in V2 use 'ta.rsi(candles, length)' (and undefined: TA, undefined: Orders, undefined: Market.Ticker)

QuantScript v1 code

This is a v1 strategy; see Moving from QuantScript v1 to v2

this Starlark dialect does not support while loops

while

for i in range(n):

got is, want ':'

x is None

x == None

got string literal, want ',' on a line with f"..."

f-string

"..." % (a, b) or +

got try, want primary expression

try / except

Remove it; check inputs (stale, is_open, status) instead. An error ends the tick and the next tick runs normally

got class, want primary expression

class

Functions and dictionaries

got del, want primary expression

del state["k"]

state.pop("k", None)

unused variable: x / unused function: helper

Warning only

Optional tidy-up; does not block the strategy

Runtime problems

These pass validation and only show up in a backtest or live.

No trades at all. Usually one of: an entry condition that never becomes true (log the values you compare); ta.cross_above(a, b) fires only on the crossing bar, so if you also require RSI below 30 on that same bar it may never coincide; or a state flag that was set and never cleared. Check the strategy log first.

order.side == Order.BUY is never true. The constants are lowercase ("buy"), a returned order's fields are uppercase ("BUY"). Compare returned orders against "BUY", "SELL", "FILLED", "REJECTED", and so on.

Orders rejected in a backtest. status == "REJECTED" means the simulated wallet could not back the order: sizing at or near 100% of the balance, or a wallet the run did not fund in that currency. Size below the full balance and read get_balance(...).free rather than assuming a number.

It works in a backtest and raises live. Two known differences: a rejected order raises live instead of returning REJECTED; and get_balance, get_account, get_position raise live on exchanges without real-time balance and position streams (today everything except Hyperliquid). Backtests simulate the wallet on every exchange.

get_account() raises on spot. A spot wallet has no equity figure. Use get_balance and, if you need portfolio value, mark holdings to market (see the reference under Balances).

A key is missing from state on the first tick. state["x"] raises when x was never stored. Read with state.get("x", default).

Values lost on restart. Only state survives a restart; top-level variables are reset, and setting state[...] = ... at the top level overwrites the saved value on every restart. Initialise inside Run() with .get.

The strategy runs every minute in a backtest although it uses 1-hour candles. Reading a ticker subscribes the strategy at 1-minute resolution. See Act once per candle in Change what a strategy does.

Stale data. stale == True means the stream behind that object is down or reconnecting. Do not open new exposure on it; exits are usually still right.

Leverage rejected. Leverage must be a positive whole number, isolated margin only, and the engine does not cap it for you. Compare against get_symbol_spec(symbol).max_leverage first.

Notifications missing. More than 20 notification.send calls in one tick are dropped silently. In a backtest nothing is delivered; notifications are recorded with the run.

Language rules and limits

QuantScript is Starlark, so most everyday Python works: if/elif/else, for loops with break and continue, functions, lists, dictionaries, tuples, comprehensions, lambdas, string methods (split, upper, startswith, replace, join), % formatting and the ternary a if cond else b.

What differs from Python:

Not available

Use instead

while loops

for i in range(n):

try / except

Nothing. An error ends the current tick, and the next tick runs normally. Check inputs (stale, is_open, status) before acting

class

Functions and dictionaries

f-strings

"%s at %s" % (a, b) or +

is / is not

== None / != None

del x[k]

state.pop(k); for local dicts, d.pop(k)

import

Everything is predeclared (see below)

open, eval, exec, compile

Not permitted

Predeclared names. These exist in every script and must not be redefined: Connection, Market, Order, Decimal, datetime, timedelta, ta, state, log, notification, math.

Validation before the first tick. Every script is checked before it runs. Besides syntax, the validator rejects: a missing Run() or a Run() with parameters; control flow or bare calls at the top level; a series used in a comparison or arithmetic without indexing; unknown attributes and methods; an unsupported timeframe, instrument or exchange constant; a symbol outside the supported catalogue; and any v1 API name. It also reports warnings (an unused variable, unreachable code, a name that shadows a built-in) which do not stop the script but are worth reading.

Limits.

What

Limit

Candles per get_candles call

1,000 (default 200)

state

1,000 keys, 256,000 bytes; kept 7 days after the strategy stops

log line

trimmed to 1,000 characters in the stored log

notification message

1,000 characters (error if longer)

notification data

16 keys, 64-character keys, 256-character values, 4,096 bytes total

Notifications per tick

20; extra calls are dropped

Leverage

positive whole number; isolated margin only

Orders per backtest

200,000; the backtest stops when exceeded

Ticks that raise an error, per backtest

1,000; the backtest is failed when exceeded

Performance. Indicators run natively; a hand-written RSI in Starlark is slower and usually wrong. Compute each series once per tick and keep it in a variable. For rolling windows use sum, avg, max and min on a series rather than a loop.

Known limitations

  • Live balances, positions and account data are Hyperliquid-only today. get_balance, get_account and get_position need the exchange's real-time streams; on Binance, Bybit and Coinbase they raise in a live strategy. In backtests they work everywhere. Market data works everywhere, live and backtest.

  • Order types are market and limit only. There are no stop or stop-limit orders yet.

  • Margin mode is isolated only. Cross margin cannot be selected from a script.

  • Order.error_code and Order.error_message are filled in by the engine but not yet accepted by the validator, so a script cannot read them.

  • No error recovery inside a script. There is no try/except; an error ends the tick, and Run() is called again on the next one. A backtest in which 1,000 ticks raise is failed.

  • Live rejections raise. A REJECTED order status is only observable in a backtest; live, place_order raises instead.

  • One id, one exchange. A connection id cannot be reopened on a second exchange in the same script.

  • state holds scalars only (up to 1,000 keys and 256,000 bytes) and is kept for 7 days after a strategy stops.

  • Notifications are Telegram-only and best effort: at most 20 per tick, no retries.

Did this answer your question?