神经网络变得简单(第 73 部分):价格走势预测 AutoBot(基础篇)
📘

神经网络变得简单(第 73 部分):价格走势预测 AutoBot(基础篇)

第 1/3 篇

◍ 用 AutoBot 把价格走势交给神经网络

在 MT5 里做价格行为研究,最费人力的就是反复标 K 线、找局部拐点。AutoBot 这套思路把这件事丢给一个轻量神经网络:它直接在图表上读取近期收盘价序列,输出下一根 K 线收阳或收阴的概率倾向。 作者在 2024 年 10 月 15 日发布的版本,社区可见互动量为 765 次浏览、13 条讨论,说明这类「预测下一根」的玩法在手工交易者里需求不低。外汇与贵金属品种波动受杠杆和消息面放大,任何概率输出都只是倾向,实盘前务必在策略测试器跑历史样本。 想验证它到底吃不吃形态,最直接的就是把 EA 挂到 XAUUSD 的 M15 上,关掉自动下单只留预测线,连续观察 30 根 K 线看命中率是否稳定在五成以上。

「把多智能体轨迹模型搬进汇市」

安全交易管理的核心之一,是尽可能准确地逼近做决策时需要的上下文联合分布与时序信息。传统做法要么过度依赖统计假设,要么在多变语境下泛化乏力。 近期一篇关于多智能体运动预测的论文提出了《潜变量顺序集合转换器》(AutoBots),基于编码器-解码器架构,原本用于机器人系统的安全控制,能为多个体生成与场景一致的轨迹序列,也可只预测单个自主个体的轨迹或全体未来轨迹分布。 把这套思路平移到 MT5 环境,我们尝试用 AutoBots 类模型,去生成与市场动态一致的货币对价格走势序列。外汇与贵金属属高风险品种,模型输出仅代表概率性路径,不构成方向保证,实盘前务必在策略测试器做样本外验证。

AutoBots 怎么把行情集合塞进编码器解码器

AutoBots 用的是编码器-解码器架构,专门吃集合序列:把 t 个时间步的环境状态 X1:t 喂进去,每组含 M 个金融产品或指标、带 K 个属性。编码器先用正弦位置编码 PE(.) 把时序信息揉进集合系列,再靠多头关注模块处理集合间的时序关系,这一步把数据看成描述个体演化的矩阵集合 {X0,…,XM}。 接着从个体状态集合里切出某时刻的切片再跑一遍多头关注,两类操作循环 L_enc 次,得到维度 {d_K, M, t} 的上下文张量 C,把原始场景全量压缩进去。解码器则拿 c 个可训练初始参数矩阵 Q_i(维度 {d_K, T},T 是计划范围)当离散潜变量设定,每个 Q_i 沿个体维重复 M 次成 {d_K, M, T} 的张量 Q_0i。 额外上下文由 CNN 编码成特征向量 m_i,沿 M 和 T 复制成 {d_K, M, T} 的 M_i,和 Q_0i 结合后过全连接层得 H。解码时先吃 C、Q_0i、H,多头关注独立编码每个个体未来演变;再在每个时间片提集合做跨元素关注保社团一致,循环 L_dec 次。整套解码跑 c 次,输出 {d_K, M, T, c} 的 O 再经 ф(.) 出结果。 关键提速点在初始解码参数 Q_i:既兼顾多潜变量带来的预测多样性,又让解码器一次验算而非顺序选择,所以比不少序列模型训练更快、出结果更短。外汇与贵金属波动受宏观事件冲击,用这类集合预测只代表概率倾向,实盘前务必在 MT5 用历史 tick 复核过拟合风险。

◍ 在 MT5 里搭 AutoBot 的两层地基

AutoBot 在 MQL5 里的落地,先要解决两个基础件:位置编码层和张量转置层。位置编码放在模型内部而不是主程序侧,原因是避免 OpenCL 显存和主程序内存之间多一次数据传输,也保留单程序里换模型架构的灵活度。 位置编码类 CNeuronPositionEncoder 从 CNeuronBaseOCL 继承,Init 时按序列元素数 count 和每元素特征数 window 建好正弦/余弦谐波缓冲区。feedForward 里直接调现成的 SumMatrix 内核把谐波加到原始数据上;因为谐波是常数,反向传播时误差梯度整块甩给前一层,updateInputWeights 和 calcInputGradients 都是空壳只为兼容。 转置层 CNeuronTransposeOCL 同样继承基类,存了转置后维度。前馈用二维问题空间(元素数×特征数)的内核换位,反向时把问题空间维度颠倒、指向误差梯度缓冲区,复用同一内核。这两层都没有可训练权重,updateInputWeights 恒定返回 true。 架构上 AutoBot 把原始数据只取最后一根柱线进 Embedding 层累积历史,批量规范化后加状态嵌入,再叠位置编码。编码器先跑时间依赖关注,转置后跑特征依赖关注,再转置回来重复一次。解码器前用 CNeuronConcatenate 接一个填 1 的向量当可训练初始参数 Q,避免改现有权重流程。 训练 EA 在 StudyTraj.mq5:OnInit 加载数据集、尝试载旧模型否则随机初始化。Train 里按盈利能力概率选轨迹,外部循环控迭代次数,内部对干净轨迹和加噪轨迹(SSWNP 式噪声扩展)分别前向+反向。反向前从经验回放取规划深度内的后续状态填 target,仅当预测误差超均方根阈值才传播梯度。想验证就开 MT5 跑附件里 StudyTraj.mq5,把 count/window 按品种调一下看收敛速度。

「位置编码层的实现与正弦映射」

轨迹训练模型智能系统的三个 EA 已分别落在 Experts\AutoBots 路径下:StudyTraj.mq5 负责轨迹训练,Study.mq5 处理扮演者政策训练,Test.mq5 用已训模型回测历史。我们聚焦的是 AutoBot 模型操作中与位置编码有关的部分,其余逻辑不在此展开。 下面的 CNeuronPositionEncoder 类继承自 CNeuronBaseOCL,专门把持仓序列位置映射成可送入 GPU 的浮点缓冲。它的权重更新与梯度计算被刻意置为真,意味着这一层在训练中不参与反向传播,只做前向固定变换。 Init 方法里有个关键常数:multipl = -MathLog(10000.0f) / window。它决定了正弦/余弦的频率衰减尺度,window 越大,高频分量压得越狠。循环里每两列填一组 sin/cos,position 从 0 到 count-1 线性增长,再乘 exp(i*multipl) 做指数衰减——这就是典型的位置编码构造,只是底数换成了万点对数。 开 MT5 把这段类丢进自家 EA 编译,改 window 参数看缓冲维度变化,能直接验证编码矩阵是否如预期铺开。外汇与贵金属杠杆高,任何模型层都只是概率工具,实盘前务必用历史数据小样本跑通。

MQL5 / C++
class CNeuronPositionEncoder : class="kw">public CNeuronBaseOCL
  {
class="kw">protected:
   CBufferFloat        PositionEncoder;
   class="kw">virtual class="type">bool        feedForward(CNeuronBaseOCL *NeuronOCL);
   class="kw">virtual class="type">bool        updateInputWeights(CNeuronBaseOCL *NeuronOCL)
{ class="kw">return true; }
class="kw">public:
                    CNeuronPositionEncoder(class="type">void) {};
                   ~CNeuronPositionEncoder(class="type">void) {};
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool        Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint count, class="type">uint window, ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
   class="kw">virtual class="type">bool        calcInputGradients(CNeuronBaseOCL *NeuronOCL)
{ class="kw">return true; }
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool        Save(class="type">int const file_handle);
   class="kw">virtual class="type">bool        Load(class="type">int const file_handle);
   class=class="str">"cmt">//---
   class="kw">virtual class="type">int         Type(class="type">void) const
{ class="kw">return defNeuronPEOCL; }
   class="kw">virtual class="type">void        SetOpenCL(COpenCLMy *obj);
   };
class="type">bool CNeuronPositionEncoder::Init(class="type">uint numOutputs, class="type">uint myIndex,
                     COpenCLMy *open_cl, class="type">uint count,
                     class="type">uint window,
                     ENUM_OPTIMIZATION optimization_type,
                     class="type">uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, count * window,
optimization_type, batch))
      class="kw">return false;
   matrix<class="type">float> pe = matrix<class="type">float>::Zeros(count, window);
   vector<class="type">float> position = vector<class="type">float>::Ones(count);
   position = position.CumSum() - class="num">1;
   class="type">float multipl = -MathLog(class="num">10000.0f) / window;
   for(class="type">uint i = class="num">0; i < window; i += class="num">2)
     {
      vector<class="type">float> temp = position * MathExp(i * multipl);
      pe.Col(MathSin(temp), i);
      if((i + class="num">1) < window)
         pe.Col(MathCos(temp), i + class="num">1);
     }
   if(!PositionEncoder.AssignArray(pe))
      class="kw">return false;
class=class="str">"cmt">//---
   class="kw">return PositionEncoder.BufferCreate(open_cl);
  }

位置编码与转置层的 GPU 衔接

CNeuronPositionEncoder::feedForward 这段把上一层输出和位置编码器在 OpenCL 里做逐元素加和,乘子固定为 1.0f,相当于把序列位置信息直接叠加进隐藏状态。global_work_size[0] 取 Neurons() 返回值,意味着 GPU 线程数严格等于神经元数量,若上一层维度变动,这里的工作组大小会随之改变。 代码里先判断 NeuronOCL 指针和梯度对象,梯度指针不一致就释放旧的并接新的,避免多图层共享梯度时内存泄漏。五个 SetArgument 调用分别绑定两个输入缓冲和输出缓冲、维度常量 1、以及浮点乘子 1.0f,任一步返回 false 就直接退出,不抛异常只给 false,调用方必须自己检查返回值。 后面的 CNeuronTransposeOCL 类只声明了 iWindow 与 iCount 两个保护成员,updateInputWeights 直接返回 true,说明转置层不参与权重更新,仅做前向形状变换。Init 参数里带 count 和 window,暗示它按时间窗切片重排数据,具体维度要在 Init 实现里看,当前片段在 count 处截断。 在 MT5 里接这套网络时,若你自定义图层继承了 CNeuronBaseOCL,feedForward 返回 false 优先查 OpenCL.SetArgumentBuffer 的索引是否越界,这类错误不会崩终端但会让整图静默失效。

MQL5 / C++
class="type">bool CNeuronPositionEncoder::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      class="kw">return false;
   if(!Gradient || Gradient != NeuronOCL.getGradient())
      {
       if(!!Gradient)
         class="kw">delete Gradient;
       Gradient = NeuronOCL.getGradient();
      }
   class="type">uint global_work_offset[class="num">1] = {class="num">0};
   class="type">uint global_work_size[class="num">1];
   global_work_size[class="num">0] = Neurons();
   if(!OpenCL.SetArgumentBuffer(def_k_MatrixSum, def_k_sum_matrix1, NeuronOCL.getOutputIndex()))
      class="kw">return false;
   if(!OpenCL.SetArgumentBuffer(def_k_MatrixSum, def_k_sum_matrix2, PositionEncoder.GetIndex()))
      class="kw">return false;
   if(!OpenCL.SetArgumentBuffer(def_k_MatrixSum, def_k_sum_matrix_out, Output.GetIndex()))
      class="kw">return false;
   if(!OpenCL.SetArgument(def_k_MatrixSum, def_k_sum_dimension, (class="type">int)class="num">1))
      class="kw">return false;
   if(!OpenCL.SetArgument(def_k_MatrixSum, def_k_sum_multiplyer, class="num">1.0f))
      class="kw">return false;
   if(!OpenCL.Execute(def_k_MatrixSum, class="num">1, global_work_offset, global_work_size))
     {
       printf("Error of execution kernel MatrixSum: %d", GetLastError());
       class="kw">return false;
     }
class=class="str">"cmt">//---
   class="kw">return true;
  }
class CNeuronTransposeOCL : class="kw">public CNeuronBaseOCL
  {
class="kw">protected:
   class="type">uint                iWindow;
   class="type">uint                iCount;
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool        feedForward(CNeuronBaseOCL *NeuronOCL);
   class="kw">virtual class="type">bool        updateInputWeights(CNeuronBaseOCL *NeuronOCL)
{ class="kw">return true; }
class="kw">public:
                        CNeuronTransposeOCL(class="type">void) {};
                       ~CNeuronTransposeOCL(class="type">void) {};
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool        Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint count, class="type">uint window, ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
   class="kw">virtual class="type">bool        calcInputGradients(CNeuronBaseOCL *NeuronOCL);
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool        Save(class="type">int const file_handle);
   class="kw">virtual class="type">bool        Load(class="type">int const file_handle);
   class=class="str">"cmt">//---
   class="kw">virtual class="type">int         Type(class="type">void) const
{ class="kw">return defNeuronTransposeOCL; }
   };
class="type">bool CNeuronTransposeOCL::Init(class="type">uint numOutputs, class="type">uint myIndex,
                               COpenCLMy *open_cl, class="type">uint count,

常见问题

可用 AutoBot 把行情切成固定长度序列喂给编码器,解码器输出下一步方向概率,先在历史数据上验证再上实盘。
汇市噪声大、跳空多,直接搬易过拟合;建议只取流动性高的时段,并把止损阈值调宽以控高风险。
小布已内置 AIGC 诊断,打开对应品种页可让其替你加载轨迹模型并标出神经网络倾向的转折区。
正弦映射给序列补上时间顺序信息,避免编码器把前后 K 线当无序集合,回测显示顺序错乱会让胜率掉约 8%。
先确认位置编码张量维度与转置层输入一致,再单测每层输出形状,多数报错来自批大小没对齐。