神经网络变得轻松(第二十部分):自动编码器·综合运用
📘

神经网络变得轻松(第二十部分):自动编码器·综合运用

第 3/3 篇

训练循环里的空值跳过与误差落盘

这段逻辑出现在神经网络自编码器的训练流程中,核心是先剔除指标缺失的 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 自动卸载脚本,省得占坑。

MQL5 / C++
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 降维。外汇与贵金属波动随机性高,这类采样仅供概率层面的模型训练参考,实盘前务必用历史数据回测验证。

MQL5 / C++
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 日线跑一遍核对落盘列。

MQL5 / C++
   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 仅提供方法框架,实盘前务必在策略测试器用历史数据验证过拟合程度。

常见问题

在循环体内加空值判断,命中就 continue 跳过当前样本,避免污染梯度更新;同时把跳过计数单独记日志便于复盘。
按固定步长(如每 100 次采样)更新一次进度变量并刷新显示,特征拼装放在采样末尾批量做,别每次迭代都重算。
小布可加载你的训练脚本做定时诊断,自动落盘局部极值 CSV 并推送异常提醒,你只需复核结果。
非线性激活能抓价格里的拐点与波动聚类,线性方法只会保均值结构;回测中重构误差平均低约 15–30%。
特征拼装与 CSV 落盘两个脚本参数最干净,改窗口长度和文件路径即可接你自己的品种数据。