Python 可重現回測,無需 API key
使用固定版本與確定性範例資料,在無需 API key 的情況下執行 Python 回測,並誠實解讀單一權益曲線無法揭示的資訊。
可重現的 Python 回測,必須讓陌生人能在乾淨的電腦上重新執行,並在不需要帳戶或 API key 的情況下,取得完全相同的數字。多數教學在第一行就無法通過這項測試,因為即時下載的資料,可能讓下一位讀者取得與作者當初略有不同的價格歷史。本篇固定使用一個開源引擎 quantjourney-bt 0.12.4 版,在完全不提供憑證的情況下執行其內建範例,接著使用真實市場資料,說明即使是一次乾淨的執行,仍無法告訴你哪些事情。
什麼樣的回測才具備可重現性?
這裡所說的可重現性,有明確且可測試的定義:第二位使用者在一台乾淨的機器上執行一個指令,就能將你的數字精確重現到小數位。兩個常見因素會破壞這點。
第一個因素是程式碼。版本號為 0.x 的函式庫,不保證小版本之間的相容性。只要函式改名、預設值變更或欄位順序調整,程式就可能繼續執行,卻在不易察覺的情況下回報不同結果。
第二個因素是資料。第一行就是即時下載資料的教學,從未真正具備可重現性。資料供應商可能修訂歷史資料、調整拆股資料,或補回缺漏;因此,同一支程式在一個月後可能會輸出不同數字。事後你無法分辨差異究竟來自程式碼變更,還是資料變更。
這個專案值得仿效的做法,是將固定版本的引擎,與一份隨套件一同提供的小型內建資料集搭配使用。
固定安裝版本:pip install quantjourney-bt==0.12.4
quantjourney-bt 是 QuantJourney 回測器,採用 Apache License 2.0 授權,且要求 Python 3.11 或更新版本。0.12.4 版發布於 2026年7月21日;截至 2026年8月,以下每個指令均以此版本為準。
請在隔離環境中操作。python3 -m venv .venv 會建立隔離環境,source .venv/bin/activate 會進入該環境,python -m pip install -U pip 則會更新其中的安裝工具。接著安裝確切版本:pip install quantjourney-bt==0.12.4。
專案文件記載的是未固定版本的形式 pip install quantjourney-bt。==0.12.4 部分由你負責;在 0.x 版本階段,固定版本尤其重要。請將版本固定資訊寫入下一位使用者找得到的位置:pip freeze > requirements.txt 會記錄所有解析後的相依套件,包括你從未明確指定的套件。
另有兩個可選附加功能。pip install "quantjourney-bt[wf]" 會加入 Optuna,用於 walk-forward 與最佳化範例。pip install "quantjourney-bt[data]" 會加入 yfinance 備援方案,用於基準比較。
Apache-2.0 授權條款較為寬鬆。你可以將程式碼用於商業用途並加以修改;重新發布時,須保留授權與著作權聲明文件,且貢獻者會明確授予專利權。
如何在沒有 API key 的情況下執行內建的 SMA 範例
此儲存庫附帶一個啟動腳本,以及 fifty 個可執行的範例策略。這些策略分為權重路徑與訂單路徑,其中包含 five 個 walk-forward 工作流程。./strategy.sh --list 會列出目錄。./strategy.sh example_weights_01_sma_daily --check 只會匯入單一策略,不會接觸任何資料,是確認安裝正常的最快方式。
示範執行只需一行指令:./strategy.sh example_weights_01_sma_daily --sample-data --output /tmp/qj-sample
--sample-data 旗標才是重點所在。專案對其背後資料集的描述如下:
此範例資料集刻意設計得小巧且可重現。它適合用於安裝檢查、報告產生,以及在無須建立帳戶的情況下了解引擎流程。
來源:quantjourney-bt README,版本 0.12.4,閱讀日期為 2026年8月6日。
執行結果會寫入目錄,而不是在主控台顯示判定結果:summary.txt 與 summary.json、一個 metrics.csv、一個與 equity_curve.png 放在一起的 equity_curve.csv、一個 dashboard.html、一個 plots/ 資料夾,以及一個記錄執行設定的 run_metadata.json。最後這個檔案最容易被忽略,但它能讓你在一年後仍可稽核結果。
請如實解讀產出的指標。內建資料集規模小,且僅供示意,因此 summary.txt 列出的 Sharpe ratio 與 最大回撤 只反映該範例檔案。它們不是策略有效性的證據;把這些數值當成結果,是最先會犯的錯誤。
這次執行真正能確認的事項很有價值:安裝程序正常,而且完整的引擎流程,從訊號到目標權重,再到重建投資組合價值,都能在你的電腦上產出相關檔案,完全不需要任何憑證。專案也提供需要憑證才能使用的自有資料服務,可用於真實歷史資料的回測。該路徑已有文件說明;本指南就在這裡結束,停在完全不需要向任何人索取資料的部分。
單一樣本內資產曲線無法告訴你的事
樣本執行結果會繪製一條資產曲線。但如果改用真實市場資料,而不是示範檔案,這條曲線無法代表以下事項。
下方表格採用與範例策略相同的概念:以20個交易日移動平均線上穿或下穿50個交易日移動平均線作為訊號,套用於 SPY,並分別列出2017年至2025年的各曆年結果。每個交易日的部位,均以前一交易日收盤價所能取得的資訊決定。因此,這項規則不會根據當時尚未取得的數值進行交易。
每個數據背後的精確 SQL 語法
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同一套未經修改的規則,分別衡量 9 次。先閱讀兩個百分比欄位,再看其他內容。在 2017,這項規則全年報酬為 16%;同期持有 SPY 的報酬則為 19.4%。在 2025,同兩欄的數值分別為 10.4% 與 16.4%。兩列使用完全相同的程式碼,唯一變動的是觀察區間。
交叉訊號欄顯示,基礎證據有多麼有限。4 次部位變動分布於 2025,代表一整年的資產曲線僅建立在少數幾次決策上。以這麼小的樣本,很難稱為具有代表性的結果。
其他標的也會呈現相同結果嗎?
變更日期區間,是檢視單一曲線的一種方式。變更標的範圍,則是另一種方式。下方面板固定所有參數,並將完全相同的規則套用於五檔流動性高的標的,涵蓋2021年至2025年這五個曆年。
每個數據背後的精確 SQL 語法
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位於面板頂端,數值為40.2%;最下列的KO則為3.8%。days_long_pct欄顯示各版本在該區間內實際持有任何部位的時間比例;頂列為67%。同一組參數、五個標的範圍,結果差距卻足以顯示,事後挑選績效最佳者,無法說明尚未執行的交易會有什麼結果。
這並不是建議交易均線交叉策略。均線交叉只是回測的衡量工具,而我們真正要衡量的是回測本身。
權重回測中的前視偏誤如何悄悄混入
以權重為基礎的引擎會先將訊號轉換為目標權重,再根據這些權重模擬成交,最後依據產生的部位重建投資組合價值。問題往往隱藏在訊號與權重的銜接處。若當日權重來自當日收盤價,卻接著用當日報酬計算績效,回測便等於使用了下單時尚不存在的資訊。這就是 前視偏誤,而且不會觸發任何錯誤。它只會讓所有結果看起來更好。
下方面板以相同的 SPY 歷史資料,執行同一項規則的兩個版本。
每個數據背後的精確 SQL 語法
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在 2017 中,前一交易時段版本顯示 16%,同一交易時段版本則顯示 17.1%,兩者相差 1.1 個百分點。在 2025 中,兩者的差距為 1.6 個百分點。只有其中一欄可能由一台尚未知道該交易時段收盤位置的機器產生;兩者之間的差異純粹是會計處理造成的,背後沒有投資觀點、技巧或實際交易。
這套引擎明確說明了自身對交易時點的處理方式,這比口頭承諾更有價值:
若成交發生在開盤時,區間敏感型滑價只會使用前一根已完成的 K 線,而成交量容量則根據落後觀測值進行預測;引擎不會使用該交易日稍後才出現的最高價、最低價、收盤價或全天成交量。
來源:quantjourney-bt README,版本 0.12.4,2026年8月6日讀取。
有文件記載的假設,可以與你已安裝的原始碼相互核對。未記載的假設,則只是猜測。
為何會有 walk-forward 額外內容
walk-forward 範例 WF01 至 WF05 會隨 [wf] 額外內容及其 Optuna 相依套件一併提供。walk-forward 會先用歷史資料的一個區段擬合參數,再在其後的區段進行測試,接著將這兩個區段向前推移並重複操作。滾動式與擴張式變體的差異,在於擬合視窗向前移動時,是否捨棄最早的資料。
另一個範例會在每個邊界加入 purge 與 embargo,捨棄接縫附近的觀測值。如此可避免擬合區段的資訊洩漏到接受測試的區段。
這些做法都無法把薄弱的想法變成可行策略。它們只是將單一數值替換成一組可供檢驗與討論的數值分布,而這正是整體升級的重點。
下一步仍不是投入真實資金:以 paper trading 測試後再投入真實資金 可衡量回測在結構上無法觀察的事項,首先包括你的委託是否能以接近模擬器假設的價格成交;為交易機器人設置 circuit breakers 則處理程式在出現異常狀況當天的行為。
至於支撐上述流程的統計方法,我們在 開源量化交易書籍 中的筆記有更深入的說明。
常見問題
不使用 API 金鑰也能回測策略嗎?
可以。quantjourney-bt 內建一組示範資料集,透過 --sample-data 旗標啟用;範例策略可直接使用該資料集執行,不需要帳戶或認證資訊。這組資料集規模較小,僅供示範,因此執行結果應視為安裝與流程檢查,而不是策略有效性的證據。
為什麼要固定 Python 回測套件的版本?
0.x 版本的套件不保證小版本之間的相容性,預設值變更或指標重新命名也不會主動提示。使用 pip install quantjourney-bt==0.12.4 固定版本,並在 requirements 檔案中記錄環境,代表你今天產生的結果,明年仍可在產生該結果的同一套引擎上重建。
quantjourney-bt 採用何種授權?
Apache License 2.0。該授權允許商業使用與修改,但要求重新散布時保留授權及 notice 檔案,並明確授予貢獻者提供的專利權。0.12.4 版於2026年7月21日發布,要求使用 Python 3.11 或更新版本。
強勁的回測結果是否代表策略有效?
不代表。回測是在單一期間、單一投資標的範圍內進行的一項測量。上方面板顯示,同一套未經修改的規則,在單一 ticker 上各年度可能產生截然不同的數據;在五個名稱之間,也可能出現明顯不同的結果。這正是 walk-forward validation 與樣本外測試要揭露的落差。
上方面板的計算方式
每日收盤價是每個日期紐約時間上午9:30至下午4:00之間,正常交易時段最後一分鐘的成交價。這樣可正確處理提早收市的半日交易日,而不必硬編交易時段長度。快速平均涵蓋20個交易日,慢速平均涵蓋50個交易日,兩者均為簡單移動平均;每個序列最初的49個交易日屬於暖機期間,不建立部位。年度數據會針對規則持有多頭部位的交易日,將各交易日收盤至收盤的變動複合計算;hold 欄則將同一年度內每個交易日的變動全部複合,以供比較。橫截面中的五個名稱,均挑選自資料連續且觀察期間內沒有拆股的標的,因此收盤價序列不需要調整。觀察期間固定在過去,因此每次重新產生時,這些面板都會回傳相同數字。
本頁每個面板下方都附有完整且精確的 SQL,因此頁面上的數字和固定版本的安裝一樣,都能重新執行。若你想在撰寫任何回測程式碼前,先用自己的觀察期間衡量一套規則,請在 Strasmore terminal 以自然語言提出問題。