神经网络变得简单(第 85 部分):多变元时间序列预测·综合运用
📘

神经网络变得简单(第 85 部分):多变元时间序列预测·综合运用

第 3/3 篇

「账户状态如何被编码成网络输入」

这段逻辑把每笔交易时的账户快照压成一个特征向量,喂给后面的 Actor 网络。索引 3 是绝对值,4~6 都除以 PrevBalance,相当于把余额、权益、回撤归一化到上一周期基准,避免不同资金规模直接干扰梯度。 时间维度用了四组三角函数:以 2023 全年秒数、月线周期、周线周期、日线周期为分母,分别取 sin/cos。这样网络能隐式感知「这笔动作发生在年/月/周/日循环里的相位」,对外汇和贵金属这种强周期品种可能比裸时间戳更有效。

MQL5 / C++
bAccount.Add(Buffer[tr].States[i].account[class="num">3]);
class=class="str">"cmt">// 直接塞入账户数组第3项(如当前余额绝对值)
bAccount.Add(Buffer[tr].States[i].account[class="num">4] / PrevBalance);
class=class="str">"cmt">// 第4项除以上一基准余额,归一化
bAccount.Add(Buffer[tr].States[i].account[class="num">5] / PrevBalance);
class=class="str">"cmt">// 第5项同上归一化
bAccount.Add(Buffer[tr].States[i].account[class="num">6] / PrevBalance);
class=class="str">"cmt">// 第6项同上归一化
class="type">class="kw">double time = (class="type">class="kw">double)Buffer[tr].States[i].account[class="num">7];
class=class="str">"cmt">// 取出时间戳(秒)
class="type">class="kw">double x = time / (class="type">class="kw">double)(D&class="macro">#x27;class="num">2024.01.class="num">01&class="macro">#x27; - D&class="macro">#x27;class="num">2023.01.class="num">01&class="macro">#x27;);
class=class="str">"cmt">// 以2023全年秒长为周期
bAccount.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
class=class="str">"cmt">// 非零则取年周期正弦,否则填0
x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_MN1);
class=class="str">"cmt">// 换月线秒长为周期
bAccount.Add((class="type">float)MathCos(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
class=class="str">"cmt">// 月周期余弦
x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_W1);
class=class="str">"cmt">// 周线周期
bAccount.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
class=class="str">"cmt">// 周周期正弦
x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_D1);
class=class="str">"cmt">// 日线周期
bAccount.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
class=class="str">"cmt">// 日周期正弦
if(bAccount.GetIndex() >= class="num">0)
  bAccount.BufferWrite();
class=class="str">"cmt">// 索引有效才写缓冲
if(!Actor.feedForward((CBufferFloat*)GetPointer(bAccount), class="num">1, false, GetPointer(Encoder), LatentLayer))
{
  PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
  Stop = true; break;
}
class=class="str">"cmt">// 前向传播失败则停训
if(!Actor.backProp(GetPointer(bActions), GetPointer(Encoder), LatentLayer))
{
  PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
  Stop = true; break;
}
class=class="str">"cmt">// 反向传播动作梯度失败则停
if(!Critic.feedForward((CNet *)GetPointer(Actor), -class="num">1, (CNet*)GetPointer(Encoder), LatentLayer))
{
  PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
  Stop = true; break;
}
class=class="str">"cmt">// 评论家从Actor输出前向,失败则停
Critic.TrainMode(false);
class=class="str">"cmt">// 评论家切推理态
if(!Critic.backProp(Result, (CNet *)GetPointer(Encoder), LatentLayer) ||
   !Actor.backPropGradient((CNet *)GetPointer(Encoder), LatentLayer, -class="num">1, true))
{
  PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
  Stop = true; break;
}
class=class="str">"cmt">// Critic与Actor梯度回传任一失败则停
喂完特征后立刻跑 Actor 的前向与反传,再让 Critic 评估并回传策略梯度;任何一步返回 false 就打印函数行号并 break。开 MT5 把这段接进自己的 CNet 派生类,先打印 bAccount 长度,确认 12 维特征(3+3+4+1 写缓冲)与 Encoder 输入对齐,否则训练会在 feedForward 直接断掉。外汇与贵金属杠杆高,这套 RL 编码只是特征工程一环,实盘前务必用历史 tick 回测验证稳定性。

MQL5 / C++
bAccount.Add(Buffer[tr].States[i].account[class="num">3]);
bAccount.Add(Buffer[tr].States[i].account[class="num">4] / PrevBalance);
bAccount.Add(Buffer[tr].States[i].account[class="num">5] / PrevBalance);
bAccount.Add(Buffer[tr].States[i].account[class="num">6] / PrevBalance);
class="type">class="kw">double time = (class="type">class="kw">double)Buffer[tr].States[i].account[class="num">7];
class="type">class="kw">double x = time / (class="type">class="kw">double)(D&class="macro">#x27;class="num">2024.01.class="num">01&class="macro">#x27; - D&class="macro">#x27;class="num">2023.01.class="num">01&class="macro">#x27;);
bAccount.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_MN1);
bAccount.Add((class="type">float)MathCos(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_W1);
bAccount.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_D1);
bAccount.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
if(bAccount.GetIndex() >= class="num">0)
   bAccount.BufferWrite();
class=class="str">"cmt">//--- Actor
if(!Actor.feedForward((CBufferFloat*)GetPointer(bAccount), class="num">1, false, GetPointer(Encoder), LatentLayer))
   {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    Stop = true;
    break;
   }
if(!Actor.backProp(GetPointer(bActions), GetPointer(Encoder), LatentLayer))
   {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    Stop = true;
    break;
   }
if(!Critic.feedForward((CNet *)GetPointer(Actor), -class="num">1, (CNet*)GetPointer(Encoder), LatentLayer))
   {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    Stop = true;
    break;
   }
Critic.TrainMode(false);
if(!Critic.backProp(Result, (CNet *)GetPointer(Encoder), LatentLayer) ||
   !Actor.backPropGradient((CNet *)GetPointer(Encoder), LatentLayer, -class="num">1, true))
   {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    Stop = true;
    break;
   }

◍ 训练进度别刷太勤

强化学习 EA 在 MT5 里跑迭代时,若每 tick 都刷新界面,CPU 会被 Comment 拖死,回测速度掉得肉眼可见。上面这段用 GetTickCount 做了个 500 毫秒节流:距上次刷新超 500ms 才重算一次进度百分比并输出 Actor / Critic 的平均误差。 iter 是当前迭代次数,Iterations 是总轮数,percent 直接乘 100.0 转成带两位小数的进度;StringFormat 里 %-14s 左对齐留 14 位,%15.8f 给误差留 8 位小数精度,足够看训练是否收敛。 收尾处先 Comment("") 清屏,再用 PrintFormat 把函数名、行号、最终误差打到日志,最后 ExpertRemove 让 EA 自退。外汇与贵金属杠杆高,这类自研训练 EA 仅限策略验证,实盘直接挂可能亏穿保证金。

MQL5 / C++
   if(GetTickCount() - ticks > class="num">500)
     {
      class="type">class="kw">double percent = class="type">class="kw">double(iter) * class="num">100.0 / (Iterations);
      class="type">class="kw">string str = StringFormat("%-14s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Actor", percent,
Actor.getRecentAverageError());
      str += StringFormat("%-14s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Critic", percent,
Critic.getRecentAverageError());
      Comment(str);
      ticks = GetTickCount();
     }
   }
  Comment("");
class=class="str">"cmt">//---
   PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Actor", Actor.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Critic", Critic.getRecentAverageError());
   ExpertRemove();
class=class="str">"cmt">//---
  }

EURUSD 实盘历史下的模型回测落点

我们用 EURUSD 的 2023 年 H1 真实历史数据训练模型,再切到 2024 年 1 月同品种同周期,在 MT5 策略测试器里跑已训练模型。由于去掉了嵌入层、并把描述单状态的柱线数调高,旧训练集不能直接复用,只能按前文算法重采一份新数据集。 首轮训练就撞上硬伤:预测品质明显偏低,大概率是输入噪声太多,加上模型对序列细节的敏感度拉高后反而把信号搅浑了。 没停手,继续让扮演者模型多轮迭代训练并回灌更新数据集,结果还是一言难尽。测试集上始终跑不出能盈利的模型,余额曲线一路向下,盈利因子大约卡在 0.5。外汇与贵金属属高风险品种,这类结果说明在高度随机环境里,当前实现的时间序列预测方案大概率撑不住预期精度,建议你开 MT5 用同样参数复跑一遍确认。

「在随机盘面上这套模型没扛住」

前面几节把 cross-variable linear-integrated enhanced transformer 的客户端实现拆完了:线性子模块抓趋势,变换器子模块抓变量间非线性依赖,作者还砍掉了时间分离的环境独立状态注意力,并把解码器换成预测层来提速。论文里的实验说,对时间序列任务,盯变量间依赖比盯时间步依赖更关键。 但我们拿这套独立实现的客户端去跑金融市场数据,结论很直接——在高度随机的行情条件下,它基本无效。外汇和贵金属这种高波动、高杠杆品种,随机扰动会反复击穿模型假设,拿学术集成的 transformer 直接套实盘信号,概率上很容易翻车。 要说明白的是,这结果只对应我们自己的那版实现。换数据窗口、换特征工程、换训练设定,完全可能得出相反结论。本文本就不是给算法做权威评估,只是让你熟悉这套方法并看一种落地写法,能不能改成你自己的盯盘插件,开 MT5 跑一遍同源代码最实在。

◍ 随包附带的八个工程文件

这套 LSTM 多元时间序列预测方案不是只给思路,作者把可直接编译运行的工程也一并放出。压缩包 MQL5.zip 约 1106.26 KB,里面按职责拆成 8 个文件,想在 MT5 里复现训练与推理流程,先把它们拖进对应目录。 具体分工:Research.mq5 与 ResearchRealORL.mq5 是两个样本收集 EA,后者用 Real-ORL 方法挑样本;Study.mq5 管模型训练,StudyEncoder.mq5 管编码训练。验证环节由 Test.mq5 承担。底层依赖三个类库——Trajectory.mqh 定义系统状态结构,NeuroNet.mqh 封装建网逻辑,NeuroNet.cl 是 OpenCL 核函数,决定能不能跑起 GPU 加速。 外汇与贵金属市场高杠杆、高波动,即便用这些现成 EA 做历史回测,结论也只代表某段样本内的概率倾向,不等于实盘可盈利。下载后建议先在本机用策略测试器跑通 Test.mq5,确认 NeuroNet.cl 在你的显卡驱动下能正常编译,再谈调参。

常见问题

用账户权益除以账户初始入金得到 0~N 的比例值,持仓浮亏转成负数后同样除以入金,避免量纲过大压垮输入层。
回测时把刷新间隔设到 500~1000 根 K 线以上,频繁重绘图表会拖慢训练速度甚至导致中断。
可以,小布盯盘的 AIGC 已内置多变量序列诊断,打开对应品种页即可加载模型看实盘历史下的回测区间。
先确认滑点和点值设置贴近真实环境,第 85 部分示例在 H1 周期偏差约 12 点,属正常概率波动。
随包附带的八个工程文件需全量放入同一目录,缺了特征工程脚本会在随机序列上无法收敛。