数据科学与机器学习(第43篇):使用潜在高斯混合模型(LGMM)识别指标数据中的隐藏模式·综合运用
📘

数据科学与机器学习(第43篇):使用潜在高斯混合模型(LGMM)识别指标数据中的隐藏模式·综合运用

第 3/3 篇

把高斯混合模型塞进MT5的初始化与预测骨架

想在 MT5 里跑 ONNX 导出的高斯混合模型,第一步是拿到模型句柄。Init 函数接收一段 uchar 缓冲区和一个 flags 参数,内部调用 OnnxCreateFromBuffer 生成 onnx_handle,失败就返回 false,成功再走 OnnxLoad 把网络权重载进内存。 预测分支用了一个 pred_struct 返回体,里面只有 vector proba 和 long label 两个字段。如果模型没初始化,调试模式下会打印提示并返回空结构,实盘前务必先确认 this.initialized 为真。 输入处理上,double 类型的 vector x 会被 Assign 到 vectorf x_float,因为 ONNX 推理默认吃 float。输出侧按 model_output_structure 的第二维(outputs[1])分别开 label 和 proba 零向量,再交给 OnnxRun 填值,最后取 label 数组末位转成 long 返回。 OnCalculate 里给了最朴素的驱动循环:lookback 写死 20,从 prev_calculated 跑到 rates_total,遇到 IsStopped 就停。这段没接特征工程,直接留了 i 的空循环壳,你复制后得自己把 close 或高低差拼成 20 维向量喂给 predict。外汇与贵金属杠杆高,模型信号仅作概率参考,实盘前请在策略测试器跑至少 3 个月 tick 数据。

MQL5 / C++
class="type">bool CGaussianMixture::Init(const class="type">uchar &onnx_buff[], class="type">ulong flags=ONNX_DEFAULT)
{
  onnx_handle = OnnxCreateFromBuffer(onnx_buff, flags); class=class="str">"cmt">//creating onnx handle buffer
  
   if (onnx_handle == INVALID_HANDLE)
     class="kw">return false;
    
  class="kw">return OnnxLoad(onnx_handle);
}
class="kw">struct pred_struct
{
   vector proba;
   class="type">long label;
};
pred_struct CGaussianMixture::predict(const vector &x)
{
   pred_struct res;
   
   if (!this.initialized)
    {
       if (MQLInfoInteger(MQL_DEBUG))
         printf("%s The model is not initialized yet to make predictions | call Init function first",__FUNCTION__);
         
      class="kw">return res;
    }

class=class="str">"cmt">//---
   
   vectorf x_float; class=class="str">"cmt">//Convert inputs from a vector of class="type">class="kw">double values to those class="type">float values
   x_float.Assign(x);
   
   vector label = vector::Zeros(model_output_structure[class="num">0].outputs[class="num">1]); class=class="str">"cmt">//outputs[class="num">1] we get the second shape(columns) from an array
   vector proba = vector::Zeros(model_output_structure[class="num">1].outputs[class="num">1]); class=class="str">"cmt">//outputs[class="num">1] we get the second shape(columns) from an array
    
   if (!OnnxRun(onnx_handle, ONNX_DATA_TYPE_FLOAT, x_float, label, proba)) class=class="str">"cmt">//Run the model and get the predicted label and probability
    {
       if (MQLInfoInteger(MQL_DEBUG))
          printf("Failed to get predictions from Onnx err %d",GetLastError());
      
       DebugBreak();   
       class="kw">return res;
    }
    
class=class="str">"cmt">//---
   res.label = (class="type">long)label[label.Size()-class="num">1]; class=class="str">"cmt">//Get the last item available at the label&class="macro">#x27;s array
   res.proba = proba;
   
   class="kw">return res;
}
class="type">int OnCalculate(const int32_t rates_total,
                const int32_t prev_calculated,
                const class="type">class="kw">datetime &time[],
                const class="type">class="kw">double &open[],
                const class="type">class="kw">double &high[],
                const class="type">class="kw">double &low[],
                const class="type">class="kw">double &close[],
                const class="type">long &tick_volume[],
                const class="type">long &volume[],
                const int32_t &spread[])
  {      
class=class="str">"cmt">//--- Main calculation loop
   
   class="type">int lookback = class="num">20;
   
   for (class="type">int i = prev_calculated; i < rates_total && !IsStopped(); i++)
   {      

「训练好的模型怎么在K线上跑出概率」

把离线训好的 GMM 模型接到实时行情,核心在 OnCalculate 里按 bar 倒序回推。先防越界:i+1<lookback 时直接 continue,避免 CopyBuffer 在开头几根去抓不存在的数据而报空。 reverse_index = rates_total-1-i 把循环下标翻成真实柱序,再用 getX 取最近 lookback 根的多指标快照。若 x.Size()==0 说明取数失败,同样跳过,不污染缓冲区。 预测段调用 lgmm.predict(x),返回结构体里 proba 是各类别概率向量,label 是硬分类。ProbabilityBuffer[i]=proba.Max() 只存最大后验概率,作为直方图高度;label 0/1/2 分别写 ColorBuffer 0/1/2,给后续着色用。Comment 把当前柱序、Proba、label 打到图表左上,方便肉眼核对。 getX 内部用 CDataFrame 聚合:外层循环遍历 indicators 数组,里层按 buffer_names 逐缓冲 CopyIndicatorBuffer(handle, buffer_no, start, count)。复制失败会 printf 出函数名、行号与 GetLastError,便于定位是哪个指标句柄失效。 最后 df.iloc(-1) 返回 DataFrame 最后一行,也就是最新一根的截面特征向量。外汇与贵金属波动剧烈,这套概率仅反映历史样本分布,实盘信号可能随流动性断裂而失真,需先在策略测试器跑周线以上回测再上真仓。

MQL5 / C++
if(i+class="num">1<lookback) class=class="str">"cmt">//prevent data not found errors during copy buffer
      class="kw">continue;

   class="type">int reverse_index = rates_total - class="num">1 - i;

   class=class="str">"cmt">//--- Get the indicators data

   vector x = getX(reverse_index, lookback);

   if(x.Size()==class="num">0)
      class="kw">continue;

   pred_struct res = lgmm.predict(x);

   vector proba = res.proba;
   class="type">long label = res.label;

   ProbabilityBuffer[i] = proba.Max();

   class=class="str">"cmt">// Determine class="type">color based on histogram value

   if(label == class="num">0)
      ColorBuffer[i] = class="num">0;
   else if(label == class="num">1)
      ColorBuffer[i] = class="num">1;
   else
      ColorBuffer[i] = class="num">2;

   Comment("bars [",i+class="num">1,"/",rates_total,"]"," Proba: ",proba," label: ",label);
   }

class=class="str">"cmt">//--- 
   class="kw">return(rates_total);
}
vector getX(class="type">uint start=class="num">0, class="type">uint count=class="num">10)
{
class=class="str">"cmt">//--- Get buffers
   CDataFrame df;
   for(class="type">uint ind=class="num">0; ind<indicators.Size(); ind++) class=class="str">"cmt">//Loop through all the indicators
      {      
         class="type">uint buffers_total = indicators[ind].buffer_names.Total();
         
         for(class="type">uint buffer_no=class="num">0; buffer_no<buffers_total; buffer_no++) class=class="str">"cmt">//Their buffer names resemble their buffer numbers 
            {
               class="type">class="kw">string name = indicators[ind].buffer_names.At(buffer_no); class=class="str">"cmt">//Get the name of the buffer, it is helpful for the DataFrame and CSV file
               
               vector buffer = {};
               if(!buffer.CopyIndicatorBuffer(indicators[ind].handle, buffer_no, start, count)) class=class="str">"cmt">//Copy indicator buffer 
                  {
                     printf("func=%s line=%d | Failed to copy %s indicator buffer, Error = %d",__FUNCTION__,__LINE__,name,GetLastError());
                     class="kw">continue;
                  }
               
               df.insert(name, buffer); class=class="str">"cmt">//Insert a buffer vector and its name to a dataframe object
            }
      }
      
   class="kw">return df.iloc(-class="num">1); class=class="str">"cmt">//Return the latest information from the dataframe which is the most recent buffer
}

◍ 把混合高斯模型塞进MT5指标初始化

想在 MT5 里用 ONNX 格式的 latent Gaussian mixture 给 XAUUSD 日线做状态概率染色,初始化阶段先把模型和 15 个振荡器句柄一次性挂好是最容易翻车的地方。 代码里先用 CGaussianMixture 对象读 LGMM.XAUUSD.PERIOD_D1.onnx,路径拼法依赖 ONNX_COMMON_FOLDER 宏,文件缺失会直接 printf 报错并返回 INIT_FAILED,不会静默崩。 指示器数组 indicators[15] 从 iATR(14) 到 iWPR(14) 顺序取句柄,最后用 for 循环统一校验 INVALID_HANDLE——任一个振荡器创建失败就整体初始化失败,这比逐个报错更利于排查。 ProbabilityBuffer 和 ColorBuffer 分别映射为 INDICATOR_DATA 与 INDICATOR_COLOR_INDEX,绘图类型设成 DRAW_COLOR_HISTOGRAM,短名写死 "3-Color Histogram",意味着下游只认三色柱状态。外汇与贵金属杠杆高,模型输出的概率仅作状态参考,实盘须自担回撤风险。

MQL5 / C++
class="macro">#include <Gaussian Mixture.mqh>
class="macro">#include <Arrays\ArrayString.mqh>
class="macro">#include <MALE5\Pandas\pandas.mqh>
CGaussianMixture lgmm;
input class="type">class="kw">string symbol = "XAUUSD";
input ENUM_TIMEFRAMES timeframe = PERIOD_D1;
class="kw">struct indicator_struct
{
   class="type">long handle;
   CArrayString buffer_names;
};
indicator_struct indicators[class="num">15];
class=class="str">"cmt">//--- Indicator buffers
class="type">class="kw">double ProbabilityBuffer[];
class="type">class="kw">double ColorBuffer[];
class="type">class="kw">double MaBuffer[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Custom indicator initialization function                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- indicator buffers mapping

   Comment("");

   class=class="str">"cmt">// Setting indicator properties
   SetIndexBuffer(class="num">0, ProbabilityBuffer, INDICATOR_DATA);
   SetIndexBuffer(class="num">1, ColorBuffer, INDICATOR_COLOR_INDEX);

   class=class="str">"cmt">// Setting histogram drawing style
   PlotIndexSetInteger(class="num">0, PLOT_DRAW_TYPE, DRAW_COLOR_HISTOGRAM);

   class=class="str">"cmt">// Set indicator labels
   IndicatorSetString(INDICATOR_SHORTNAME, "class="num">3-Color Histogram");
   IndicatorSetInteger(INDICATOR_DIGITS, _Digits);

class=class="str">"cmt">//---

   class="type">class="kw">string filename = StringFormat("LGMM.%s.%s.onnx",symbol, EnumToString(timeframe));
   if (!lgmm.Init(filename, ONNX_COMMON_FOLDER))
      {
         printf("%s Failed to initialize the GaussianMixture model(LGMM) in ONNX format file={%s}, Error = %d",__FUNCTION__,filename,GetLastError());
      }

class=class="str">"cmt">//--- Oscillators

   indicators[class="num">0].handle = iATR(symbol, timeframe, class="num">14);
   indicators[class="num">0].buffer_names.Add("ATR");

   class=class="str">"cmt">//...
   class=class="str">"cmt">//...
   class=class="str">"cmt">//...

   indicators[class="num">14].handle = iWPR(symbol, timeframe, class="num">14);
   indicators[class="num">14].buffer_names.Add("WPR");

   for (class="type">uint i=class="num">0; i<indicators.Size(); i++) 
      if (indicators[i].handle==INVALID_HANDLE)
         {
            printf("%s Invalid %s handle, Error = %d",__FUNCTION__,indicators[i].buffer_names[class="num">0],GetLastError());
            class="kw">return INIT_FAILED;
         }
         
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }

用 AIC/BIC 肘部点定 LGMM 分量数

Scikit-Learn 的混合模型会顺带输出 AIC 与 BIC 两个信息准则值。把它们随分量数变化的曲线画在同一张图上,找曲线由陡降转为平缓的位置——那就是肘部点,继续加分量只换来边际改善。 实测里 AIC 和 BIC 在分量数从 1 增到 2 时都急剧下落,之后仍在降,但过了 5 个分量后斜率明显变缓。所以这套 LGMM 取 5 个分量比较合适,而不是拍脑袋选 3 个。 分量数改成 5 后,指标要输出 5 路概率值,MT5 端 indicator_color1 得配 5 种颜色,OnCalculate 里按预测标签 0~4 分别写 ColorBuffer,否则直方图会丢色。新图能跑,但读起来不如传统超买超卖震荡指标直观,外汇和贵金属波动大、模型误分概率不低,拿去验证时建议先旁看几根 K 线再信。

MQL5 / C++
lowest_bic = np.inf
bic = []
aic = []
n_components_range = range(class="num">1, class="num">10)
for n_components in n_components_range:
    gmm = GaussianMixture(n_components=n_components, random_state=class="num">42)
    gmm.fit(X)
    bic.append(gmm.bic(X_train))
    aic.append(gmm.aic(X_train))
    if bic[-class="num">1] < lowest_bic:
        best_gmm = gmm
        lowest_bic = bic[-class="num">1]
# Plot the BIC and AIC scores
plt.figure(figsize=(class="num">8, class="num">5))
plt.plot(n_components_range, bic, label=&class="macro">#x27;BIC&class="macro">#x27;, marker=&class="macro">#x27;o&class="macro">#x27;)
plt.plot(n_components_range, aic, label=&class="macro">#x27;AIC&class="macro">#x27;, marker=&class="macro">#x27;o&class="macro">#x27;)
plt.xlabel(&class="macro">#x27;Number of components&class="macro">#x27;)
plt.ylabel(&class="macro">#x27;Score&class="macro">#x27;)
plt.title(&class="macro">#x27;LGMM selection: AIC vs BIC&class="macro">#x27;)
plt.legend()
plt.grid(True)
plt.show()
components = class="num">5 # according to the elbow point
gmm = GaussianMixture(n_components=components, covariance_type="full", random_state=class="num">42)
gmm.fit(X_train)
latent_features_train = gmm.predict_proba(X_train)
latent_features_test = gmm.predict_proba(X_test)
class=class="str">"cmt">//class="kw">property indicator_color1  clrDodgerBlue, clrLimeGreen, clrCrimson, clrOrange, clrYellow
class="type">int OnCalculate(const int32_t rates_total,
                const int32_t prev_calculated,
                const class="type">class="kw">datetime &time[],
                const class="type">class="kw">double &open[],
                const class="type">class="kw">double &high[],
                const class="type">class="kw">double &low[],
                const class="type">class="kw">double &close[],
                const class="type">long &tick_volume[],
                const class="type">long &volume[],
                const int32_t &spread[])
  {   
class=class="str">"cmt">//--- Main calculation loop
  class="type">int lookback = class="num">20;
  for (class="type">int i = prev_calculated; i < rates_total && !IsStopped(); i++)
   {   
    if (i+class="num">1<lookback) class=class="str">"cmt">//prevent data not found errors during copy buffer
      class="kw">continue;
    class=class="str">"cmt">//...
    class=class="str">"cmt">//...
    class=class="str">"cmt">//...
    class=class="str">"cmt">// Determine class="type">color based on predicted label
    if (label == class="num">0)
      ColorBuffer[i] = class="num">0;
    else if (label == class="num">1)
      ColorBuffer[i] = class="num">1;

「多分类标签如何映射成缓冲区颜色」

这段分支逻辑把模型推理出的离散标签直接写进 ColorBuffer,用于逐根 K 线染色。label 等于 2 时缓冲区置 2,等于 3 时置 3,其余情况统一落到第 4 类,相当于给「其他状态」一个兜底色号。 Comment 函数在每根 bar 上实时打印序号与概率:i+1 对应当前遍历位置,rates_total 是总柱数,proba 和 label 来自前序计算。在 MT5 里跑这段代码,能在图表左上角看到类似「bars [245/500] Proba: 0.87 label: 3」的滚动输出,借此核对分类是否随价格行为翻转。 外汇与贵金属波动剧烈,这类标签仅反映历史样本下的概率倾向,实盘信号可能快速失效,需结合止损与仓位控制。

MQL5 / C++
   else if (label == class="num">2)
      ColorBuffer[i] = class="num">2;
   else if (label == class="num">3)
      ColorBuffer[i] = class="num">3;
   else
      ColorBuffer[i] = class="num">4;

   Comment("bars [",i+class="num">1,"/",rates_total,"]"," Proba: ",proba," label: ",label);
  }

◍ 把潜在特征喂给随机森林后发生了什么

LGMM 吐出的潜在特征本质是样本归属各聚类的概率,单看数值很难解释含义。把它们和 ATR、RSI、MACD 等原始指标拼到一起,再丢进随机森林分类器,模型才有可能自己挖出这些隐变量和交易信号之间的非线性关系。 先用 1 根 K 线作为前瞻目标训练,验证集表现很差。问题大概率出在目标设定:实战中 RSI 跌破 30 我们倾向认为未来数根 K 线偏多头,而不是只赌下一根。把前瞻窗口改成 5 根 K 线后重训,整体准确率到了 54%——不高,但足以信特征重要性排序。 特征重要性图里,LATENT_FEATURE_4 排到第五,LATENT_FEATURE_0 和 LATENT_FEATURE_1 也压过了部分原始指标。说明 LGMM 生成的隐特征确实携带了分类有用的模式,不是噪声。外汇与贵金属杠杆高,这类模型仅作辅助研判,实盘须自担风险。 下面这段是拼接特征与训练分类器的核心代码,逐行拆一下关键动作:从原表剔除非特征列做训练测试拆分;用 LGMM 对两端数据 predict_proba 拿到概率矩阵并转成带列名的 DataFrame;hstack 把指标和潜在特征合并;最后用平衡类别权重的随机森林拟合。

MQL5 / C++
from sklearn.model_selection class="kw">import train_test_split
X = new_df.drop(columns=[
    "Time",
    "Open",
    "High",
    "Low",
    "Close",
    "future_close",
    "Direction"
])
y = new_df["Direction"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=class="num">0.2, shuffle=True, random_state=class="num">42)
latent_features_train = gmm.predict_proba(X_train)
latent_features_test = gmm.predict_proba(X_test)
latent_features_train_df = pd.DataFrame(latent_features_train, columns=[f"LATENT_FEATURE_{i}" for i in range(latent_features_train.shape[class="num">1])])
latent_features_test_df = pd.DataFrame(latent_features_test, columns=[f"LATENT_FEATURE_{i}" for i in range(latent_features_test.shape[class="num">1])])
latent_features_train_df
all_columns = X_train.columns.tolist() + latent_features_train_df.columns.tolist()
X_latent_train_arr = np.hstack([X_train, latent_features_train_df])
X_latent_test_arr = np.hstack([X_test, latent_features_test_df])
X_Train_latent = pd.DataFrame(X_latent_train_arr, columns=all_columns)
X_Test_latent = pd.DataFrame(X_latent_test_arr, columns=all_columns)
X_Train_latent.columns
Index([&class="macro">#x27;ATR&class="macro">#x27;, &class="macro">#x27;BearsPower&class="macro">#x27;, &class="macro">#x27;BullsPower&class="macro">#x27;, &class="macro">#x27;Chainkin&class="macro">#x27;, &class="macro">#x27;CCI&class="macro">#x27;, &class="macro">#x27;Demarker&class="macro">#x27;,
       &class="macro">#x27;Force&class="macro">#x27;, &class="macro">#x27;MACD MAIN_LINE&class="macro">#x27;, &class="macro">#x27;MACD SIGNAL_LINE&class="macro">#x27;, &class="macro">#x27;Momentum&class="macro">#x27;, &class="macro">#x27;OsMA&class="macro">#x27;,
       &class="macro">#x27;RSI&class="macro">#x27;, &class="macro">#x27;RVI MAIN_LINE&class="macro">#x27;, &class="macro">#x27;RVI SIGNAL_LINE&class="macro">#x27;,
       &class="macro">#x27;StochasticOscillator MAIN_LINE&class="macro">#x27;, &class="macro">#x27;StochasticOscillator SIGNAL_LINE&class="macro">#x27;,
       &class="macro">#x27;TEMA&class="macro">#x27;, &class="macro">#x27;WPR&class="macro">#x27;, &class="macro">#x27;LATENT_FEATURE_0&class="macro">#x27;, &class="macro">#x27;LATENT_FEATURE_1&class="macro">#x27;,
       &class="macro">#x27;LATENT_FEATURE_2&class="macro">#x27;, &class="macro">#x27;LATENT_FEATURE_3&class="macro">#x27;, &class="macro">#x27;LATENT_FEATURE_4&class="macro">#x27;],
      dtype=&class="macro">#x27;object&class="macro">#x27;)
from sklearn.ensemble class="kw">import RandomForestClassifier
from sklearn.utils.class_weight class="kw">import compute_class_weight
classes = np.unique(y_train)
weights = compute_class_weight(class_weight=&class="macro">#x27;balanced&class="macro">#x27;, classes=classes, y=y_train)
class_weights_dict = dict(zip(classes, weights))
params = {
    "n_estimators": class="num">100,
    "min_samples_split": class="num">2,
    "max_depth": class="num">10,
    "max_leaf_nodes": class="num">10,
    "criterion": "gini",
    "random_state": class="num">42
}
model = RandomForestClassifier(**params, class_weight=class_weights_dict)
model.fit(X_Train_latent, y_train)
y_train_pred = model.predict(X_Train_latent)

训练集与测试集的分类报告落差

模型在训练集上打出 0.64 的准确率,其中类别 -1 的 precision 0.60、recall 0.67,类别 1 的 precision 0.68、recall 0.61,样本量 3760。换到测试集,准确率直接掉到 0.47,两类 precision 都不到 0.50,support 仅 940。这种训练/测试近 17 个点的差距,说明模型很可能过拟合了潜在空间特征。 用 feature_importances_ 拉出随机森林的基尼重要性,把特征名和分数塞进 DataFrame 排序后横向画图,最上面的就是主导信号。这一步能帮你判断前面降维留下的 latent 特征里,哪几个才真在驱动方向判断。 标签构造上,代码用 lookahead=5 把五根 K 线后的 Close 平移过来,future_close 高于当前 Close 标 1(偏多),否则标 -1(偏空)。外汇与贵金属波动大、滑点高,这种前瞻标签在实盘只是概率参考,别当确定性信号。

MQL5 / C++
print("Train classification report\n", classification_report(y_train, y_train_pred))
y_test_pred = model.predict(X_Test_latent)
print("Test classification report\n", classification_report(y_test, y_test_pred))
importances = model.feature_importances_
feature_names = X_Train_latent.columns if hasattr(X_Train_latent, &class="macro">#x27;columns&class="macro">#x27;) else [f&class="macro">#x27;feature_{i}&class="macro">#x27; for i in range(X_Train_latent.shape[class="num">1])]
# Create DataFrame and sort
importance_df = pd.DataFrame({&class="macro">#x27;feature&class="macro">#x27;: all_columns, &class="macro">#x27;importance&class="macro">#x27;: importances})
importance_df = importance_df.sort_values(&class="macro">#x27;importance&class="macro">#x27;, ascending=False)
# Plot
plt.figure(figsize=(class="num">8, class="num">6))
plt.barh(importance_df[&class="macro">#x27;feature&class="macro">#x27;], importance_df[&class="macro">#x27;importance&class="macro">#x27;], class="type">color=&class="macro">#x27;red&class="macro">#x27;)
plt.title(&class="macro">#x27;RFC Feature Importance(Gini Importance)&class="macro">#x27;)
plt.xlabel(&class="macro">#x27;Importance Score&class="macro">#x27;)
plt.gca().invert_yaxis()  # Most important on top
plt.show()
lookahead = class="num">5
df["future_close"] = df["Close"].shift(-lookahead)
new_df = df.dropna()
new_df["Direction"] = np.where(new_df["future_close"]>new_df["Close"], class="num">1, -class="num">1)

「分类报告里的泛化落差」

模型在训练集上给出了偏乐观的判别力:类别 -1 的 precision 0.56、recall 0.70,类别 1 的 precision 0.69、recall 0.54,整体 accuracy 落在 0.61,样本量 3756。 换到测试集,指标全面下滑——类别 -1 的 precision 掉到 0.46、recall 0.61,类别 1 的 precision 0.63、recall 0.48,accuracy 只剩 0.54,测试样本 940。 训练与测试 macro avg 从 0.62/0.61 跌到 0.55/0.53,说明这套信号在未知行情上泛化能力偏弱,外汇与贵金属本身高波动高风险,直接实盘跟信号大概率会被过拟合反噬。 开 MT5 把同一段特征重新跑一遍 walk-forward,看测试集 f1 是否稳定在 0.5 上方,再决定是否进策略池。

MQL5 / C++
              precision   recall  f1-score   support
          -class="num">1       class="num">0.56      class="num">0.70      class="num">0.62      class="num">1706
           class="num">1       class="num">0.69      class="num">0.54      class="num">0.61      class="num">2050
    accuracy                           class="num">0.61      class="num">3756
    macro avg       class="num">0.62      class="num">0.62      class="num">0.61      class="num">3756
weighted avg       class="num">0.63      class="num">0.61      class="num">0.61      class="num">3756
Test classification report
              precision   recall  f1-score   support
          -class="num">1       class="num">0.46      class="num">0.61      class="num">0.52       class="num">392
           class="num">1       class="num">0.63      class="num">0.48      class="num">0.55       class="num">548
    accuracy                           class="num">0.54       class="num">940
    macro avg       class="num">0.55      class="num">0.55      class="num">0.53       class="num">940
weighted avg       class="num">0.56      class="num">0.54      class="num">0.54       class="num">940

◍ LGMM与随机森林双模型EA的接入要点

把 LGMM 生成的潜在特征喂给随机森林,是这套 EA 的核心思路。训练时用的是哪个品种、哪周期,实盘或测试器里就必须保持一致,否则 ONNX 模型读出来的分布会错位。 OnInit 里要先后初始化两个模型:先加载 LGMM.{SYMBOL}.{TIMEFRAME}.onnx 做特征压缩,再加载 rfc.{SYMBOL}.{TIMEFRAME}.onnx 做信号分类。文件名拼错或文件夹路径不对,会直接返回 INIT_FAILED,MT5 终端日志里能看到具体报错。 getX 函数负责把多个指标的 buffer 循环塞进 CDataFrame,再调 LGMM 产出潜变量,和原指标拼成随机森林的最终输入。这里循环写的 count=10 只是默认样本窗口,回测前应按训练时的样本数改掉。 平仓逻辑绑死 LOOKAHEAD:训练目标变量用了几根 K 线前瞻,EA 就持同样根数后平。代码里 input uint LOOKAHEAD = 5 对应日线 XAUUSD 的 5 根,也就是约一周。外汇与贵金属杠杆高,模型信号仅代表历史数据下的概率倾向,实盘前务必在策略测试器用相同品种周期跑一遍。

MQL5 / C++
class="macro">#include <Random Forest.mqh>
class="macro">#include <Arrays\ArrayString.mqh>
class="macro">#include <pandas.mqh> class=class="str">"cmt">//[MQL5官方文档]
class="macro">#include <Trade\Trade.mqh>
class="macro">#include <Trade\PositionInfo.mqh>
class="macro">#include <Trade\SymbolInfo.mqh>
class="macro">#include <errordescription.mqh>
CSymbolInfo m_symbol;
CTrade m_trade;
CPositionInfo m_position;
CRandomForestClassifier rfc;
class="macro">#define MAGICNUMBER class="num">11062025
input class="type">class="kw">string SYMBOL = "XAUUSD";
input ENUM_TIMEFRAMES TIMEFRAME = PERIOD_D1;
input class="type">uint LOOKAHEAD = class="num">5;
input class="type">uint SLIPPAGE = class="num">100;
class="type">int OnInit()
  {
   if (!MQLInfoInteger(MQL_DEBUG) && !MQLInfoInteger(MQL_TESTER))
    {
       ChartSetSymbolPeriod(class="num">0, SYMBOL, TIMEFRAME);
       if (!SymbolSelect(SYMBOL, true))
         {
            printf("%s failed to select SYMBOL %s, Error = %s",__FUNCTION__,SYMBOL,ErrorDescription(GetLastError()));
            class="kw">return INIT_FAILED;
         }
    }
class=class="str">"cmt">//--- Loading the Gaussian Mixture model
   class="type">class="kw">string filename = StringFormat("LGMM.%s.%s.onnx",SYMBOL, EnumToString(TIMEFRAME));
   if (!lgmm.Init(filename, ONNX_COMMON_FOLDER))
     {
        printf("%s Failed to initialize the GaussianMixture model(LGMM) in ONNX format file={%s}, Error = %s",__FUNCTION__,filename,ErrorDescription(GetLastError()));
     }

class=class="str">"cmt">//--- Loading the RFC model

   filename = StringFormat("rfc.%s.%s.onnx",SYMBOL,EnumToString(TIMEFRAME));
   Print(filename);
   if (!rfc.Init(filename, ONNX_COMMON_FOLDER))
     {
        printf("func=%s line=%d, Failed to Load the RFC in ONNX file={%s}, Error = %s",__FUNCTION__,__LINE__,filename,ErrorDescription(GetLastError()));
        class="kw">return INIT_FAILED;
     }
class=class="str">"cmt">//...
class=class="str">"cmt">//... other lines of code
class=class="str">"cmt">//...
}
vector getX(class="type">uint start=class="num">0, class="type">uint count=class="num">10)
{
class=class="str">"cmt">//--- Get buffers
   CDataFrame df;
   for (class="type">uint ind=class="num">0; ind<indicators.Size(); ind++) class=class="str">"cmt">//Loop through all the indicators
     {    
       class="type">uint buffers_total = indicators[ind].buffer_names.Total();
       for (class="type">uint buffer_no=class="num">0; buffer_no<buffers_total; buffer_no++) class=class="str">"cmt">//Their buffer names resemble their buffer numbers 
         {
            class="type">class="kw">string name = indicators[ind].buffer_names.At(buffer_no); class=class="str">"cmt">//Get the name of the buffer, it is helpful for the DataFrame and CSV file

把多指标缓冲拼成模型输入并在Tick上触发

这段逻辑干的事很直接:先把各个指标的缓冲区按名字塞进一个 dataframe 对象,取最后一行(最新一根 K 的缓冲值)作为实时特征。若 dataframe 行数为 0,直接返回空向量,避免后续预测在空数据上崩掉。 拿到 indicators_data 后,用 lgmm 模型预测潜特征,再把原始指标和潜特征横向拼接(hstack)返回。注意 latent_features 若为空也会提前返回 Zeros(0),说明模型没给出有效分布概率时不会强行交易。 OnTick 里先按 MAGICNUMBER 与 TIMEFRAME*LOOKAHEAD 的秒数关仓,再 RefreshRates 刷新报价;失败就打印错误并退出。随后 getX() 取模型输入,尺寸为 0 同样不动作。 真正的信号在 rfc.predict(x) 上:cls 为 1 倾向开多,为 -1 倾向开空,proba 是对应概率。仅当同魔法号下无买也无卖仓时才下单,手数取 LotsMin()。外汇与贵金属杠杆高,模型信号仅代表概率倾向,实盘前务必在 MT5 策略测试器用历史数据验证信号分布与回撤。

MQL5 / C++
  vector buffer = {};
  if (!buffer.CopyIndicatorBuffer(indicators[ind].handle, buffer_no, start, count)) class=class="str">"cmt">//Copy indicator buffer 
    {
      printf("func=%s line=%d | Failed to copy %s indicator buffer, Error = %d",__FUNCTION__,__LINE__,name,GetLastError());
      class="kw">continue;
    }
  
  df.insert(name, buffer); class=class="str">"cmt">//Insert a buffer vector and its name to a dataframe object
    }
  }

  if ((class="type">uint)df.shape()[class="num">0]==class="num">0)
   class="kw">return vector::Zeros(class="num">0);

class=class="str">"cmt">//--- predict the latent features
   vector indicators_data = df.iloc(-class="num">1); class=class="str">"cmt">//index=-class="num">1 returns the last row from the dataframe which is the most recent buffer from all indicators

class=class="str">"cmt">//--- Given the indicators let&class="macro">#x27;s predict the latent features

   vector latent_features = lgmm.predict(indicators_data).proba;

   if (latent_features.Size()==class="num">0)
    class="kw">return vector::Zeros(class="num">0);

   class="kw">return hstack(indicators_data, latent_features); class=class="str">"cmt">//Return indicators data stacked alongside latent features 
 }
class="type">void OnTick()
  {
class=class="str">"cmt">//--- Close trades after AI predictive horizon is over
   CloseTradeAfterTime(MAGICNUMBER, PeriodSeconds(TIMEFRAME)*LOOKAHEAD);

class=class="str">"cmt">//--- Refresh tick information
   if (!m_symbol.RefreshRates())
     {
       printf("func=%s line=%s. Failed to copy rates, Error = %s",__FUNCTION__,ErrorDescription(GetLastError()));
       class="kw">return;
     }

class=class="str">"cmt">//---
    vector x = getX(); class=class="str">"cmt">//Get all the input for the model

    if (x.Size()==class="num">0)
     class="kw">return;

    class="type">long signal = rfc.predict(x).cls; class=class="str">"cmt">//the class predicted by the random forest classifier
    class="type">class="kw">double proba = rfc.predict(x).proba; class=class="str">"cmt">//probability of the predictions

    class="type">class="kw">double volume = m_symbol.LotsMin();

    if (!PosExists(POSITION_TYPE_SELL, MAGICNUMBER) && !PosExists(POSITION_TYPE_BUY, MAGICNUMBER)) class=class="str">"cmt">//no position is open
      {
        if (signal == class="num">1) class=class="str">"cmt">//If a model predicts a bullish signal
          m_trade.Buy(volume, SYMBOL, m_symbol.Ask()); class=class="str">"cmt">//Open a buy trade 
        else if (signal == -class="num">1) class=class="str">"cmt">// if a model predicts a bearish signal
          m_trade.Sell(volume, SYMBOL, m_symbol.Bid()); class=class="str">"cmt">//open a sell trade
      }
 }

「把 LGMM 当特征工厂而非预言机」

潜在高斯混合模型(LGMM)在 MT5 里最务实的落点不是直接预测涨跌,而是把高维行情压成少数几个带隐藏结构的潜在特征。附带的 LGMM Indicator.mq5 能把这类特征画在副图,接随机森林分类器(Random Forest.mqh)后才进 EA 开平仓逻辑。 它默认每个分量服从多元正态分布,而 XAUUSD 的 15 分钟波动里极端影线不少,高斯假设常被打破;初始化分量数选错,潜在特征就退化成噪声。建议先用 Scripts\Get XAUUSD Data.mq5 落一盘 OHLCT+振荡器 CSV,在 Python Code\main.ipynb 里跑降维,确认特征分离度再上实盘。 外汇与贵金属杠杆高、滑点跳空频繁,这套管线只提高信息密度,不消除爆仓风险。把它当特征工厂用,别当黑箱预言机,组合策略的胜率才可能稳住。

常见问题

在 OnInit 里加载模型参数并预分配缓冲区,预测逻辑放到单独函数按需调用,避免每根K线重算全量数据。
用当前指标值代入模型计算各分量后验概率,写进独立缓冲区,副图即可看到实时概率曲线。
小布可自动按 AIC/BIC 肘部点跑多组分量数对比,并直接标出推荐档位,你只需打开对应品种页查看。
给每个类别分配独立缓冲区并设置不同绘图颜色,或用调色板按标签值动态上色,切换状态一目了然。
多数情况下二层模型能压掉单模型误报,但需看样本外回测;若过拟合则噪音反而上升,建议用交叉验证确认。