Reproduceerbare backtest in Python zonder API key
Voer een reproduceerbare backtest uit in Python zonder API key. Gebruik een vaste installatie met deterministische data en leer wat een equity curve voor u verborgen houdt.
Reproduceerbare backtests in Python
Een reproduceerbare backtest in Python is een test die een buitenstaander op een schone machine opnieuw kan uitvoeren om exact dezelfde cijfers te verkrijgen, zonder dat daarvoor een account of API-sleutel nodig is. De meeste handleidingen falen al bij de eerste regel, waarbij een live download de volgende lezer een net iets andere prijshistorie voorschotelt dan de auteur ontving. Deze walkthrough legt de focus op één open-source engine, quantjourney-bt in versie 0.12.4, voert het bijgeleverde voorbeeld uit zonder inloggegevens, en gebruikt vervolgens echte marktdata om aan te tonen wat een enkele schone run u nog steeds niet kan vertellen.
Wat maakt een backtest reproduceerbaar?
Reproduceerbaarheid heeft hier een nauwe, toetsbare betekenis: een tweede persoon voert één commando uit op een schone machine en krijgt exact uw cijfers terug, tot op de decimaal. Twee alledaagse zaken verstoren dit proces.
De eerste is de code. Een library met versie 0.x biedt geen enkele garantie op compatibiliteit tussen minor releases. Een naamswijziging, een gewijzigde standaardinstelling of een kolom die van volgorde verandert, zorgt ervoor dat uw script blijft draaien terwijl het stilletjes andere resultaten rapporteert.
De tweede is de data. Een tutorial waarvan de eerste regel een live download bevat, was nooit reproduceerbaar. Dataleveranciers herzien historische gegevens, corrigeren voor aandelensplitsingen en vullen gaten aan, waardoor hetzelfde script een maand later andere cijfers genereert. Achteraf kunt u een wijziging in de code niet meer onderscheiden van een wijziging in de data.
Het patroon dat het waard is om van dit project over te nemen, is het koppelen van een vaste engine-versie aan een kleine, gebundelde dataset die in het pakket zelf wordt meegeleverd.
Installatie vastleggen: pip install quantjourney-bt==0.12.4
quantjourney-bt is de QuantJourney backtester, uitgebracht onder de Apache License 2.0 en vereist Python 3.11 of nieuwer. Versie 0.12.4 werd gepubliceerd op 21 juli 2026 en is de versie waarnaar elk onderstaand commando verwijst, zoals gedocumenteerd in augustus 2026.
Werk binnen een geïsoleerde omgeving. python3 -m venv .venv maakt er een aan, source .venv/bin/activate opent deze en python -m pip install -U pip werkt het installatieprogramma daarin bij. Gebruik vervolgens de exacte versie: pip install quantjourney-bt==0.12.4.
Het project documenteert de niet-vastgelegde vorm, pip install quantjourney-bt. Het ==0.12.4-gedeelte is uw eigen verantwoordelijkheid en bij een 0.x-versie bewijst het zijn waarde. Noteer de pin op een plek waar de volgende persoon deze kan vinden: pip freeze > requirements.txt legt elke opgeloste dependency vast, inclusief de dependencies die u nooit expliciet heeft benoemd.
Er zijn twee optionele extra's beschikbaar. pip install "quantjourney-bt[wf]" voegt Optuna toe voor de walk-forward en optimalisatievoorbeelden. pip install "quantjourney-bt[data]" voegt een yfinance-fallback toe die wordt gebruikt voor benchmarks.
Apache-2.0 is permissief. U mag de code commercieel gebruiken en aanpassen, u behoudt de licentie- en kennisgevingsbestanden bij elke herdistributie en bijdragers verlenen expliciet octrooirechten.
Hoe het gebundelde SMA-voorbeeld uit te voeren zonder API-key
De repository bevat een launcher-script naast vijftig uitvoerbare voorbeeldstrategieën, onderverdeeld in een pad op basis van wegingen en een pad op basis van orders, waaronder vijf walk-forward workflows. ./strategy.sh --list print de catalogus. ./strategy.sh example_weights_01_sma_daily --check importeert een enkele strategie en raakt geen data aan; dit is de snelste bevestiging dat een installatie correct is verlopen.
De demo-run zelf bestaat uit één regel: ./strategy.sh example_weights_01_sma_daily --sample-data --output /tmp/qj-sample
De --sample-data-vlag is het belangrijkste onderdeel. Het project beschrijft de achterliggende dataset als volgt:
De voorbeelddataset is bewust klein en reproduceerbaar. Deze is nuttig voor installatiecontroles, het genereren van rapporten en het inzichtelijk maken van de engine-flow zonder dat er een account nodig is.
Bron: quantjourney-bt README, versie 0.12.4, gelezen op 6 augustus 2026.
De run schrijft een directory in plaats van een oordeel in de console: summary.txt en summary.json, een metrics.csv, een equity_curve.csv naast de equity_curve.png, een dashboard.html, een plots/-map en een run_metadata.json waarin wordt vastgelegd hoe de run was geconfigureerd. Dat laatste bestand is wat de meeste mensen overslaan, terwijl het juist zorgt dat een resultaat een jaar later nog controleerbaar is.
Beoordeel de resulterende metrics kritisch. De gebundelde dataset is klein en illustratief, dus de Sharpe ratio en de maximum drawdown die in summary.txt worden geprint, beschrijven slechts een voorbeeldbestand. Het is geen bewijs voor de werking van een strategie; deze als resultaat beschouwen is de eerste fout die u kunt maken.
Wat de run wel aantoont, is waardevol: de installatie werkt en de volledige engine-flow, van signaal naar doelwegingen tot de gereconstrueerde portefeuillewaarde, produceert zijn artefacten op uw machine zonder dat er inloggegevens nodig zijn. Er bestaat een pad met inloggegevens naar de eigen dataservice van het project voor backtests op echte historische data. Dat pad is gedocumenteerd, en deze walkthrough stopt hier, bij het onderdeel waarvoor niets van derden vereist is.
Wat één in-sample equity curve u niet vertelt
De sample run tekent één equity curve. Hieronder staat wat die curve niet kan, gemeten aan de hand van reële marktdata in plaats van een demobestand.
Het onderstaande paneel hanteert hetzelfde idee als de voorbeeldstrategie, een voortschrijdend gemiddelde van twintig sessies dat een gemiddelde van vijftig sessies kruist, past dit toe op SPY en rapporteert elk kalenderjaar afzonderlijk van tweeduizendzeventien tot en met tweeduizendvijfentwintig. De positie in elke sessie wordt bepaald door de slotkoers van de voorgaande sessie, waardoor de regel nooit handelt op basis van een getal waarover hij nog niet beschikte.
De exacte SQL achter elk getal
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 yearEén ongewijzigde regel, gemeten 9 afzonderlijke keren. Lees de twee procentkolommen voordat u iets anders leest. In 2017 eindigde de regel het jaar op 16% tegenover 19.4% voor het aanhouden van SPY over dezelfde periode. In 2025 staan in diezelfde twee kolommen 10.4% en 16.4%. De code is in beide rijen identiek. Alleen het venster is verschoven.
De crossover-kolom laat zien hoe dun het onderliggende bewijs wordt. 4 positiewijzigingen over 2025 betekent dat een volledig jaar aan equity curve rust op een handvol beslissingen, wat een zeer kleine steekproef is om een resultaat te noemen.
Geldt dezelfde regel ook voor andere namen?
Het aanpassen van het tijdsvenster is één manier om een curve te analyseren. Het wijzigen van het universum is de andere. Het onderstaande overzicht houdt de parameters gelijk en past de identieke regel toe op vijf liquide namen over de vijf kalenderjaren 2021 tot en met 2025.
De exacte SQL achter elk getal
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 staat bovenaan het overzicht op 40.2%, en de onderste rij, KO, komt uit op 3.8%. De kolom days_long_pct rapporteert welk deel van het venster elke versie daadwerkelijk een positie aanhield, 67% voor de bovenste rij. Eén parameterset, vijf universa en een spread die groot genoeg is om aan te tonen dat het achteraf kiezen van de winnaar niets zegt over de run die u nog moet maken.
Niets hiervan is een aanbeveling om een crossover te traden. De crossover is een meetlat voor de backtest, en de backtest is wat wij meten.
Waar look-ahead bias in een backtest van wegingen sluipt
Een op wegingen gebaseerde engine zet een signaal om in doelwegingen, simuleert uitvoeringen op basis van die wegingen en herberekent vervolgens de portefeuillewaarde op basis van de resulterende posities. De fout schuilt in de koppeling tussen het signaal en de weging. Als de weging van vandaag gebaseerd is op de slotkoers van vandaag en vervolgens het rendement van vandaag behaalt, heeft de backtest gehandeld op basis van informatie die nog niet bestond op het moment dat de order zou zijn ingelegd. Dat is look-ahead bias, en dit genereert geen foutmelding. Het laat alles er simpelweg beter uitzien.
Onderstaand overzicht toont beide versies van één regel over dezelfde SPY-historie.
De exacte SQL achter elk getal
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 yearIn 2017 genereerde de versie op basis van de voorgaande sessie 16%, terwijl de versie op basis van dezelfde sessie 17.1% genereerde, een verschil van 1.1 procentpunt. In 2025 bedroeg het verschil tussen beide 1.6 procentpunt. Slechts één van die kolommen kan worden geproduceerd door een systeem dat niet vooraf wist waar de sessie zou sluiten; het verschil tussen beide is puur boekhoudkundig, zonder enig idee, vaardigheid of transactie als basis.
Deze engine neemt zelf een standpunt in over de timingkwestie, wat meer waard is dan een belofte:
Voor uitvoeringen bij de opening houdt range-sensitive slippage alleen rekening met de voorgaande voltooide bar en wordt de volumecapaciteit voorspeld op basis van vertraagde observaties; de engine gebruikt niet de high, low, close of het volledige dagvolume van die dag.
Bron: quantjourney-bt README, versie 0.12.4, gelezen op 6 augustus 2026.
Een gedocumenteerde aanname kan worden getoetst aan de bron die u reeds heeft geïnstalleerd. Een ongedocumenteerde aanname is slechts een gok.
Waarom de walk-forward extra bestaat
De walk-forward voorbeelden, WF01 tot en met WF05, worden geleverd met de [wf] extra en de bijbehorende Optuna-afhankelijkheid. Bij walk-forward worden parameters gefit op één historisch segment, gemeten op het daaropvolgende segment, waarna het paar wordt opgeschoven en het proces zich herhaalt. De rolling- en expanding-varianten verschillen in de vraag of het fit-venster de oudste data verwijdert naarmate het vordert. Een ander voorbeeld voegt een purge en embargo toe bij elke grens, waarbij de observaties die het dichtst bij de overgang liggen worden verwijderd, zodat een gefit segment niet kan lekken naar het segment waarop het wordt gemeten.
Niets daarvan maakt van een zwak idee een werkend concept. Het vervangt een enkel getal door een verdeling van getallen waarover gediscussieerd kan worden, en dat is de volledige verbetering. De stap daarna is nog steeds geen echt geld: paper trading voordat u met echt geld belegt meet wat een backtest structureel niet kan zien, te beginnen met de vraag of uw order wordt uitgevoerd tegen een prijs die in de buurt ligt van de prijs die de simulator aannam, en circuit breakers voor trading bots behandelen wat uw code doet op de dag dat het misgaat. Voor de statistiek die hieraan ten grondslag ligt, gaan onze aantekeningen in het open-source quant trading boek een niveau dieper.
Veelgestelde vragen
Kunt u een strategie backtesten zonder API-sleutel?
Ja. quantjourney-bt wordt geleverd met een gebundelde voorbeeld-dataset achter een --sample-data-vlag, en de voorbeeldstrategieën draaien hierop zonder account of inloggegevens. De dataset is klein en illustratief; beschouw een dergelijke run daarom als een installatie- en pipeline-controle en niet als bewijs voor de werking van een strategie.
Waarom de versie van een Python backtesting-pakket vastzetten?
Een pakket in de 0.x-fase biedt geen compatibiliteitsgarantie tussen minor releases; een gewijzigde standaardinstelling of een hernoemde metriek kondigt zichzelf niet aan. Het vastzetten met pip install quantjourney-bt==0.12.4 en het vastleggen van de omgeving in een requirements-bestand zorgt ervoor dat een resultaat dat u vandaag genereert, volgend jaar opnieuw kan worden opgebouwd met de engine die het heeft geproduceerd.
Onder welke licentie wordt quantjourney-bt uitgegeven?
Apache License 2.0. Deze staat commercieel gebruik en aanpassing toe, vereist dat u de licentie- en kennisgevingsbestanden bij herdistributie behoudt, en bevat een expliciete octrooitoekenning van bijdragers. Versie 0.12.4 werd gepubliceerd op 21 juli 2026 en vereist Python 3.11 of nieuwer.
Betekent een sterk backtest-resultaat dat de strategie werkt?
Nee. Een backtest is één meting, over één tijdvenster, op één universum. De bovenstaande panelen tonen hoe één ongewijzigde regel zeer verschillende jaarcijfers oplevert voor één ticker en zeer uiteenlopende cijfers over vijf namen; dit is precies het gat dat walk-forward validatie en out-of-sample testen moeten blootleggen.
Hoe de bovenstaande panelen zijn berekend
Dagelijkse slotkoersen zijn de laatste prints van de reguliere handelssessie voor elke datum, genomen in New York-tijd tussen 09:30 en 16:00 uur. Hierdoor blijven dagen met een vervroegde sluiting correct zonder dat een sessielengte hard hoeft te worden gecodeerd. Het snelle gemiddelde beslaat twintig sessies en het trage gemiddelde vijftig; beide zijn enkelvoudig en de eerste negenenveertig sessies van elke reeks dienen als opwarmperiode waarin geen positie wordt ingenomen. Jaarcijfers berekenen het samengestelde rendement van de close-to-close beweging van elke sessie waarin de regel een long-positie aanhield, en de 'hold'-kolom berekent ter vergelijking het samengestelde rendement van elke sessie in hetzelfde jaar. De vijf namen in de dwarsdoorsnede zijn geselecteerd op basis van hun continue historie zonder aandelensplitsing binnen het venster, waardoor de reeks slotkoersen geen correctie behoeft. Vensters liggen vast in het verleden, waardoor deze panelen bij elke herberekening dezelfde cijfers opleveren.
Elk paneel hier bevat de exacte SQL die eronder ligt, waardoor de cijfers op deze pagina net zo reproduceerbaar zijn als de vastgezette installatie. Om een regel over uw eigen tijdvenster te meten voordat u backtest-code schrijft, kunt u de vraag in begrijpelijk Engels stellen op de Strasmore-terminal.