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 ───────────┘

The Test (backtest) dialog does not configure timer intervals; on_timer is not invoked during historical backtests from the IDE. Use on_bar / on_tick for bar-aligned logic. In backtest, on_tick fires four times per bar on synthetic OHLC from each bar (no real tick history). On forward runs (chart forward, bulk forward / Quant Cloud), on_tick runs on every broker price update from Alpaca; those runs can also set timerIntervalMs to call on_timer when defined.

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 quantcraft.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 quantcraft.backtest.runtime). Legacy from ide.backtest.runtime import … still works.

Runtime globals

The engine exposes these from quantcraft.backtest.runtime before callbacks run (legacy ide.backtest.runtime is equivalent):

NamePurpose
accountPaperAccount instance configured from the run dialog
chart_indicatorsChartIndicatorSeries collector merged into the result
qc_fundamentalsOptional QcFundamentals handle when fundamentals are loaded
qc_fama_frenchOptional QcFamaFrench when global Fama-French factors are loaded; prefer fama_french.qc in callbacks — see Fama-French factors

Prefer the bars argument of on_finish(bars, ...) for full-series math — do not rely on module-level qc_ohlcv in IDE strategies (it is often None at import time). Read prices from the bar object in callbacks.


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 — intrabar and live price updates

on_tick uses the same signature in backtest and forward runs, but when it fires and what price means depend on the run mode.

Backtest (Test)

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

No real tick data in backtest. Historical runs do not load broker tick history. The four calls per bar use synthetic prices derived from each bar's OHLC only. You can approximate intrabar fills in simulation, but backtest on_tick behavior will not match live forward ticks. For bar-close decisions, on_bar is enoughon_tick with tick_in_bar == 3 is equivalent to acting on the bar's close in simulation.

def on_tick(bar_index, tick_in_bar, price, bar, fundamentals=None, symbol=None): if tick_in_bar == 3: # Backtest simulation: closing tick fill account.close_trade(position_id, price)

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

Forward runs (chart forward, bulk forward / Quant Cloud)

On forward runs, on_tick runs every time the platform receives a price from the broker (Alpaca) while your algo is attached. There is no fixed four-tick OHLC walk — each call reflects a live quote.

def on_tick(bar_index, tick_in_bar, price, bar, fundamentals=None, symbol=None): # Forward run: react to each broker price update if price <= stop_level: account.close_trade(position_id, price)
  • price — the latest broker quote for this symbol.
  • bar — the current bar view for history (same shape as in backtest).
  • tick_in_bar — may still be present for signature compatibility; on forward runs, branch on price, not on synthetic OHLC phases.

Use forward on_tick for live intrabar logic — stops, trailing exits, or any reaction to marks between bar closes.

Parameters (both modes)

  • bar_index — same as in on_bar.
  • tick_in_bar — in backtest, 0, 1, 2, or 3 (synthetic OHLC step). On forward runs, do not rely on it for bar-phase logic.
  • price — synthetic OHLC step in backtest; live broker quote on forward runs.
  • bar — the same bar view as in on_bar.
  • fundamentals, symbol — same meaning as in on_bar.

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


on_timer — optional periodic callback

Backtest: The Test (backtest) dialog does not expose timerIntervalMs, so on_timer is not invoked during historical backtests from the IDE — even if you define on_timer in your strategy file. Put periodic or time-based logic in on_bar or on_tick for backtests.

Forward runs (chart forward test, bulk forward / Quant Cloud) can set timerIntervalMs in their run modal. When configured, on_timer runs at a fixed time interval between ticks, only when two conditions are met:

  1. You have configured timer_interval_ms / timerIntervalMs 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, the engine walks symbols in list order:

  1. on_bar runs once per loaded ticker at this bar time (before any ticks for that bar).
  2. Backtest only: for each tick_in_bar (0 = open, 1 = high, 2 = low, 3 = close), on_tick runs once per ticker on synthetic OHLC (no real tick data).
  3. Backtest only: after the four tick rounds, an extra close tick may run per symbol (engine close phase).
  4. Forward runs: on_tick runs per broker price update per symbol as quotes arrive from Alpaca (not limited to four calls per bar).
  5. on_timer (if used on forward runs with timerIntervalMs configured — not in IDE backtest) follows the same per‑ticker pattern between ticks.

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 — in backtest, for synthetic intrabar fills (e.g. tick_in_bar == 3 for the closing tick); on forward runs, to react to live broker prices between bar closes.
  4. Optionally uses on_timer on forward runs when timerIntervalMs is configured (not available in the IDE backtest dialog).
  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, Fama-French factors, OHLCV and bar data, Indicator series, or the in-app Documentation tab in the right sidebar (workspace layout).