Share Notes

chundev

View the Project on GitHub latteouka/share-notes

交易系統三方對齊:回測引擎、前端 API、自動下單 Executor

日期:2026-03-17 技術棧: TypeScript(回測引擎 + Next.js API)、Python(Executor)、Docker + crontab


TL;DR

回測引擎的加碼/保本減碼用「當天收盤價即時執行」,但實盤 executor 只能在隔天開盤下單(T+1)。這個時序差異讓回測虛高 10%。修正方式:引擎改成 T+1 模型,加碼改走夜盤 open(當日 17:21 執行),最終回收 57% 的損失,且三方邏輯完全一致。


背景

一個期貨策略系統有三個執行層:

  1. 回測引擎(TypeScript)— 跑歷史回測,用收盤價做所有決策
  2. 前端 API(tRPC)— 提供即時策略訊號給使用者和 executor
  3. Executor(Python)— 自動下單,透過券商 API 買賣期貨

三者應該在「什麼時候做什麼」上完全一致,否則回測績效就不可信。


問題:回測用即時執行,實盤只能 T+1

回測引擎的加碼邏輯:

D 日收盤 → 判斷浮盈 ≥ 5% → 立刻加碼(用 D 日 close 價)

但 executor 的實際流程:

D 日 13:45 日盤收盤
D 日 15:30 資料更新(API 才看得到 D 日數據)
D+1 08:30 executor 讀到 D 日訊號 → 用 D+1 open 價下單

時序差距約 18 小時,在趨勢延續中 D+1 open 通常比 D close 更高,加碼成本更貴。

同樣的問題也出現在保本減碼:引擎即時砍半,但 executor 只能隔天砍。


影響量化:回測虛高 10%

把引擎所有操作改成 T+1 open 後:

指標 即時執行(舊) T+1 open(修正後)
年化報酬 67.4% 57.3%
最終資金 2,055 萬 1,233 萬
最大回撤 31.9% 32.2%

年化差 10.1%,最終資金差 40%。 原因是加碼成本更高 × 8 年複利放大。


解法:夜盤加碼 + T+1 保本

分析訊號日的夜盤 open vs 隔日日盤 open:

訊號日 夜盤 open T+1 日盤 open 差值
2024-02-15 647 698 +51
2024-06-12 884 927 +43
2025-12-08 1465 1505 +40
2026-01-05 1605 1675 +70
2026-02-10 1825 1890 +65

夜盤 open 永遠比 T+1 日盤 open 便宜(平均差 43 點 ≈ 8.6 萬/口)。

因為夜盤 15:00 開盤,資料 15:30 更新後立刻可以判斷,17:21 下單距離收盤只有 ~2 小時,價格漂移最小。而 T+1 日盤 open 距離收盤 18 小時,趨勢延續讓價格更高。

但保本減碼是賣出,賣越高越好。所以保本維持 T+1 日盤 open,讓賣出價格更高。

最終方案:

操作 執行時機 理由
進場 T+1 日盤 open(08:30) 已對齊
出場 T+1 日盤 open(08:30) 已對齊
加碼 D 夜盤 open(17:21) 買得便宜
保本 T+1 日盤 open(08:30) 賣得更高

結果:

指標 全 T+1 夜盤加碼 + T+1 保本
年化報酬 57.3% 63.1%
最終資金 1,233 萬 1,659 萬

回收了 57% 的 T+1 損失。


除錯過程中的其他發現

1. Prisma db push vs migrate deploy

開發時用 db push 新增的 model 沒有 migration 檔案。Docker entrypoint 跑 migrate deploy 是空操作 → 新的 table 在 runtime 不存在 → API 500。

❌ entrypoint: prisma migrate deploy  → 無 migration 可跑
✅ entrypoint: prisma db push --skip-generate  → 直接同步 schema

教訓: 如果團隊用 db push 開發,部署也要用 db push,不能混用 migrate deploy

2. 配置合併遺漏

loadProductionConfig() 只合併了 params(flat key-value),但 dynamicPositionSizingentryConfidence 是頂層物件,沒被合併到 config → 前端用的 tiers 跟 CLI 不同。

// 修正前:只覆蓋 entry/exit params
const config = applyOptimizedParams(file.params, defaultConfig);

// 修正後:額外合併頂層區塊
if (file.dynamicPositionSizing) {
  config.dynamicPositionSizing = { ...config.dynamicPositionSizing, ...file.dynamicPositionSizing };
}

3. 保本與加碼互斥

保本減碼(砍半口數)觸發後,同筆交易不應再加碼回去。否則等於「砍完又買回來」,失去保本意義。

❌ D: 加碼 8→9 口 + 保本 9→5 口 → D+1: 又加碼回 9 口
✅ D: 加碼 8→9 口 + 保本 9→5 口 → 之後禁止加碼直到出場

capitalProtectionTriggered flag 控制,一旦設為 true,整筆交易不再觸發加碼。

4. 部署後 Smoke Test

部署只檢查 /api/health 不夠。加碼/保本的 API 可能因為 Prisma Client 沒有新 model 而 500,但 health check 通過。

smoke-test:
    curl /api/health          # 基本健康
    curl /api/executor/signal # 策略訊號 API
    curl /api/executor/report # 回報 API(POST hold)

三個都通過才算部署成功。


學到的事


參考資料