← 回到筆記
可重現性

筆記中的每一個數字,以及它的來源

足夠讓你手動核對算式,或利用相同的公開與授權資料來源重建一項等價的研究。

資料與樣本範圍

股票。十檔美國大型股,於2022年12月31日依2022年10月至12月的每日成交金額中位數選出。資料來源為符合NBBO資格的一分鐘成交與報價資料,排除零股與非NBBO成交場所。標的以穩定的證券識別碼為鍵值,並在識別碼變更時串接調整因子,因此股票分割與代號重複使用不會扭曲報酬率。

加密貨幣。十檔以USDT計價保證金的Binance永續合約,以相同方式從公開的K線與資金費率歷史資料中選出,每一檔都要求在樣本開始前已有24個月真實成交量。

樣本期間。樣本外測試涵蓋2023年1月1日至2026年7月31日,兩個市場皆以15分鐘與1小時兩種K棒頻率測試。

特徵集

每個模型家族都使用相同的62個特徵,全部僅由OHLCV構建而成:多個落後期的報酬與動能、滾動已實現波動度、區間內位置、與滾動高低點的距離、成交量比率與變化,以及K棒形態特徵(例如實體比例與影線長度)。特徵矩陣中不包含任何成本、價差、手續費或資金費率欄位,這一點在任何樣本外結果產生之前就已經以程式驗證過。

# 6根K棒的已實現波動度,以及在近期高低點區間內的相對位置
df["vol_6"] = df["ret_1"].rolling(6).std()
df["pos_in_range_6"] = (
    (df["close"] - df["low"].rolling(6).min())
    / (df["high"].rolling(6).max() - df["low"].rolling(6).min())
)

三種模型家族:一個正則化線性模型、一個梯度提升樹集成模型,以及一個小型前饋神經網路。每個家族恰好取得30組候選配置,這些配置在任何樣本外窗口評分之前就已經產生並計算雜湊值。每次執行都會先核對此雜湊值才觸碰資料,一旦不符就會中止執行,而不是讓網格在不同執行之間悄悄漂移。

import hashlib, json

configs = {"linear": linear_grid, "gbt": gbt_grid, "mlp": mlp_grid}
digest = hashlib.sha256(json.dumps(configs, sort_keys=True).encode()).hexdigest()
assert digest == "6c6d89b6cfd435ce875876820443405f5d383956a5673ea38f5d2d410b490e47"

Walk-forward流程

六種訓練長度(1、2、3、6、12、24個日曆月)與三種樣本外排程(固定一個月、固定三個月,以及訓練長度的三分之二)交叉組合,共產生216個結果格。將這套流程沿完整樣本向前推進,總共產生了5,868個滾動模型窗口與176,040次配置嘗試。

每個窗口內的選擇都按時間順序進行,絕不打亂順序,勝出的配置會先在完整的樣本內窗口重新訓練,才會碰觸樣本外區塊。每個窗口的配置分數、中繼資料、預測結果與完整交易帳本都會先落地保存,之後才寫入完成標記,因此整個執行可以逐窗口恢復與覆核。

for window in rolling_windows(train_months, schedule):
    fit_rows, val_rows = chronological_split(window.is_slice, frac=0.8)

    scored = [fit_and_validate(cfg, fit_rows, val_rows) for cfg in configs[model_family]]
    winner = max(scored, key=lambda s: s.validation_ic)

    final_model = fit(winner.config, window.is_slice)   # 在完整樣本內窗口重新訓練
    preds = final_model.predict(window.oos_slice)       # 僅在樣本外評分一次

投資組合規則與成本模型

預測目標是下一根K棒的開盤到收盤報酬。交易規則買進橫斷面預測值最高的兩檔標的、放空最低的兩檔,各配置25%的絕對權重,於下一根K棒開盤進場、同一根K棒收盤出場。

book = pd.concat([
    ranked.head(2).assign(side=+1),
    ranked.tail(2).assign(side=-1),
]).assign(weight=lambda d: 0.25 * d["side"])

gross = book["weight"] * book["fwd_return"]
net = (
    gross
    - book["weight"].abs() * book["spread"]
    - book["weight"].abs() * fee_bps           # 5個基點taker費率(加密貨幣),0.5個基點(股票)
    - book["weight"].abs() * slippage_bps      # 2個基點(僅加密貨幣)
    + book["weight"] * book["funding_signed"]  # 僅加密貨幣,於真實結算K棒計入
)

加密貨幣支付實測月度價差、5個基點的taker手續費,以及每筆成交2個基點的滑價,再加上於真實八小時結算事件當下計入、帶正負號的實際資金費率:正資金費率會向多方扣款、向空方入帳,負資金費率則相反。股票支付實測的每K棒價差與每筆成交0.5個基點的手續費,沒有資金費率這一項。毛損益、價差、手續費、滑價、資金費率與淨損益都各自保留為獨立的帳本欄位,且加總結果會與這些組成項目精確吻合。整個執行過程中觀察到的最大帳本一致性誤差為0.0基點。

統計推論

比較採用以週為叢集單位、針對彙總樣本外序列的bootstrap方法,而非逐窗口的夏普平均值或逐列重抽樣,因為報酬在時間序列與跨標的之間都存在相關性。Benjamini-Hochberg程序則控制每一個受測格(包含未顯著的格子)的偽發現率。

boot_diffs = week_clustered_bootstrap(shorter_length_pnl, baseline_24m_pnl, n_boot=10_000)
p_values = [two_sided_p(observed_diff, boot_diffs) for observed_diff in cell_diffs]
significant = benjamini_hochberg(p_values, alpha=0.05)

可信度與資料洩漏檢查

在任何樣本外數字被檢視之前:未來截斷測試以最大0.0的誤差重現了每一個特徵;預測目標與下一根K棒實際的開盤到收盤報酬吻合,誤差在0.0000018基點以內;門檻樣本中所有資金費率事件都與其原始交易所時間戳吻合;把目標直接植入特徵集的正向對照組,將彙總IC從0.0172推高到0.9999;三組會破壞任何真實時間對齊關係的循環旋轉負向對照組,則分別回傳0.0165、0.0418與−0.0461的IC,與雜訊表現一致。

一個通用型前視偏差防護機制最初標記了大多數代表性樣本組,因為它假設一個與時間戳t的報酬相關的分數,是在同一根K棒的報酬期間內被交易的。而這個引擎的慣例並不相同:時間戳t的報酬是在該K棒收盤時觀察到的輸入值,部位則在下一根K棒開盤時才進場。一項逐列的時間軸核對,將決策、進場、出場與目標值回溯連結到來源面板,涵蓋3,563,992筆預測資料,結果沒有發現任何時間錯置、任何進出場價格不符,且目標重建的最大誤差僅0.0000008基點——這證實了出錯的是防護機制的假設,而不是這套流程本身。

執行環境

Python 3.10.12
polars 1.40.1        numpy 2.2.6          scipy 1.15.3
scikit-learn 1.7.2   lightgbm 4.6.0       torch 2.11.0+cu130
matplotlib 3.10.9    pyarrow 24.0.0       markdown 3.10.2

所有模型、bootstrap與配置產生所用的隨機種子都是固定的。只有當某個窗口的完成標記與配置雜湊值都與目前執行的檔案相符時,該窗口才會沿用先前執行的結果;否則就會重新計算。

筆記中的圖表都是根據 已發布的結果數據表繪製,與文字中引用數字的來源完全相同。

索取完整流程程式碼

這一頁的內容,已足夠讓你手動核對筆記中的每一個數字,或利用相同的公開與授權資料來源重建一項等價的研究。生產環境用的程式碼——資料擷取、面板構建、實驗執行引擎、分析與圖表生成——並未附在這裡。請寄信至 daniel@daru.finance,我會把程式碼寄給你。

← 回到筆記