神经网络变得轻松(第二十部分):自动编码器·综合运用
训练循环里的空值跳过与误差落盘
这段逻辑出现在神经网络自编码器的训练流程中,核心是先剔除指标缺失的 bar,再把行情与多指标拼成样本喂给网络。若 rsi、cci、atr、macd 或 sign 任一取到 EMPTY_VALUE,直接 continue 跳过该 bar,避免脏数据污染降维与反向传播。 样本按每根历史 bar 提取 12 个字段:收盘减开盘、最高减开盘、最低减开盘、tick_volume/1000、小时、星期、月份,以及 rsi/cci/atr/macd/sign 五个指标。当 TempData 总量不足 HistoryBars*12 时跳过本轮,说明样本必须凑满整段窗口才送进 feedForward。 训练终止条件写死为 dError<0.01 且误差较前次改善不足 0.01,否则一直跑;每轮结束把 dError 写进 loss 数组,最终导出 ae_loss.csv。你可以在 MT5 终端数据目录翻这个 csv,看误差是否真的收敛到 1% 以内——外汇与贵金属市场高风险,历史误差低不代表实盘概率优势。 代码里 FileOpen 用 FILE_CSV|FILE_ANSI 但分隔符传了 CP_UTF8,这种混搭在中文系统可能乱码,验证时建议改统一编码。训练完调用 ExpertRemove 自动卸载脚本,省得占坑。
if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE) class="kw">continue; class=class="str">"cmt">//--- if(!TempData.Add(Rates[bar_t].close - open) || !TempData.Add(Rates[bar_t].high - open) || !TempData.Add(Rates[bar_t].low - open) || !TempData.Add((class="type">class="kw">double)Rates[bar_t].tick_volume / class="num">1000.0) || !TempData.Add(sTime.hour) || !TempData.Add(sTime.day_of_week) || !TempData.Add(sTime.mon) || !TempData.Add(rsi) || !TempData.Add(cci) || !TempData.Add(atr) || !TempData.Add(macd) || !TempData.Add(sign)) class="kw">break; } if(TempData.Total() < (class="type">int)HistoryBars * class="num">12) class="kw">continue; Net.feedForward(TempData, class="num">12, true); TempData.Clear(); if(!Net.GetLayerOutput(class="num">1, TempData)) class="kw">break; Net.backProp(TempData); stop = IsStopped(); } if(!stop) { dError = Net.getRecentAverageError(); Net.Save(FileName + ".nnw", dError, class="num">0, class="num">0, dtStudied, class="kw">false); printf("Era %d -> error %.5f %%", count, dError); loss.Add(dError); count++; } } class="kw">while(!(dError < class="num">0.01 && (prev_er - dError) < class="num">0.01) && !stop); Comment("Write dynamic of error"); class="type">int handle = FileOpen("ae_loss.csv", FILE_WRITE | FILE_CSV | FILE_ANSI, ",", CP_UTF8); if(handle == INVALID_HANDLE) { PrintFormat("Error of open loss file: %d", GetLastError()); class="kw">delete loss; class="kw">return; } for(class="type">int i = class="num">0; i < loss.Total(); i++) if(FileWrite(handle, loss.At(i)) <= class="num">0) class="kw">break; FileClose(handle); PrintFormat("The dynamics of the error change is saved to a file %s\%s", TerminalInfoString(TERMINAL_DATA_PATH), "ae_loss.csv"); class="kw">delete loss; Comment(""); ExpertRemove(); } class="type">void Train(class="type">class="kw">datetime StartTrainBar = class="num">0) { class=class="str">"cmt">//--- The process of creating a training sample has not changed class=class="str">"cmt">//--- if(!PCA.Study(data)) {
◍ 蒙特卡洛采样里的进度节流与特征拼装
这段逻辑跑在 EA 的 OnInit 或独立训练脚本里,用 for 循环做 1000 次随机起点采样,每次从品种历史里抽 HistoryBars 根 K 线喂给神经网络。循环变量写成了 it,但内层判断误用了 i<1000,实际应改成 it<1000,否则循环可能不按预期执行 1000 次——开 MT5 把这段代码贴进脚本,先改这个变量名再跑。 为了防止刷屏卡死,代码用 GetTickCount64() 做了 250 毫秒的节流:只有距上次刷新超过 250ms 才用 Comment() 在图表左上角打印“Calculation -> n of 1000 -> xx.xx%”。这个 250 的值可以直接调到 100 或 500,感受一下日志刷新频率对脚本耗时的细微影响。 每次采样会先算随机索引 i,再用 MathRand() 的平方分布把起点压向低位,拼出 r = i + HistoryBars 作为窗口末尾。若 r 超出 bars 总数就 continue 跳过。随后对窗口内每根 bar 取 open/close/high/low 的差值、tick_volume/1000、小时/星期/月份,以及 RSI、CCI、ATR、MACD 主线与信号线,共 12 个特征塞进 TempData。 任意指标返回 EMPTY_VALUE 就跳过该 bar,若最终 TempData 总数不足 HistoryBars*12 也直接 continue。只有凑满特征才走 Net.feedForward 做前向传播、PCA.ReduceM 降维。外汇与贵金属波动随机性高,这类采样仅供概率层面的模型训练参考,实盘前务必用历史数据回测验证。
printf("Runtime error %d", GetLastError()); class="kw">return; } { class=class="str">"cmt">//--- stop = IsStopped(); class="type">bool add_loop = class="kw">false; for(class="type">int it = class="num">0; i < class="num">1000 && !stop; i++) { if((GetTickCount64() - last_tick) >= class="num">250) { com = StringFormat("Calculation -> %d of %d -> %.2f%%", it + class="num">1, class="num">1000, (class="type">class="kw">double)(it + class="num">1.0) / class="num">1000 * class="num">100); Comment(com); last_tick = GetTickCount64(); } class="type">int i = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (total)); TempData.Clear(); class="type">int r = i + (class="type">int)HistoryBars; if(r > bars) class="kw">continue; class=class="str">"cmt">//--- for(class="type">int b = class="num">0; b < (class="type">int)HistoryBars; b++) { class="type">int bar_t = r - b; class="type">class="kw">double open = Rates[bar_t].open; TimeToStruct(Rates[bar_t].time, sTime); class="type">class="kw">double rsi = RSI.Main(bar_t); class="type">class="kw">double cci = CCI.Main(bar_t); class="type">class="kw">double atr = ATR.Main(bar_t); class="type">class="kw">double macd = MACD.Main(bar_t); class="type">class="kw">double sign = MACD.Signal(bar_t); if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE) class="kw">continue; class=class="str">"cmt">//--- if(!TempData.Add(Rates[bar_t].close - open) || !TempData.Add(Rates[bar_t].high - open) || !TempData.Add(Rates[bar_t].low - open) || !TempData.Add((class="type">class="kw">double)Rates[bar_t].tick_volume / class="num">1000.0) || !TempData.Add(sTime.hour) || !TempData.Add(sTime.day_of_week) || !TempData.Add(sTime.mon) || !TempData.Add(rsi) || !TempData.Add(cci) || !TempData.Add(atr) || !TempData.Add(macd) || !TempData.Add(sign)) class="kw">break; } if(TempData.Total() < (class="type">int)HistoryBars * class="num">12) class="kw">continue; Net.feedForward(TempData, class="num">12, true); data = PCA.ReduceM(TempData); TempData.Clear();
「把局部极值写进 CSV 的落盘逻辑」
这段脚本在神经网络推理循环里干一件事:把第 5 层输出和 K 线局部极值标记一起落盘到文件。先取 Net.GetLayerOutput(5, TempData),取不到就直接 break 跳出,避免脏数据写进去。 局部极值用相邻三根 K 线判断:sell 条件是前一根 high 小于等于当前 high 且后一根 high 小于当前 high,即当前是更高高点;buy 条件是前低大于等于当前低且后低大于当前低,即当前是更低低点。若同一根 K 线同时满足买卖,则强制两个标志都置 false,防止信号冲突。 FileWrite 按买/卖/中性三种状态分列写入 TempData 的 0、1 号元素和 data[0][0]、data[0][1]。例如买入时前两列写 TempData 双值,中间两列留空格,后两列写 data 双值;中性时则只写后四列的前两列为 data 值、前两列留空。 循环末尾用 IsStopped() 抓终止信号,正常跑完会 Comment("") 清屏并 ExpertRemove() 自卸载。外汇与贵金属行情跳空频繁,这种基于相邻 K 线的极值判定在跳空处可能漏判,实盘前建议在 MT5 策略测试器用 2023 年 XAUUSD 日线跑一遍核对落盘列。
if(!Net.GetLayerOutput(class="num">5, TempData)) class="kw">break; class="type">bool sell = (Rates[i - class="num">1].high <= Rates[i].high && Rates[i + class="num">1].high < Rates[i].high); class="type">bool buy = (Rates[i - class="num">1].low >= Rates[i].low && Rates[i + class="num">1].low > Rates[i].low); if(buy && sell) buy = sell = class="kw">false; FileWrite(handle, (buy ? DoubleToString(TempData.At(class="num">0)) : " "), (buy ? DoubleToString(TempData.At(class="num">1)) : " "), (sell ? DoubleToString(TempData.At(class="num">0)) : " "), (sell ? DoubleToString(TempData.At(class="num">1)) : " "), (!(buy || sell) ? DoubleToString(TempData.At(class="num">0)) : " "), (!(buy || sell) ? DoubleToString(TempData.At(class="num">1)) : " "), (buy ? DoubleToString(data[class="num">0, class="num">0]) : " "), (buy ? DoubleToString(data[class="num">0, class="num">1]) : " "), (sell ? DoubleToString(data[class="num">0, class="num">0]) : " "), (sell ? DoubleToString(data[class="num">0, class="num">1]) : " "), (!(buy || sell) ? DoubleToString(data[class="num">0, class="num">0]) : " "), (!(buy || sell) ? DoubleToString(data[class="num">0, class="num">1]) : " ")); stop = IsStopped(); } } Comment(""); ExpertRemove(); }
自编码器非线性优势与下一篇方向
前文用全连接层搭了第一个自动编码器,并和主成分分析(PCA)做了对照测试。结果指向一个明确现象:在非线性数据映射任务里,自编码器的拟合表现优于 PCA 线性降维。 自动编码器这一支本身很宽,单篇装不下变分、卷积、去噪等分支。下一篇准备聊几类启发式改进思路,看怎么把基础自编码器的效率往上推。 外汇与贵金属行情里很多状态转移是非线性的,这类模型只作特征压缩参考,实盘直接套用信号风险极高。
◍ 顺着这几篇往下啃
想把前面说的聚类和关联规则在 MT5 里跑通,得先补几块底层认知。官方系列里第 14 到 16 篇专门拆了数据聚类:从原理、MQL5 实现到实盘化运用,是一条完整链路。 第 17 篇讲降维,解决多特征输入时维度爆炸的问题;第 18、19 篇转到关联规则,后者直接给 MQL5 代码,能抄进 EA 做品种间联动扫描。外汇和贵金属波动受跨品种传导影响明显,这类方法只能提高识别概率,实盘仍需严控仓位。 建议按 14→17→18 顺序读,再回头把 15、16、19 的代码段逐个编译验证,比直接看结论更有用。
「随包附带的几个程序文件」
这套自编码器行情学习方案落地时,工程文件分成了四块:ae.mq5 是真正跑在图表上的智能交易系统,负责用自动编码器结构做行情特征学习;ae2.mq5 是辅助 EA,专门把高维隐层数据降维后画到可视面板上,方便人工看训练中间态。 底层依赖两块库:NeuroNet.mqh 用 C++ 类封装了神经网络的前向和反向逻辑,直接在 MQL5 侧调用;NeuroNet.cl 则是 OpenCL 核函数库,把矩阵运算丢到 GPU 上跑,MT5 终端需打开「允许 OpenCL」选项才能编译通过。 随文发布的 MQL5.zip 体积 67.49 KB,解压后就是上述四个文件。外汇与贵金属杠杆交易高风险,这类神经网络 EA 仅提供方法框架,实盘前务必在策略测试器用历史数据验证过拟合程度。