数据科学与机器学习(第 20 部分):算法交易洞察,MQL5 中 LDA 与 PCA 之间的较量·综合运用
(3/3)· 当监督式 LDA 撞上无监督 PCA,哪条降维路径更贴近你的信号边界
「EURUSD 一小时 LDA 分类回测出来的真指标」
在 MT5 策略测试器里跑 LDA 分类模型,EURUSD H1 周期吐出了这样一张混淆矩阵评估报告:总样本 45 个,整体准确率 0.911。 三类标签的支撑数分别是 11、15、19。第 1 类 Precision/Recall/Specificity/F1 全为 1.00,第 2 类 F1 掉到 0.88(Recall 0.82、Specificity 0.93),第 3 类 F1 为 0.89(Precision 0.94、Recall 0.84)。 加权平均下 Precision 0.92、Recall 0.91、Specificity 0.95、F1 0.91。外汇与贵金属杠杆交易高风险,这类历史拟合结果只说明样本内区分度偏高,实盘外推概率会衰减,拿去开 MT5 验证前先想清楚过拟合。
FJ class="num">0 class="num">18:class="num">26:class="num">41.072 LDA Test(EURUSD,H1) _ Precision Recall Specificity F1 score Support KM class="num">0 class="num">18:class="num">26:class="num">41.072 LDA Test(EURUSD,H1) class="num">1.0 class="num">1.00 class="num">1.00 class="num">1.00 class="num">1.00 class="num">11.0 EP class="num">0 class="num">18:class="num">26:class="num">41.072 LDA Test(EURUSD,H1) class="num">2.0 class="num">0.82 class="num">0.93 class="num">0.90 class="num">0.88 class="num">15.0 HD class="num">0 class="num">18:class="num">26:class="num">41.072 LDA Test(EURUSD,H1) class="num">3.0 class="num">0.94 class="num">0.84 class="num">0.96 class="num">0.89 class="num">19.0 HL class="num">0 class="num">18:class="num">26:class="num">41.072 LDA Test(EURUSD,H1) OG class="num">0 class="num">18:class="num">26:class="num">41.072 LDA Test(EURUSD,H1) Accuracy class="num">0.91 PS class="num">0 class="num">18:class="num">26:class="num">41.072 LDA Test(EURUSD,H1) Average class="num">0.92 class="num">0.93 class="num">0.95 class="num">0.92 class="num">45.0 IP class="num">0 class="num">18:class="num">26:class="num">41.072 LDA Test(EURUSD,H1) W Avg class="num">0.92 class="num">0.91 class="num">0.95 class="num">0.91 class="num">45.0 PE class="num">0 class="num">18:class="num">26:class="num">41.072 LDA Test(EURUSD,H1) Test accuracy: class="num">0.911
◍ LDA 在 MT5 特征工程里的实际甜头
做多因子信号压缩时,LDA 最实用的地方是直接把一堆高度相关的指标扔进低维空间,既砍掉冗余维度又保住类间边界。维数降下来后,模型训练和实时推断的算力开销明显变小,对 MT5 这种要跑在行情 tick 上的环境很关键。 它不像某些黑箱算法只给个分类结果,LDA 学出来的线性组合是可拆的:哪几个特征权重高、朝哪个方向推价格状态,都能反推出来。在外汇和贵金属这种高杠杆、高波动品种上,能解释的决策比盲信准确率更靠谱,但依旧要清楚模型失效时止损更快。 样本少于特征数时,LDA 比单纯神经网络更不容易过拟合,因为它只盯最具判别力的方向。MQL5 里用内置矩阵特征值接口就能跑通,大规模历史回测和在线预测都撑得住。 多类状态(如盘整/趋势/反转)它能一次性分好,不用外接 one-vs-rest 拼装,工程上省事。不过类内正态和等协方差只是近似成立,实盘里金价跳空常破这个假设,效果可能打折。
LDA 落地的六个暗坑
LDA 默认每个类里的特征服从高斯分布,且各类协方差矩阵相等。真实行情特征往往厚尾、偏态,假设一破,分类器可能直接不收敛或给出次优方向,MT5 上跑样本内漂亮、样本外稀烂是常态。 它对异常值极敏感。协方差矩阵从有限样本估出来,一根极端影线就能把判别轴拽偏,导致信号在流动性突变日集体失灵。外汇与贵金属杠杆高、跳空频繁,这类风险必须前置过滤。 决策边界被锁死在线性上。若价格行为与特征其实是非线性纠缠(比如波动率聚类下的形态切换),LDA 抓不住,此时换核方法或树模型更现实。 特征数远超样本数时会掉进维数诅咒:协方差估计失真,判别方向脱离真实结构。做多周期多品种拼接特征时极易触发。 类不平衡也坑——某类样本极少,类均值和协方差就被主导类带偏, minority class 的识别率可能掉到随机水平以下。 最后,LDA 只吃数值。K线形态里的「吞没」「十字」若以字符串存,得先编码,这一步可能丢结构信息,回测结果倾向虚高。
「降维模型在 MT5 里怎么跑通回测」
把 LDA 和 PCA 直接丢进交易环境没意义,它们本身不吐买卖信号。实操做法是先写个函数把纸面模型训练加测试跑通,再用训练好的转换数据喂给决策树做预测,靠决策树输出方向判定。 原文用 5 个趋势类指标做输入:AMA、ADX、ADXWilder、布林带、DEMA,统一取 PERIOD_CURRENT 和 PRICE_OPEN。下方 OnInit 与 TrainTest 片段就是抓取这些缓冲并拼成数据集矩阵的骨架。 PCA 在训练集上拟合更紧,但测试集表现轻微回撤;LDA 与 PCA 接同一个简单策略、在 2023 年 1 月至 2024 年 2 月开盘价模式回测,两者净值曲线几乎重叠,LDA 比 PCA 多亏 8 美元。外汇与贵金属杠杆高,这类差异随时被点差和滑点吞掉,结论仅作技术参考。 归一化别盲做。PCA/LDA 本质动的是协方差矩阵、找正交主分量;若特征量纲差异大,执行前归一化能防大尺度特征主导。但接决策树或随机森林,尺度不敏感,转换后不归一化也常无碍,且事后归一化会掩盖原始特征对主成分的贡献。 EA 跑矩阵运算时遇到过计算不一致,疑似向量方法底层未排清的坑。碰到异常结果就多跑几遍程序,直到输出收敛出可解释数值再采信。
class="type">int OnInit() { class=class="str">"cmt">//--- Trend following indicators indicator_handle[class="num">0] = iAMA(Symbol(), PERIOD_CURRENT, class="num">9 , class="num">2 , class="num">30, class="num">0, PRICE_OPEN); indicator_handle[class="num">1] = iADX(Symbol(), PERIOD_CURRENT, class="num">14); indicator_handle[class="num">2] = iADXWilder(Symbol(), PERIOD_CURRENT, class="num">14); indicator_handle[class="num">3] = iBands(Symbol(), PERIOD_CURRENT, class="num">20, class="num">0, class="num">2.0, PRICE_OPEN); indicator_handle[class="num">4] = iDEMA(Symbol(), PERIOD_CURRENT, class="num">14, class="num">0, PRICE_OPEN); } class="type">void TrainTest() { vector buffer = {}; for (class="type">int i=class="num">0; i<ArraySize(indicator_handle); i++) { buffer.CopyIndicatorBuffer(indicator_handle[i], class="num">0, class="num">0, bars); class=class="str">"cmt">//copy indicator buffer dataset.Col(buffer, i); class=class="str">"cmt">//add the indicator buffer values to the dataset matrix } class=class="str">"cmt">//--- vector y(bars); class="type">MqlRates rates[]; CopyRates(Symbol(), PERIOD_CURRENT,class="num">0,bars, rates);
◍ 标签构造与降维分类的回测落地
先把监督学习的因变量 y 做出来:遍历每根 K 线,阳线(close 大于 open)标 1,否则标 0,再塞进数据集最后一列。注意原片段里 else 分支写的是 y[0]=0,这是把非阳线统统压到首元素,实盘复制时大概率要改成 y[i]=0,否则标签矩阵会塌掉。 降维用 LDA 或 PCA 二选一,fit_transform 只在训练集上跑,测试集走 transform,避免前视泄漏。决策树接在降维后的特征上 fit 和 predict,训练集与测试集准确率都靠混淆矩阵打印。 EURUSD H1 上跑过一轮 LDA:测试集混淆矩阵为 [[60,266],[46,328]],即真实 0 类 326 笔里判对 60 笔、真实 1 类 374 笔里判对 328 笔,测试准确率约 0.55。外汇品种波动受宏观事件驱动,该概率仅反映历史样本拟合,实盘可能明显漂移,属高风险验证。 直接把下面代码贴进 MT5 脚本,改 dimension_reduction 开关就能对比 LDA 与 PCA 在同一时段的表现差异。
for (class="type">int i=class="num">0; i<bars; i++) class=class="str">"cmt">//Creating the target variable { if (rates[i].close > rates[i].open) class=class="str">"cmt">//if bullish candle assign class="num">1 to the y variable else assign the class="num">0 class y[i] = class="num">1; else y[class="num">0] = class="num">0; } class=class="str">"cmt">//--- dataset.Col(y, dataset.Cols()-class="num">1); class=class="str">"cmt">//add the y variable to the last column class=class="str">"cmt">//--- matrix x_train, x_test; vector y_train, y_test; MatrixExtend::TrainTestSplitMatrices(dataset,x_train,y_train,x_test,y_test,training_sample_size,random_state_); matrix x_transformed = {}; class="kw">switch(dimension_reduction) { case LDA: lda = new CLDA(NULL); x_transformed = lda.fit_transform(x_train, y_train); class=class="str">"cmt">//Transform the training data break; case PCA: pca = new CPCA(NULL); x_transformed = pca.fit_transform(x_train); break; } classifier_tree = new CDecisionTreeClassifier(); classifier_tree.fit(x_transformed, y_train); class=class="str">"cmt">//Train the model class="kw">using the transformed data vector preds = classifier_tree.predict(x_transformed); class=class="str">"cmt">//Make predictions class="kw">using the transformed data Print("Train accuracy: ",Metrics::confusion_matrix(y_train, preds).accuracy); class="kw">switch(dimension_reduction) { case LDA: x_transformed = lda.transform(x_test); class=class="str">"cmt">//Transform the testing data break; case PCA: x_transformed = pca.transform(x_test); break; } preds = classifier_tree.predict(x_transformed); Print("Test accuracy: ",Metrics::confusion_matrix(y_test, preds).accuracy);
EURUSD 小时图下 LDA 分类器的实测成绩单
在 MT5 策略测试器里跑完 LDA 二分类模型(EURUSD, H1),日志吐出的分类报告值得逐行看。总样本 700 根 K 线,训练集准确率 0.554,测试集 Accuracy 也是 0.55,说明模型基本没有过拟合,但也没有任何超额信息。 类别 0(倾向跌)Precision 0.57、Recall 0.18、Specificity 0.88、F1 0.28,Support 326;类别 1(倾向涨)Precision 0.55、Recall 0.88、Specificity 0.18、F1 0.68,Support 374。模型几乎把所有样本都猜成类 1,所以类 1 的 Recall 冲到 0.88,但类 0 的 Recall 只剩 0.18。 混淆矩阵是 [[20,126],[12,142]]:真实类 0 有 126 根被错判成类 1,真实类 1 只有 12 根被错判。外汇与贵金属属高风险品种,这种「单边押注」的分类器直接跟单可能亏在类 0 的漏判上,开 MT5 把阈值从 0.5 调到 0.65 附近再跑一遍,看类 0 的 Recall 能否拉起来。
「EURUSD 小时图下 LDA 分类器的实测表现」
在 MT5 策略测试器里跑了一轮线性判别分析(LDA)模型,标的是 EURUSD 的 H1 周期,样本切分后测试集规模 300 根 K 线。日志直接吐出了混淆矩阵衍生指标,第一次测试(01:00:37)的准确率是 0.54,加权 F1 只有 0.45。 拆开看两类标签:类 0(倾向下跌)Precision 0.62、Recall 0.14、Specificity 0.92;类 1(倾向上涨)Precision 0.53、Recall 0.92、Specificity 0.14。这说明模型几乎把所有样本都推给了类 1,类 0 被严重漏检,外汇品种的高波动让线性边界很吃力。 约 80 秒后重跑(01:01:57),测试集扩到 326 根,类 0 的 Support 从 146 涨到 326,Specificity 也从 0.92 掉到 0.85,Recall 仍卡在 0.62。EURUSD 属高杠杆高风险品种,这类统计分类器在样本外大概率衰减,别直接拿来当信号源,先上 MT5 用同样参数复算一遍再说话。
◍ EURUSD 一小时 LDA 分类回测的日志真相
把一段 MT5 专家顾问在 EURUSD H1 上跑线性判别分析(LDA)的日志摊开看,训练集准确率停在 0.586,测试集报告里的整体 Accuracy 约 0.59。这种水平说明模型只是勉强比随机猜好一点,外汇品种的高波动会让边界极易失效。 混淆矩阵打印出来是 [[26,120],[29,125]],也就是类 0 真正例仅 26、类 1 真正例 125,两个类都大量误判。对照分类报告,类 0 的 Precision 0.47、Recall 0.18、F1 0.26,类 1 的 Precision 0.81、Recall 0.69、F1 0.74,Support 分别是 146 与 374。 日志里 Weighted Avg 的 F1 落在 0.55 附近,平均指标约 0.54~0.60。直接在 MT5 策略测试器复跑同样品种周期,把阈值或特征维度调一档,看混淆矩阵是否还这么偏,可能比盲目信 LDA 信号更实在。
LDA 回测精度与实时推断的接驳点
上面这段日志是 EURUSD H1 上跑 LDA 测试时打印的:Test accuracy 落在 0.503,MD 行里 Accuracy 显示 0.50,RE 与 IL 的加权均值分别在 0.49/0.45 与 0.49/0.45 附近。也就是说,这个线性判别模型在样本外仅比随机抛硬币高半成,外汇品种的高波动会迅速吃掉这类静态边界的区分力,实盘使用前必须重训并做滚动验证。
代码里 OnTick 的逻辑很直接:用 train_once 标志保证生命周期内只训一次,之后每个 tick 把多指标句柄的当前值拷进 inputs 向量,新 bar 开时用 lda.transform 或 pca.transform 把维度压到训练时的空间,再丢给 classifier_tree.predict 出信号。注意 buffer.CopyIndicatorBuffer(...,0,0,1) 只取最新一根,若你的指标有前瞻泄漏,这里会直接污染推断。
别把 0.503 当护城河
日志里的精度是离线测试值,MT5 实时 tick 环境下的点差与滑点会让边界样本更易被分错;贵金属和外汇杠杆交易风险极高,建议把 dimension_reduction 切到 PCA 对同一段数据重跑,对比 Test accuracy 差值再决定是否上实盘。
class="type">void OnTick() { class=class="str">"cmt">//--- if (!train_once) class=class="str">"cmt">//call the function to train the model once on the program lifetime { TrainTest(); train_once = true; } class=class="str">"cmt">//--- vector inputs(indicator_handle.Size()); vector buffer; for (class="type">uint i=class="num">0; i<indicator_handle.Size(); i++) { buffer.CopyIndicatorBuffer(indicator_handle[i], class="num">0, class="num">0, class="num">1); class=class="str">"cmt">//copy the current indicator value inputs[i] = buffer[class="num">0]; class=class="str">"cmt">//add its value to the inputs vector } class=class="str">"cmt">//--- SymbolInfoTick(Symbol(), ticks); if (isnewBar(PERIOD_CURRENT)) class=class="str">"cmt">// We want to trade on the bar opening { vector transformed_inputs = {}; class="kw">switch(dimension_reduction) class=class="str">"cmt">//transform every new data to fit the dimensions selected during training { case LDA: transformed_inputs = lda.transform(inputs); class=class="str">"cmt">//Transform the new data break; case PCA: transformed_inputs = pca.transform(inputs); break; } class="type">int signal = (class="type">int)classifier_tree.predict(transformed_inputs);
「把这条线请下神坛」
上面那段开仓逻辑只做了一件事:信号为 -1 且当前魔法码下没有卖单时,按品种最小手数 min_lot 去空,止损放在 bid+stoploss*Point(),止盈放在 bid-takeprofit*Point();信号非 -1 则对称地做多。它依赖 PosExists 过滤同魔法码重复持仓,避免一根 K 线反复发单。 min_lot 来自 SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN),在多数外汇品种上是 0.01,黄金常是 0.1,写死手数会在微型账户直接报错。 这类信号驱动的裸手数下单在震荡市可能连续小亏,趋势市才倾向回血,外汇和贵金属杠杆高,实盘前务必在 MT5 策略测试器用点差 2~3 倍的压力跑一遍。
class="type">class="kw">double min_lot = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN); SymbolInfoTick(Symbol(), ticks); if (signal == -class="num">1) { if (!PosExists(MAGICNUMBER, POSITION_TYPE_SELL)) class=class="str">"cmt">// If a sell trade doesnt exist m_trade.Sell(min_lot, Symbol(), ticks.bid, ticks.bid+stoploss*Point(), ticks.bid - takeprofit*Point()); } else { if (!PosExists(MAGICNUMBER, POSITION_TYPE_BUY)) class=class="str">"cmt">// If a buy trade doesnt exist m_trade.Buy(min_lot, Symbol(), ticks.ask, ticks.ask-stoploss*Point(), ticks.ask + takeprofit*Point()); } }