OHLCV and bar data
QuantCraft backtests need OHLCV (open, high, low, close, volume) candles for each symbol you trade. The engine loads that data before your strategy runs, then drives your strategy lifecycle callbacks with a current bar view (bar) on each step. At the end of the run you can optionally work with the full series passed into on_finish(bars, …). Configure loads from Test (Running code).
This page covers where data comes from (loaders), how date ranges and warmup work, and how to use the full-series bars argument in on_finish. Pair with Fundamentals when you mix price and filings data; see Account model for how trades relate to prices.
Loaders (data sources)
On the Symbol & data tab of Run backtest you choose a price feed. The engine uses one of two loaders behind the scenes:
Connected broker feed
- Loads candles from your connected broker account using your API key and secret, or an OAuth-linked connection with a valid link id.
- Supports US equities and crypto (you pick the asset type in the dialog).
- Supports many timeframes — daily, weekly, hourly, 4-hour, and several intraday minute bars (aligned with the app’s chart timeframe labels).
- History is fetched in chunks until the requested range is satisfied (or until a minimum bar count is reached when you use the legacy “no explicit dates” mode).
Use the connected broker feed when you want broker-connected data or the full intraday timeframe set (1-minute through 4-hour, plus daily and weekly).
QuantCraft storage (hosted)
- Loads candles from QuantCraft hosted storage when you are signed in (the app sends your session token).
- Files are organized by symbol and timeframe. On a free plan the backtest modal offers daily, weekly and monthly; Premium unlocks intraday and extended timeframes.
- The full file is fetched, then optionally filtered to your chosen calendar date range.
- Daily bars carry a date-only
tsuch as"2024-01-02", read as UTC.
QuantCraft B2 timeframe codes: 1s, 1m, 2m, m5, 15m, 30m, h1, 2h, 4h, d, w, m, q, y. Watch out: m means monthly, not minutes — 1-minute is 1m, 5-minute is m5.
If a date range has no bars, loading fails with a message like No AAPL bars (d) between 2010-01-01 and 2010-02-01. Available range is … to … — adjust the backtest dates.
Use QuantCraft storage when you are signed in without broker credentials, or when you want hosted OHLCV bundles from the QuantCraft symbol catalog (5-minute, hourly, daily, weekly, or monthly).
Choosing between them
| Need | Typical choice |
|---|---|
| Full intraday set from connected broker (1m, 5m, 15m, 30m, 1H, 4H, …) | Connected broker feed |
| 5-minute or hourly from QuantCraft catalog | QuantCraft storage |
| Daily / weekly / monthly from hosted files | QuantCraft storage |
| Connected broker account + keys | Connected broker feed |
| Signed-in access without broker credentials | QuantCraft storage |
In strategy callbacks, read prices from the
barobject the engine passes you — not from a global OHLCV singleton. In the IDE, module-levelqc_ohlcvis often unset when your file loads; relying on it can break. It is alsoNoneon a screener-target run, and where it is set it spans the full loaded range including warmup — not the executed window the chart draws — so it is the wrong source for chart series. Thebarview is always correct for the current simulation step.
Time ranges
You control which bars participate in the backtest with start date, end date, and optionally warmup.
Calendar range (start_date + end_date)
When you set both dates on the Simulation tab (format YYYY-MM-DD, interpreted as UTC calendar days):
- The engine keeps only bars that fall in that window.
- The range is treated as a half-open interval: from the start of the start day through the end of the end day in the product’s usual convention (bars on the end calendar day are included as configured by the engine).
Connected broker feed: candles are requested for that calendar span.
QuantCraft storage: the full series for the file is downloaded, then filtered to the same calendar-day window.
If you omit both dates:
- Connected broker feed uses a legacy backward walk: the engine walks backward in time until enough history is loaded (subject to timeframe limits), rather than a fixed calendar window you typed.
- QuantCraft storage uses all bars in the file (no extra filter).
Warmup bars (warmup_bars)
Optional non-negative integer: extra bars loaded before your start date so indicators can look back without running out of history.
- Warmup bars are visible through
bar.close[1],bar.close[2], … andbar.bars_backon the first execution bar. on_bar/on_tick/on_timerstill run only inside your selected execution window — warmup is for history only, not extra simulated “trading days” in the result.
bar_index vs calendar
bar_indexcounts execution bars only (first bar inside your selected range is0, then1, …). It does not count warmup-only bars.- Bars are simulated oldest to newest within the execution window.
Bar data inside callbacks (bar)
On every on_bar and on_tick (and on_timer on forward runs when configured), the engine passes bar: an OHLC bar view (OhlcvBarView) for the symbol that callback is for. It behaves like a read-only mapping but isinstance(bar, dict) is false — use bar["close"] or attribute-style access, not dict-only APIs.
In backtest, on_tick receives synthetic prices from each bar's OHLC (four calls per bar, no real tick history). On forward runs, on_tick fires on each live broker quote from your connected broker while the price argument tracks the latest mark — see Strategy lifecycle.
You can use it like a read-only mapping:
o = float(bar["open"])
h = float(bar["high"])
l = float(bar["low"])
c = float(bar["close"])
v = float(bar["volume"])
t = bar["t"] # ISO timestamp string (QuantCraft B2 daily bars: a date like "2024-01-02", UTC)You can also read history relative to the current bar with offset indexing (same bar, previous closes, etc.):
current_close = bar.close[0]
prev_close = bar.close[1]bar.close[0]is the close for the current bar context (same idea asfloat(bar["close"])in backteston_bar).- Larger indexes walk backward in time;
bar.bars_backtells you how far you can look.
Forward runs (chart toolbar timeframe):
- In
on_barafter a bar completes,bar.close[0]is the just-completed close;bar.close[1]is the previous completed bar. - In
on_tick,bar.close[0]is the forming bar / live price; the last completed close is atbar.close[1]while a bar is forming.
Live data accuracy (forward runs)
The live price stream from your broker is an approximation, not a complete record of the market:
- Free live-data plans typically see trades from a single exchange only — a small fraction of all US trading — so live-built candles show much lower volume and can miss the true highs and lows.
- Paid consolidated-feed plans see every exchange, but trades can still arrive late in bursts, especially around the open on very active stocks.
The app replaces each live-built candle with the broker's finalized historical bar shortly after it closes. On small timeframes such as 1-minute, that means:
| Index | What it is | Use it for |
|---|---|---|
bar.close[0] | The forming candle, built from live ticks | The current price estimate |
bar.close[1] | The bar that just closed — may still be tick-built for the first ~10–30 seconds | Use with care right after a new bar opens |
bar.close[2] | A fully finalized bar | Signals that need a confirmed close |
For signals that depend on a confirmed close on 1-minute bars, prefer bar.close[2].
Volume at [0]:
- Forward
on_tick:volume[0]on the forming candle is provisional (often a single tick’s size). Usevolume[1]for the last completed bar’s volume. - Backtest: all four synthetic ticks use the full bar row —
volume[0]is the full bar volume, not a provisional tick size.
This is the right place for per-step trading logic: entries, exits at bar close, simple moving averages, and anything that only needs the current bar plus lookback.
Primary and additional timeframes (get_bar)
The primary run timeframe is always available via rt.get_bar (you do not need to list it under Additional timeframes). When your strategy also needs bars at a different resolution (for example daily trend filters on a 5-minute backtest), select those extras in the Run backtest modal Additional timeframes dropdown on Symbol & data, then read them from any callback:
from quantcraft import Timeframe
import quantcraft.runtime as rt
def on_bar(bar_index, bar, fundamentals=None, symbol=None):
# Primary TF works without listing it under Additional timeframes
# (use the Timeframe member that matches the modal primary, e.g. M5):
prev_primary = rt.get_bar(Timeframe.M5, shift=1)
daily = rt.get_bar(Timeframe.DAILY, shift=1)
if daily and bar.close[0] > daily.close:
... # primary bar closed above yesterday's daily closeConfigure Additional timeframes in Backtests; save selections in run presets (.qcs) under secondaryTimeframes.
BarSnapshot fields
get_bar returns a BarSnapshot (or None) with: open, high, low, close, volume, t.
shift semantics
shift | .close | .high / .low / .open |
|---|---|---|
0 | Current execution price — identical across all timeframes at the same callback (backtest; see forward note below) | From that timeframe's stored bar — not unified with the primary TF |
>= 1 | Completed bar close on that TF | Full completed-bar OHLC — preferred for previous-candle wicks |
Backtest: get_bar only ever sees bars up to the current simulation step — never later ones — on the primary timeframe and on every additional one. At shift=0, secondary high/low are time-aligned to the simulation clock for each TF. get_bar reads the clock symbol's bars; in a multi-symbol run use each ticker's own bar.close[i] for its history.
Forward run: the primary bar follows the chart toolbar timeframe. Secondary buffers update when a bar completes on that TF's stream — high/low at shift=0 may lag until that bucket completes; prefer shift=1 for reliable previous-bar OHLC. Forward shift=0 .close is each buffer's own latest close, not unified across timeframes. In on_tick, volume[0] on the primary bar may be provisional (still forming).
Returns None when that timeframe has no buffer for the run (not the primary TF and not selected under Additional timeframes), on a Screener backtest (no clock symbol — use each ticker's own bar.close[i] offsets for history), or when data is unavailable. One backtest exception: shift=0 on a timeframe you didn't select returns the primary timeframe's current snapshot rather than None.
tf may be a Timeframe member or a string like "1D" / "5Min".
Timeframe enum
Import with from quantcraft import Timeframe:
| Member | String value | Description |
|---|---|---|
M1 | "1Min" | 1-minute bars |
M5 | "5Min" | 5-minute bars |
M15 | "15Min" | 15-minute bars |
M30 | "30Min" | 30-minute bars |
H1 | "1Hour" | Hourly bars |
H4 | "4Hour" | 4-hour bars |
DAILY | "1D" | Daily bars |
WEEKLY | "1Week" | Weekly bars |
Some QuantCraft B2 timeframes (2-minute, 2-hour, 1-second, monthly m, quarterly q, yearly y) have no Timeframe member — pass them to get_bar as strings.
Optional full series: on_finish(bars, …)
When the simulation ends, the engine calls on_finish once. If your strategy defines the extended signature, you receive full-series OHLC rows for post-processing — indicators over the whole run, exporting series, final refresh_metrics marks, etc.
def on_finish(bars, symbol=None, bars_by_symbol=None):
...bars — clock symbol, oldest first
barsis alistof dictionaries for the clock symbol (scriptSYMBOLif that ticker is loaded, otherwise the first loaded symbol — the modal has no primary-symbol control). On a Screener run there is no clock symbol:barsis[]andsymbolis"", so usebars_by_symbol.- Order is chronological: oldest bar first, newest last — the opposite convention from
QcOhlcvindex[0] = newestin notebook-style APIs. - Each row includes at least
open,high,low,close,volume, andtwhen timestamps exist. When fundamentals are enabled, rows may also include afundamentalssnapshot for that bar.
Typical uses:
def on_finish(bars, symbol=None, bars_by_symbol=None):
if not bars:
return
closes = [float(b["close"]) for b in bars]
last = float(bars[-1]["close"])
sym = (symbol or "AAPL").strip().upper()
metrics = account.refresh_metrics({sym: last})
chart_indicators.add(...)Use bars[-1]["close"] (or the last row’s fields) as the final mark when calling account.refresh_metrics({symbol: last_price}) so open positions are valued at the end of the run.
For chart_indicators, prefer values you accumulated per symbol as the run walked bars, and use on_finish only to hand them over; recompute from bars only for indicators you did not track per bar — see Indicator series.
bars_by_symbol — multi-symbol runs
When you backtest more than one ticker, bars still refers to the clock symbol only. For every symbol’s full aligned series, use bars_by_symbol:
- A mapping from uppercase ticker → same shape of list as
bars(chronological dicts per symbol). - Use it when you need cross-sectional or per-symbol full histories at finish time (correlation, relative strength, per-symbol indicators).
When you don’t need on_finish
If your strategy only needs the current bar and lookback offsets, you can implement on_bar / on_tick only and skip heavy full-series work. on_finish is optional for logic — but it is still where chart_indicators series are handed over and where refresh_metrics gets the last price. Note that every return in on_finish is silent: the run still succeeds, it just skips whatever came after.
How this ties to Test Results
The structured backtest result includes an ohlcv.bars (or equivalent) list: one row per simulated execution step, with OHLC fields and optional fundamentals snapshots — aligned with what you saw in callbacks. on_finish(bars, …) is the same chronological series the engine uses for “whole run” math on the clock symbol, so your finish-time code matches what appears in results and charts.
Quick reference
| Topic | User-facing summary |
|---|---|
| Loaders | Connected broker feed (keys + broad intraday) vs QuantCraft storage (signed in; daily / weekly / monthly, more with Premium). |
| Dates | YYYY-MM-DD start + end → calendar-filtered window; omit both → broker feed legacy lookback or QuantCraft storage full file. |
| Warmup | Extra history before start for bar offsets; callbacks only in the execution window. |
| Per step | Use bar / bar["close"] / bar.close[k] in on_bar / on_tick. |
| Primary / additional TFs | Primary always via rt.get_bar; extras via Additional timeframes + rt.get_bar(Timeframe.X, shift). |
| Full series | Use on_finish(bars, symbol=..., bars_by_symbol=...) — bars oldest-first for the clock symbol; bars_by_symbol for all symbols in multi-symbol runs. |
| Global factor returns | Not on QcOhlcv or bar — use Fama-French factors (fama_french.qc). Monthly rows are a YYYYMM dict; use latest_row, not rows[0]. |
