数据科学与机器学习(第43篇):使用潜在高斯混合模型(LGMM)识别指标数据中的隐藏模式·综合运用
把高斯混合模型塞进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 数据。
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 最后一行,也就是最新一根的截面特征向量。外汇与贵金属波动剧烈,这套概率仅反映历史样本分布,实盘信号可能随流动性断裂而失真,需先在策略测试器跑周线以上回测再上真仓。
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",意味着下游只认三色柱状态。外汇与贵金属杠杆高,模型输出的概率仅作状态参考,实盘须自担回撤风险。
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 线再信。
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」的滚动输出,借此核对分类是否随价格行为翻转。 外汇与贵金属波动剧烈,这类标签仅反映历史样本下的概率倾向,实盘信号可能快速失效,需结合止损与仓位控制。
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 把指标和潜在特征合并;最后用平衡类别权重的随机森林拟合。
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(偏空)。外汇与贵金属波动大、滑点高,这种前瞻标签在实盘只是概率参考,别当确定性信号。
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 上方,再决定是否进策略池。
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 根,也就是约一周。外汇与贵金属杠杆高,模型信号仅代表历史数据下的概率倾向,实盘前务必在策略测试器用相同品种周期跑一遍。
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 策略测试器用历史数据验证信号分布与回撤。
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 里跑降维,确认特征分离度再上实盘。 外汇与贵金属杠杆高、滑点跳空频繁,这套管线只提高信息密度,不消除爆仓风险。把它当特征工厂用,别当黑箱预言机,组合策略的胜率才可能稳住。