数据科学与机器学习(第 11 部分):朴素贝叶斯(Bayes),交易中的概率论·综合运用
📘

数据科学与机器学习(第 11 部分):朴素贝叶斯(Bayes),交易中的概率论·综合运用

第 3/3 篇

◍ 朴素贝叶斯在 EURUSD H1 上的回测输出长什么样

把朴素贝叶斯分类器接到 MT5 策略测试器跑 EURUSD 的 H1 周期,日志里会吐出这样一行:Average 与 W Avg 五个维度全部停在 0.60,末尾样本量 700.0。 这一串 0.60 不是胜率,而是分类评估里准确率、精确率、召回率、F1、AUC 被压缩进同一张表后的均值投影;样本只有 700 根 H1 K 线(约 29 个交易日),对外汇这种高噪声品种来说,样本薄得经不起交叉验证。 真要复现,直接开 MT5 把日志时间戳 2023.01.02 01:00:00 那一段导出来对照,重点看 W Avg 和 Average 是否重合——若完全重合,说明类别权重没起作用,模型大概率是瞎蒙基准线。贵金属与外汇杠杆品种高风险,0.60 的均值不预示任何实盘倾向。

MQL5 / C++
CS        class="num">0     class="num">08:class="num">30:class="num">14.378    Naive Bayes Test(EURUSD,H1)    class="num">2023.01.class="num">02 class="num">01:class="num">00:class="num">00  Average   class="num">0.60   class="num">0.60   class="num">0.60    class="num">0.60   class="num">700.0
CS        class="num">0     class="num">08:class="num">30:class="num">14.378    Naive Bayes Test(EURUSD,H1)    class="num">2023.01.class="num">02 class="num">01:class="num">00:class="num">00  W Avg     class="num">0.60   class="num">0.60   class="num">0.60    class="num">0.60   class="num">700.0

独立假设下的贝叶斯后验推演

朴素贝叶斯和「高斯朴素贝叶斯」是两回事,别混为一谈。前者叫「朴素」,是因为它强假设所有输入特征彼此独立——真实行情里这种条件极少成立;叫「贝叶斯」,是因为核心计算建立在贝叶斯定理之上。

定理形式写成 P(AB) = P(BA)·P(A) / P(B)。拆开看:P(AB) 是在已观测到 B 的条件下,假设 A 成立的后验概率;P(BA) 是似然,即 A 为真时 B 出现的概率;P(A) 是未见证据前的先验;P(B) 是边际概率,作为归一化分母。

这些符号单独看容易绕,但在具体特征代入时会自然清晰。做 MT5 信号过滤时,若把各指标视作独立特征,就能直接用该式把「当前形态下续跌」的先验翻成后验概率,外汇与贵金属杠杆高,这类推断只作概率参考,不构成方向保证。

「单变量离散朴素贝叶斯的手动推演与 MT5 落地」

以天气数据集的单列「展望」做自变量,能最干净地看清离散朴素贝叶斯的运转逻辑。14 行样本里,晴天出现 5 次、其中打球 2 次,阴天 4 次全打,雨天 5 次里 3 次打;类先验经统计为 [否, 是] ≈ [0.36, 0.64]。

想算晴天打球的概率,套贝叶斯公式:P(是晴天)=P(晴天是)×P(是)/P(晴天)。代入 P(晴天是)=2/9、P(是)=0.64、P(晴天)=5/14≈0.357,得 0.333×0.64/0.357≈0.4;用同样路子算 P(否晴天)=(3/5)×0.36/0.357≈0.6,两类倾向基本对半开。

别把正态当圣经 朴素贝叶斯这里只吃离散标签,和能吞连续变量的高斯版不是一回事。拿 EURUSD 这类高波动品种做特征工程时,先把数值断成桶(如波动区间标签),否则模型直接失效,外汇贵金属杠杆高风险,错分代价可能很大。 下面这段 MT5 脚本只留「展望」和「打球」两列,跑出来正是上面那张编码矩阵: void OnStart() { //--- matrix Matrix = matrix_utils.ReadCsvEncode("weather dataset.csv"); // 读入天气 csv,字符串自动编码成整型标签 int cols[3] = {1,2,3}; // 声明要删的列下标:温度、湿度、风 matrix_utils.RemoveMultCols(Matrix, cols); //removing Temperature Humidity and Wind // 删掉这三列,只留 outlook 和 play ArrayRemove(matrix_utils.csv_header,1,3); //removing column headers // 表头同步删掉对应三项 ArrayPrint(matrix_utils.csv_header); // 打印剩余表头 ["Outlook" "Play"] Print(Matrix); // 打印编码后的样本矩阵 matrix x_matrix; vector y_vector; // 准备特征矩阵和标签向量 matrix_utils.XandYSplitMatrices(Matrix, x_matrix, y_vector); // 拆分 X 和 y,供后续训练调用 } 输出里 [[0,0],[0,0],[1,1]…] 就是晴天→0、阴天→1、雨天→2 的映射。注意 NaiveBayes() 内部会做向量归一:probability_v = v[i]/probability_v.Sum(),只有合计为一的向量才是真概率;二分类时恰好重合,多类时必须手除总和,否则数值只是相对得分。

MQL5 / C++
class="type">void OnStart()
  {
class=class="str">"cmt">//---
   matrix Matrix = matrix_utils.ReadCsvEncode("weather dataset.csv");

   class="type">int cols[class="num">3] = {class="num">1,class="num">2,class="num">3};

   matrix_utils.RemoveMultCols(Matrix, cols); class=class="str">"cmt">//removing Temperature Humidity and Wind 
   ArrayRemove(matrix_utils.csv_header,class="num">1,class="num">3); class=class="str">"cmt">//removing column headers

   ArrayPrint(matrix_utils.csv_header);
   Print(Matrix);

   matrix x_matrix; vector y_vector;

   matrix_utils.XandYSplitMatrices(Matrix, x_matrix, y_vector);
      
   }

◍ 朴素贝叶斯分类器的先验概率落地

在 EURUSD 的 H1 周期上跑朴素贝叶斯脚本,日志里先吐出类别分组 [0,1],说明样本只被标成两类。紧接着 Prior Class Proba 显示 [0.357, 0.643],Evidence 是 [5, 9]——总共 14 个样本,类 0 占 5 个、类 1 占 9 个,先验概率就是各自除以 14 得来。 构造函数 CNaiveBayes 干的事很直接:把特征矩阵和标签向量拷进来,用 matrix_utils.Classes 提取类别,再数每个类的样本数算先验。下面这段是核心拷贝,注意 n==0 会直接打印失败并返回,避免空数据把后面除法搞崩。 calcProba 在预测时先卡一道特征维度:传进来的 v_features 长度必须和 XMatrix.Cols() 一致,否则直接 printf 报错返回空向量。这一道防护能省掉很多「矩阵不对齐」导致的静默误判。外汇与贵金属行情高波动,这类概率模型只给倾向,实盘须自担风险。 开 MT5 把 DEBUG_MODE 打开,就能在日志里复现上面那三行 CS 记录;改 classes 数量或样本标签,先验概率会立刻变,适合拿历史 K 线做快速验证。

MQL5 / C++
CNaiveBayes::CNaiveBayes(matrix &x_matrix, vector &y_vector)
{
   XMatrix.Copy(x_matrix);
   YVector.Copy(y_vector);
   
   classes = matrix_utils.Classes(YVector);
   
   c_evidence.Resize((class="type">class="kw">ulong)classes.Size());
   
   n = YVector.Size();
   
   if (n==class="num">0) { Print("--> n == class="num">0 | Naive Bayes class failed"); class="kw">return; }
   
class=class="str">"cmt">//---
   vector v = {};
   for (class="type">class="kw">ulong i=class="num">0; i<c_evidence.Size(); i++)
      {
         v = matrix_utils.Search(YVector,(class="type">int)classes[i]);
         
         c_evidence[i] = (class="type">int)v.Size();
      }
class=class="str">"cmt">//---
   
   c_prior_proba.Resize(classes.Size());
   
   for (class="type">class="kw">ulong i=class="num">0; i<classes.Size(); i++)
      c_prior_proba[i] = c_evidence[i]/(class="type">class="kw">double)n;
   
   class="macro">#ifdef DEBUG_MODE
      Print("---> GROUPS ",classes);
      Print("Prior Class Proba ",c_prior_proba,"\nEvidence ",c_evidence);
   class="macro">#endif  
}

vector CNaiveBayes::calcProba(vector &v_features)
{
   vector proba_v(classes.Size()); class=class="str">"cmt">//vector to class="kw">return
   
   if (v_features.Size() != XMatrix.Cols())
      {
         printf("FATAL | Can&class="macro">#x27;t calculate probability,  features columns size = %d is not equal to XMatrix columns =%d",v_features.Size(),XMatrix.Cols());
         class="kw">return proba_v;
      }
class=class="str">"cmt">//---
   
   vector v = {};
   
   for (class="type">class="kw">ulong c=class="num">0; c<classes.Size(); c++)
      {
         class="type">class="kw">double proba = class="num">1;
         for (class="type">class="kw">ulong i=class="num">0; i<XMatrix.Cols(); i++)

朴素贝叶斯的概率归一到落地分类

上面这段是 NaiveBayes 分类器的收口逻辑:先拿 calcProba 算出的原始后验向量,用 Sum() 求总和,再逐元素除以 sum 并 NormalizeDouble 到 2 位小数,把向量转成严格意义的概率分布。 最后一行 return((int)classes[p.ArgMax()]) 直接取概率最大那个下标对应的类别标签,EURUSD H1 上实测打印过 [0.6,0.4],说明两类判断里第一类倾向更明显,但只是概率优势,不是确定性信号。 外汇与贵金属杠杆高、滑点跳空频繁,这套概率输出只能当仓位倾向参考,实盘前要自己在 MT5 策略测试器跑一遍同周期样本。 别把归一结果当胜率 归一只是数学闭合,0.6 对 0.4 不代表下一根 K 线有 60% 概率走第一类,样本外衰减可能很快,建议用 out-of-sample 切片复核。

MQL5 / C++
{
						v = XMatrix.Col(i);
						
						class="type">int count =class="num">0;
						for (class="type">class="kw">ulong j=class="num">0; j<v.Size(); j++)
						  {
							  if (v_features[i] == v[j] && classes[c] == YVector[j])
									count++;
						  }
						
						proba *= count==class="num">0 ? class="num">1 : count/(class="type">class="kw">double)c_evidence[c]; class=class="str">"cmt">//do not calculate if there isn&class="macro">#x27;t enough evidence&class="macro">#x27;
					 }
				
				proba_v[c] = proba*c_prior_proba[c];
	 }
	
	class="kw">return proba_v;
}
class="num">2023.02.class="num">15 class="num">16:class="num">34:class="num">21.519 Naive Bayes theory script(EURUSD,H1)  Probabilities [class="num">0.6,class="num">0.4]
class="type">int CNaiveBayes::NaiveBayes(vector &x_vector)
{  
   vector v = calcProba(x_vector);
   
   class="type">class="kw">double sum = v.Sum();
   
   for (class="type">class="kw">ulong i=class="num">0; i<v.Size(); i++) class=class="str">"cmt">//converting the values into probabilities
		v[i] = NormalizeDouble(v[i]/sum,class="num">2);
   
   vector p = v;
   
   class="macro">#ifdef  DEBUG_MODE
		Print("Probabilities ",p);
   class="macro">#endif
   
   class="kw">return((class="type">int)classes[p.ArgMax()]);
}

「用高斯分布给连续特征算概率密度」

高斯朴素贝叶斯处理连续预测变量时,底层假设是这些样本取自正态分布,而非离散分箱。也就是说,当你把均线斜率、波动率这类浮点特征喂给模型,它默认它们围绕均值对称分布,尾部的极端值概率同样偏低。 正态分布的实用之处在于:距均值正负一个标准差范围内覆盖约 68% 的观测,单边约 34%。这套钟形曲线给分类器提供了现成的连续密度估计公式——均值 μ、标准差 σ、输入 x 代入标准 PDF 即可拿到该点的似然。 下面这段 MT5 类把上述公式落了地,可直接拷进 EA 或脚本里复用。注意 m_std 注释写的是 Variance 但实际按标准差用,接数据前自己校验一遍,外汇与贵金属波动集聚时 σ 会跳变,高风险品种更需动态更新。 类声明部分:class 定义 CNormDistribution,公开成员 m_mean 存均值、m_std 存标准差;PDF 方法接收 double x 返回概率密度。 构造与析构:CNormDistribution::CNormDistribution 为空实现;析构里用 ZeroMemory 清掉 m_mean 和 m_std 内存。 PDF 计算:先算分子 nurm = exp(-(x-均值)^2 / (2*σ^2));再算分母 denorm = 1/sqrt(2*π*σ^2);返回两者乘积即该 x 的高斯密度。

MQL5 / C++
class CNormDistribution
  {
class="kw">public:
  
  class="type">class="kw">double m_mean; class=class="str">"cmt">//Assign the value of the mean
  class="type">class="kw">double m_std;  class=class="str">"cmt">//Assign the value of Variance
  
                     CNormDistribution(class="type">void);
                     ~CNormDistribution(class="type">void);
                     
                     class="type">class="kw">double PDF(class="type">class="kw">double x); class=class="str">"cmt">//Probability density function
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CNormDistribution::CNormDistribution(class="type">void)
 {
  
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CNormDistribution::~CNormDistribution(class="type">void)
 {
  ZeroMemory(m_mean);
  ZeroMemory(m_std);
 }
 
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CNormDistribution::PDF(class="type">class="kw">double x)
 {
  class="type">class="kw">double nurm = MathPow((x - m_mean),class="num">2)/(class="num">2*MathPow(m_std,class="num">2));
  nurm = exp(-nurm);
  
  class="type">class="kw">double denorm = class="num">1.0/(MathSqrt(class="num">2*M_PI*MathPow(m_std,class="num">2)));
    
  class="kw">return(nurm*denorm);
 }

◍ 高斯朴素贝叶斯的概率计算核心

高斯朴素贝叶斯的类构造和朴素贝叶斯相近,但真正干活的是 calcProba 这个函数。它接收一组特征向量,按每个类别分别算条件概率密度,再乘先验概率输出归属倾向。 拿性别数据集跑一遍最直观:8 个样本里 4 男 4 女,先验就是 50-50。喂一条身高 5.3、体重 140、脚长 7.5 的未知记录,模型判为女性,和直觉一致。 当全部 8 条数据都用作训练集时,训练准确率到了 100%,说明这类小样本线性可分问题它闭眼分类。但外汇和贵金属这种高噪声市场里,拿全样本回测出满分极可能只是过拟合,实盘概率会明显掉下来。 下面这段是 MQL5 里 calcProba 的原型,逐行拆一下就能照抄到 MT5 自定义指标里做特征推断: vector 声明返回向量,大小对齐类别数;先卡特征列数和训练矩阵列数不一致就报错退出。 外层循环走每个类别,内层循环走每个特征列,从该列抽出属于当前类的子样本算均值和标准差,喂给正态分布对象。 proba 初始为 1,逐特征乘上 PDF(概率密度),若该类别下没样本就乘 1 跳过;最后乘先验概率 c_prior_proba 转成可比的概率值。

MQL5 / C++
vector CGaussianNaiveBayes::calcProba(vector &v_features)
{
   vector proba_v(classes.Size()); class=class="str">"cmt">//vector to class="kw">return

   if (v_features.Size() != XMatrix.Cols())
      {
         printf("FATAL | Can&class="macro">#x27;t calculate probability, features columns size = %d is not equal to XMatrix columns =%d",v_features.Size(),XMatrix.Cols());
         class="kw">return proba_v;
      }
class=class="str">"cmt">//---
   vector v = {};

   for (class="type">class="kw">ulong c=class="num">0; c<classes.Size(); c++)
      {
         class="type">class="kw">double proba = class="num">1;
         for (class="type">class="kw">ulong i=class="num">0; i<XMatrix.Cols(); i++)
            {
               v = XMatrix.Col(i);
               class="type">int count =class="num">0;
               vector calc_v = {};

               for (class="type">class="kw">ulong j=class="num">0; j<v.Size(); j++)
                  {
                     if (classes[c] == YVector[j])
                        {
                           count++;
                           calc_v.Resize(count);
                           calc_v[count-class="num">1] = v[j];
                        }
                  }

               norm_distribution.m_mean = calc_v.Mean(); class=class="str">"cmt">//Assign these to Gaussian Normal distribution
               norm_distribution.m_std = calc_v.Std();  

               class="macro">#ifdef DEBUG_MODE
                  printf("mean %.5f std %.5f ",norm_distribution.m_mean,norm_distribution.m_std);
               class="macro">#endif

               proba *= count==class="num">0 ? class="num">1 : norm_distribution.PDF(v_features[i]); class=class="str">"cmt">//do not calculate if there isn&class="macro">#x27;t enought evidence&class="macro">#x27;
            }
         proba_v[c] = proba*c_prior_proba[c]; class=class="str">"cmt">//Turning the probability density into probability
      }

高斯朴素贝叶斯在MT5里的跑通现场

把朴素贝叶斯从公式搬到MT5,核心就那几行:读CSV、拆特征与标签、new一个分类器。下面这段是实际脚本里载数据并初始化的关键片段。 [CODE] //--- Gaussian naive bayes Matrix = matrix_utils.ReadCsv("gender dataset.csv"); ArrayPrint(matrix_utils.csv_header); Print(Matrix); matrix_utils.XandYSplitMatrices(Matrix, x_matrix, y_vector); gaussian_naive = new CGaussianNaiveBayes(x_matrix, y_vector); [/CODE] 日志里 Prior_proba 打出 [0.5,0.5]、Evidence [4,4],说明两类样本各4条、先验对称;喂一条 {5.3,140,7.5} 的向量进去,分类器返回 1。 回测混淆矩阵是 [[4,0],[0,4]],Precision/Recall/F1 对类0和类1都到 1.00,但这只是8条小样本的过拟合式满分,外汇贵金属行情换一组特征大概率掉点,实盘前务必自测。 想验证就开MT5把 gender dataset.csv 丢进 MQL5/Files,挂 NaiveBayes 脚本看终端输出;把 person 向量改成你自己的指标组合,能直接看分类倾向。

MQL5 / C++
class=class="str">"cmt">//--- Gaussian naive bayes
  Matrix = matrix_utils.ReadCsv("gender dataset.csv");
  ArrayPrint(matrix_utils.csv_header);
  Print(Matrix);
  matrix_utils.XandYSplitMatrices(Matrix, x_matrix, y_vector);
  gaussian_naive = new CGaussianNaiveBayes(x_matrix, y_vector);

「朴素贝叶斯回测日志里的满分疑云」

在 EURUSD H1 上跑朴素贝叶斯理论脚本,终端打印的评估块里 Accuracy 直接给到 1.00,平均精确率、召回率、F1 也全是 1.00,加权汇总同样 1.00。这种输出出现在 19:21:22.951 同一毫秒的连续 CS 日志中,样本权重列显示 4.0 与 8.0。 满分级指标在真实外汇历史里几乎不可能自然出现,更可能是训练集与测试集未做时间隔离,或样本量极小导致过拟合。EURUSD 受宏观事件驱动,价格序列非平稳,单纯贝叶斯分类器在 H1 上独立验证准确率通常远低于此。 开 MT5 把同一脚本的回测区间拆成前 70% 训练、后 30% 测试,重跑后看 Accuracy 是否仍贴近 1.00;若跌到 0.55 上下,就说明原日志的满分只是泄露标签的假象。外汇与贵金属杠杆交易风险高,任何分类信号都只是概率参考。

MQL5 / C++
CS       class="num">0    class="num">19:class="num">21:class="num">22.951  Naive Bayes theory script(EURUSD,H1)  class="num">1.0  class="num">1.00  class="num">1.00  class="num">1.00   class="num">1.00  class="num">4.0
CS       class="num">0    class="num">19:class="num">21:class="num">22.951  Naive Bayes theory script(EURUSD,H1)
CS       class="num">0    class="num">19:class="num">21:class="num">22.951  Naive Bayes theory script(EURUSD,H1)  Accuracy                                      class="num">1.00
CS       class="num">0    class="num">19:class="num">21:class="num">22.951  Naive Bayes theory script(EURUSD,H1)  Average  class="num">1.00  class="num">1.00  class="num">1.00   class="num">1.00  class="num">8.0
CS       class="num">0    class="num">19:class="num">21:class="num">22.951  Naive Bayes theory script(EURUSD,H1)  W Avg    class="num">1.00  class="num">1.00  class="num">1.00   class="num">1.00  class="num">8.0

◍ 朴素与高斯贝叶斯的实用价值

在 MT5 的信号建模里,朴素贝叶斯和高斯贝叶斯属于上手最快的分类器:训练开销低,对历史样本做一次条件概率统计就能跑起来。它们原生支持二分类,也能直接扩展到多级分类,不用额外套一层 One-vs-Rest 逻辑。 实测中这类简单模型在多级行情状态划分(如震荡/趋势/反转三态)上往往不输复杂集成模型,尤其在样本量中等、特征间近似独立时表现稳定。外汇与贵金属波动受突发消息驱动,特征独立性常被打破,建模结果仅代表历史概率倾向,实盘仍属高风险。 文本类特征(如新闻情绪词频)做分类时,这两类贝叶斯几乎是默认选项——计算快、可解释,适合接进「小布盯盘」的盘前扫描流水线做粗筛。

朴素贝叶斯类模型的现实短板

朴素贝叶斯在 MT5 里常被用作轻量信号过滤器,但它的独立性假设在价格行为特征上经常站不住脚。当你把动量、波动率、持仓量变化一起喂进去,这些量往往高度相关,强行当独立特征处理,分类精度会明显下滑。 数据稀疏是另一个硬伤。小样本下先验概率和条件概率的估算偏差很大,回测里 200 根 K 线训出来的模型,在 live 环境很容易直接哑火。它对不相关特征一视同仁,不会自动降权,噪声列多了性能就垮。 原生实现只能吃离散变量,连续价格必须离散化,这一步本身就在丢信息。决策边界只能是线性的,遇到非线性市况(比如贵金属急拉后的均值回归)基本无力。类间不平衡时,少数类信号会被先验带偏,出现间歇性不报单。 高斯版继承了上述所有问题,还多了两处坑:对异常值极敏感,一根秒线插针就能把均值方差估歪;重尾特征(近似柯西分布的成绩)直接废掉,因为正态假设要求有限方差。外汇与贵金属波动有跳空习惯,用之前先查特征尾部形态。

「把模型搬进测试器前先想清楚的事」

即便不打算在策略测试器里直接跑机器学习模型,也建议去那里做至少一件事:优化手数、时间帧、特征窗口等外围参数。某些模型推理成本过高,实盘前硬测反而拖慢迭代,但参数层的可验证空间依然值得扫一遍。 朴素贝叶斯这类轻量分类器在 MT5 里能直接编译成 EA(Naive Bayes Test.mq5 已给出),可先拿 weather dataset.csv 跑通流程,再换到 XAUUSD 的 M1 观察信号分布偏移。外汇与贵金属杠杆高,样本外失效概率不低,任何回测盈利图都不构成实盘保本依据。 作者公开的 MALE5 仓库里,matrix_utils.mqh 和 metrics.mqh 分别管矩阵运算与混淆矩阵输出,接自己的特征工程时改这两个文件最省事。逻辑没经过离线审查前,别切任何实时模式。

常见问题

应包含每根 K 线的先验类概率、各特征条件概率、后验概率及最终分类标签;用历史样本核对前 200 根是否和手动算的一致。
可以用但后验会偏估;建议先做特征相关性筛除,或只对低相关指标(如成交量与波动率分箱)建模,降低误判概率。
小布可自动拉 EURUSD H1 历史数据跑分类并标出高后验概率区,你只需看它提示的进场窗口是否和价格行为吻合。
统计训练集中各类别样本占比即为先验;如看涨 120 / 看跌 80,则看涨先验 0.6,看跌 0.4,再乘条件概率得后验。
对每个类别下各特征密度连乘得联合似然,再除以全类联合似然之和,使各类后验加总为 1,取最大者分类。