Appearance
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 the AST passes, the engine executes the file in the sandbox namespace. It is the equivalent of an exec(your_code, safe_globals):
- 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:
python
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 is persisted to the database across backend restarts. Module-level globals are lost if the process 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.
If none is found, the engine raises:
ProtocolError: Strict Mode. Your strategy must define a function
'main(df=None, sdk=None, params={})' or '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:
python
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:
python
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.candleswith the latest list.- Default mode: replaces the entire list (
reset). appendmode: appends the new candle to the end.replace_lastmode: updates only the last one (rare, used for intra-bar).
- Default mode: replaces the entire list (
- 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: if the script needs state that must survive a restart, save it to sdk.state at the start of every call. The engine persists sdk.state to the database when it is safe to do so.
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.