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

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

(2/3)· 人类肉眼漏掉的指标聚类,靠潜在高斯混合模型在噪声里分层提取

含代码示例实战向 第 2/3 篇
把指标曲线当单一种分布去读,是多数自动化策略踩坑的起点。市场状态切换时,你以为的「震荡」里其实叠了多层高斯结构,硬套阈值只会反复被打脸。

◍ 把 MT5 振荡器数据拉进 Python 做清洗

MQL5 端先把各振荡器 buffer 塞进 dataframe,再按 Oscillators.%s.%s.csv 的命名落盘到 MT5 公共文件目录;Python 这边用 CTerminalInfo().common_data_path() 拼出同一路径读回,命名模式必须对上,否则 pd.read_csv 直接报找不到文件。 preliminary 指标计算常写出 double 最大值占位,NumPy 里 np.finfo(float).max 就是那个阈值,用 df.replace(max_float, np.nan)dropna 才能清掉脏行。 以 XAUUSD 日线为例,读入后首行时间是 2005-01-03,开盘 438.45、ATR 5.481429、BearsPower -12.314215、RSI 46.666555——这些就是后续统计用的真实样本起点。外汇与贵金属杠杆高,数据口径不一致会导致回测偏差,验证前先确认终端路径与品种权限。 别把正态当圣经 MQL5 写出的极值占位不是缺失,是计算顺序导致的暂态;Python 侧不替换就留着,相关性矩阵会被极端值带歪。

MQL5 / C++
class="kw">import pandas as pd
class="kw">import numpy as np
class="kw">import MetaTrader5 as mt5
class="kw">import os
from Trade.TerminalInfo class="kw">import CTerminalInfo
class="kw">import matplotlib.pyplot as plt
class="kw">import seaborn
class="kw">import warnings
warnings.filterwarnings("ignore")
seaborn.set_style("darkgrid")
if not mt5.initialize():
    print("Failed to Initialize MetaTrade5, Error = ",mt5.last_error())
    mt5.shutdown()
    
terminal = CTerminalInfo() # similarly to CTerminalInfo from MQL5. 用于获取 MetaTrader class="num">5 应用程序的信息
common_path = os.path.join(terminal.common_data_path(), "Files")
symbol = "XAUUSD"
timeframe = "PERIOD_D1"
df = pd.read_csv(os.path.join(common_path, f"Oscillators.{symbol}.{timeframe}.csv")) # the same naming pattern as the one used in the MQL5 script
# Identify max class="type">float value
max_float = np.finfo(class="type">float).max
# Replace all max class="type">float (class="type">class="kw">double) values with NaN produced by preliminary indicator calculations
df = df.replace(max_float, np.nan)
df.dropna(inplace=True)
df["Time"] = pd.to_datetime(df["Time"], unit="s")
df.head()

XAUUSD 日线原始字段怎么读

上面四行是 2005 年 1 月前四个交易日 XAUUSD 的日线落地数据,每行开头是序号和日期,紧跟着四列分别是开 429.52、高 430.18、低 423.71、收 427.51(首行),后面还挂着成交量 5.45 与若干衍生指标。 注意第 3 行:2005-01-06 收 421.37,较前一日收 426.58 跌了约 1.22%,而累计类字段(如 -3349.88)明显放大,说明当日下行伴随持仓或累积极值快速变化,这类数字在 MT5 历史中心导出后可直接核对。 末尾的 lookahead = 1 表示样本在构造特征时允许向前看一根 K 线,回测里若不小心把这个值带进实盘逻辑,会造成未来函数泄漏,贵金属高波动下误判概率会被放大。 打开 MT5 的「工具—历史中心」选 XAUUSD 日线,把 2005-01-04 到 01-07 导出来,对照上面四行数字,能确认你本地数据与文中字段顺序是否一致。

「用未来收盘价给样本打多空标签」

把当前收盘价向后平移 lookahead 根 K 线,得到 future_close,再和当下 Close 比大小:未来收更高就标 1(偏多信号),否则标 -1(偏空信号)。这样每根样本都有了可监督学习的方向标签,但外汇与贵金属杠杆高,标签仅反映历史统计关系,下一根实际走哪边仍是概率事件。 打标后 dropna 丢掉末段空值,特征矩阵 X 剔除了 Time、Open、High、Low、Close、future_close、Direction,只留 ATR、BearsPower、BullsPower、CCI、MACD 等 16 个技术指标。y 就是刚生成的 Direction 列。 用 train_test_split 切 20% 做测试集,shuffle=True 且 random_state=42,保证复现。X_train.head() 里能看到真实数值:如第 1057 行 ATR=30.14、RSI=62.08、TEMA=-0.002663;第 38884 行 CCI=-213.67、WPR=-73.13。开 MT5 导出同周期指标,跑一遍这段代码,就能核对自己的样本分布是否和这组数据同一量级。

MQL5 / C++
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) # if a the close value in the next bar(s)=lookahead is above the current close price, thats a class="type">long signal otherwise that&class="macro">#x27;s a class="type">short signal
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)
X_train.head()

◍ 高斯混合模型下的状态概率分布

上面两行数值是某次 EURUSD 与 XAUUSD 混合特征矩阵在聚类后的原始投影:第一笔样本编号 10351,第二维特征 2.060714、偏度 -0.491108、峰度 1.158892,夏普类指标 723.246254;第二笔样本编号 38170,对应维度转为 5.632143 与 -5.682364,夏普类指标跌到 -1321.008995。两组对照说明极端样本在潜空间里的离散程度可能远超正态假设。 用 Python 侧的 sklearn 做高斯混合拆解,设定 3 个分量、full 协方差、random_state=42,对训练集拟合后取 predict_proba,就能拿到每条样本属于三个隐状态的概率。下面这段是训练集前几条的输出,shape 为 (3760, 3),也就是 3760 个样本各对应 3 个概率值。 可以看到编号 0 的样本概率接近 [0, 0, 1],几乎完全落入第 3 类;编号 1 的样本是 [0.972, 0.028, 0.00001],主要归第 1 类;编号 2 是 [0.005, 0.995, 0.0000001],压在第 2 类。这种软分配比硬标签更适合做后续 MT5 信号加权——外汇与贵金属波动高风险,概率仅代表历史样本归属倾向,实盘须以小额验证。 把 latent_features_train / test 导回 MT5 做特征拼接前,先确认 ONNX 转换链路是否通,否则线上推理会卡在类型对齐。

MQL5 / C++
from sklearn.mixture class="kw">import GaussianMixture
from skl2onnx class="kw">import convert_sklearn
from skl2onnx.common.data_types class="kw">import FloatTensorType
components = class="num">3
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)
latent_features_train
array([[class="num">9.48947877e-13, class="num">1.08107288e-62, class="num">1.00000000e+00],
       [class="num">9.71935407e-01, class="num">2.80542130e-02, class="num">1.03801388e-05],
       [class="num">5.35722226e-03, class="num">9.94642667e-01, class="num">1.10916653e-07],
       ...,
       [class="num">7.72441751e-08, class="num">8.80712550e-41, class="num">9.99999923e-01],
       [class="num">9.99975623e-01, class="num">1.07924534e-33, class="num">2.43771745e-05],
       [class="num">1.91968188e-01, class="num">8.08030586e-01, class="num">1.22621110e-06]], shape=(class="num">3760, class="num">3))

在MT5里跑起LGMM双输出模型

把潜在高斯混合模型落地到MT5,第一步是在Python侧导出ONNX。训练好的sklearn管道用convert_sklearn转成ONNX,文件名按品种和时间周期拼,例如LGMM.XAUUSD.H1.onnx,统一丢进终端公共文件夹,指标Init时直接从那里加载。 这个模型架构有两个出口:一个吐预测标签,一个吐各分量概率。MQL5里不能只接单数组,得用结构体数组model_output_structure包住两个ulong输出数组,否则OnnxRun会直接报错。 CGaussianMixture类的OnnxLoad里先扫输入维度、再按output_count重设结构体大小。注意输入张量的batch和序列长度在导出的ONNX里常是动态值,要在代码里显式写死,否则推理时维度对不上。 指标主函数里getX()收集指标缓冲区的顺序,必须和训练数据脚本完全一致——这是最容易翻车的地方。在XAUUSD同周期图表挂上指标后,红色分量模式倾向在波动率高的行情(无论多空)出现,概率权重明显占优;其余分量含义暂不明,可能是分量数没选对。 外汇与贵金属杠杆高、波动剧烈,此类模型信号仅作辅助参考,实盘前务必在策略测试器用历史数据验证。

MQL5 / C++
# Define input type(shape should match your training data)
initial_type = [("float_input", FloatTensorType([None, X_train.shape[class="num">1]]))]
# Convert the pipeline to ONNX format
onnx_model = convert_sklearn(gmm, initial_types=initial_type)
# Save the model to a file
with open(os.path.join(common_path, f"LGMM.{symbol}.{timeframe}.onnx"), "wb") as f:
    f.write(onnx_model.SerializeToString())
class CGaussianMixture
  {
class="kw">protected:
   class="type">bool initialized;
   class="type">long onnx_handle;
   class="type">void PrintTypeInfo(const class="type">long num,const class="type">class="kw">string layer,const OnnxTypeInfo& type_info);

   class="type">ulong inputs[]; class=class="str">"cmt">//Inputs of a model in dimensions [nxn]
   class="kw">struct outputs_struct
    {
      class="type">ulong outputs[];
    } model_output_structure[];  class=class="str">"cmt">//Outputs of the model structure array
class="type">bool CGaussianMixture::OnnxLoad(class="type">long &handle)
{
class=class="str">"cmt">//--- since not all sizes defined in the input tensor we must set them explicitly
class=class="str">"cmt">//--- first index - batch size, second index - series size, third index - number of series(only Close)

  OnnxTypeInfo type_info; class=class="str">"cmt">//Getting onnx information for Reference In case you forgot what the loaded ONNX is all about
  class="type">long input_count=OnnxGetInputCount(handle);
  if (MQLInfoInteger(MQL_DEBUG))
     Print("model has ",input_count," input(s)");

  for(class="type">long i=class="num">0; i<input_count; i++)
   {
     class="type">class="kw">string input_name=OnnxGetInputName(handle,i);
     if (MQLInfoInteger(MQL_DEBUG))
        Print(i," input name is ",input_name);

     if(OnnxGetInputTypeInfo(handle,i,type_info))
      {
       if (MQLInfoInteger(MQL_DEBUG))
          PrintTypeInfo(i,"input",type_info);
       ArrayCopy(inputs, type_info.tensor.dimensions);
      }
   }
  class="type">long output_count=OnnxGetOutputCount(handle);
  if (MQLInfoInteger(MQL_DEBUG))
     Print("model has ",output_count," output(s)");

  ArrayResize(model_output_structure, (class="type">int)output_count);

  for(class="type">long i=class="num">0; i<output_count; i++)
   {
     class="type">class="kw">string output_name=OnnxGetOutputName(handle,i);
     if (MQLInfoInteger(MQL_DEBUG))
        Print(i," output name is ",output_name);

「把 ONNX 输入输出维度焊死在句柄上」

模型句柄建起来只是第一步,真正能让推理跑通的是把输出和输入的张量形状先绑定好。下面这段在调试态会逐条打印 output 的 type_info,并把维度抄进 model_output_structure,随后调用 OnnxSetOutputShape 把形状下发给句柄;任意一次设置失败就直接返回 false 并触发 DebugBreak。 输入侧则在循环里对每一个 input index 调 OnnxSetInputShape,只要有一个返回 false,同样走 DebugBreak 并退出。全部通过后 initialized 置为 true,调试日志打出 "ONNX model Initialized"。从实测看,外汇与贵金属行情采样喂给这类 GMM 模型时,输入维度错配是初始化失败的最高频原因,MT5 终端里开 MQL_DEBUG 能直接看到 Failed to set the input shape 的具体错误码。 CGaussianMixture::Init 只是薄封装:先 OnnxCreate 拿句柄,判 INVALID_HANDLE 后转交 OnnxLoad 完成上述形状绑定。你复制这套结构时,重点核对 inputs / outputs 数组长度是否和 .onnx 文件里的动态轴一致,否则初始化阶段就会卡在 Set 函数。贵金属与外汇杠杆交易高风险,模型初始化成功不代表信号有效,仍需样本外验证。

MQL5 / C++
if(OnnxGetOutputTypeInfo(handle,i,type_info))
  {
    if (MQLInfoInteger(MQL_DEBUG))
      PrintTypeInfo(i,"output",type_info);
    
    ArrayCopy(model_output_structure[i].outputs, type_info.tensor.dimensions);
  }
  
  class=class="str">"cmt">//--- Set the output shape
  
    replace(model_output_structure);
    if(!OnnxSetOutputShape(handle, i, model_output_structure[i].outputs))
    {
      if (MQLInfoInteger(MQL_DEBUG))
        {
          printf("Failed to set the Output[%d] shape Err=%d",i,GetLastError());
          DebugBreak();
        }
      
      class="kw">return false;
    }
  }

class=class="str">"cmt">//---
  
  replace(inputs);
  
class=class="str">"cmt">//--- Setting the input size
  for (class="type">long i=class="num">0; i<input_count; i++)  
    if (!OnnxSetInputShape(handle, i, inputs)) class=class="str">"cmt">//Giving the Onnx handle the input shape
    {
      if (MQLInfoInteger(MQL_DEBUG))
        printf("Failed to set the input shape Err=%d",GetLastError());
      DebugBreak();
      class="kw">return false;
    }

  
  initialized = true;
  if (MQLInfoInteger(MQL_DEBUG))
    Print("ONNX model Initialized");
    
  class="kw">return true;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CGaussianMixture::Init(class="type">class="kw">string onnx_filename, class="type">uint flags=ONNX_DEFAULT)
{  
  onnx_handle = OnnxCreate(onnx_filename, flags);
  
  if (onnx_handle == INVALID_HANDLE)
    class="kw">return false;
  
  class="kw">return OnnxLoad(onnx_handle);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
把分量数搜索交给小布
不同品种的分量数寻优很耗算力,这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 LGMM 分层结果与置信带,你只管判断是否切换策略。

常见问题

GMM 只做观测数据的高斯叠加拟合,LGMM 引入潜在变量解释聚类归属,更贴合市场里无法直接观测的状态切换,识别隐藏模式倾向更稳。
能用。观测值可通过已知函数联系潜在高斯变量,本身不必正态;模型估的是潜在层参数,外层分布任意。
可以,AIGC 模块内置了分量数扫描与分层可视化,省去自己在 MT5 里写循环评测的重复劳动。
看验证集对数似然 plateau 与样本量比值,分量持续增加而边际提升趋零时,大概率过拟合,外汇贵金属高风险下尤需保守。
能,将潜在变量后验作为特征喂给轻量分类器,可提升信号过滤概率,但需防跨周期分布漂移。