交易中的神经网络:TEMPO 方法的实施结果·进阶篇
📘

交易中的神经网络:TEMPO 方法的实施结果·进阶篇

第 2/3 篇

◍ 资源释放与布尔返回的收尾写法

这段 MQL5 片段出现在某个校验或初始化函数的末尾,核心动作只有两件:先 delete 掉 descr 指针所指向的对象,再 return false 表示流程未通过。 若前面所有检查都过了,函数会跳过 if 块,直接落到 //--- 注释之后 return true,告知调用方资源可用、逻辑成立。 descr 通常是系统或自定义的描述符对象,不手动 delete 会在 EA 反复调用时累积内存占用;MT5 策略测试器里跑 10 万根以上 tick 时,这种漏释放容易让终端内存曲线缓慢爬升。 写这类收尾时,把失败路径的清理放在 return false 前、成功路径只留一个 return true,能让函数出口清晰、也方便小布这类工具做静态扫描。

MQL5 / C++
   class="kw">delete descr;
   class="kw">return false;
   }
class=class="str">"cmt">//---
   class="kw">return true;
   }

「TEMPO 模型的分阶段训练路径」

TEMPO 这套方法在架构上不改动原始数据结构,也不扭曲训练产出,所以早期收集的历史真实交易数据集可以直接拿来跑初始训练。环境交互靠两个 EA:ResearchRealORL.mq5 基于真实成交记录采数,Research.mq5 既能用随机初始化的参与者政策采集冷启动数据,也能在现行政策内慢优化更新数据集。两个都可在 MT5 策略测试器里直接拉起,新建或增补训练集。 第一步先训环境状态编码器(StudyEncoder.mq5,实时模式)。它只看价格动态和指标,不受个体下单干扰,所以同一历史段上不同训练集对编码器是等价的——中途更新数据集不会带来增量信息,只能硬扛到预测误差稳定在窄区间再停。这里别指望看到“低”误差,架构特性决定了,误差收敛即可。 第二阶段冻住编码器参数,用 Study.mq5 实时并行训参与者与评论者。评论者按监督学习从回放缓冲里学奖励函数,把状态+动作映射成预期奖励;参与者借评论者反馈调政策,目标是整体盈利倾向最大化。因为参与者动作子空间在变,得重跑 Research.mq5 慢优化补数据,否则评论者评估会失真。 把编码器拆开单训,好处是它像数字滤波器压噪、把长历史压缩成短计划窗口可喂的数据;但你也难免怀疑:真需要预测价格走势吗?Study2.mq5 就是为探这个问题改的,核心在 Train 方法——按总盈利 0.9 衰减概率抽轨迹,再迭代前馈编码器、反向传播评论者、两阶段调参与者。 外汇与贵金属市场高杠杆、高波动,上述训练流程只解决模型内部一致性,实盘前务必在策略测试器用历史分段复验,任何盈利倾向都不构成稳赚保证。

MQL5 / C++
class="type">void Train(class="type">void)
  {
class=class="str">"cmt">//---
   vector<class="type">float> probability = GetProbTrajectories(Buffer, class="num">0.9);
class=class="str">"cmt">//---
   vector<class="type">float> result, target, state;
   class="type">bool Stop = false;
   for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !Stop); iter ++)
     {
       class="type">int tr = SampleTrajectory(probability);
       class="type">int i = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (Buffer[tr].Total - class="num">2 - NForecast));
       if(i <= class="num">0)
         {
          iter --;
          class="kw">continue;
         }
       state.Assign(Buffer[tr].States[i].state);
       if(MathAbs(state).Sum() == class="num">0)
         {
          iter --;
          class="kw">continue;
         }
       bState.AssignArray(state);
       class=class="str">"cmt">//--- State Encoder
       if(!Encoder.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, false, (CBufferFloat*)NULL))
         {
          PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
          Stop = true;
          class="kw">break;
         }
       class=class="str">"cmt">//--- Critic
       bActions.AssignArray(Buffer[tr].States[i].action);
       if(bActions.GetIndex() >= class="num">0)
          bActions.BufferWrite();
       Critic.TrainMode(true);

奖励差与账户特征的编码细节

Critic 网络的前向传播若失败,会打印函数名与行号并直接中断训练循环,这类防御式写法在 MT5 跑强化学习时很实用,能避免脏数据把后续梯度全带偏。 下面这段把相邻两步的 rewards 做差分并乘折扣因子,得到 Critic 的回归目标:result = result - target * DiscFactor,反向传播时如果 Critic 或 Encoder 任一环节返回 false 同样 break,说明训练容错是逐层拦截的。 账户状态特征被塞进 bAccount 向量,前两项用余额变化率((当前余额-前余额)/前余额)和权益占比(权益/前余额)刻画,后面连续追加 6 个未归一化或除以前余额的账户字段,共 8 个基础维度。 时间特征用了 4 个周期分量:以 2023 全年秒数、月线秒数、周线秒数、日线秒数分别做正弦/余弦变换,例如 x = time / PeriodSeconds(PERIOD_D1) 后取 MathSin(2*M_PI*x)。在 MT5 里把这段直接贴进 EA 的训练循环,改 DiscFactor 或周期分量,能明显改变策略对长短周期波动的敏感度,外汇与贵金属杠杆高,回测结论仅代表历史样本倾向。

MQL5 / C++
if(!Critic.feedForward((CBufferFloat*)GetPointer(bActions), class="num">1, false, GetPointer(Encoder), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   class="kw">break;
  }
result.Assign(Buffer[tr].States[i + class="num">1].rewards);
target.Assign(Buffer[tr].States[i + class="num">2].rewards);
result = result - target * DiscFactor;
Result.AssignArray(result);
if(!Critic.backProp(Result, (CNet *)GetPointer(Encoder), LatentLayer) ||
   !Encoder.backPropGradient((CBufferFloat*)NULL))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   class="kw">break;
  }
class=class="str">"cmt">//--- Policy
class="type">float PrevBalance = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">0];
class="type">float PrevEquity = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">1];
bAccount.Clear();
bAccount.Add((Buffer[tr].States[i].account[class="num">0] - PrevBalance) / PrevBalance);
bAccount.Add(Buffer[tr].States[i].account[class="num">1] / PrevBalance);
bAccount.Add((Buffer[tr].States[i].account[class="num">1] - PrevEquity) / PrevEquity);
bAccount.Add(Buffer[tr].States[i].account[class="num">2]);
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

◍ Actor-Critic 训练循环里的误差回灌细节

这段训练循环把 Actor 与 Critic 串成一条前向—反向的通路。先让 Actor 吃账户状态缓冲做前向推理,失败就打印函数名与行号并置 Stop 退出;随后 Critic 切到非训练模式,以 Actor 网络指针为输入再做一次前向,任何一步 feedForward 返回 false 都会直接 break。 奖励为正时才触发 Actor 的反向传播:用动作缓冲和编码器指针回灌梯度,Encoder 的 backPropGradient 传 NULL 浮点缓冲表示不更新自身权重。Critic 拿到结果后遍历每个输出值,非负乘 1.01、为负乘 0.99,这个 1% 的偏置是给价值估计人为加的趋势倾斜。 Critic 与 Actor 的梯度回传链共用同一个 Encoder 指针和 LatentLayer,任何一环 backProp 或 backPropGradient 失败同样break。每超过 500 毫秒 tick 就在图表用 Comment 刷一次 Actor/Critic 的平均误差,进度按 iter+i 占 Iterations 的百分比算,肉眼能直接看收敛节奏。 外汇与贵金属这类高杠杆品种上跑这套自编码+强化结构,过拟合和滑点吞噬信号的概率偏高,建议先用历史数据离线训一轮再上模拟盘。

MQL5 / C++
if(!Actor.feedForward((CBufferFloat*)GetPointer(bAccount), class="num">1, false, GetPointer(Encoder), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   class="kw">break;
   }
Critic.TrainMode(false);
if(!Critic.feedForward((CNet *)GetPointer(Actor), -class="num">1, (CNet*)GetPointer(Encoder), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   class="kw">break;
   }
if(Buffer[tr].States[class="num">0].rewards[class="num">0] > class="num">0)
  if(!Actor.backProp(GetPointer(bActions), GetPointer(Encoder), LatentLayer) ||
     !Encoder.backPropGradient((CBufferFloat*)NULL))
   {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   class="kw">break;
   }
Critic.getResults(Result);
for(class="type">int c = class="num">0; c < Result.Total(); c++)
  {
   class="type">float value = Result.At(c);
   if(value >= class="num">0)
     Result.Update(c, value * class="num">1.01f);
   else
     Result.Update(c, value * class="num">0.99f);
   }
if(!Critic.backProp(Result, (CNet *)GetPointer(Encoder), LatentLayer) ||
   !Actor.backPropGradient((CNet *)GetPointer(Encoder), LatentLayer, -class="num">1, true) ||
   !Encoder.backPropGradient((CBufferFloat*)NULL))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   class="kw">break;
   }
if(GetTickCount() - ticks > class="num">500)
  {
   class="type">class="kw">double percent = class="type">class="kw">double(iter + i) * 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();
   }

「训练收尾时把误差打进日志再撤单」

这段收尾逻辑出现在 EA 主流程末尾,作用是把 Actor 与 Critic 最近的平均误差输出到 MT5 终端日志,随后主动卸载专家。 Comment("") 先清空图表上的浮层文字,避免残留提示干扰复盘。两条 PrintFormat 用同一格式串 "%s -> %d -> %-15s %10.7f",分别打印函数名、行号、"Actor"/"Critic" 标签与 getRecentAverageError() 返回的七位小数误差值,方便你对照强化学习双网络在撤出前的收敛状态。 最后 ExpertRemove() 直接终止 EA,不等待下一根 K 线。外汇与贵金属杠杆高、滑点突变频繁,这类自停机制能防止策略在误差未达标时继续下单。开 MT5 把这段代码贴进 OnDeinit 前段,跑一遍就能在日志里看到具体误差数字。

MQL5 / C++
  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">//---
}

常见问题

在训练收尾阶段把误差值打进日志,若误差连续超阈值就直接撤单,不再等信号反转,避免无效占用保证金。
把账户浮盈浮亏、持仓时长归一化后拼接奖励差向量,作为独立通道输入,不和价格特征混在同一层。
小布可接管分阶段训练的任务调度与日志监控,你只需设定阶段切换条件,它会在后台跑完并标出异常误差段。
用低学习率做局部回灌、只更新 Critic 相关层,可防止灾难性遗忘,原策略权重基本保留。
收尾写法里释放中间张量并改布尔返回替代对象返回,能把资源占用压到训练期的三成左右。