Strasmore Research
Siasatan Matt ConnorOleh Matt Connor · Dikemas kini 2026-08-08

Cara Backtest Python Boleh Dihasilkan Semula Tanpa API

Jalankan backtest Python yang boleh dihasilkan semula tanpa kunci API menggunakan pemasangan versi tetap dan data sampel. Ketahui batasan sebenar lengkung ekuiti anda.

2025
Backtest yang boleh dihasilkan semula dalam Python ialah backtest yang boleh dijalankan semula oleh orang asing pada mesin yang bersih dan menghasilkan angka yang tepat seperti anda, tanpa halangan akaun atau kunci API. Kebanyakan tutorial gagal dalam ujian tersebut pada baris pertama, di mana muat turun secara langsung memberikan sejarah harga yang sedikit berbeza kepada pembaca berbanding apa yang diperoleh penulis. Panduan ini menetapkan satu enjin sumber terbuka, quantjourney-bt pada versi 0.12.4, menjalankan contoh yang disertakan tanpa sebarang kelayakan, kemudian menggunakan data pasaran sebenar untuk menunjukkan perkara yang tidak dapat diberitahu oleh satu larian bersih.

Apakah yang menjadikan ujian semula (backtest) boleh dihasilkan semula?

Kebolehhasilan semula mempunyai maksud yang sempit dan boleh diuji di sini: orang kedua menjalankan satu arahan pada mesin yang bersih dan mendapat nombor yang sama sehingga ke titik perpuluhan. Dua perkara biasa merosakkannya.

Perkara pertama ialah kod. Pustaka pada versi 0.x tidak membawa janji keserasian antara keluaran kecil. Penamaan semula, tetapan lalai yang diubah, lajur yang disusun semula, dan skrip anda terus berjalan sambil melaporkan sesuatu yang lain secara senyap.

Perkara kedua ialah data. Tutorial yang baris pertamanya adalah muat turun secara langsung tidak pernah boleh dihasilkan semula. Vendor menyemak semula sejarah, melaraskan untuk pecahan saham (splits), dan mengisi jurang data, dan skrip yang sama mencetak nombor yang berbeza sebulan kemudian. Selepas fakta berlaku, anda tidak boleh memisahkan perubahan kod daripada perubahan data.

Corak yang wajar ditiru daripada projek ini ialah gandingan versi enjin yang tetap dengan set data kecil yang dibundel yang disertakan di dalam pakej itu sendiri.

Tetapkan pemasangan: pip install quantjourney-bt==0.12.4

quantjourney-bt ialah penguji belakang (backtester) QuantJourney, yang dikeluarkan di bawah Lesen Apache 2.0 dan memerlukan Python 3.11 atau lebih baharu. Versi 0.12.4 telah diterbitkan pada 21 Julai 2026, dan ia merupakan versi yang dirujuk oleh setiap arahan di bawah, seperti yang didokumenkan pada Ogos 2026.

Bekerja di dalam persekitaran terasing. python3 -m venv .venv mencipta satu persekitaran, source .venv/bin/activate memasukinya, dan python -m pip install -U pip mengemas kini pemasang di dalamnya. Kemudian ambil versi tepat: pip install quantjourney-bt==0.12.4.

Projek tersebut mendokumenkan bentuk tanpa pin, pip install quantjourney-bt. Bahagian ==0.12.4 adalah tanggungjawab anda, dan pada 0.x ia membuktikan nilainya. Tuliskan pin tersebut di tempat yang akan ditemui oleh orang seterusnya: pip freeze > requirements.txt menangkap setiap kebergantungan yang diselesaikan, termasuk yang tidak pernah anda namakan.

Terdapat dua tambahan pilihan. pip install "quantjourney-bt[wf]" menambah Optuna untuk contoh walk-forward dan pengoptimuman. pip install "quantjourney-bt[data]" menambah sandaran yfinance yang digunakan untuk penanda aras.

Apache-2.0 adalah bersifat permisif. Anda boleh menggunakan dan mengubah suai kod tersebut secara komersial, anda perlu mengekalkan fail lesen dan notis dengan sebarang pengagihan semula, dan penyumbang memberikan hak paten secara eksplisit.

Cara menjalankan contoh SMA terangkum tanpa kunci API

Repositori ini membekalkan skrip pelancar bersama lima puluh strategi contoh yang boleh dijalankan, dibahagikan kepada laluan berasaskan wajaran dan laluan berasaskan pesanan, dengan lima aliran kerja walk-forward antaranya. ./strategy.sh --list mencetak katalog tersebut. ./strategy.sh example_weights_01_sma_daily --check mengimport satu strategi dan tidak menyentuh sebarang data, yang merupakan pengesahan paling pantas bahawa pemasangan adalah sempurna.

Jalankan demo itu sendiri hanya dengan satu baris: ./strategy.sh example_weights_01_sma_daily --sample-data --output /tmp/qj-sample

Bendera --sample-data membawa maksud keseluruhan perkara ini. Projek tersebut menerangkan set data di sebaliknya seperti berikut:

Set data sampel sengaja dibuat kecil dan boleh dihasilkan semula. Ia berguna untuk pemeriksaan pemasangan, penjanaan laporan, dan membaca aliran enjin tanpa perlu mencipta akaun.
Sumber: quantjourney-bt README, versi 0.12.4, dibaca pada 6 Ogos 2026.

Jalankan tersebut menulis direktori dan bukannya keputusan konsol: summary.txt dan summary.json, satu metrics.csv, satu equity_curve.csv di sebelah equity_curve.png-nya, satu dashboard.html, satu folder plots/, dan satu run_metadata.json yang merekodkan bagaimana jalankan tersebut dikonfigurasikan. Fail terakhir itu adalah fail yang paling kerap diabaikan oleh orang ramai dan fail yang menjadikan sesuatu keputusan boleh diaudit setahun kemudian.

Baca metrik yang terhasil dengan jujur. Set data terangkum adalah kecil dan bersifat ilustrasi, jadi Sharpe ratio dan maximum drawdown yang dicetak dalam summary.txt hanya menerangkan fail sampel. Ia bukan bukti mengenai sesuatu strategi, dan menganggapnya sebagai keputusan adalah kesilapan pertama yang boleh anda lakukan.

Apa yang dibuktikan oleh jalankan tersebut adalah berbaloi untuk dimiliki: pemasangan berfungsi, dan aliran enjin penuh, daripada isyarat kepada wajaran sasaran sehingga nilai portfolio yang dibina semula, menghasilkan artifaknya pada mesin anda tanpa memerlukan satu pun kelayakan. Laluan berkelayakan ke perkhidmatan data projek itu sendiri wujud untuk backtest pada sejarah sebenar. Laluan itu didokumenkan, dan panduan ini berakhir di sini, pada bahagian yang tidak memerlukan apa-apa daripada sesiapa.

Apa yang tidak diberitahu oleh satu keluk ekuiti dalam sampel

Larian sampel melukis satu keluk ekuiti. Berikut adalah perkara yang tidak boleh dilakukan oleh keluk tersebut, diukur berdasarkan data pasaran sebenar dan bukannya fail demo.

Panel di bawah menggunakan idea yang sama seperti strategi contoh, iaitu purata bergerak dua puluh sesi yang bersilang dengan purata bergerak lima puluh sesi, mengaplikasikannya kepada SPY, dan melaporkan setiap tahun kalendar secara berasingan dari tahun dua ribu tujuh belas hingga dua ribu dua puluh lima. Posisi dalam setiap sesi ditetapkan oleh harga penutup sesi sebelumnya, jadi peraturan tersebut tidak pernah berdagang berdasarkan angka yang belum diperoleh.

PertanyaanPersilangan purata bergerak 20/50 pada SPY, mengikut tahun, berbanding pegangan
SQL tepat di sebalik setiap nombor
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 year
Run this yourself

Satu peraturan yang tidak berubah, diukur sebanyak 9 kali. Baca dua lajur peratusan ke bawah sebelum membaca perkara lain. Pada 2017, peraturan tersebut menamatkan tahun pada 16% berbanding 19.4% bagi memegang SPY dalam tempoh yang sama. Pada 2025, dua lajur yang sama menunjukkan 10.4% dan 16.4%. Kodnya adalah sama dalam kedua-dua baris. Hanya tetingkap masa yang berubah.

Lajur silangan menunjukkan betapa nipisnya bukti asas yang diperoleh. 4 perubahan posisi sepanjang 2025 bermakna satu tahun penuh keluk ekuiti hanya bersandarkan kepada segelintir keputusan, yang merupakan sampel yang sangat kecil untuk dianggap sebagai satu hasil.

Adakah peraturan yang sama bertindak dengan cara yang sama pada nama lain?

Menukar tetingkap tarikh adalah satu cara untuk meneliti satu keluk. Menukar universe adalah cara yang lain. Panel di bawah mengekalkan parameter tetap dan menjalankan peraturan yang sama pada lima nama yang mempunyai kecairan tinggi sepanjang lima tahun kalendar dari dua ribu dua puluh satu hingga dua ribu dua puluh lima.

PertanyaanPeraturan 20/50 yang sama pada lima nama cair, 2021 hingga 2025
SQL tepat di sebalik setiap nombor
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 DESC
Run this yourself

QQQ berada di bahagian atas panel pada 40.2%, dan baris bawah, KO, berada pada 3.8%. Lajur days_long_pct melaporkan berapa banyak tetingkap yang dihabiskan oleh setiap versi untuk memegang sebarang aset, 67% untuk baris atas. Satu set parameter, lima universe, dan spread yang cukup luas sehingga memilih pemenang selepas fakta tidak memberikan sebarang petunjuk tentang dagangan yang belum anda lakukan.

Tiada satu pun daripada ini merupakan cadangan untuk mendagangkan crossover. Crossover ialah kayu pengukur untuk backtest, dan backtest ialah perkara yang sedang kami ukur.

Di mana look-ahead bias menyelinap ke dalam backtest wajaran

Enjin berasaskan wajaran menukarkan isyarat kepada wajaran sasaran, mensimulasikan pelaksanaan pesanan (fills) berdasarkan wajaran tersebut, kemudian membina semula nilai portfolio daripada posisi yang terhasil. Kegagalan tersembunyi pada titik pertemuan antara isyarat dan wajaran. Jika wajaran hari ini diperoleh daripada harga penutup hari ini dan kemudian memperoleh pulangan hari ini, backtest tersebut telah berdagang menggunakan maklumat yang belum wujud semasa pesanan sepatutnya dihantar. Itulah look-ahead bias, dan ia tidak mencetuskan sebarang ralat. Ia hanya menjadikan segala-galanya kelihatan lebih baik.

Panel di bawah menjalankan kedua-dua versi bagi satu peraturan ke atas sejarah SPY yang sama.

PertanyaanPeraturan yang sama, isyarat sesi terdahulu berbanding isyarat sesi semasa, SPY mengikut tahun
SQL tepat di sebalik setiap nombor
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 year
Run this yourself

Dalam 2017, versi sesi terdahulu mencatatkan 16% manakala versi sesi yang sama mencatatkan 17.1%, dengan perbezaan sebanyak 1.1 mata peratusan. Dalam 2025, jarak antara kedua-duanya diukur sebanyak 1.6 mata peratusan. Hanya satu daripada lajur tersebut boleh dihasilkan oleh mesin yang tidak mengetahui di mana sesi ditutup, dan perbezaan antara keduanya hanyalah perakaunan semata-mata, tanpa idea, tanpa kemahiran, dan tanpa perdagangan yang mendasarinya.

Enjin ini menyatakan posisinya sendiri mengenai persoalan pemasaan, yang lebih bernilai daripada sekadar janji:

Bagi pelaksanaan pesanan pada harga pembukaan, slippage yang sensitif terhadap julat hanya melihat bar lengkap sebelumnya dan kapasiti volum diramal daripada pemerhatian yang tertinggal (lagged); enjin tidak menggunakan harga tinggi, rendah, penutup atau volum penuh hari tersebut.
Sumber: quantjourney-bt README, versi 0.12.4, dibaca pada 6 Ogos 2026.

Andaian yang didokumenkan boleh disemak berbanding sumber yang telah anda pasang. Andaian yang tidak didokumenkan hanyalah satu tekaan.

Mengapa tambahan walk-forward wujud

Contoh walk-forward, WF01 hingga WF05, didatangkan dengan tambahan [wf] serta kebergantungan Optuna. Walk-forward memadankan parameter pada satu bahagian sejarah, mengukurnya pada bahagian yang menyusul, kemudian menggerakkan pasangan tersebut ke hadapan dan mengulanginya. Varian rolling dan expanding berbeza dari segi sama ada tetingkap pemadanan menggugurkan data paling lama apabila ia bergerak ke hadapan. Satu lagi contoh menambah proses pembersihan (purge) dan embargo pada setiap sempadan, dengan menggugurkan pemerhatian yang paling hampir dengan titik pertemuan, supaya bahagian yang telah dipadankan tidak boleh bocor ke dalam bahagian yang sedang diukur.

Tiada satu pun daripada proses ini mengubah idea yang lemah menjadi idea yang berkesan. Ia menggantikan satu angka dengan taburan angka yang boleh anda pertikaikan, dan itulah keseluruhan peningkatan tersebut. Langkah selepas itu masih belum melibatkan wang sebenar: dagangan kertas sebelum wang sebenar mengukur perkara yang tidak dapat dilihat secara struktur oleh backtest, bermula dengan sama ada pesanan anda dipenuhi berhampiran harga yang diandaikan oleh simulator, dan pemutus litar untuk bot dagangan merangkumi tindakan kod anda pada hari ia tidak berfungsi. Bagi statistik di sebalik semua ini, nota kami mengenai buku dagangan kuantitatif sumber terbuka pergi ke tahap yang lebih mendalam.

Soalan Lazim

Bolehkah anda melakukan backtest sesuatu strategi tanpa kunci API?

Ya. quantjourney-bt didatangkan dengan set data sampel yang dibundel di sebalik tanda --sample-data, dan strategi contohnya boleh dijalankan menggunakan set data tersebut tanpa akaun atau kelayakan. Set data ini kecil dan bersifat ilustrasi, jadi anggaplah percubaan tersebut sebagai semakan pemasangan dan talian paip (pipeline) dan bukannya sebagai bukti keberkesanan sesuatu strategi.

Mengapa perlu menetapkan (pin) versi pakej backtesting Python?

Pakej pada versi 0.x tidak membawa jaminan keserasian antara keluaran minor, dan perubahan pada tetapan lalai atau metrik yang dinamakan semula tidak akan dimaklumkan. Menetapkan versi dengan pip install quantjourney-bt==0.12.4 dan merekodkan persekitaran dalam fail keperluan (requirements file) bermakna hasil yang anda peroleh hari ini boleh dibina semula pada tahun hadapan menggunakan enjin yang sama.

Di bawah lesen apakah quantjourney-bt dikeluarkan?

Lesen Apache 2.0. Ia membenarkan penggunaan komersial dan pengubahsuaian, meminta anda mengekalkan fail lesen dan notis bersama sebarang pengedaran semula, serta merangkumi pemberian paten eksplisit daripada penyumbang. Versi 0.12.4 telah diterbitkan pada 21 Julai 2026 dan memerlukan Python 3.11 atau lebih baharu.

Adakah hasil backtest yang kukuh bermakna strategi itu berkesan?

Tidak. Backtest hanyalah satu ukuran, dalam satu tempoh masa, pada satu alam semesta (universe) aset. Panel di atas menunjukkan satu peraturan yang tidak berubah menghasilkan angka tahunan yang sangat berbeza pada satu ticker dan angka yang sangat berbeza merentas lima nama, iaitu jurang yang bertujuan didedahkan oleh pengesahan walk-forward dan ujian out-of-sample.

Bagaimana panel di atas dikira

Harga penutup harian adalah print minit sesi biasa terakhir bagi setiap tarikh, diambil mengikut waktu New York antara 9:30 pagi dan 4:00 petang, yang memastikan hari separuh sesi (early-close) adalah tepat tanpa perlu mengekod panjang sesi secara manual. Purata pantas meliputi 20 sesi dan purata perlahan 50 sesi, kedua-duanya adalah purata mudah, dan 49 sesi pertama bagi setiap siri adalah tempoh pemanasan (warm-up) yang tidak memegang sebarang posisi. Angka tahunan mengkompaun pergerakan close-to-close setiap sesi bagi sesi di mana peraturan tersebut berada dalam posisi long, dan lajur pegangan (hold) mengkompaun setiap sesi dalam tahun yang sama untuk perbandingan. Lima nama dalam keratan rentas (cross-section) dipilih kerana mempunyai sejarah berterusan tanpa sebarang pecahan (split) dalam tempoh tersebut, jadi siri harga penutup tidak memerlukan pelarasan. Tempoh masa ditetapkan pada masa lalu, jadi panel ini menghasilkan nombor yang sama pada setiap penjanaan semula.


Setiap panel di sini membawa kod SQL tepat di bawahnya, yang menjadikan nombor pada halaman ini boleh dijalankan semula seperti pemasangan yang telah ditetapkan versinya. Untuk mengukur sesuatu peraturan sepanjang tempoh masa anda sendiri sebelum menulis sebarang kod backtest, ajukan soalan dalam bahasa Inggeris yang mudah pada terminal Strasmore.

#backtesting#python#open source#reproducibility#quantjourney