接受者操作特征(ROC)曲线入门(基础篇)
「ROC曲线在MT5信号评估里的实际位置」
ROC(Receiver Operating Characteristic)曲线原本出自信号检测理论,在MT5里被用来衡量一个自定义指标或EA给出的“多/空触发”信号,区分真实行情方向与噪声的能力。横轴是假阳性率(FPR),纵轴是真阳性率(TPR),曲线越贴近左上角,分类边界越干净。 很多交易者直接看胜率就下单,但胜率会被样本里涨跌比例扭曲;ROC把阈值扫描一遍,画出不同cut-off下的表现,能看出你的信号到底靠不靠阈值吃饭。2025年12月3日某公开示例帖被浏览665次,说明这类基础评估工具在社群里仍有高频需求。 在MT5里验证一条信号线是否真有判别力,最简办法是导出历史bar的 signal>0/<=0 与后续N根是否同向,用Python或Excel算TPR/FPR描点。若曲线接近对角线,这条信号大概率只是随机扰动,外汇与贵金属杠杆品种里据此开仓风险极高。
◍ ROC 曲线为什么适合金融分类器
ROC 图把分类器的真正例率(TPR)和假正例率(FPR)画在同一张图上,最早来自信号检测理论,本质是在看「抓得准」和「误报多」之间的权衡。 真正让它在金融场景里站得住脚的,是类别分布极度不均时曲线形状依然稳定——比如贵金属 1 分钟序列里趋势段可能只占 8%,ROC 不会像准确率那样被多数类带偏。 外汇与贵金属杠杆高、跳空频繁,错分类的代价两边不对称,ROC 不预设错误成本也能暴露这种结构缺陷,这是它比单一准确率指标更实用的地方。 不过概念好懂不代表用起来没坑,实际回测里对阈值选取和样本切分的误解很常见,后面会拆开说。
二元分类只盯一个目标类
做信号识别或交易风控时,很多任务本质就是二元分类:一个样本要么落在目标类,要么落在其补集。雷达回波判坦克还是非坦克、信用卡判欺诈还是合法,都是这个结构。 在 MT5 里写策略,我们常不显式把样本分去两个标签,而是只回答一个问题——这根 K 线 / 这笔订单属不属于目标类。目标类可以是恶性组织、盈利平仓,也可以是欺诈交易。 这种表述的关键点在于:你只定义一个主类,其余一切归为补集。它不限于军事语境,外汇与贵金属的高波动环境下,用二元思路做入场过滤或异常检测,能直接降低标签噪声。
「用 2x2 矩阵看清分类器错在哪」
把训练好的分类器丢到测试集上,每个样本只会有四种归宿:正类判正(TP)、正类判负(FN)、负类判负(TN)、负类判正(FP)。这四格拼成一张 2x2 混淆矩阵,是所有性能指标的母表。 真正例率和误报率是最常被盯的两个数。召回率 = TP / (TP + FN),衡量你漏掉了多少该抓的信号;误报率 = FP / (TN + FP),衡量你误杀了多少正常样本。在外汇或贵金属的行情分类里,这两者的权衡直接决定策略是钝刀割肉还是乱开枪。 漏报率 = FN / (TP + FN) 与特异度 = TN / (TN + FP) 补足了另一半视角。前者是「该赚没赚到」的比例,后者是「本分样本认得准不准」。MT5 里跑完 Classification 后,把这四个值打印出来,比只看总准确率更有用——小布盯盘的高风险品种上,准确率 90% 也可能全是 TN 堆出来的。
◍ 阈值滑动下的命中率与误报率博弈
二元分类模型内部先输出一个数值预测,再拿它和预定阈值比大小:达到或超过阈值判为目标,否则判为非目标。阈值直接决定命中率与误报率——把阈值压到最小可能预测值以下,全部实例都判目标,命中率满格但误报率同样糟糕;阈值抬到最大可能值以上,全部判非目标,命中率和误报率都归零。 最佳阈值落在两个极端之间。系统地把阈值扫过整个取值范围,命中率和误报率各自从 0 走到 1,且一方改善往往以另一方恶化为代价。把误报率放横轴、命中率放纵轴,以阈值为隐含参数对应描点,得到的就是 ROC 曲线。 若模型输出纯随机、和真实类别无关,平均来看命中率≈误报率,ROC 近似左下到右上的对角线。比如以 50% 概率随机判正类,正负样本各猜对一半,坐标落在 (0.5, 0.5);以 90% 概率判正类,真正例率 90% 同时假正例率也 90%,坐标到 (0.9, 0.9)。想挤进左上三角(优于随机),模型必须吃到数据里真实存在的模式。 完美模型存在某个阈值能干净切开两类,ROC 走右上角→左上角→左下角:阈值从最小升到最优,误报率 1 降到 0 而命中率恒为 1;再升阈值,命中率降到 0 而误报率恒为 0。普通模型曲线夹在对角线和左上角之间,偏离对角线越远性能越好。 ROC 图左侧靠横轴的分类器偏保守,要有实锤证据才判正类,假正例率低但真正例率也常低;右上角的敢在证据薄弱时出手,真正例率高但假正例率也高。在负类占多数的实盘环境里,左侧区域的表现更值得盯。右下三角劣于随机,通常不会有点;把分类决策整体反转(否定),右下点的真正例变假负例、假正例变真负例,就映射到左上三角。对角线代表无可辨别信息,对角线下方则说明握有模式只是用反了方向。
从混淆矩阵里挑阈值
光看 ROC 曲线形状只能算粗筛,真要定模型在哪个决策阈值上干活,得把每个阈值对应的混淆矩阵摊开算。以二元分类为例,目标类标 1、非目标标 0,遍历阈值就能拿到一串 TPR、FPR、精确率之类的指标,写进表裡直接比对。 阈值从大到小扫一遍,TPR 和 FPR 都会从 1 掉到 0,但有能力区分的模型里 FPR 掉得更快——样本表里 FPR 已经触 0 时,TPR 还挂在 1.000 上。所以最优阈值大概率落在这两个极值夹出的区间裡,除非你特别怕某一类错。 两类错误成本差不多时,看 M_E(漏报率加误报率除以二)就够;成本差很多就得盯 PREC 和 NPREC。表裡还有 ACC 和 B_ACC,后者是灵敏度和特异度均值,专门压类别不平衡的偏。 算这些数的核心在 roc_curves.mqh:roc_stats() 吃真实标签向量、预测概率向量和阈值,填进 conf_stats 结构;roc_table() 吐出整张 ROC 矩阵,只认二元模型,乱喂多类会直接报错返零矩阵。下面这段是脚本跑 Crash 1000 Index M5 时打印的头部几行,FPR 全 0 而 TPR 随阈值下降,印证了上文说的下降速率差。 外汇或贵金属上套这类分类信号,记住杠杆品种错判成本可能极高,阈值宁可偏保守。
KG <span class="number">class="num">0</span> <span class="number">class="num">17</span>:<span class="number">class="num">13</span>:<span class="number">class="num">16.458</span> ROC_curves_table_demo(Crash <span class="number">class="num">1000</span> Index,M5) TPR FPR FNR TNR PREC NPREC M_E ACC B_ACC THRESH NF <span class="number">class="num">0</span> <span class="number">class="num">17</span>:<span class="number">class="num">13</span>:<span class="number">class="num">16.458</span> ROC_curves_table_demo(Crash <span class="number">class="num">1000</span> Index,M5) <span class="number">class="num">0.038</span> <span class="number">class="num">0.000</span> <span class="number">class="num">0.962</span> <span class="number">class="num">1.000</span> <span class="number">class="num">1.000</span> <span class="number">class="num">0.000</span> <span class="number">class="num">0.481</span> <span class="number">class="num">0.667</span> <span class="number">class="num">0.519</span> <span class="number">class="num">0.98195</span> DI <span class="number">class="num">0</span> <span class="number">class="num">17</span>:<span class="number">class="num">13</span>:<span class="number">class="num">16.458</span> ROC_curves_table_demo(Crash <span class="number">class="num">1000</span> Index,M5) <span class="number">class="num">0.077</span> <span class="number">class="num">0.000</span> <span class="number">class="num">0.923</span> <span class="number">class="num">1.000</span> <span class="number">class="num">1.000</span> <span class="number">class="num">0.000</span> <span class="number">class="num">0.462</span> <span class="number">class="num">0.680</span> <span class="number">class="num">0.538</span> <span class="number">class="num">0.97708</span> OI <span class="number">class="num">0</span> <span class="number">class="num">17</span>:<span class="number">class="num">13</span>:<span class="number">class="num">16.458</span> ROC_curves_table_demo(Crash <span class="number">class="num">1000</span> Index,M5) <span class="number">class="num">0.115</span> <span class="number">class="num">0.000</span> <span class="number">class="num">0.885</span> <span class="number">class="num">1.000</span> <span class="number">class="num">1.000</span> <span class="number">class="num">0.000</span> <span class="number">class="num">0.442</span> <span class="number">class="num">0.693</span> <span class="number">class="num">0.558</span> <span class="number">class="num">0.97556</span> GI <span class="number">class="num">0</span> <span class="number">class="num">17</span>:<span class="number">class="num">13</span>:<span class="number">class="num">16.458</span> ROC_curves_table_demo(Crash <span class="number">class="num">1000</span> Index,M5) <span class="number">class="num">0.154</span> <span class="number">class="num">0.000</span> <span class="number">class="num">0.846</span> <span class="number">class="num">1.000</span> <span class="number">class="num">1.000</span> <span class="number">class="num">0.000</span> <span class="number">class="num">0.423</span> <span class="number">class="num">0.707</span> <span class="number">class="num">0.577</span> <span class="number">class="num">0.97334</span> GH <span class="number">class="num">0</span> <span class="number">class="num">17</span>:<span class="number">class="num">13</span>:<span class="number">class="num">16.458</span> ROC_curves_table_demo(Crash <span class="number">class="num">1000</span> Index,M5) <span class="number">class="num">0.192</span> <span class="number">class="num">0.000</span> <span class="number">class="num">0.808</span> <span class="number">class="num">1.000</span> <span class="number">class="num">1.000</span> <span class="number">class="num">0.000</span> <span class="number">class="num">0.404</span> <span class="number">class="num">0.720</span> <span class="number">class="num">0.596</span> <span class="number">class="num">0.97221</span>