数据科学与机器学习(第43篇):使用潜在高斯混合模型(LGMM)识别指标数据中的隐藏模式·进阶篇
(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 侧不替换就留着,相关性矩阵会被极端值带歪。
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 导出同周期指标,跑一遍这段代码,就能核对自己的样本分布是否和这组数据同一量级。
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 转换链路是否通,否则线上推理会卡在类型对齐。
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同周期图表挂上指标后,红色分量模式倾向在波动率高的行情(无论多空)出现,概率权重明显占优;其余分量含义暂不明,可能是分量数没选对。 外汇与贵金属杠杆高、波动剧烈,此类模型信号仅作辅助参考,实盘前务必在策略测试器用历史数据验证。
# 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 函数。贵金属与外汇杠杆交易高风险,模型初始化成功不代表信号有效,仍需样本外验证。
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">//+------------------------------------------------------------------+