param_grid_search 补全回撤/Sharpe 指标:从"胜率游戏"到完整风险评估
本文最后更新于 2026年8月29日 凌晨
我曾经只看胜率做网格搜索,挑出一组胜率 78% 的参数,上线后当天回撤 15%。后来才明白:胜率高不等于抗跌强,最大回撤和 Sharpe 才是参数搜索真正的底线。
下面用纯 pandas 写一版 param_grid_search,把这两个指标塞进评分函数。
只按胜率排序,错在哪
胜率只是赢利交易占总交易的比例,它完全不说输的时候有多狠。
做 100 笔交易,赢 70 笔每笔赚 1 元,输 30 笔每笔亏 3 元。胜率 70%,净亏 20 元。
网格搜索里,按胜率排序就会专挑这种「赢小钱、输大钱」的脆弱参数组。它们胜率可能漂亮到 80%,但偶尔一次爆仓就把前几个月的利润吞干净。
所以评分函数至少要知道三件事:赚得多吗(收益)、赚得稳吗(波动)、亏得狠吗(回撤)。胜率只回答了一半。
回撤要算权益曲线
很多人直接用收盘价算回撤。这会把持仓期间的浮盈浮亏全部漏掉。
持有一笔多单,盘内最高涨 3%,收盘跌回原点。用收盘价算,回撤 0%。真实账户明明摸到过 +3%,然后全吐回去。
正确的做法是用权益曲线(equity curve)——账上现金加所有持仓的市值,也就是你随时能变现的总资产。比例回撤的标准公式是:
1 | |
equity.cummax() 是从起点到当前点的历史最高点。当前权益除以它再减一,就是从峰到谷的跌幅百分比。
拿权益曲线 [100, 110, 90, 95] 举例,历史最高点序列是 [100, 110, 110, 110],对应 drawdown 为 [0, 0, -0.1818, -0.1364],最大回撤就是 -0.1818,约 18.2%。
这里用的是除法,不是减法。 算 equity.cummax() - equity 得到的是绝对亏损额,比如上面例子里的 [0, 0, 20, 15]。这在不同资金规模下不可比——10 万账户亏 2 万和 100 万账户亏 2 万,意义完全不同。我们要比例,才能跨策略、跨资金量比较抗压能力。
实现时还要确保权益曲线的索引是单调递增的时间序列,否则 cummax() 会给出错误结果。
Sharpe 的年化因子要对频率
Sharpe 比率等于年化收益率除以年化波动率。看起来简单,年化因子一旦用错,结果立刻失真。
因子取决于数据频率:
| 数据频率 | 年化因子 |
|---|---|
| 日线 | 252 |
| 周线 | 52 |
| 小时线 | 252 × 6(假设每天 6 个交易小时) |
关键点在于:分子分母都要年化,缺一不可。
1 | |
如果你只把分母乘了 sqrt(252),分子却忘了年化,Sharpe 会系统性偏低差不多 25 倍(就是 sqrt(252) 本身)。我自己就踩过这个坑:跑出来的 Sharpe 全是 0.3 左右,以为策略完全不行,其实是公式少了一头。
再看因子方向。我用同一条权益曲线测了三种频率,结果是:
- 日线(252)→ Sharpe 13.70
- 周线(52)→ Sharpe 6.23
- 小时线(252×6)→ Sharpe 33.57
也就是说,日线数据误套小时线因子,Sharpe 被抬高 2.4 倍;误套周线因子,被压掉一半多。两种错法都不报错,数字照样打印出来,只是完全不可信。
另外,交易次数太少时 Sharpe 没有统计意义。只跑了两笔交易,赢一输一,胜率 50%,这种小样本算出的 Sharpe 基本是噪声。所以评分前要加 min_trades 过滤。阈值看策略频率,日线策略 30 笔够用,高频策略可能要几百笔。
评分函数:Sharpe 除以绝对回撤
我常用的组合是:
1 | |
mdd 是最大回撤(负数),取绝对值后变成正数当惩罚项。分子越大越好(收益好、波动小),分母越小越好(抗跌强),用除法刚好兼顾两头。
这不是唯一选择:
- 更看重绝对收益:
score = sharpe * (1 - abs(mdd)) - 极度怕回撤:
score = sharpe / (abs(mdd) ** 2),回撤惩罚更重
关键是把风险指标塞进评分逻辑,别让胜率一家独大。
纯 pandas 实现
假设你有一个 run_backtest(params),接收参数字典,返回权益曲线和交易记录:
equity:时间索引的pd.Series,必须包含未平仓浮盈亏trades:含pnl列的pd.DataFrame,每笔交易一行,正为赢、负为输
1 | |
两个细节值得说。
第一,returns.std(ddof=1) 用的是样本标准差。几百个点用 ddof=0 也差不了多少,但样本少的时候,ddof=0 会让 Sharpe 虚高。
第二,mdd 为 0 必须单独处理。曲线一路单调上涨时回撤恒为 0,sharpe / 0 直接炸成无穷大,把这种参数组顶到第一名。这类结果通常是回测没跑满或数据没接对。标记 -inf 让它沉到最底下,比硬除一个 1e-9 干净。
完整代码
run_backtest 需要你自己实现,接口约定如上。这里给一个确定性的假回测函数(用三角函数造曲线,不用随机数,跑多少次结果都一样),用来跑通整条流水线:
1 | |
实际输出:
1 | |
最后一行最值得看。ma_len=20 那三组的 Sharpe 有 28,比排第一的 13.8 高出一倍,但回撤是 0。只看 Sharpe 它会稳赢,可这个 0 说明曲线全程单调上涨,多半是回测数据有问题。标成 -inf 沉到最底下,比让它骗你强。
再对比前六行:胜率最高的是 ma_len=5, thresh=0.03(0.583),排序里它排第六。按胜率选参数,你会选它;按 score 选,你会选 ma_len=10, thresh=0.01。两个答案差得很远,因为 0.583 那组回撤 8.9%,是 0.541 那组的四倍多。
怎么选
- 旧代码能跑,只想快速止血:加一行回撤过滤,
if mdd < -0.3: continue。今天就能改完。 - 想认真评估风险:补 Sharpe,用
sharpe / abs(mdd)排序。改动就是上面三个函数。 - 想更进一步:把胜率、回撤、Sharpe、盈亏比、交易次数一起输出,跑完先看分布再定参数。
思路和我之前写的《用 5 个公开指标量化「美元信用健康度」》一脉相承:单一指标永远讲不清全故事,得把维度打开看。
能赚钱的策略很多,能赚钱还能好好睡觉的才难得。