把报酬打乱几千次之后,你的成绩排在哪里——以及这个数字不回答什么。
回测跑完,Sharpe 看起来不错。但你心里那个问题没被回答:这是策略真的抓到了什么,还是你刚好在一段上涨行情里随便切了几刀?一个能回答这题的方法是把数据打乱几千次,看你的成绩在那堆随机数里排第几——这就是工作页回测分页那一列「MCPT p 值」在做的事。这篇讲它在检验什么、怎么读、以及它过关之后仍然没回答的那个问题。
MCPT(Monte Carlo Permutation Test,蒙特卡洛排列检验)是一个统计检验:它把回测期间的报酬序列随机重排很多次,每次都用同一组进出场位置重新算一次 Sharpe,得到一整条「纯靠运气会长什么样」的分布,再看你的实际成绩落在这条分布的哪个位置。
| 项目 | 内容 |
|---|---|
| 虚无假设 | 这支策略选中的那些报酬期,不比随机选的好 |
| p 值定义 | 打乱后的 Sharpe 大于或等于实际 Sharpe 的比例 |
| 判读 | p < 0.05 → 95% 信心水准下有统计显著的优势 |
| 检验范围 | 整段回测数据,没有训练/测试切分 |
p 值越小,代表「随便打乱也能打赢你」的情况越少见。以默认的 2000 次为例,p = 0.05 表示这 2000 次里有 100 次的随机数成绩不输你;p = 0.005 表示只有 10 次。
这是整个设计里最关键、也最容易误解的一点:被打乱的是前向报酬序列,你的部位序列全程固定。
每一次置换做的事: position(你的部位) ← 固定不动,循环外只算一次 × vol_scalar(部位缩放) ← 固定不动,循环外只算一次 − fee_cost(手续费) ← 固定不动,循环外只算一次 × 打乱后的报酬序列 ← 只有这一项每次重抽 = 这一次置换的 Sharpe
因为手续费与部位缩放都在循环外算完,每一次置换承受的交易成本与杠杆都一样。剩下唯一的变数就是「报酬出现的顺序」——也就是你挑中的那些时间点,比随机挑的时间点好吗。
因为那是一道送分题。打乱一个二元的部位阵列,会造出远多于策略本身的进出场切换;在 fee = 0.0005 之下,每一次置换都要承受大约 30 到 40 倍的手续费拖累,所有置换后的 Sharpe 都会被逼成深度负值。不管你的策略有没有真优势,p 值都会漂亮得没有意义。
它是自动的,不是选配。每一支 Type A 策略跑回测时都会跑一次,结果直接写进回测统计、显示在工作页回测分页。
| 设定 | 默认 | 说明 |
|---|---|---|
| 置换次数 | 2000 | 可在策略里用 MCPT_N 复写,但仍受下面那条预算公式限制 |
| 随机数种子 | 42 | 私有随机数生成器,不动全局种子——同一份回测永远得到同一个 p 值 |
| 手续费 | 策略自己的 FEE | 回测用什么就传什么 |
| 关掉 | MCPT = False | 整个跳过,不写任何 MCPT 字段 |
两个值得记住的行为:扫参数时不跑 MCPT(一次扫描是几十到几百次回测,加上它会慢到不能用),所以你看到的 p 值永远来自「采用参数之后的那一次回测」;而策略上线之后,每一次 live 或排程触发都会把同一组 MCPT 字段原样带着走——同一份码、同一组参数,p 值仍然成立,所以上线中的策略会一直看得到它。
工作页的回测分页不判过不过,它只显示数字,加一句说明。p < 0.05 时写的是:
没过的时候写的是:
真正的「过不过」出现在策略库的三道品质关卡。关卡 2 叫「统计显著」,说明写「MCPT 随机置换检验 p < 0.05」,而实际判定要两个条件同时成立:
| 条件 | 门槛 |
|---|---|
| p 值 | < 0.05 |
| 置换次数 | ≥ 1000 |
| 组合策略(Type C) | 显示「MCPT 不适用(组合策略)」,不算失败 |
三道关卡全部通过或不适用,策略才会挂上「已验证」徽章。分工讲白一点:你的机器只负责算出 p 值与置换次数,「这样算不算过」是上架端的判断。
拿 workspace 附的教学范例 btc_sma_cross 跑一次(BTCUSDT 1h,2022-01-01 起,SMA_FAST = 45/SMA_SLOW = 100,参数一字未改,41,287 根 K 线)。以下所有数字都来自 2026-09-17 那一次执行。
| 回测 | 值 |
|---|---|
| 总报酬 | +123.95% |
| Benchmark(买入持有) | +64.36% |
| 最大回撤 | −44.93% |
| Sharpe Ratio | 0.6751 |
| 交易笔数 | 478 |
| 已付手续费(占本金) | 23.90% |
| MCPT | 值 |
|---|---|
| p 值 | 0.0160 |
| 置换次数 | 2000 |
| 直方图红线「实际 Sharpe」 | 0.8985 |
| 分布范围 | −0.8565 ~ 1.4218 |
回测 stdout 自己印的那一行是:
MCPT p-value: 0.0160 (n=2000, significant edge at 95%)
怎么读:p = 0.0160 代表 2000 次随机打乱里,有 32 次的 Sharpe 不输给它——换句话说,这组进出场时点在这段历史上不太像是随便挑的。置换次数 2000 也高于 1000 的关卡要求,所以这一项是过的。
这是本文最重要的一段。上面那支策略在同一天、同一份码之下,另外跑了一次 walk-forward 样本外验证:
| 检验 | 它问的问题 | 2026-09-17 那次执行 | 结果 |
|---|---|---|---|
| MCPT | 在这组固定的部位序列上,这个成绩分不分得出实力与运气? | p = 0.0160(n = 2000) | 过 |
| Walk-forward | 照历史挑参数这件事本身,到下一段还管不管用? | 样本外效率 −0.334(样本外 Sharpe −0.45) | 远低于门槛 |
一个过、一个没过,而且两个都没算错——因为它们回答的根本不是同一个问题。MCPT 检验的是「这一组已经决定好的进出场,放在这段历史上不像乱猜」;walk-forward 检验的是「用历史挑参数这个动作,换到没看过的下一段还成不成立」。前者过关不蕴含后者。
这不是在说这支策略坏掉了——它是 workspace 附的教学范例,重点在两个检验的语意差异。完整的 walk-forward 数字与怎么读,见〈样本外验证怎么做〉。
如果你让 agent 把 MCPT 的直方图画出来传进对话,那张图的图例上会印着一行字:
Actual OOS Sharpe = ⟨你的数字⟩
OOS 是 out-of-sample(样本外)的缩写。那行字是旧注解的残留标签,不要照着理解。 MCPT 从头到尾跑在整段回测数据上,没有任何训练窗、测试窗、或比例切分——它不可能是样本外的东西,上面那张对照表就是实例。
工作页回测分页里的那张图没有这个问题,红线标的是「实际 Sharpe」。另外,对话里那张图是手动重跑产生的,手动重跑用的参数跟自动那次不一定一样,所以图上的数字也不必然等于分页上那一列。
自动 MCPT 实际跑几次,不完全由你决定。它有一条执行预算公式:
实跑次数 = min( MCPT_N, 20000, max( 200, 4e8 ÷ K 线数 ) )
K 线越多,4e8 ÷ K 线数 越小。上面那次执行是 41,287 根 K 线,离天花板还很远,所以 2000 次一次都没被砍。但官方文件记载的对照是:4 万根 → 2000 次;20 万根(5 分线两年)→ 2000 次;100 万根(1 分线两年)→ 400 次。
问题在于上架关卡卡的是置换次数 ≥ 1000。照公式推算,K 线数超过约四十万根时实跑次数就会低于 1000——就算 p = 0.001,关卡照样判不通过。而画面上完全没有提示:被砍的消息只写进主机的 log,工作页的说明文字照样带入被砍过的次数,没有任何警示样式。
看回上面那两张表:同一次回测,Sharpe Ratio 卡写 0.6751,直方图红线的「实际 Sharpe」写 0.8985,差了 0.22,肉眼就看得出来。这不是 bug,是两条不同的算法:
| 项目 | MCPT 内部的 Sharpe(0.8985) | 回测分页的 Sharpe Ratio(0.6751) |
|---|---|---|
| 报酬怎么算 | 简单报酬,不分开盘/收盘段 | 隔夜段与盘中段分开计价 |
| 部位缩放 | 一定会套:30% 目标波动、2 倍杠杆上限 | 策略自己写了才有 |
关键在第二列:自动 MCPT 不会把策略自己的目标波动设定传进去,它一律用函式的默认值。btc_sma_cross 没有做波动率目标化,但它的 MCPT Sharpe 仍然是「套了 30% 目标波动、2 倍上限之后」的数字(目标波动在做什么,见〈波动率目标化怎么运作〉)。两个数字不一样是正常的,界面目前没有说明这件事。
失败时它是安静的:没有任何错误消息,回测照常成功,前端就是少那一列。原因至少有四种,而画面不区分:
| 原因 | 发生了什么 |
|---|---|
| 策略写了 MCPT = False | 直接跳过 |
| 这次回测 0 笔交易 | 没有部位可以检验,跳过 |
| 主机上的 lib 是旧版 | 加载失败,跳过 |
| 数据太短、Sharpe 算出 NaN、或任何例外 | 跳过,回测统计照写 |
宁可不写也不写错的理由很实际:任何 NaN 或 inf 都会让整台机器的策略上传被打回,所以只要数字不干净,这几个字段就整组不写。
Type C(组合策略,N 档标的配一组权重)的回测不跑 MCPT。原因是这个检验的结构是「一条价格序列对一条部位序列」,组合策略没有这个结构。值得诚实讲的是:没有替代检验。
| 想做的事 | 实际情况 |
|---|---|
| 把整个组合压成一条报酬序列再重抽 | 那是对已实现报酬做 bootstrap,答的是另一个问题,而且产生不出 p 值——p 值正是 MCPT 存在的全部意义。这条路是明文禁止的,agent 也不准自己手刻一个东西顶替 |
| 期待关卡卡片显示什么 | 「MCPT 不适用(组合策略)」,不是「未通过」——它不算失败,不影响「已验证」徽章 |
置换次数是多少? 说明文字里的那个次数低于 1000,这个 p 值就过不了上架关卡,不管它多小。先看次数,再看 p 值。
这是哪一次回测的 p 值? 扫参数的过程不跑 MCPT。你手上的 p 值只属于「采用参数之后跑的那一次回测」;中间改过码就要重跑才算数。
红线跟卡片对不起来? 正常。上面那次执行是 0.8985 对 0.6751,算法不同而已,不用去 debug。
你是不是把「显著」读成「会赚」了? 同一支策略可以 p = 0.016 过关、样本外效率 −0.334 惨败。p 值对未来没有任何陈述。
那一列根本不见? 先确认是四种原因里的哪一种(策略关掉了/0 笔交易/lib 旧版/数据不够),不要当成「检验没过」。
p 值看懂了,下一个问题就是上面那张对照表的右半边:换一段没看过的数据还成不成立。那条路是样本外验证,工作页有独立的分页在做。策略库里挂「已验证」徽章的官方策略,关卡 2 过的就是本文讲的这个检验——每一支都附真实回测,数字可以直接看:策略库。