Script lifecycle
This section describes how the engine loads, validates, and executes your code. The knowledge is useful for diagnosing errors and for writing code that correctly takes advantage of sdk.state and global variables.
Overview
Phase 1 - Validation
Before executing a single line, the engine validates the code. If any check fails, the engine never runs the code and raises SecurityError with the offending line and reason. See Sandbox limits for the allowed surface.
Phase 2 - Loading
If validation passes, the engine loads the file: it runs the top level of your script once so the names it defines become available.
- Root-level definitions (
DECLARATION = {...},def main(...),def _helper(...)) are registered. - Declared global variables (
PARAMS = {...}) become live. - Root-level statements run (
print("loaded")here appears in the logs exactly once).
This phase happens exactly once, at script load time. If it fails (syntax error, top-level exception), the engine aborts.
Global variables survive
Because the module stays loaded, anything at the root level persists between calls:
GLOBAL_CACHE = {} # empty at load time
def on_bar_strategy(sdk, params):
# GLOBAL_CACHE is the SAME object across every call
GLOBAL_CACHE[sdk.candles[-1]["time"]] = sdk.candles[-1]["close"]This provides persistence at no extra cost, but it is considered an anti-pattern: prefer sdk.state, which persists across every bar/frame within the same running session. Both sdk.state and module-level globals are lost if the process/backend restarts.
Phase 3 - Entrypoint discovery
After loading, the engine searches for one of the accepted entrypoints (in order):
- Function
main(df=None, sdk=None, params={})- recommended, canonical mode. - Function
on_bar(sdk)- legacy, no dispatcher. - An event hook -
on_trade_tick,on_quote_tick, oron_book_update. - A
Strategyclass - the engine instantiates it and binds.on_bar(or.next), plus any of the three event hooks above.
Precedence is top-to-bottom: main wins over on_bar, which wins over the event hooks, which win over a Strategy class. A convention name like on_bar_strategy is not one of these — it is only reached when your own main() calls it.
If none is found, the engine raises:
ProtocolError: Strict Mode. Your strategy must define a function
'main(df=None, sdk=None, params={})', 'on_bar(sdk)', ...Phase 4 - Metadata (main() with no args)
As soon as the script loads, the engine calls main() with no arguments to obtain the DECLARATION:
main() # returns DECLARATIONThe return value is used to:
- Build the editable parameters panel in the UI (
inputs). - Discover the plots required for the chart (
plots). - Read
entry_conditions/exit_conditionsif declarative mode is used.
If this call fails (exception in main() when df and sdk are None), the engine does not build the panel. Robust scripts guarantee a fallback:
def main(df=None, sdk=None, params={}):
params = params or {}
if sdk is not None:
return on_bar_strategy(sdk, params)
if df is not None:
return _build_chart(df, params)
return DECLARATION # <<<< always returns something herePhase 5 - Per-candle loop
This is where the strategy is executed. For every closed candle:
The engine updates
sdk.candles. This is one persisted buffer that grows across bars — the engine sends only the delta each bar, not a fresh copy:appendmode (steady state): appends the newly closed candle to the end. This is what happens on essentially every bar.resetmode: replaces the entire list. Sent only on the first frame or on a misalignment/history rewrite (rewind) - not per candle.replace_lastmode: updates only the last candle (rare, used for intra-bar updates).
Because the buffer persists and grows, update incremental state once per bar (a streaming indicator, or an accumulator in
sdk.state) rather than recomputing over the wholesdk.candleslist every frame.Calls
main(sdk=sdk, params=params).The code reads
sdk.candles, makes a decision, callssdk.buy/sell/close/....Each action call adds a signal to the
signalsbuffer.When
mainreturns, the engine collects the buffer and routes the orders.
sdk between calls
The same sdk object is reused for every candle. Properties such as sdk.position, sdk.cash, sdk.equity are updated by the engine before each call.
sdk.state persists between calls of the same script.
Editing parameters in the UI
New values arrive in sdk.params (and in the params argument) on the next call. The script requires no additional handling; simply read the parameters via params.get(...).
Phase 6 - Plots phase (df= branch)
When it is called: once per run (backtest), or when the user requests the script to be loaded on the chart (chart trading).
What the engine passes: main(df=pandas_dataframe, params=params).
What the script returns: {"plots": [...], "series": {...}}.
This is a parallel phase, independent from the candle loop in phase 5. The script may be running bar-by-bar while the frontend requests a re-render of the plots (the engine calls main(df=) again). The two calls do not interfere with each other.
Persistence across backend restarts
Chart trading is a long-running process. If the backend restarts (deploy, crash):
- Orders and positions are persisted in the database. On return, the engine rehydrates the ledger state and the engine state from storage.
sdk.stateis reinitialized. Volatile script state (flags, cooldowns, trailing high-water) may reset.- Module-level globals (
PARAMS,GLOBAL_CACHE) also reset.
Mitigation: sdk.state only survives while the session/process is alive, so it cannot be relied upon across a restart. State that must survive a restart should be rederived on reload from durable sources such as sdk.candles or the persisted orders/positions, or recomputed from scratch.
For most scripts, restarts are rare and do not affect the strategy. The concern is only relevant for critical logic that depends on state accumulated over many bars (for example, a custom manual EMA fed bar by bar).
Complete diagram
Next steps
- The
main()dispatcher - the 3 contexts in detail. - SDK reference - what is available in each call.
- Sandbox limits - what is accepted and what is rejected.