Reproducible Backtest for Python Without API Key
Run reproducible backtest for Python without API key: pin install, use deterministic sample data, then see wetin one equity curve still dey hide.
A reproducible backtest for Python na one wey stranger fit run again for clean machine and get your exact numbers back, without account or API key blocking am. Most tutorials no pass this test from line one, because live download dey give the next reader price history wey small different from the one wey author receive. This walkthrough pin one open-source engine, quantjourney-bt for version 0.12.4, run the example wey come with am with zero credentials, then use real market data show wetin one clean run still no fit tell you.
Wetin dey make backtest reproducible?
For this matter, reproducibility get one narrow meaning wey person fit test: another person run one command for clean machine and e get your numbers back exactly to the decimal. Two normal things fit break am.
The first one na the code. Library wey dey version 0.x no promise compatibility between minor releases. Rename, changed default, or column order wey change fit make your script still run, but e go quietly report another result.
The second one na the data. Tutorial wey first line na live download no be reproducible from the beginning. Vendors dey revise history, adjust for splits, and fill gaps later. So the same script fit print different numbers one month later. Afterward, you no fit tell whether na code change or data change cause am.
The pattern wey make sense to copy from this project na to pair fixed engine version with small bundled dataset wey dey ship inside the package itself.
Pin the install: pip install quantjourney-bt==0.12.4
quantjourney-bt na the QuantJourney backtester. E dey under Apache License 2.0, and e need Python 3.11 or newer. Dem publish version 0.12.4 on 21 July 2026. Na this version every command below dey refer to, according to the documentation for August 2026.
Work inside isolated environment. python3 -m venv .venv go create one, source .venv/bin/activate go enter am, and python -m pip install -U pip go update the installer inside am. Then use the exact version: pip install quantjourney-bt==0.12.4.
The project documentation show the unpinned form, pip install quantjourney-bt. Na you go handle the ==0.12.4 part, and for 0.x e dey important well-well. Write the pin where the next person fit find am: pip freeze > requirements.txt go record every resolved dependency, including the ones wey you no name.
Two optional extras dey. pip install "quantjourney-bt[wf]" go add Optuna for the walk-forward and optimization examples. pip install "quantjourney-bt[data]" go add a yfinance fallback wey dem use for benchmarks.
Apache-2.0 permissive. You fit use and modify the code for commercial work. You need keep the licence and notice files with any redistribution, and contributors dey grant patent rights explicitly.
How to run the bundled SMA example with no API key
The repository dey ship one launcher script together with fifty runnable example strategies. Dem split into weight-based path and order-based path. Five walk-forward workflows dey among them. ./strategy.sh --list dey print the catalog. ./strategy.sh example_weights_01_sma_daily --check dey import one strategy and e no touch any data at all. Na the fastest way to confirm say installation dey okay.
The demo run na one line: ./strategy.sh example_weights_01_sma_daily --sample-data --output /tmp/qj-sample
The --sample-data flag na the main point. The project describe the dataset behind am like this:
The sample dataset small and reproducible on purpose. E useful for installation checks, report generation, and to follow the engine flow without opening account.
Source: quantjourney-bt README, version 0.12.4, read 6 August 2026.
The run dey write directory instead of console verdict: summary.txt and summary.json, one metrics.csv, one equity_curve.csv beside its equity_curve.png, one dashboard.html, one plots/ folder, and one run_metadata.json wey record how dem configure the run. Na that last file most people dey skip, but na im make result auditable one year later.
Read the metrics wey come out with honesty. The bundled dataset small and illustrative, so the Sharpe ratio and the maximum drawdown wey summary.txt print describe one sample file. Dem no be evidence about any strategy. To treat dem as a result na the first mistake wey you fit make.
Wetin the run confirm worth having: the installation dey work, and the full engine flow, from signal to target weights to reconstructed portfolio value, dey produce its artifacts for your machine without one credential. Credentialed path to the project own data service dey available for backtests on real history. Dem document that path, and this walkthrough stop here, for the part wey no need anything from anybody.
One in-sample equity curve no fit tell you everything
The sample run dey draw one equity curve. But this curve no fit do some things, when you measure am with real market data instead of demo file.
The panel below dey use the same idea wey the example strategy use: 20-session moving average crossing 50-session moving average. E apply am to SPY, then e report each calendar year separately from 2017 reach 2025. The position for each session dey fixed by the previous session close. So the rule no ever trade with number wey e never get at that time.
The exact SQL behind every number
WITH daily AS
(
SELECT
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2016-01-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY d
),
averaged AS
(
SELECT
d,
px,
avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
d,
px,
if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
toYear(d) AS year,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
round((exp(sum(log(px / px_prior))) - 1) * 100, 1) AS hold_pct,
countIf(long_today != long_prior) AS crossover_count
FROM positioned
WHERE px_prior > 0
AND toYear(d) >= 2017
GROUP BY year
ORDER BY yearNa one unchanged rule, measured 9 different times. First read down the two percent columns before you read anything else. For 2017, the rule finish the year at 16%, against 19.4% for holding SPY through the same period. For 2025, those same two columns show 10.4% and 16.4%. The code dey identical for both rows. Na only the window change.
The crossover column dey show how small the underlying evidence be. 4 position changes across 2025 means say one full year of equity curve dey rest on only a few decisions. That one na very small sample to call am result.
Rule still dey behave the same way for other names?
Changing the date window na one way to examine one curve. Changing the universe na the other way. The panel below keep the parameters fixed and run the same rule on five liquid names across the five calendar years from 2021 to 2025.
The exact SQL behind every number
WITH daily AS
(
SELECT
ticker,
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('SPY', 'QQQ', 'AAPL', 'MSFT', 'KO')
AND window_start >= '2020-07-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY ticker, d
),
averaged AS
(
SELECT
ticker,
d,
px,
avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (PARTITION BY ticker ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
ticker,
d,
px,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
ticker AS symbol,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
round((exp(sum(log(px / px_prior))) - 1) * 100, 1) AS hold_pct,
round(avg(long_prior) * 100, 0) AS days_long_pct
FROM positioned
WHERE px_prior > 0
AND d >= toDate('2021-01-01')
GROUP BY symbol
ORDER BY rule_pct DESCQQQ dey on top of the panel at 40.2%, while the bottom row, KO, come in at 3.8%. The days_long_pct column show how much of the window each version spend holding anything at all, with 67% for the top row. Na one parameter set, five universes, and spread wide enough to show say picking the winner after the fact no tell us anything about the run we never make yet.
None of this one na recommendation to trade crossover. The crossover na measuring stick for the backtest, and na the backtest we dey measure.
Wey look-ahead bias dey enter weights backtest
Weights-based engine dey turn signal into target weights, simulate fills against those weights, then rebuild portfolio value from the positions wey result. The problem dey hide for the join between signal and weight. If today’s weight come from today’s close and then earn today’s return, the backtest don trade with information wey no dey available when the order suppose enter. Na look-ahead bias be that, and e no dey raise any error. E just make everything look better.
The panel below run both versions of one rule over the same SPY history.
The exact SQL behind every number
WITH daily AS
(
SELECT
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2016-01-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY d
),
averaged AS
(
SELECT
d,
px,
avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
d,
px,
if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
toYear(d) AS year,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS next_bar_pct,
round((exp(sum(log(if(long_today = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS same_bar_pct,
round(abs(exp(sum(log(if(long_today = 1, px / px_prior, 1.0))))
- exp(sum(log(if(long_prior = 1, px / px_prior, 1.0))))) * 100, 1) AS gap_pp
FROM positioned
WHERE px_prior > 0
AND toYear(d) >= 2017
GROUP BY year
ORDER BY yearFor 2017, the prior-session version print 16%, while the same-session version print 17.1%. The gap between dem na 1.1 percentage points. For 2025, the distance between both measure 1.6 percentage points. Na only one of those columns fit come from machine wey no already know where the session close. The difference between dem na pure accounting, with no idea, no skill and no trade behind am.
This engine explain its own position on the timing question, and that worth more than promise:
For fills at the open, range-sensitive slippage dey use only the previous completed bar, while volume capacity forecast dey come from lagged observations. The engine no use that day’s later high, low, close or full-day volume.
Source: quantjourney-bt README, version 0.12.4, read 6 August 2026.
You fit check documented assumption against the source wey you don already install. If assumption no dey documented, na guess.
Why walk-forward extra dey exist
The walk-forward examples, WF01 reach WF05, dey come with the [wf] extra and im Optuna dependency. Walk-forward dey fit parameters on one slice of history, measure dem on the slice wey follow, then move the pair forward and repeat am. The rolling and expanding variants different for whether the fitting window dey drop im oldest data as e dey advance. Another example add purge and embargo for each boundary, dropping the observations wey near the seam, so fitted slice no fit leak enter the slice wey dem dey use measure am.
None of dis one fit turn weak idea into working one. E replace one number with distribution of numbers wey you fit challenge, and na the whole upgrade be that. The next step still no be live money: paper trading before real money dey measure wetin backtest structurally no fit see, starting with whether your order fill anywhere near the price wey simulator assume. circuit breakers for trading bots cover wetin your code dey do on the day e no work. For the statistics underneath everything, our notes on the open-source quant trading book go deeper.
FAQ
You fit backtest strategy without API key?
Yes. quantjourney-bt come with small sample dataset wey hide behind --sample-data flag, and the example strategies dey run on top am without account or credentials. The dataset small and na mainly for illustration, so treat the run as install and pipeline check, no be proof say strategy go work.
Why you need pin the version of Python backtesting package?
Package wey dey 0.x no get compatibility guarantee between minor releases. If dem change default or rename metric, package no go necessarily tell you. If you pin am with pip install quantjourney-bt==0.12.4 and record the environment for requirements file, result wey you produce today fit rebuild next year with the same engine wey produce am.
Which licence quantjourney-bt dey use?
Apache License 2.0. E allow commercial use and modification. E ask make you keep the licence and notice files with any redistribution. E also include clear patent grant from contributors. Version 0.12.4 publish on 21 July 2026, and e require Python 3.11 or newer.
Strong backtest result mean say the strategy dey work?
No. Backtest na one measurement, for one window and one universe. The panels above show one unchanged rule wey produce very different yearly figures for one ticker, plus very different figures across five names. Na this gap walk-forward validation and out-of-sample testing dey designed to expose.
How dem calculate the panels above
Daily closes na the last regular-session minute print for each date. Dem take am using New York clock time between 9:30 a.m. and 4:00 p.m. This one keep half-days wey market close early correct, without hardcoding session length. Fast average cover 20 sessions, while slow average cover 50. Both na simple averages. The first 49 sessions for every series na warm-up period, and the rule no hold position during that time. Yearly figures compound each session’s close-to-close move for sessions wey the rule hold long position. The hold column compound every session for the same year for comparison. Dem pick the five names for the cross-section because dem get continuous histories and no split inside the window, so the close series no need adjustment. The windows fixed for the past, so the panels return the same numbers every time dem regenerate am.
Every panel here get the exact SQL underneath am. This one make the numbers on this page as easy to rerun as the pinned install. To measure rule across your own window before you write any backtest code, ask the question in plain English on the Strasmore terminal.