【Python量化策略评估体系 #06】策略上线前 8 项必查:一份可落地的评估 checklist

前面 5 篇已经把策略评估的零部件拆完了:Brinson、因子归因、滚动回测、三视角业绩拆解、压力测试。

但零部件不等于流程。真正上线前,你需要一份 checklist,逐条过、不过关就不部署。本文给你 8 个必查项和一段可运行的自动评分脚本。

一、本文你将得到什么

  1. 策略上线前的 8 项核心检查清单。
  2. 一段 Python 脚本,自动对回测结果打分,输出 PASS/FAIL。
  3. 一个可落地的决策规则:FAIL 项超过 2 项,建议暂缓上线。

二、为什么需要 checklist

人脑在回测曲线好看的时候会自动降智:

  • 「夏普 1.5 还不错」→ 但最大回撤 -35%,你能扛住吗?
  • 「胜率 60%」→ 但样本外完全失效,过拟合了。
  • 「总收益 50%」→ 但压力测试下五天腰斩,上线就是赌命。

checklist 的作用是用同一套标准,把「我觉得可以」变成「8 项里 6 项 PASS,可以上」。

三、8 项检查清单

编号 检查项 阈值示例 意义
1 夏普比率 ≥ 0.8 单位风险带来的超额收益
2 最大回撤 ≤ -15% 最坏情况下会亏多少
3 日胜率 ≥ 45% 策略日常是否稳定盈利
4 信息比率 ≥ 0.3 相对基准是否有稳定 Alpha
5 换手率 ≤ 30% 交易成本与策略容量
6 最大持仓集中度 ≤ 80% 单一标的是否押注过重
7 样本外稳健性 |train - test| ≤ 0.5 是否过拟合
8 极端压力 5 日连跌 3% 总损失 ≤ -15% 黑天鹅下的生存能力

阈值可以根据你的风险偏好调整,关键是「先定规则,再谈上线」。

四、代码实战

import json
import random
import numpy as np
from mairui import Client

DEMO_LICENCE = "LICENCE-66D8-9F96-0C7F0FBCD073" # 演示证书,请换成你自己的正式证书
CODE = "600519"
INIT_CAPITAL = 100000.0

def perturb_ohlc(bars, seed=2026091306):
rng = random.Random(seed)
out = []
for b in bars:
k = 1.0 + (rng.random() - 0.5) * 0.10
nb = dict(b)
for f in ("o", "c", "h", "l"):
if f in nb and nb[f] is not None:
nb[f] = round(float(nb[f]) * k, 2)
out.append(nb)
return out

def fmt_pct(x):
return f"{float(x) * 100:.2f}%"

def sharpe(returns, risk_free=0.0, periods=252):
excess = returns - risk_free / periods
if excess.std() == 0:
return 0.0
return float(excess.mean() / excess.std() * np.sqrt(periods))

def max_drawdown(nav):
peak = np.maximum.accumulate(nav)
dd = (nav - peak) / peak
return float(dd.min())

def information_ratio(strategy_returns, benchmark_returns):
diff = strategy_returns - benchmark_returns
if diff.std() == 0:
return 0.0
return float(diff.mean() / diff.std() * np.sqrt(252))

client = Client(DEMO_LICENCE)
bars = perturb_ohlc(client.stock_history(code=CODE, period="d", dividend="f"))
closes = np.array([float(b["c"]) for b in bars])

简单双均线动量策略

short_window, long_window = 5, 20
short_ma = np.convolve(closes, np.ones(short_window) / short_window, mode="valid")
long_ma = np.convolve(closes, np.ones(long_window) / long_window, mode="valid")
align = min(len(short_ma), len(long_ma))
short_ma, long_ma = short_ma[-align:], long_ma[-align:]
prices = closes[-align:]

position = 0
nav = [INIT_CAPITAL]
trades = []
for i in range(1, align):
signal = 1 if short_ma[i] > long_ma[i] else 0
if signal != position:
trades.append({"day": i, "action": "buy" if signal == 1 else "sell"})
position = signal
if position == 1:
nav.append(nav[-1] * prices[i] / prices[i - 1])
else:
nav.append(nav[-1])

nav = np.array(nav)
strategy_returns = np.diff(nav) / nav[:-1]
benchmark_returns = np.diff(prices) / prices[:-1]

---------- 8 项检查 ----------

checks = []

1. 夏普

s = sharpe(strategy_returns)
checks.append({"项": "1. 夏普比率", "阈值": ">= 0.8", "实测": f"{s:.3f}", "结果": "PASS" if s >= 0.8 else "FAIL"})

2. 最大回撤

dd = max_drawdown(nav)
checks.append({"项": "2. 最大回撤", "阈值": "<= -15%", "实测": fmt_pct(dd), "结果": "PASS" if dd >= -0.15 else "FAIL"})

3. 胜率

wins = (strategy_returns > 0).sum()
total = (strategy_returns != 0).sum()
win_rate = wins / total if total else 0.0
checks.append({"项": "3. 日胜率", "阈值": ">= 45%", "实测": fmt_pct(win_rate), "结果": "PASS" if win_rate >= 0.45 else "FAIL"})

4. 信息比率

ir = information_ratio(strategy_returns, benchmark_returns[-len(strategy_returns):])
checks.append({"项": "4. 信息比率", "阈值": ">= 0.3", "实测": f"{ir:.3f}", "结果": "PASS" if ir >= 0.3 else "FAIL"})

5. 换手率

turnover = len(trades) / len(strategy_returns) if len(strategy_returns) else 0
checks.append({"项": "5. 换手率", "阈值": "<= 30%", "实测": fmt_pct(turnover), "结果": "PASS" if turnover <= 0.30 else "FAIL"})

6. 集中度(单标的满仓,仅做示例检查)

max_concentration = 1.0
checks.append({"项": "6. 最大持仓集中度", "阈值": "<= 80%", "实测": fmt_pct(max_concentration), "结果": "PASS" if max_concentration <= 0.80 else "FAIL"})

7. 样本外稳健性

split = int(len(strategy_returns) * 0.6)
train_sharpe = sharpe(strategy_returns[:split])
test_sharpe = sharpe(strategy_returns[split:])
robust = abs(train_sharpe - test_sharpe) <= 0.5
checks.append({"项": "7. 样本外稳健性", "阈值": "|train - test| <= 0.5", "实测": f"{train_sharpe:.3f}/{test_sharpe:.3f}", "结果": "PASS" if robust else "FAIL"})

8. 极端压力

stress_nav = nav[-1]
for _ in range(5):
stress_nav *= 0.97
stress_loss = stress_nav / nav[-1] - 1
checks.append({"项": "8. 极端压力", "阈值": "5日连跌3% <= -15%", "实测": fmt_pct(stress_loss), "结果": "PASS" if stress_loss >= -0.15 else "FAIL"})

summary = {
"标的": CODE,
"期末净值": round(float(nav[-1]), 2),
"总收益率": fmt_pct(nav[-1] / INIT_CAPITAL - 1),
"交易次数": len(trades),
"检查清单": checks,
"PASS 数": sum(1 for c in checks if c["结果"] == "PASS"),
"FAIL 数": sum(1 for c in checks if c["结果"] == "FAIL"),
}
print(json.dumps(summary, ensure_ascii=False, indent=2))

五、运行结果(演示环境)

{
"标的": "600519",
"期末净值": 98018.28,
"总收益率": "-1.98%",
"交易次数": 6,
"检查清单": [
{"项": "1. 夏普比率", "阈值": ">= 0.8", "实测": "0.014", "结果": "FAIL"},
{"项": "2. 最大回撤", "阈值": "<= -15%", "实测": "-10.78%", "结果": "PASS"},
{"项": "3. 日胜率", "阈值": ">= 45%", "实测": "52.17%", "结果": "PASS"},
{"项": "4. 信息比率", "阈值": ">= 0.3", "实测": "-1.428", "结果": "FAIL"},
{"项": "5. 换手率", "阈值": "<= 30%", "实测": "20.00%", "结果": "PASS"},
{"项": "6. 最大持仓集中度", "阈值": "<= 80%", "实测": "100.00%", "结果": "FAIL"},
{"项": "7. 样本外稳健性", "阈值": "|train - test| <= 0.5", "实测": "-0.355/0.527", "结果": "FAIL"},
{"项": "8. 极端压力", "阈值": "5日连跌3% <= -15%", "实测": "-14.13%", "结果": "PASS"}
],
"PASS 数": 4,
"FAIL 数": 4
}

这个示例策略 4 项 FAIL,显然不能上线。这就是 checklist 的价值:用数字打断自我感觉良好。

六、常见坑与注意

  1. 阈值不是死的。如果你的资金属性是长期配置,可以放宽换手率;如果是高频交易,夏普阈值要更高。
  2. 不要只看 PASS 数。如果「最大回撤」或「极端压力」FAIL,即使其他项全 PASS 也建议不上线。
  3. 样本外划分要合理。不要用训练集调参后再用同一段数据做样本外,要按时间顺序切分。
  4. 演示证数据有限。正式环境换成自己的 licence 后,历史数据更长,指标会更稳定。

七、小结

上线前的 checklist 不是形式主义,而是:

  • 把「我觉得可以」变成「8 项里 6 项 PASS」。
  • 把「这次不一样」变成「历史极端场景下我能亏多少」。
  • 把「上线再优化」变成「不过关就不部署」。

建议把这 8 项检查写成你策略上线 pipeline 的最后一个 stage,FAIL 即阻断发布。

至此,《Python 量化策略评估体系》系列 6 篇完结。下一系列将继续从「实盘交易工程」方向深入。


了解更多

免责声明:本文仅为技术方法演示,不构成投资建议。历史表现不代表未来收益,投资有风险,入市需谨慎。

posted @ 2026-10-11 07:23  量化分析码农  阅读(6)  评论(0)    收藏  举报