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)
In the Test (backtest) dialog you choose a data source. The engine uses one of two loaders behind the scenes:
Alpaca
- Loads candles from Alpaca using your API key and secret.
- 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 Alpaca when you want live-style feeds or intraday bars that are not available from the other source.
B2 (QuantCraft storage)
- Loads candles from QuantCraft B2 storage using your B2 authorization token.
- Files are organized by symbol and coarse timeframe: daily (
d), weekly (w), or monthly (m). - The full file is fetched, then optionally filtered to your chosen calendar date range.
Use B2 when you already have hosted OHLCV bundles for symbols you care about and mainly need daily / weekly / monthly data.
Choosing between them
| Need | Typical choice |
|---|---|
| Intraday (1m, 5m, 1H, …) | Alpaca |
| Daily / weekly / monthly from hosted files | B2 |
| Alpaca account + keys | Alpaca |
| Token-only access to stored series | B2 |
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. 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 in the Test dialog (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).
Alpaca: candles are requested for that calendar span.
B2: the full series for the file is downloaded, then filtered to the same calendar-day window.
If you omit both dates:
- Alpaca 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.
- B2 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 if used), the engine passes bar: an OHLC bar view for the symbol that callback is for.
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"] # timestamp when presentYou 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 always the current execution bar’s close (same idea asfloat(bar["close"])).- Larger indexes walk backward in time;
bar.bars_backtells you how far you can look.
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.
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 (the symbol that drives the execution schedule — usually the first in your list, or the one you set as primary).- 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.
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 the right place for chart_indicators payloads and refresh_metrics that need the last price.
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 | Alpaca (keys + intraday-capable) vs B2 (token + daily/weekly/monthly files). |
| Dates | YYYY-MM-DD start + end → calendar-filtered window; omit both → Alpaca legacy lookback or B2 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. |
| 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. |