帳面數字會騙人

我們的 Paper Trading 系統跑了一個月,帳面上看起來漂亮極了:

勝率 87.5%,7 勝 1 負 2 平

看到這數字,一般人的反應是「太棒了,可以上真錢了!」

我們的反應是:「等等,先讓數學說話。」

什麼是 Z-score?

Z-score 本質上就是一個問題:你的成績,跟猜硬幣比,差多少?

$$Z = \frac{\hat{p} - 0.5}{\sqrt{0.5 \times 0.5 / n}}$$

  • $\hat{p}$ 是你的勝率
  • $n$ 是交易筆數
  • 如果 Z > 1.65,代表 95% 信心你比猜硬幣好(p < 0.05)

聽起來很簡單,但大多數交易者從不做這個檢驗。他們看到 70% 勝率就直接上真錢,然後虧光才來問「回測明明很好啊」。

Python 實作:一行就能算

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import math

def z_score(wins: int, total: int) -> tuple[float, float]:
    """計算勝率的 Z-score 和 p-value"""
    if total == 0:
        return 0.0, 1.0
    p_hat = wins / total
    z = (p_hat - 0.5) / math.sqrt(0.25 / total)
    # 單尾 p-value(用常態分佈近似)
    p_value = 0.5 * math.erfc(z / math.sqrt(2))
    return z, p_value

# 範例:8 筆贏 7 筆
z, p = z_score(wins=7, total=8)
print(f"Z = {z:.2f}, p = {p:.3f}")
# 輸出: Z = 2.12, p = 0.017 — 看起來顯著?
# 但只有 8 筆,Bayesian 調整後會大幅修正

你也可以用 scipy.stats 做更精確的計算:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from scipy import stats

def z_test_proportion(wins: int, total: int, null_p: float = 0.5):
    """完整的比例檢定,回傳 Z-score、p-value 和信賴區間"""
    p_hat = wins / total
    se = math.sqrt(null_p * (1 - null_p) / total)
    z = (p_hat - null_p) / se
    p_value = 1 - stats.norm.cdf(z)  # 單尾
    # 95% 信賴區間
    ci_se = math.sqrt(p_hat * (1 - p_hat) / total)
    ci_lower = p_hat - 1.96 * ci_se
    ci_upper = p_hat + 1.96 * ci_se
    return {
        "z_score": round(z, 3),
        "p_value": round(p_value, 4),
        "win_rate": round(p_hat * 100, 1),
        "ci_95": (round(ci_lower * 100, 1), round(ci_upper * 100, 1)),
        "significant": p_value < 0.05
    }

注意 ci_95 欄位——信賴區間比 p-value 更直覺。如果信賴區間包含 50%,代表你的策略還不能排除「和猜硬幣一樣」的可能。

真實數據:33 筆交易的殘酷真相

我們把所有已平倉的 33 筆交易拿去跑 Z-score 檢驗。如果你還不熟悉我們的多策略交易架構,這裡簡單說:我們同時跑四種策略源,每個都獨立產生訊號。結果如下:

策略筆數原始勝率調整勝率Z-score顯著?
CEX Volume + Funding1145.5%46.2%-0.30
TradingView 信號862.5%60.0%+0.71
Pipeline728.6%33.3%-1.13
其他728.6%33.3%-1.13
整體3342.4%42.9%-0.87

每一個策略的 p 值都 > 0.05,沒有任何一個通過統計檢驗。

那個 87.5% 的勝率呢?那只是 Paper Trading 模式下的 8 筆手動管理交易——樣本太小,Bayesian 調整後實際只有 60%,而且 p = 0.24。統計學的回答是:你和猜硬幣沒有顯著差異。

視覺化怎麼看?

如果把每個策略的 Z-score 畫成水平柱狀圖,加上 Z = 1.65 的紅色虛線(顯著門檻),你會看到所有柱子都落在虛線左邊。TradingView 信號的 Z = 0.71 是最接近的,但距離門檻還差一半。

更直覺的看法是畫勝率的 95% 信賴區間。整體 42.4% 的信賴區間大約是 [25.6%, 59.2%]——橫跨 50% 的中線,代表「比猜硬幣好」和「比猜硬幣差」都有可能。當你的信賴區間包含 50%,就是統計在告訴你:「樣本不夠,下不了結論。」

Bayesian 調整勝率:小樣本的解藥

我們加了一個 Bayesian 調整機制,用 Beta(1,1) 先驗讓小樣本的勝率自動向 50% 收斂:

$$\text{調整勝率} = \frac{wins + 1}{total + 2} \times 100%$$

效果:

  • 3 筆全贏 → 原始 100% → 調整 80%
  • 7 筆贏 1 負 → 原始 87.5% → 調整 80%
  • 70/100 → 原始 70% → 調整 69.6%(大樣本幾乎不影響)

這確保了不會被「3 筆 100%」的幻覺欺騙。

Python 實作 Bayesian 調整

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def bayesian_win_rate(wins: int, total: int, alpha: float = 1, beta: float = 1) -> float:
    """
    用 Beta 先驗調整勝率。
    alpha=1, beta=1 是均勻分佈(無資訊先驗)。
    alpha=2, beta=2 讓收斂更積極。
    """
    return (wins + alpha) / (total + alpha + beta) * 100

# 批次計算所有策略
strategies = [
    {"name": "CEX Volume + Funding", "wins": 5, "total": 11},
    {"name": "TradingView 信號",      "wins": 5, "total": 8},
    {"name": "Pipeline",              "wins": 2, "total": 7},
    {"name": "其他",                   "wins": 2, "total": 7},
]

for s in strategies:
    raw = s["wins"] / s["total"] * 100
    adj = bayesian_win_rate(s["wins"], s["total"])
    z, p = z_score(s["wins"], s["total"])
    print(f"{s['name']:20s}  原始 {raw:5.1f}%  調整 {adj:5.1f}%  Z={z:+.2f}  p={p:.3f}")

為什麼選 Beta(1,1) 而不是更強的先驗?

Beta(1,1) 等價於「假裝你做了一筆贏一筆輸」——影響最小。如果你對自己的策略有更多先驗知識(例如這類策略的歷史平均勝率是 55%),可以用 Beta(5.5, 4.5) 等非對稱先驗。但在策略初期,我們寧願保守,用最弱的先驗讓數據自己說話。

那帳面是正的嗎?

是的,整體 PnL 是 +0.57%

這代表即使勝率低於 50%,我們的自適應風控系統在正常工作:虧損交易的平均虧損 < 盈利交易的平均盈利

這其實是好跡象——系統靠「賺多虧少」而不是「猜得準」來盈利,這也是為什麼倉位管理比勝率更重要的原因。但 33 筆的樣本量不足以下結論。

需要多少筆才夠?

真實勝率達到 p < 0.05 最少需要
55%~384 筆
60%~96 筆
65%~44 筆
70%~24 筆

如果你的策略真的有 65% 的勝率,大約 44 筆交易就能證明。我們現在 33 筆,WR 42%——距離「有統計意義的 edge」還有一段路。

用 Python 算你自己需要幾筆

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import math

def min_trades_for_significance(true_wr: float, alpha: float = 0.05) -> int:
    """給定策略的真實勝率,計算達到顯著性所需的最少交易筆數"""
    z_alpha = 1.645  # 單尾 95% 門檻
    # 從 Z = (p - 0.5) / sqrt(0.25/n) 反推 n
    n = math.ceil((z_alpha / (true_wr - 0.5)) ** 2 * 0.25)
    return n

# 算出不同勝率需要的筆數
for wr in [0.55, 0.60, 0.65, 0.70]:
    n = min_trades_for_significance(wr)
    print(f"真實勝率 {wr:.0%} → 至少需要 {n} 筆")

這個計算假設你的勝率是穩定的。如果策略在不同市場環境(趨勢、震盪、崩盤)下表現差異很大,你需要在每種 regime 下分別累積足夠的樣本。

過擬合指數(OFI)

除了 Z-score,我們還加了過擬合指數。如果你跑過回測與實盤的差距分析,你會知道回測績效 ≠ 實盤績效。OFI 量化了這個落差:

$$OFI = \frac{IS_PF}{OOS_PF}$$

IS(樣本內)的 Profit Factor 除以 OOS(樣本外)的 Profit Factor。

  • OFI < 1.5 → 低過擬合風險 ✓
  • OFI 1.5-2.0 → 中等風險 ⚠️
  • OFI > 2.0 → 高過擬合風險 ✗
  • OFI > 3.0 → 嚴重過擬合 ✗✗

當你的回測績效遠好於實盤,OFI 會直接告訴你。

Python 實作 OFI

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def overfit_index(is_pf: float, oos_pf: float) -> dict:
    """
    計算過擬合指數。
    is_pf: 樣本內 Profit Factor(回測)
    oos_pf: 樣本外 Profit Factor(實盤或 WFO 驗證)
    """
    if oos_pf <= 0:
        return {"ofi": float("inf"), "risk": "CRITICAL", "action": "策略無效,停止使用"}

    ofi = is_pf / oos_pf
    if ofi < 1.5:
        risk, action = "LOW", "穩健,可增加倉位"
    elif ofi < 2.0:
        risk, action = "MEDIUM", "謹慎,維持小倉"
    elif ofi < 3.0:
        risk, action = "HIGH", "高過擬合風險,減少參數或增加樣本"
    else:
        risk, action = "CRITICAL", "嚴重過擬合,不建議上實盤"

    return {"ofi": round(ofi, 2), "risk": risk, "action": action}

# 範例:回測 PF=2.5,實盤 PF=1.1
result = overfit_index(is_pf=2.5, oos_pf=1.1)
print(f"OFI = {result['ofi']} ({result['risk']})")
print(f"建議:{result['action']}")
# 輸出: OFI = 2.27 (HIGH)
# 建議:高過擬合風險,減少參數或增加樣本

如果你在做 Walk-Forward 驗證,可以直接用各 fold 的 IS/OOS PF 來計算 OFI,更能反映策略在不同時期的泛化能力。

新的判定邏輯

以前我們用硬編碼門檻(IS-OOS gap > 15% = 過擬合)。現在改成:

1
2
3
4
5
6
交易 < 5 筆     → "數據不足"
p ≥ 0.05        → "統計不顯著"
OFI > 2.0       → "過擬合"
調整 WR ≥ 62%   → "穩健 ✓"
調整 WR ≥ 58%   → "可接受"
其他            → "待觀察"

注意第二行——p ≥ 0.05 直接判不顯著。以前很多看似「穩健」的策略,其實只是樣本不夠大而被誤判。

所以我們的策略很爛嗎?

不,我們的策略尚未被證明有效。這是完全不同的兩件事。

33 筆交易太少了。我們的計畫是:

  1. 繼續累積數據,不改參數,跑到 50+ 筆
  2. 50 筆後重跑 Z-score——如果某策略 p < 0.05,加大倉位
  3. 淘汰不及格的——50 筆後仍 p > 0.10 的策略關閉

這就是量化交易跟「感覺交易」的差別:你不是在猜,你是在等數學給你答案。

結語

大部分散戶虧錢不是因為策略差,而是因為他們從不驗證策略是否真的有效。

Z-score 統計檢驗不難實作,卻能幫你避開「小樣本高勝率 → 上真錢 → 爆倉」的經典路徑。

如果你也在做量化交易,在上真錢之前,先問自己這個問題:

「我的勝率,跟猜硬幣比,統計上有顯著差異嗎?」

如果答案是「不確定」——那就是「沒有」。

完整驗證腳本

把上面所有工具串起來,一個函式搞定策略健檢:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def strategy_health_check(wins: int, total: int,
                          is_pf: float = None, oos_pf: float = None):
    """一站式策略統計健檢"""
    raw_wr = wins / total * 100 if total > 0 else 0
    adj_wr = bayesian_win_rate(wins, total)
    z, p = z_score(wins, total)
    min_n = min_trades_for_significance(raw_wr / 100) if raw_wr > 50 else "N/A (WR ≤ 50%)"

    report = {
        "trades": total,
        "raw_win_rate": f"{raw_wr:.1f}%",
        "bayesian_win_rate": f"{adj_wr:.1f}%",
        "z_score": round(z, 3),
        "p_value": round(p, 4),
        "significant": p < 0.05,
        "min_trades_needed": min_n,
    }

    if is_pf and oos_pf:
        ofi = overfit_index(is_pf, oos_pf)
        report["overfit_index"] = ofi

    # 綜合判定
    if total < 5:
        report["verdict"] = "數據不足"
    elif p >= 0.05:
        report["verdict"] = "統計不顯著"
    elif is_pf and oos_pf and ofi["risk"] in ("HIGH", "CRITICAL"):
        report["verdict"] = "過擬合風險"
    elif adj_wr >= 62:
        report["verdict"] = "穩健 ✓"
    elif adj_wr >= 58:
        report["verdict"] = "可接受"
    else:
        report["verdict"] = "待觀察"

    return report

# 使用範例
result = strategy_health_check(wins=7, total=8, is_pf=2.5, oos_pf=1.1)
for k, v in result.items():
    print(f"  {k}: {v}")

把這段丟進你的回測框架,每個策略跑完自動出報告。不需要再「感覺」策略好不好——讓數學替你判斷。

AI×交易 完整攻略 — 13 章實戰課程
$49 · 技術分析 + 風控管理 + Python 自動化交易
了解更多 →

延伸閱讀