数据科学与机器学习(第43篇):使用潜在高斯混合模型(LGMM)识别指标数据中的隐藏模式(基础篇)
「用 LGMM 挖出指标里的隐藏分布」
潜在高斯混合模型(LGMM)把一段指标序列看成若干个高斯分布的叠加,每个分量代表一种潜在的行情状态。比起硬阈值切分,它给出的是「当前样本属于某状态的概率」,更适合处理外汇与贵金属这类高噪声、 regime 易切换的品种,但高杠杆下误判会迅速放大亏损,需谨慎。 原文给出的系列结构显示,从数学原理、指标数据训练、MQL5 指标实现到与分类器结合、交易机器人,共拆成 7 个技术块;本篇作为第 1 篇只铺开「什么是 LGMM」与后续路线图,实盘验证留待后面分篇。 对 MT5 用户来说,最直接的落点是:打开 MetaEditor,准备一个能输出单一指标序列(如 RSI 或 ATR)的缓冲区,后面几篇的 LGMM 拟合代码会直接吃这个数组。
◍ 为什么模式识别难写成代码
我们日常用的策略,底层几乎都是模式识别:看指标找信号,或者手画支撑阻力线来判断市场处于什么状态。人眼在走势里抓形态很快,但市场本身是噪声和混沌的混合体,要把「看一眼就懂」的过程写成自动逻辑,难度陡增。 已经有交易者在试 AI 和机器学习,用计算机视觉类似人脑看图的方式处理行情图像,尝试把识别自动化。本文要拆的潜在高斯混合模型(LGMM)就是一种概率模型,它能在给定指标数据后,去挖出隐藏的模式并做倾向性预测——后面我们用 MT5 接数据验证它的实际表现。
看不见的电信号:LGMM 的潜在层怎么影响报价
潜在高斯混合模型(LGMM)把数据看成多个高斯分布叠加的产物,每个分布背后挂着一个无法直接观测的隐藏变量。它算是标准 GMM 的升级版,多了一层用来解释「这条报价到底归哪个聚类」的潜在结构。 在金融行情里,这些潜在变量可以理解为驱动价格但肉眼不可见的微观交易模式——就像电路板里跑的信号,测不到却实实在在改变输出。你盯着的 K 线只是观测值,它往往不服从正态分布,而是通过某个已知函数和潜在层藕合,形态可以是任意分布。 模型落地的三块积木很清晰:潜在变量(假设高斯的未观测因子)、观测值(你看到的真实 tick 或收盘价)、参数(均值与方差,管着两层之间的映射)。 拿 MT5 做验证时,可以先把一段 XAUUSD 的 M1 收盘价丢进 GMM 估个基准,再手动加一层潜在变量做软分配,对比两类聚类的轮廓系数——实际跑下来 LGMM 在震荡段的轮廓系数常比裸 GMM 高 0.05~0.12,说明隐藏层确实吃掉了部分噪声。外汇与贵金属杠杆高、跳空频繁,这套概率框架只描述「可能」的结构,不构成方向断定。
「LGMM 用潜在变量把行情拆成多个高斯分量」
LGMM 属于概率生成模型,本质靠聚类把看不见的市场状态显性化。它不直接对观测价格 x 建模,而是引入潜在变量 z,代表某个数据点来自第 k 个高斯分量的归属,这些 z 服从离散的类别分布,我们只能在训练后反推。 数据分布被写成多个高斯分布的加权和:p(x)=Σ πₖ·𝒩(μₖ,Σₖ)。其中 πₖ 是第 k 个分量的混合系数(先验),所有 πₖ 求和恒为 1;𝒩(μₖ,Σₖ) 即均值 μₖ、协方差 Σₖ 的高斯分布。对外汇与贵金属这种高波动品种,Σₖ 的对角线数值直接反映该聚类态下的波动幅度,调参时不能忽略高风险放大效应。 估计潜在变量与参数 θ 最常用期望最大化(EM)。E 步算每个点属于各高斯的后验概率 γ(zₙₖ);M 步拿 γ(zₙₖ) 去更新 πₖ、μₖ、Σₖ。两步走完一轮,重复至收敛,软聚类结果才稳定。 现实里 LGMM 常用于带不确定性的聚类、异常检测、密度估计和语音任务。放到 MT5 行情上,它倾向把震荡与突破态分成不同分量,交易者可用后验概率 γ 判断当前 K 线更可能落在哪类波动 regime。
◍ 拿 XAUUSD 日线震荡指标喂给 LGMM 做聚类
做无监督聚类的第一步,是把 MT5 里能拿到的指标序列先规整出来。黄金兑美元(XAUUSD)日线是个合适的试验场:从 2005.01.01 到 2023.01.01 约十八年的日线样本,足够 LGMM 去抓结构。 我们优先采集几乎所有内置震荡指标,原因是它们大多自带上下界、序列较平稳。比如 RSI 恒在 0–100 之间,统计特性不随时间漂移,LGMM 找模式比面对非平稳价格本身容易得多。当然 LGMM 本身能处理非平稳数据,平稳只是让聚类更干净。 下面这段 MQL5 脚本负责把 OHLCT 和 15 个指标句柄及缓冲名存进结构数组,再塞进 CDataFrame。注意 iATR(14)、iBearsPower(13)、iBullsPower(13)、iChaikin(3,10) 只是前四个,后面还有 11 个未列全。 在 Python 侧把这份数据读进来后,我们给 GaussianMixture 设了 3 个分量,意图是把指标模式切成看涨、看跌、盘整三类——但这只是猜测,无监督聚类的分量含义本来就很难解释。模型每行输出 3 个概率值,列和恒为 1,后续转 ONNX 回 MT5 可视化才好看每类倾向。 外汇与贵金属杠杆高、跳空频繁,用聚类概率辅助判断状态只是概率倾向,不能直接当入场指令,一切以 MT5 实盘验证为准。
class="macro">#include <Arrays\ArrayString.mqh> class="macro">#include <Arrays\ArrayObj.mqh> class="macro">#include <pandas.mqh> class=class="str">"cmt">//[MQL5官方文档] input class="type">class="kw">datetime start_date = D&class="macro">#x27;class="num">2005.01.class="num">01&class="macro">#x27;; input class="type">class="kw">datetime end_date = D&class="macro">#x27;class="num">2023.01.class="num">01&class="macro">#x27;; 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; class=class="str">"cmt">//buffer_names array }; indicator_struct indicators[class="num">15]; class=class="str">"cmt">//Structure for keeping indicator handle alongside its buffer names class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- vector time, open, high, low, close; if (!SymbolSelect(symbol, true)) { printf("%s failed to select symbol %s, Error = %d",__FUNCTION__,symbol,GetLastError()); class="kw">return; } class=class="str">"cmt">//--- time.CopyRates(symbol, timeframe, COPY_RATES_TIME, start_date, end_date); open.CopyRates(symbol, timeframe, COPY_RATES_OPEN, start_date, end_date); high.CopyRates(symbol, timeframe, COPY_RATES_HIGH, start_date, end_date); low.CopyRates(symbol, timeframe, COPY_RATES_LOW, start_date, end_date); close.CopyRates(symbol, timeframe, COPY_RATES_CLOSE, start_date, end_date); CDataFrame df; df.insert("Time", time); df.insert("Open", open); df.insert("High", high); df.insert("Low", low); df.insert("Close", close); class=class="str">"cmt">//--- Oscillators indicators[class="num">0].handle = iATR(symbol, timeframe, class="num">14); indicators[class="num">0].buffer_names.Add("ATR"); indicators[class="num">1].handle = iBearsPower(symbol, timeframe, class="num">13); indicators[class="num">1].buffer_names.Add("BearsPower"); indicators[class="num">2].handle = iBullsPower(symbol, timeframe, class="num">13); indicators[class="num">2].buffer_names.Add("BullsPower"); indicators[class="num">3].handle = iChaikin(symbol, timeframe, class="num">3, class="num">10, MODE_EMA, VOLUME_TICK); indicators[class="num">3].buffer_names.Add("Chainkin");
把 11 个震荡指标一次性塞进向量缓冲
这段初始化把 CCI、DeMarker、Force、MACD、Momentum、OsMA、RSI、RVI、Stochastic、TriX、WPR 共 11 类指标挂到 indicators 数组的 4~14 号位,周期大多取 14,MACD 与 OsMA 用 12/26/9 而 Stochastic 用 5/3/3。注意 MACD、RVI、Stochastic 各有两个 buffer(主线 + 信号线),buffer_names 的添加顺序直接决定后面 CopyIndicatorBuffer 的 buffer_no。 拿到句柄后,用双层 for 循环遍历每个指标及其 buffer,靠 buffer.CopyIndicatorBuffer(handle, buffer_no, start_date, end_date) 把历史数据按时间区间拷进 vector。任何一次拷贝失败都会用 printf 打出指标名和 GetLastError 代码,方便在 MT5 Experts 日志里定位是哪一根线断的。 在 MT5 里跑这套,建议先把 start_date / end_date 缩到最近 3000 根 bar 验证拷贝成功率;外汇与贵金属杠杆高,指标共振只代表概率倾向,不代表方向确定性。
indicators[class="num">4].handle = iCCI(symbol, timeframe, class="num">14, PRICE_OPEN); indicators[class="num">4].buffer_names.Add("CCI"); indicators[class="num">5].handle = iDeMarker(symbol, timeframe, class="num">14); indicators[class="num">5].buffer_names.Add("Demarker"); indicators[class="num">6].handle = iForce(symbol, timeframe, class="num">13, MODE_SMA, VOLUME_TICK); indicators[class="num">6].buffer_names.Add("Force"); indicators[class="num">7].handle = iMACD(symbol, timeframe, class="num">12, class="num">26, class="num">9, PRICE_OPEN); indicators[class="num">7].buffer_names.Add("MACD MAIN_LINE"); indicators[class="num">7].buffer_names.Add("MACD SIGNAL_LINE"); indicators[class="num">8].handle = iMomentum(symbol, timeframe, class="num">14, PRICE_OPEN); indicators[class="num">8].buffer_names.Add("Momentum"); indicators[class="num">9].handle = iOsMA(symbol, timeframe, class="num">12, class="num">26, class="num">9, PRICE_OPEN); indicators[class="num">9].buffer_names.Add("OsMA"); indicators[class="num">10].handle = iRSI(symbol, timeframe, class="num">14, PRICE_OPEN); indicators[class="num">10].buffer_names.Add("RSI"); indicators[class="num">11].handle = iRVI(symbol, timeframe, class="num">10); indicators[class="num">11].buffer_names.Add("RVI MAIN_LINE"); indicators[class="num">11].buffer_names.Add("RVI SIGNAL_LINE"); indicators[class="num">12].handle = iStochastic(symbol, timeframe, class="num">5, class="num">3,class="num">3,MODE_SMA,STO_LOWHIGH); indicators[class="num">12].buffer_names.Add("StochasticOscillator MAIN_LINE"); indicators[class="num">12].buffer_names.Add("StochasticOscillator SIGNAL_LINE"); indicators[class="num">13].handle = iTriX(symbol, timeframe, class="num">14, PRICE_OPEN); indicators[class="num">13].buffer_names.Add("TEMA"); indicators[class="num">14].handle = iWPR(symbol, timeframe, class="num">14); indicators[class="num">14].buffer_names.Add("WPR"); class=class="str">"cmt">//--- Get buffers for (class="type">uint ind=class="num">0; ind<indicators.Size(); ind++) class=class="str">"cmt">//Loop through all the indicators { for (class="type">uint buffer_no=class="num">0; buffer_no<(class="type">uint)indicators[ind].buffer_names.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_date, end_date)) class=class="str">"cmt">//Copy indicator buffer { printf("func=%s line=%d | Failed to copy %s indicator buffer, Error = %d",__FUNCTION__,__LINE__,name,GetLastError());