帳面數字會騙人#
我們的 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 + Funding | 11 | 45.5% | 46.2% | -0.30 | ✗ |
| TradingView 信號 | 8 | 62.5% | 60.0% | +0.71 | ✗ |
| Pipeline | 7 | 28.6% | 33.3% | -1.13 | ✗ |
| 其他 | 7 | 28.6% | 33.3% | -1.13 | ✗ |
| 整體 | 33 | 42.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 筆交易太少了。我們的計畫是:
- 繼續累積數據,不改參數,跑到 50+ 筆
- 50 筆後重跑 Z-score——如果某策略 p < 0.05,加大倉位
- 淘汰不及格的——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 自動化交易
了解更多 →
延伸閱讀#