Documentation

Strategy Lifecycle

A QuantCraft strategy is a regular Python file that defines a small set of callback functions. The backtest engine calls those functions in a fixed order while it walks through historical data. You don't call them yourself — the engine does. Use Test to drive them (running code).

The lifecycle, from start to finish:

on_init → on_bar → on_tick (×4 per bar) → on_timer (optional) → on_finish └─────────── repeats for every bar ───────────┘

In historical backtest (Test), on_timer is never called — use on_bar / on_tick for bar-aligned logic.

This page covers each callback, how multi-symbol strategies plug into them, and the basics you need to write your first strategy.


The strategy file

A minimal strategy looks like this:

from ide.backtest.runtime import account, chart_indicators def on_init(): pass def on_bar(bar_index, bar, fundamentals=None, symbol=None): close_px = float(bar["close"]) def on_tick(bar_index, tick_in_bar, price, bar, fundamentals=None, symbol=None): if tick_in_bar == 3: close_px = price def on_finish(bars, symbol=None, bars_by_symbol=None): pass

A few rules apply across all callbacks:

  • They must be top-level functions in your file (not nested or inside a class).
  • All except on_init and on_finish are optional — define only the ones you need. (For the Compile button, you do need on_init, on_bar, and on_finish.)
  • Inside callbacks you place orders through account and record indicator values through chart_indicators (indicator series) (both imported from ide.backtest.runtime).

on_init — set things up

Runs once, right after the engine has prepared the account, loaded market data, and built the runtime — but before any bars are processed.

Use it to:

  • Initialize variables your strategy carries between bars (counters, state machines, parameter values).
  • Configure indicators or warmup buffers.
  • Read input parameters so they can drive the rest of the run.
def on_init(): global lookback, position_size lookback = 20 position_size = 100

You don't have access to a current bar in on_init — at this point the simulation hasn't started yet.


on_bar — once per bar

Runs once per execution bar, before the four intrabar ticks for that bar. This is where most strategies make decisions.

def on_bar(bar_index, bar, fundamentals=None, symbol=None): if bar.bars_back < 1: return if bar.close[0] > bar.close[1]: account.open_trade(...)

Parameters:

  • bar_index — zero-based index of the current execution bar.
  • bar — the current bar view. Behaves like a read-only mapping (bar["open"], bar["high"], bar["low"], bar["close"], bar["volume"], bar["t"]) and also exposes history through offsets: bar.close[0] is the current bar's close, bar.close[1] is the previous bar, and so on. len(bar.close) and bar.bars_back tell you how much history is available.
  • fundamentals — fundamentals snapshot for this bar, or None if not configured.
  • symbol — uppercase ticker string. Useful in multi-symbol strategies; you can omit it if you only load one symbol.

Common patterns:

  • Decide on entries/exits at bar close: pass bar.close[0] (or float(bar["close"])) to account.open_trade / account.close_trade.
  • Compute simple indicators by reading offsets like bar.close[0], bar.close[1], …
  • Skip early bars that don't have enough history (if bar.bars_back < N: return).

Tip: on_bar always runs before any tick callbacks for the same bar.


on_tick — fine-grained intrabar timing

Runs up to four times per bar, after on_bar, to simulate movement inside the bar. The engine walks the OHLC of each bar in this order:

tick_in_barMeaning
0Bar open
1Bar high
2Bar low
3Bar close
def on_tick(bar_index, tick_in_bar, price, bar, fundamentals=None, symbol=None): if tick_in_bar == 3: # Closing tick fill account.close_trade(position_id, price)

Parameters:

  • bar_index — same as in on_bar.
  • tick_in_bar0, 1, 2, or 3.
  • price — the price for this synthetic tick (open / high / low / close of the current bar).
  • bar — the same bar view as in on_bar.
  • fundamentals, symbol — same meaning as in on_bar.

Use on_tick when timing within the bar matters — for example to fill on the closing tick, simulate an intrabar stop, or evaluate a touch of the bar's high or low.

If you only need bar-close decisions, you can ignore on_tick entirely and put your logic in on_bar.


on_timer — optional periodic callback

Backtest: Historical backtests (Test) never call on_timer, even if you define it and set timer_interval_ms. Timer-driven code runs only where wall-clock timing is supported (for example Run). For backtests, put periodic or time-based logic in on_bar or on_tick.

When timers are supported, on_timer runs at a fixed time interval between ticks, only when two conditions are met:

  1. You have configured timer_interval_ms for the run (a positive integer in milliseconds).
  2. Your file defines an on_timer function.
def on_timer(bar_index, tick_in_bar, price, bar, fundamentals=None, symbol=None): pass

The signature is the same shape as on_tick. Use on_timer for time-based behaviors (for example: refresh a slow-moving signal every minute, or schedule a check independent of bar count). If timer_interval_ms is not set, on_timer is never called even if it's defined.


on_finish — wrap up

Runs once, after the last bar. Use it to compute final outputs, build chart indicators, or print a summary.

def on_finish(bars, symbol=None, bars_by_symbol=None): closes = [b["close"] for b in bars] chart_indicators.add(...)

Parameters:

  • bars — chronological list of OHLC dictionaries for the clock symbol, oldest first. Each entry has t, open, high, low, close, volume, and optionally fundamentals. This is the canonical full-history series for on_finish.
  • symbol — uppercase ticker of the clock symbol.
  • bars_by_symbol — for multi-symbol runs, a dict mapping each ticker to its chronological list of bars (aligned with the execution clock). For single-symbol runs you can ignore this.

Common uses:

  • Compute series-wide indicators and pass them to chart_indicators.add(...) so they appear on the result charts.
  • Print or log a final summary to the Output Panel.
  • Refresh portfolio metrics for the result payload.

Multi-symbol basics

QuantCraft strategies can run against one symbol or several at once. The lifecycle stays the same — only the callback signatures and the data they receive change slightly.

Configuring multiple symbols

In the Test (backtest) dialog, set the symbol field to a single ticker (for example AAPL) or a list of uppercase tickers (for example ["AAPL", "MSFT", "GOOG"]). All listed symbols are loaded and simulated together.

The clock symbol

When you run multiple symbols, one of them drives the clock — its bar timestamps decide when callbacks run. By default this is the first symbol in your list, but you can override it:

  • Set primary in your run configuration to choose a clock symbol.
  • Or set a top-level SYMBOL = "AAPL" in your strategy file; if it matches a loaded ticker, it overrides primary.

The clock symbol is also what on_finish(bars, symbol=...) uses for its bars and symbol arguments.

Per-symbol callbacks

For each execution bar:

  1. on_bar runs once per loaded ticker, in the order you listed them.
  2. For each tick_in_bar (0, 1, 2, 3), on_tick runs once per ticker.
  3. on_timer (if used — not in backtest) follows the same per‑ticker pattern.

That means a multi-symbol on_bar is invoked, for example, three times per bar if you have three symbols loaded. Use the symbol parameter to know which ticker the call is for:

def on_bar(bar_index, bar, fundamentals=None, symbol=None): if symbol == "AAPL": ... elif symbol == "MSFT": ...

The bar view always belongs to the symbol the callback was invoked for. The same applies to price in on_tick.

on_finish for multi-symbol

on_finish still runs once at the end. To work with all symbols' history, use bars_by_symbol:

def on_finish(bars, symbol=None, bars_by_symbol=None): if bars_by_symbol: for ticker, ticker_bars in bars_by_symbol.items(): ...

Putting it together

A typical single-symbol strategy:

  1. Sets input parameters or other state in on_init.
  2. Reads price history (OHLCV) through bar.close[0], bar.close[1], … in on_bar to make trading decisions, and calls account.open_trade / account.close_trade.
  3. Optionally uses on_tick to fill at a specific intrabar moment (e.g. tick_in_bar == 3 for the closing tick).
  4. Optionally uses on_timer for time-based logic when timer_interval_ms is configured and you are not relying on historical backtest (Test never invokes on_timer).
  5. Builds chart indicators and finalizes results in on_finish using bars.

A multi-symbol strategy uses the same callbacks, just branches on the symbol argument inside them and reads per-ticker data through the bar it receives — and through bars_by_symbol in on_finish.

Need more detail — see Account model, Fundamentals, OHLCV and bar data, Indicator series, or the in-app Documentation tab in the right sidebar (workspace layout).