交易中的神经网络:TEMPO 方法的实施结果·进阶篇
◍ 资源释放与布尔返回的收尾写法
这段 MQL5 片段出现在某个校验或初始化函数的末尾,核心动作只有两件:先 delete 掉 descr 指针所指向的对象,再 return false 表示流程未通过。 若前面所有检查都过了,函数会跳过 if 块,直接落到 //--- 注释之后 return true,告知调用方资源可用、逻辑成立。 descr 通常是系统或自定义的描述符对象,不手动 delete 会在 EA 反复调用时累积内存占用;MT5 策略测试器里跑 10 万根以上 tick 时,这种漏释放容易让终端内存曲线缓慢爬升。 写这类收尾时,把失败路径的清理放在 return false 前、成功路径只留一个 return true,能让函数出口清晰、也方便小布这类工具做静态扫描。
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 衰减概率抽轨迹,再迭代前馈编码器、反向传播评论者、两阶段调参与者。 外汇与贵金属市场高杠杆、高波动,上述训练流程只解决模型内部一致性,实盘前务必在策略测试器用历史分段复验,任何盈利倾向都不构成稳赚保证。
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 或周期分量,能明显改变策略对长短周期波动的敏感度,外汇与贵金属杠杆高,回测结论仅代表历史样本倾向。
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 的百分比算,肉眼能直接看收敛节奏。 外汇与贵金属这类高杠杆品种上跑这套自编码+强化结构,过拟合和滑点吞噬信号的概率偏高,建议先用历史数据离线训一轮再上模拟盘。
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 前段,跑一遍就能在日志里看到具体误差数字。
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">//--- }