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):
passA few rules apply across all callbacks:
- They must be top-level functions in your file (not nested or inside a class).
- All except
on_initandon_finishare optional — define only the ones you need. (For the Compile button, you do needon_init,on_bar, andon_finish.) - Inside callbacks you place orders through
accountand record indicator values throughchart_indicators(indicator series) (both imported fromide.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 = 100You 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)andbar.bars_backtell you how much history is available.fundamentals— fundamentals snapshot for this bar, orNoneif 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](orfloat(bar["close"])) toaccount.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_baralways 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_bar | Meaning |
|---|---|
0 | Bar open |
1 | Bar high |
2 | Bar low |
3 | Bar 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 inon_bar.tick_in_bar—0,1,2, or3.price— the price for this synthetic tick (open / high / low / close of the current bar).bar— the same bar view as inon_bar.fundamentals,symbol— same meaning as inon_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_tickentirely and put your logic inon_bar.
on_timer — optional periodic callback
Backtest: Historical backtests (Test) never call
on_timer, even if you define it and settimer_interval_ms. Timer-driven code runs only where wall-clock timing is supported (for example Run). For backtests, put periodic or time-based logic inon_baroron_tick.
When timers are supported, on_timer runs at a fixed time interval between ticks, only when two conditions are met:
- You have configured
timer_interval_msfor the run (a positive integer in milliseconds). - Your file defines an
on_timerfunction.
def on_timer(bar_index, tick_in_bar, price, bar, fundamentals=None, symbol=None):
passThe 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 hast,open,high,low,close,volume, and optionallyfundamentals. This is the canonical full-history series foron_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
primaryin 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 overridesprimary.
The clock symbol is also what on_finish(bars, symbol=...) uses for its bars and symbol arguments.
Per-symbol callbacks
For each execution bar:
on_barruns once per loaded ticker, in the order you listed them.- For each
tick_in_bar(0,1,2,3),on_tickruns once per ticker. 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:
- Sets input parameters or other state in
on_init. - Reads price history (OHLCV) through
bar.close[0],bar.close[1], … inon_barto make trading decisions, and callsaccount.open_trade/account.close_trade. - Optionally uses
on_tickto fill at a specific intrabar moment (e.g.tick_in_bar == 3for the closing tick). - Optionally uses
on_timerfor time-based logic whentimer_interval_msis configured and you are not relying on historical backtest (Test never invokeson_timer). - Builds chart indicators and finalizes results in
on_finishusingbars.
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).