Skip to content
Single .md

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:

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 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):

  1. Function main(df=None, sdk=None, params={}) - recommended, canonical mode.
  2. Function on_bar(sdk) - legacy, no dispatcher.
  3. An event hook - on_trade_tick, on_quote_tick, or on_book_update.
  4. A Strategy class - 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:

python
main()  # returns DECLARATION

The 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_conditions if 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 here

Phase 5 - Per-candle loop

This is where the strategy is executed. For every closed candle:

  1. 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:

    • append mode (steady state): appends the newly closed candle to the end. This is what happens on essentially every bar.
    • reset mode: replaces the entire list. Sent only on the first frame or on a misalignment/history rewrite (rewind) - not per candle.
    • replace_last mode: 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 whole sdk.candles list every frame.

  2. Calls main(sdk=sdk, params=params).

  3. The code reads sdk.candles, makes a decision, calls sdk.buy/sell/close/....

  4. Each action call adds a signal to the signals buffer.

  5. When main returns, 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.state is 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