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 |
| No | Add one; put per-tick logic inside it |
|
|
|
| A call such as | Move it into |
| Control flow at the top level | Move it into a function |
|
| Index: |
| The symbol is not in the supported catalogue | Use the exchange's format. On Hyperliquid |
| Timeframe outside the allowed set | One of |
| Unsupported instrument or exchange constant | Instruments: |
| Reading | Not readable from a script yet. Branch on |
| QuantScript v1 code | This is a v1 strategy; see Moving from QuantScript v1 to v2 |
|
|
|
|
|
|
| f-string |
|
|
| Remove it; check inputs ( |
|
| Functions and dictionaries |
|
|
|
| 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 |
|
|
| Nothing. An error ends the current tick, and the next tick runs normally. Check inputs ( |
| Functions and dictionaries |
f-strings |
|
|
|
|
|
| Everything is predeclared (see below) |
| 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 | 1,000 (default 200) |
| 1,000 keys, 256,000 bytes; kept 7 days after the strategy stops |
| trimmed to 1,000 characters in the stored log |
| 1,000 characters (error if longer) |
| 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_accountandget_positionneed 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_codeandOrder.error_messageare 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, andRun()is called again on the next one. A backtest in which 1,000 ticks raise is failed.Live rejections raise. A
REJECTEDorder status is only observable in a backtest; live,place_orderraises instead.One
id, one exchange. A connection id cannot be reopened on a second exchange in the same script.stateholds 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.
