← 回到筆記
可重現性

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

足夠讓你手動核對算式,或利用相同的公開與授權資料來源重建一項等價的研究,其中也包括增補研究改動了什麼,以及每一次執行是以哪個雜湊值凍結。

資料與樣本範圍

加密貨幣。十檔以USDT計價保證金的Binance永續合約,於2022年12月31日依2022年10月至12月的每日名目成交金額中位數選出:BTC、ETH、DOGE、XRP、BNB、SOL、MATIC、LTC、CHZ與ETC。K線與完整的資金費率歷史來自Binance公開檔案庫。

股票。十檔美國大型個股,以相同方式選出:TSLA、AAPL、NVDA、AMZN、MSFT、AMD、META、GOOGL、NFLX與XOM。資料來源為符合NBBO資格的一分鐘成交與報價資料,以穩定的證券識別碼為鍵值,並在識別碼變更時串接調整因子,因此股票分割與代號重複使用不會扭曲報酬率。

樣本範圍。兩種K棒頻率,15分鐘與1小時,構成四個格子:crypto:15m、crypto:1h、equity:15m與equity:1h。輸入資料的歷史從2021年1月1日開始,樣本外評分則從2023年1月1日跑到2026年7月31日。

每一個輸入檔案都在資料清單中計算雜湊值,執行程序在觸碰任何一根K棒之前都會先驗證。資金費率、價差、手續費、滑價、最高與最低價的時間戳、路徑順序欄位、未來報酬與交易結果都屬於執行中繼資料,禁止出現在基礎訊號定義與權益狀態的輸入中。

策略庫,以及它是怎麼校準的

八個家族共八十個機械式策略,每一個都是正負號對稱的。策略庫只校準過一次,而且只用2021年1月到2022年12月的資料,目的是讓持有期間接近主觀日內與波段交易員的樣子:多數部位在數小時內平倉,尾部大約延伸到兩週。校準過程讀取候選訊號數量、持有期間與標的覆蓋率,不讀取任何報酬、Sharpe、勝率或回撤。它在三輪之內收斂,共替換五個策略,全部都在RSI反轉家族,每一個都是因為覆蓋率太薄。

只有在策略的方向觀點真正翻轉時,也就是由多轉空或由空轉多,才會產生一個候選訊號。中性的K棒不算訊號,也不會重設觀點:

# The candidate grid: genuine inversions only, never a re-entry from neutral.
view = raw_view.replace(0, method="ffill")        # neutral holds the previous view
signal = view != view.shift(1)                    # a transition, not a level

這件事的影響比看起來大。如果把每一次從中性重新進場都當成新訊號,那麼條件在某個區間邊緣來回跳動的規則,每跳一次就會發出一個候選訊號;只計算真正的翻轉之後,候選訊號的數量大約降到三分之一。

這些訊號轉折同時也是所有政策共用的候選訊號網格。在每一個轉折點上,每一套政策都會把手上的部位平掉,然後往新訊號的方向開倉,因此交易的邊界與交易筆數在設計上就在各政策之間完全相同,唯一不同的是每筆交易怎麼結束、結束在什麼價位。

障礙設定,以及各項處理必須擊敗的基準

障礙寬度與報酬風險比是策略參數,不是普世常數,因此它們依策略與窗口在樣本內以登記好的目標函數調校,並計入與管理疊加相同的搜尋預算。

stop in {3, 6} ATR  x  reward-to-risk in {1.5, 3}   =  4 base configurations

這會得到 (4.5, 3)、(9, 3)、(9, 6) 與 (18, 6) ATR 這四組停利與停損配對。ATR採用Wilder ATR(14),計算到訊號K棒收盤為止,並在交易存續期間凍結;一個風險單位是在基準部位大小下、進場當時一個ATR的價格移動。用這種方式調校基準會拉高各項處理必須跨過的門檻:誠實的比較對象是經過調校的靜態管理,而不是一個容易改善的普世二比一稻草人。

權益狀態變數

八個有方向性的變數,每一個都定義成數值越大代表近期表現越好:前10筆交易的累計淨損益;同一項但改用50筆;權益減去它的20筆移動平均,再除以前20次增量的標準差;前50筆已完結交易的最大回撤,以非正值儲存;帶正負號的連勝連敗長度;20筆交易的勝率;50筆交易的勝率;以及20筆交易的平均淨損益。

state = {
    "cum_r_10":     net_r.rolling(10).sum(),
    "cum_r_50":     net_r.rolling(50).sum(),
    "equity_vs_ma": (equity - equity.rolling(20).mean()) / net_r.rolling(20).std(),
    "max_dd_50":    (equity - equity.rolling(50).max()).rolling(50).min(),   # <= 0
    "streak":       signed_streak(net_r > 0),
    "win_rate_20":  (net_r > 0).rolling(20).mean(),
    "win_rate_50":  (net_r > 0).rolling(50).mean(),
    "mean_r_20":    net_r.rolling(20).mean(),
}

第九個候選變數,也就是相對於歷史最高權益的當前回撤,在執行前就基於結構性理由被移除。對期望值為負的策略而言,這個變數是無界的,而且會單調地往下漂移,因此樣本內配適出來的門檻在樣本外會永久被突破:在一個實測案例中,整個樣本外的取值範圍都落在樣本內最低的切點之下,濾網一筆交易都沒有執行。這是一個定態性失效,不必參照獲利能力就能觀察到,所以我們改用以窗口計算的50筆交易回撤。

在某個變數的回看期尚未完整之前,管理維持在選定的基準設定,決策軌跡則記錄 insufficient_history。每一組基礎策略、市場、時間框架與政策的組合各有一條權益曲線,一格中的十檔標的共用同一條。

管理規則庫與搜尋預算

管理疊加的動作是套在選定基準上的乘數,因此即使基準會浮動,家族的意義仍然不變。均值回歸的反應方式則把狀態到動作的對應關係反轉過來。

狀態僅停利僅停損停利+停損部位規模交易濾網
10.5倍停利0.5倍停損兩者0.5倍0.5R略過
20.75倍停利0.75倍停損兩者0.75倍1R略過
31倍1倍1倍1R執行
41.5倍停利1.5倍停損兩者1.5倍1.5R執行
52倍停利2倍停損兩者2倍2R執行

八個狀態變數乘以兩個方向,每個家族得到16種管理疊加;16種疊加再乘以4組基準配置,每個家族在每個窗口有64次嘗試。每個家族拿到的預算完全相同,這就是這裡所謂搜尋對等的意思:嘗試過的配置數量相同,而不是可調旋鈕的數量相同。

狀態的來源經過挑選,讓任何家族都無法自己製造自己的狀態。障礙家族讀取自己已實現的淨損益;部位規模家族讀取自己在單位部位大小下的已實現損益,因此槓桿無法灌大驅動它的那個變數;交易濾網家族讀取一份持續更新的基準影子帳本,因此關閉狀態永遠不會變成吸收態。

Walk-forward

從2023Q1到2026Q2共十四個完整的季度樣本外窗口,另加一個事先登記、只涵蓋2026年7月的部分窗口;這個窗口會評分、標記,並在任何納入它的地方揭露。樣本內一律是前24個日曆月,而且日曆邊界在兩個市場之間完全一致。

for window in quarterly_windows(oos_start="2023-01-01", oos_end="2026-08-01"):
    grid = candidate_grid(strategy, window)                    # no equity state, no OOS values
    is_candidates = [c for c in grid if c.entry_ts < window.oos_start
                                     and c.exit_ts < window.oos_start]

    thresholds = state_thresholds(baseline_replay(is_candidates))
    attempts   = [simulate(cfg, is_candidates, thresholds) for cfg in family_grid]  # all 64
    persist(attempts)                                          # including degenerate and inactive

    winner = max(attempts, key=lambda a: (a.net_r_per_common_candidate,
                                          a.executed_trades, -a.canonical_id))
    winner.freeze()                                            # config and thresholds fixed
    score(winner, window.oos_slice)                            # OOS starts flat, state from IS replay

選擇按時間順序進行,每一次嘗試都會落地保存,包括那些什麼也沒做的嘗試,而選中的管理疊加,其樣本外狀態是由它在樣本內完整重播的結果初始化。

K棒內執行

準備好的面板帶有每一根母K棒最終最高價與最低價的時間戳,因此K棒內的路徑會重建為開盤、第一個記錄到的極值、第二個記錄到的極值、收盤,並沿著這些單調線段判斷障礙是否被觸及。

跳空穿過停損時,以第一個可成交的開盤價出場;跳空穿過停利時則在停利價位成交,這正是掛著的限價單會有的行為。如果只有一個障礙可及,就由它成交;如果兩個都可及,就由記錄到的極值順序決定;而如果那個順序未知,就讓停損先成交,同時計算有多少交易依賴這項約定,並揭露它們的經濟貢獻。

成本模型

成交手續費滑價價差
進場,不論原因taker 5基點2基點實測值的一半
出場:停利,或跳空穿過停利maker 2基點
出場:停損,或跳空穿過停損taker 5基點2基點實測值的一半
出場:反向訊號翻倉taker 5基點2基點實測值的一半

股票每筆成交支付固定0.5基點的佣金與費用準備,不區分maker或taker。加密貨幣的資金費率在未平倉部位經過的每一個結算時點計入,並採用當時觀察到的正負號。2021到2026年間,資金費率有74.5%的時間為正,平均每八小時 +0.87 基點,對一個永遠做多的帳本而言大約是每年9.5%,這就是為什麼它要依交易方向拆開報告,而不是併成單一的淨值數字。

每一份帳本都把毛價格損益、進場與出場價差、進場與出場手續費、進場與出場滑價、帶正負號的資金費率,以及淨損益分別保留為獨立欄位,而且這個恆等式必須在1e-9個風險單位以內對得起來:

net_r = (gross_price_r
         - entry_spread_r - exit_spread_r
         - entry_fee_r    - exit_fee_r
         - entry_slip_r   - exit_slip_r
         + funding_r)                       # signed: debits longs when funding is positive
assert abs(net_r - ledger.net_r).max() < 1e-9

統計推論

配對bootstrap以週一到週日的整個日曆週為單位重抽樣,一週之內的每一檔標的與每一個策略一起移動,共重抽10,000次,隨機種子固定。逐列重抽樣、對窗口Sharpe取平均,以及把窗口數乘進策略數,這三種做法一律禁止。

weeks  = pd.Grouper(key="ts", freq="W-MON")
blocks = [df for _, df in paired_deltas.groupby(weeks)]      # whole weeks move together

draws = [np.concatenate(rng.choice(blocks, len(blocks))).mean() for _ in range(10_000)]
p = 2 * min((np.array(draws) <= 0).mean(), (np.array(draws) >= 0).mean())
q = benjamini_hochberg(p_values, alpha=0.05)                 # 20 contrasts in the declared family

多重性校正在已宣告的家族內進行,也就是2個市場乘以2種時間框架乘以5個管理家族,共20組對比,未通過的也算在內。

動作輪轉安慰劑拿同一套政策,把它的動作在同一批候選訊號上輪轉,共抽1,000次固定種子的樣本,並以曝險匹配作為阻斷式檢查:輪轉後的虛無版本必須完全重現真實管理疊加的週轉率、成交筆數與名目動作曝險,因此唯一被破壞的,就是權益狀態與交易之間的對應關係。

擾動套件會執行20次1%的候選訊號隨機刪除,以及20次成交衝擊,成本在0.75倍到1.25倍之間縮放,並要求至少80%的抽樣保住效果的正負號、決策一致率的中位數維持在80%以上,權益曲線相關係數的中位數維持在0.9以上。

八項認定條件

  1. 彙總後每個共同候選訊號的淨損益高於基準
  2. 已成交交易的期望值改善,而期望值無定義時視為未通過
  3. 每單位平均名目毛風險的淨損益沒有惡化
  4. 配對且以週為叢集的差異,其BH校正後的q值低於0.05
  5. 完整窗口的勝率高於50%,且報酬風險比高於1
  6. 刪除表現最好的樣本外窗口之後,差異仍然為正
  7. 結果擊敗條件匹配的動作輪轉安慰劑的第95百分位
  8. 路徑相依性穩定度檢核通過

第4項條件在主研究的全部20組比較與增補研究的全部24組比較中都沒有通過。第2項條件的寫法會讓週轉率為零的政策直接失敗,因為這種政策只要什麼都不做,就能在第1項條件上擊敗一個虧損的基準,而分析套件測試的正是這個情況。

增補研究改動了什麼

樣本範圍、資料、時間框架、walk-forward排程、可在樣本內調校的障礙網格、八個權益狀態變數、64次嘗試的預算、選擇規則、合格條件、bootstrap、安慰劑、擾動套件與八項認定條件,全部原封不動地沿用。有四件事不同。

出場

部位只有在停利或停損成交時才會平倉。如果到該標的歷史資料結束時兩者都沒有成交,這筆交易就會在最後一根觀察到的K棒截斷並加註標記;截斷率是規定必須報告的統計量,因為截斷率若很高,就代表障礙在樣本內無法結清,這套協定也沒有交出它承諾的公平檢定。

障礙寬度

拿掉反向出場會拉長持有期間,因此停損網格在執行前依格重新校準過,只讀取持有期間與資料頻率:

格子停損(ATR)四組基準的持有期間中位數持有兩週以上的比例
crypto:15m3.0, 6.06.2小時至38.5小時0.0%至4.4%
equity:15m3.0, 6.06.0小時至28.9小時0.6%至24.3%
crypto:1h1.5, 3.06.5小時至38.0小時0.0%至3.8%
equity:1h1.5, 2.257.0小時至21.0小時0.3%至13.7%

十六組格子與基準的組合中,有十五組落在約定的區間內。例外是equity:15m在最寬的基準設定下,我們把它寫進筆記的限制一節,而不是把它移除。

持倉佔用

每個分支都要處理部位開著時進來的訊號,兩者也都以相同的分母評分,也就是該窗口內的每一次訊號轉折,而這個數量由規則與價格序列固定下來:

if branch == "single" and book.holds(instrument):
    decision = "declined_as_occupied"      # earns nothing, still counts in the denominator
else:
    book.open(instrument, side, size, tp, sl)

一套正在忙、因而略過訊號的政策,就是從那個訊號賺不到任何東西。這樣一來各政策之間的交易筆數會不同,但那是一項該報告的結果,不是壞掉的比較,週轉率也會一併報告。

多重性校正

增補研究在自己宣告的家族內校正,也就是3個家族乘以4個格子乘以2個分支,共24組對比。兩個分支放在一起校正,因為兩者都跑了、也都要報告,分開評分會低估搜尋的規模。增補研究的對比絕不與主研究的合併,兩者也都不會重新評分對方。

驗證與凍結的雜湊值

Rust執行引擎與一份獨立撰寫的Python參考實作逐行比對過。在增補研究的引擎上,兩者在362,100列資料的每一個出場K棒、出場價格與淨損益上都完全吻合,容差為1e-9。一致性測試是那裡最重要的一套,因為拿掉反向出場等於改動了執行核心本身,而凍結機制不接受早於它所認證程式碼的判定結果,所以沒有任何判定是從主研究的引擎繼承而來。

套件主研究增補研究
執行一致性,Rust引擎對Python參考實作通過通過
凍結前校準,僅持有期間通過通過
推論基礎元件通過通過
分析檢查通過通過
狀態、選擇、穩健性與產物一致性未重跑未重跑
植入缺陷偵測、因果性與資料洩漏檢核未重跑未重跑

兩者都是在任何樣本外數字產生或被打開之前就以書面訂定並凍結,而每一個報告出來的數字都由儲存的產物生成,不是手動打上去的。增補研究帶著自己的管理疊加與實作雜湊值,這正是它之所以是增補而不是重新評分的原因:

產物SHA-256
strategy_configs.json,兩者共用d5c1fabbd22fcf20907e24025c6868f739afb11a2a952fc71241630f318cf68e
overlay_configs.json,主研究c0eaaaedd74800e2e973dda23a23053ee7966a07762803e7b2592cabfed5320f
analysis_config.json,主研究1afcb713478d37d234268038280d9a83f38dfc535c8b1e3fbb9b6acc649e4fef
data_manifest.json,主研究93aefeaa5ad78d920fdaf48d36cc61794921eaaaa92cbf52f3b7f5119c855a59
strategy_selection.json,主研究a37718869d539141b64c492f1d221a7a9ba8395f7e9ca323d2c536028a21df4c
分析實作,主研究e46041ab30b8f44ad1029a798e1d0bb58aa97959ba88bfb0558bf4468393600f
overlay_configs.json,增補研究7d575275961bee72f5f6557ae5c253545f69cc352d5b9e35a216643714b7331e
分析實作,增補研究667f6ab6e65c2421c670179c6a5de8405209e2d8db35347e40fd71acb5fb4105

索取完整流程程式碼

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