神经网络变得简单(第 80 部分):图形变换器生成式对抗模型(GTGAN)·综合运用
◍ 编码器到解码器的层栈拼装
这段逻辑在 MT5 的自定义 AI 模块里把编码器(encoder)和解码器(decoder)逐层堆起来,每一层都用 CLayerDescription 描述形状与类型,任何一步 Add 失败就 delete 并回 false,避免半吊子模型挂进图表。 先看编码器第 2 层:类型为 defNeuronEmbeddingOCL,把上一层的输出数量 prev_count 塞进 windows 数组,本层神经元数设为 GPTBars,输出窗口取 EmbeddingSize 的一半(prev_wout = EmbeddingSize/2)。第 3 层接卷积 defNeuronConvOCL,step 和 window 都等于 prev_wout,输出窗口扩回 EmbeddingSize。 第 4 层放 dropout,概率写死 0.4f,神经元数 = prev_count*prev_wout,无激活;第 5 层是位置编码 defNeuronPEOCL。第 6 到 14 层用 for 循环连跑 8 次 defNeuronGTE,step 固定为 4,window_out 每次砍成 prev_wout/4,这是把序列特征压缩进局部窗口的典型手法。 解码器先 Clear,再铺输入层 defNeuronBaseOCL,节点数 = prev_count*prev_wout,优化器选 ADAM;随后第 1 层卷积 step=prev_wout、window_out=EmbeddingSize/4。外汇与贵金属行情噪声大,这类深栈网络过拟合概率偏高,实盘前务必在 MT5 策略测试器用历史数据跑通 Add 全流程。
class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronEmbeddingOCL; { class="type">int temp[] = {prev_count}; ArrayCopy(descr.windows, temp); } prev_count = descr.count = GPTBars; class="type">int prev_wout = descr.window_out = EmbeddingSize / class="num">2; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConvOCL; descr.count = prev_count; descr.step = descr.window = prev_wout; prev_wout = descr.window_out = EmbeddingSize; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronDropoutOCL; descr.count = prev_count*prev_wout; descr.probability= class="num">0.4f; descr.activation=None; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">5 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronPEOCL; descr.count = prev_count; descr.window = prev_wout; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">6 - class="num">14 for(class="type">int i = class="num">0; i < class="num">8; i++) { if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronGTE; descr.count = prev_count; descr.window = prev_wout; descr.step = class="num">4; descr.window_out = prev_wout / descr.step; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } } class=class="str">"cmt">//--- Decoder decoder.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = prev_count * prev_wout; descr.activation = None; descr.optimization = ADAM; if(!decoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConvOCL; descr.count=prev_count; descr.window = prev_wout; descr.step=prev_wout; descr.window_out=EmbeddingSize/class="num">4; descr.optimization = ADAM; descr.activation = None; if(!decoder.Add(descr)) { class="kw">delete descr;
解码器与双网络层的描述装配
在 MT5 的 OpenCL 神经网络封装里,解码器(decoder)靠 CLayerDescription 逐层堆叠。第二层用 defNeuronSoftMaxOCL 类型,count 取上一层权重输出数 prev_wout,step 设为 prev_count,优化器统一挂 ADAM,激活函数留 None。 第三层切到 defNeuronBaseOCL,节点数按 prev_count*EmbeddingSize/2 算——假设 EmbeddingSize=64、prev_count=32,那这层就是 1024 个裸节点,同样不走激活、只跑 ADAM。任何一层 Add 失败就 delete 描述对象并回 false,避免显存泄漏。 CreateDescriptions 里 actor 与 critic 先判空再 new,空指针时直接返回 false。Actor 输入层 count = AccountDescr(账户特征维数),第一隐藏层改为 EmbeddingSize 个节点并套 SIGMOID;第二层用 defNeuronCrossAttenOCL 做交叉注意力,units 填 {prev_count, GPTBars}、windows 填 {EmbeddingSize, EmbeddingSize},window_out=16、step=4。 把 window_out 从 16 调到 8、step 从 4 调到 2,可能让注意力窗口更密但显存占用倾向翻倍。外汇与贵金属波动剧烈,这类模型仅作概率辅助,实盘前务必在 MT5 策略测试器跑回测验证。
class="kw">return false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronSoftMaxOCL; descr.count = prev_wout; descr.step = prev_count; descr.activation = None; descr.optimization = ADAM; if(!decoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = prev_count*EmbeddingSize/class="num">2; descr.activation = None; descr.optimization = ADAM; if(!decoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CreateDescriptions(CArrayObj *actor, CArrayObj *critic) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!actor) { actor = new CArrayObj(); if(!actor) class="kw">return false; } if(!critic) { critic = new CArrayObj(); if(!critic) class="kw">return false; } class=class="str">"cmt">//--- Actor actor.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; class="type">int prev_count = descr.count = AccountDescr; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = EmbeddingSize; descr.activation = SIGMOID; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronCrossAttenOCL; { class="type">int temp[] = {prev_count,GPTBars}; ArrayCopy(descr.units, temp); } { class="type">int temp[] = {EmbeddingSize, EmbeddingSize}; ArrayCopy(descr.windows, temp); } descr.window_out = class="num">16; descr.step = class="num">4; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronCrossAttenOCL; {
「Actor与Critic网络的层堆叠实写」
这段把强化学习里的 actor 和 critic 两套网络用 MQL5 的 CLayerDescription 逐层拼出来。actor 从第 4 层起连续用了两次 CrossAttenOCL 交叉注意力层,units 都塞进 {prev_count, GPTBars}、windows 写死为 {EmbeddingSize, EmbeddingSize},window_out=16、step=4 这两个值直接决定注意力滑动窗口的吞吐节奏。 后续 actor 第 5~7 层依次是 BaseOCL(LatentCount 个 SIGMOID 单元)、BaseOCL(2*NActions 个无激活单元)、VAEOCL(NActions 个单元)。VAE 层放在最末,意味着策略输出走变分自编码结构,动作采样可能带随机性。 critic 侧先铺一层 BaseOCL 输入层,prev_count 被重新赋值为 GPTBars*EmbeddingSize,这是把前面 GPT 时序嵌入拉平当 Critic 的输入维度。接着第 1 层用 defNeuronConcatenate,window=prev_count、step=NActions、count=LatentCount,等于把状态嵌入和动作做拼接后压进潜在空间。 所有层优化器统一挂 ADAM,但激活函数混用 None / SIGMOID。在 MT5 里跑这套描述前,先确认 GPTBars、EmbeddingSize、LatentCount、NActions 这几个宏已在文件头部定义,否则 actor.Add 会直接返回 false 导致建网失败。外汇与贵金属行情高波动,此类网络仅作概率性信号参考,实盘前务必用历史数据回测。
class="type">int temp[] = {prev_count,GPTBars}; ArrayCopy(descr.units, temp); } { class="type">int temp[] = {EmbeddingSize, EmbeddingSize}; ArrayCopy(descr.windows, temp); } descr.window_out = class="num">16; descr.step = class="num">4; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronCrossAttenOCL; { class="type">int temp[] = {prev_count,GPTBars}; ArrayCopy(descr.units, temp); } { class="type">int temp[] = {EmbeddingSize, EmbeddingSize}; ArrayCopy(descr.windows, temp); } descr.window_out = class="num">16; descr.step = class="num">4; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">5 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = SIGMOID; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">6 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = class="num">2 * NActions; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">7 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronVAEOCL; descr.count = NActions; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- Critic critic.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count=descr.count = GPTBars*EmbeddingSize; descr.activation = None; descr.optimization = ADAM; if(!critic.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type=defNeuronConcatenate; descr.window=prev_count; descr.step = NActions; descr.count=LatentCount; descr.optimization = ADAM; descr.activation = SIGMOID; if(!critic.Add(descr)) { class="kw">delete descr; class="kw">return false; }
◍ Critic 网络堆叠与采样训练循环
在构建 Critic 网络时,第二层与第三层都通过 CLayerDescription 动态申请并挂接。第二层设为隐含层,节点数由 LatentCount 控制,激活用 SIGMOID、优化用 ADAM;第三层输出层节点数等于 NRewards,激活设为 None 直接吐原始值,同样走 ADAM。若 Add 失败则立即 delete 描述对象并回 false,避免野指针。 训练函数 Train 里先按 0.9 阈值取轨迹概率向量,再用双重 MathRand 平方归一化来挑起始 state,使采样偏向靠后的片段。batch 尺寸固定为 GPTBars+48,若算出的 state 小于等于 0 就 iter-- 并 continue 跳过本轮。 主循环从 state 跑到 state+batch 与 Buffer 总长的最小值,每步把状态数组塞进 Encoder 做前馈(训练标志 false),再让 Decoder 以 Encoder 为输入做反向步长 -1 的前馈。任一层 feedForward 返回 false 就打印函数名与行号、置 Stop 并 break,方便在 MT5 Experts 日志里定位是哪一层断的。 外汇与贵金属行情具有高杠杆高风险,上述网络训练只是概率性拟合,实盘前务必用历史数据做离线回测验证稳定性。
class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = SIGMOID; descr.optimization = ADAM; if(!critic.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = NRewards; descr.activation = None; descr.optimization = ADAM; if(!critic.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Train function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Train(class="type">void) { class=class="str">"cmt">//--- vector<class="type">float> probability = GetProbTrajectories(Buffer, class="num">0.9); vector<class="type">float> result, target; class="type">bool Stop = false; class=class="str">"cmt">//--- class="type">uint ticks = GetTickCount(); class="type">int tr = SampleTrajectory(probability); class="type">int batch = GPTBars + class="num">48; class="type">int state = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (Buffer[tr].Total - class="num">2 - batch)); if(state <= class="num">0) { iter--; class="kw">continue; } Encoder.Clear(); class="type">int end = MathMin(state + batch, Buffer[tr].Total); for(class="type">int i = state; i < end; i++) { bState.AssignArray(Buffer[tr].States[i].state); class=class="str">"cmt">//--- Trajectory if(!Encoder.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, false, (CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; class="kw">break; } if(!Decoder.feedForward((CNet*)GetPointer(Encoder),-class="num">1,(CBufferFloat *)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; class="kw">break; }
自编码器训练循环与模型加载的容错断点
这段训练主循环把 LatentLayer 的输出取出来后,立刻让 Decoder 做反向传播,Encoder 只算梯度不回传误差。只要任意一步 backProp 返回 false,就打印函数名加行号、置 Stop 并 break,等于在每轮迭代里埋了硬断点,避免脏梯度污染权重。 每过 500 毫秒(GetTickCount 差值 > 500)才刷新一次 Comment,用 (i-state)/(end-state) 加 iter 归一化算训练百分比,同时把 Decoder.getRecentAverageError() 的 8 位小数误差打到图表左上角。这种节流写法能让 MT5 在跑重网络时不卡顿报价刷新。 OnInit 里先 LoadTotalBase,失败直接 INIT_FAILED;随后分别 Load 三个 .nnw 文件——Enc.nnw 必载,Act.nnw 与 Crt.nnw 任一缺失才现场 CreateDescriptions 建网。注意 Encoder 加载失败没有任何兜底新建逻辑,实盘前务必确认 Enc.nnw 已落盘,否则 EA 起不来。 外汇与贵金属行情下用这类自编码结构做特征压缩,网络误差可能随波动率跳变,任何训练中断都只是概率性保护,不构成策略收益保证。
Encoder.GetLayerOutput(LatentLayer,Result); if(!Decoder.backProp(Result,(CBufferFloat*)NULL) || !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(i - state) / ((end - state)) + iter) * class="num">100.0 / (Iterations); class="type">class="kw">string str = StringFormat("%-14s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Decoder", percent, Decoder.getRecentAverageError()); Comment(str); ticks = GetTickCount(); } } } Comment(""); class=class="str">"cmt">//--- PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Decoder", Decoder.getRecentAverageError()); ExpertRemove(); class=class="str">"cmt">//--- } CNet Encoder; CNet Actor; CNet Critic; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- ResetLastError(); if(!LoadTotalBase()) { PrintFormat("Error of load study data: %d", GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- load models class="type">float temp; if(!Encoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true)) { Print("Can&class="macro">#x27;t load pretrained Encoder"); class="kw">return INIT_FAILED; } if(!Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) || !Critic.Load(FileName + "Crt.nnw", temp, temp, temp, dtStudied, true) ) { CArrayObj *actor = new CArrayObj(); CArrayObj *critic = new CArrayObj(); if(!CreateDescriptions(actor, critic)) { class="kw">delete actor; class="kw">delete critic; class="kw">return INIT_FAILED; } if(!Actor.Create(actor) || !Critic.Create(critic)) {
「初始化失败点与训练采样逻辑」
在 EA 初始化阶段,若 Actor 输出维度与动作数不匹配,代码会直接打印 'The scope of the actor does not match the actions count' 并返回 INIT_FAILED;同样,Encoder 第 0 层输出总量若不等于 HistoryBars * BarDescr,也会触发 INIT_FAILED。这类硬校验能避免后续在 GPU/OpenCL 上跑空模型,开 MT5 加载前先确认 NActions 与 HistoryBars、BarDescr 的乘积一致。 训练函数 Train 里用 GetProbTrajectories(Buffer, 0.9) 取概率轨迹,循环上限由 Iterations 控制。每次迭代先用 SampleTrajectory 抽一条轨迹,batch 大小固定为 GPTBars + 48,state 起点用两次 MathRand 平方归一化后偏移,范围受 Buffer[tr].Total - 2 - PrecoderBars - batch 限制,若算出 state <= 0 就 iter-- 并重抽。 下面这段是初始化收尾与训练函数头的原文代码,可直接复制到 MT5 头文件对照看校验分支。 从 state 到 end 的切片用 MathMin(state + batch, Buffer[tr].Total - PrecoderBars) 封顶,随后逐根把 Buffer[tr].States[i].state 塞进 bState。外汇与贵金属行情跳空频繁,这类基于历史片段的强化学习训练在实盘前务必用策略测试器跑小样本,高风险品种上过拟合概率偏高。
class="kw">delete actor; class="kw">delete critic; class="kw">return INIT_FAILED; } class="kw">delete actor; class="kw">delete critic; } OpenCL = Encoder.GetOpenCL(); Actor.SetOpenCL(OpenCL); Critic.SetOpenCL(OpenCL); Encoder.TrainMode(false); Actor.getResults(Result); if(Result.Total() != NActions) { PrintFormat("The scope of the actor does not match the actions count(%d <> %d)", NActions, Result.Total()); class="kw">return INIT_FAILED; } Encoder.GetLayerOutput(class="num">0, Result); if(Result.Total() != (HistoryBars * BarDescr)) { PrintFormat("Input size of Encoder doesn&class="macro">#x27;t match state description(%d <> %d)", Result.Total(), (HistoryBars * BarDescr)); class="kw">return INIT_FAILED; } if(!bGradient.BufferInit(MathMax(AccountDescr, NForecast), class="num">0) || !bGradient.BufferCreate(OpenCL)) { PrintFormat("Error of create buffers: %d", GetLastError()); class="kw">return INIT_FAILED; } if(!EventChartCustom(ChartID(), class="num">1, class="num">0, class="num">0, "Init")) { PrintFormat("Error of create study event: %d", GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Train function | class=class="str">"cmt">//+------------------------------------------------------------------+ 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; class="type">bool Stop = false; class=class="str">"cmt">//--- class="type">uint ticks = GetTickCount(); for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !Stop); iter ++) { class="type">int tr = SampleTrajectory(probability); class="type">int batch = GPTBars + class="num">48; class="type">int state = (class="type">int)((MathRand()*MathRand() / MathPow(class="num">32767, class="num">2))*(Buffer[tr].Total - class="num">2 - PrecoderBars - batch)); if(state <= class="num">0) { iter--; class="kw">continue; } Encoder.Clear(); class="type">int end = MathMin(state + batch, Buffer[tr].Total - PrecoderBars); for(class="type">int i = state; i < end; i++) { bState.AssignArray(Buffer[tr].States[i].state);
◍ 把账户状态喂给策略网络
强化学习里策略网络不能直接吃原始资金曲线,得先做成相对变化率和周期相位特征。下面这段 MT5 代码把上一根与当前根的余额、净值差值归一化,再叠上基于年/月/周/日周期的三角函数值,拼成一个 15 维左右的账户特征向量。 先取上一状态(i-1,边界用 MathMax 钳到 0)的余额与净值,当前余额相对上一余额的变化率写进 bAccount 第 0 位,当前净值占上一余额比写第 1 位,净值变化率写第 2 位,后面几个 account[2]~[6] 也分别除以 PrevBalance 做缩放,避免数量级压倒梯度。 时间特征用 2023.01.01 到 2024.01.01 的秒数(约 31536000 秒)当年化基准,再除月线、周线、日线周期秒数,分别算 sin/cos。PERIOD_MN1 约 2592000 秒、PERIOD_W1 604800 秒、PERIOD_D1 86400 秒,这些相位项让网络能感知‘现在处于哪个月/周/日位置’,对外汇和贵金属这种受周期扰动的高风险品种可能有用。 最后 Encoder 先跑一步前向(不训练、不回传),失败就打印函数名加行号并 break;Actor 再吃 bAccount 和 Encoder 做前向,同样失败即停。你开 MT5 把这段贴进 EA 的 OnTick 或回测循环,把 D'2024.01.01' 改成你数据起点次年,能直接看特征维度是否对得上。
class=class="str">"cmt">//--- Trajectory 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">//--- 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 if(!Actor.feedForward((CBufferFloat*)GetPointer(bAccount),class="num">1,false,GetPointer(Encoder))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; class="kw">break; }
Critic 与 Actor 的反向传播闭环
这段训练循环把 Critic 网络先推前向,拿 Encoder 的输出和 Actor 指针喂进去;任一环节返回 false 就打印函数名加行号、置 Stop 并 break,避免脏梯度继续写权。 Result 先取当前 state 的 action 数组,交给 Actor 做 backProp;随后用 Buffer 里 i+1 与 i+2 的 rewards 算 TD 残差:result = result - target * DiscFactor,再回灌 Critic 与 Actor 的梯度。 每 500 毫秒(GetTickCount 差值 > 500)才刷一次 Comment,把 Actor / Critic 的近期平均误差按百分比进度打印出来,训练不至于因频繁 UI 调用拖慢。 跑完双层循环后清 Comment,用 PrintFormat 把两个网络的最终平均误差打到日志(精度 10.7f),随后 ExpertRemove 自卸载——EA 只做这一次离线训练,不留在图表上吃资源。
class=class="str">"cmt">//--- Critic if(!Critic.feedForward((CNet *)GetPointer(Encoder), -class="num">1, (CNet*)GetPointer(Actor))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; class="kw">break; } Result.AssignArray(Buffer[tr].States[i].action); if(!Actor.backProp(Result, GetPointer(Encoder))) { 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(Actor)) || !Actor.backPropGradient(GetPointer(Encoder))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; class="kw">break; } class=class="str">"cmt">//--- if(GetTickCount() - ticks > class="num">500) { class="type">class="kw">double percent = (class="type">class="kw">double(i - state) / ((end - state)) + 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 H1 上的 GTGAN 实测与训练拆分」
我们在 MetaTrader 5 策略测试器里跑通了前面几节搭好的 GTGAN 流程,直接用真实报价验证。模型喂的是 EURUSD H1 历史数据:训练窗口取 2023 年前 7 个月,测试窗口取 2023 年 8 月,和之前文章里源数据的用法一致。 动作向量与状态转换奖励沿用了前作设定,所以经验回放缓冲区能直接复用——把旧文件重命名为 “GTGAN.bd” 即可接着训,不用重新采集样本。 训练拆成两个阶段:先训编码器,再训扮演者行为策略。这种两阶段切分让收敛明显更稳更快,不容易在初期乱漂。 从 8 月测试看,模型很快从回放缓冲里学到了一套接近平均值的通用动作政策。问题在于我的缓冲区里正向验算样本偏少,模型学到的策略也就只贴近均值,实盘倾向给出中性甚至偏负的结果。值得一试的是只拿正向验算片段重训,看看策略分布能否偏移。
◍ GTGAN 落地 MT5 后还差什么
GTGAN 算法 2024 年 1 月才推出,核心是用编码器 GTE 做当前状态综合分析,把注意力机制和卷积图模型揉在一起,省去单独堆模块的麻烦。我们在 MetaTrader 5 策略测试器里用 MQL5 跑了真实数据,模型能跑通,但离直接拿来用还早。 回测结论很直白:这套方法在外汇和贵金属这种高波动市场里,只是验证了架构可行,离稳定信号还差额外工程。想自己验,开 MT5 把 GTE 编码器接进策略测试器跑一遍 EURUSD 的 H1 就行。 后续系列会拆图形变换器 GAN 的掩码建模细节,这一节先到这——架构跑通不代表能下单,杠杆市场里任何新模型都得先当概率玩具看。
配套的程序与类库清单
这套 LSTM 预测方案落地到 MT5,靠的是一组分工明确的 mq5 与 mqh 文件。样本收集由 Research.mq5 与 ResearchRealORL.mq5 两个 EA 完成,后者专门用 Real-ORL 方法抓示例;模型训练则交给 Study.mq5,表述模型学习另由 StudyEncoder.mq5 负责,Test.mq5 做推理测试。 系统状态结构定义在 Trajectory.mqh,神经网络创建依赖 NeuroNet.mqh,底层 OpenCL 加速代码放在 NeuroNet.cl。整套压缩包 MQL5.zip 约 1008.2 KB,下载后可直接在 MT5 里分模块加载验证。 外汇与贵金属行情受杠杆与跳空影响,跑通上述任一 EA 都存在实盘亏损的高风险,建议先开策略测试器用历史数据核对输入输出维度是否对齐。