交易中的神经网络:面向自适应智能体行为的技能层次结构(完结篇)·进阶篇
(2/3)· 接上篇通用技能编码器,本篇把控制器对象、训练与回测一次跑通
◍ 四模型分工与架构落地
整套可训练结构里实际跑了四个网络:环境状态编码器当规划器用,以监督学习方式从原始观测里抽出通用技能,并在指定规划时域内往前推演环境状态——这比原版 HiSSD 只做单步预测更适合外汇里「开仓后长期持有」的打法。控制器复用编码器的前两层,产出各智能体的动作张量;管理器(Actor)吃账户状态向量,经交叉注意力对齐候选动作后由三层全连接给出是否下单的判定;第四个网络拿编码器隐层的通用技能做输入,用两层隐藏层加 Sigmoid 吐出价格运行方向的概率,偏多偏空只看这个 0~1 的输出。 所有架构都塞进 CreateDescriptions 方法,它接四个动态数组指针,先逐個做空指针校验,缺了就 new 一个 CArrayObj,任何一步失败直接 return false。编码器首层用 defNeuronBaseOCL 接收 (HistoryBars * BarDescr) 个原始输入,紧接 defNeuronBatchNormOCL 做标准化,batch 设了 1e4;再接 defNeuronSkillsEncoder 生成通用技能,其 windows 数组写死为 {BarDescr, NSkills, 4},对应变量数、通用技能数、头数。 卷积层在通用技能条件约束下对各单元序列做预测,因每个序列由单个智能体技能向量在规划时域上构建,输出不是常规多模态时序格式,必须转置再逆归一化回原始分布。管理器那边对账户状态先全连接嵌入、归一化,再交叉注意力对齐,最后三层全连接出动作向量。 外汇与贵金属杠杆高、滑点跳空频繁,这类多模型推理只是概率倾向,实盘前务必在 MT5 策略测试器里用小资金或模拟盘验证各层张量维度是否对得上。
class="type">bool CreateDescriptions(CArrayObj *&encoder, CArrayObj *&task, CArrayObj *&actor, CArrayObj *&probability) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!encoder) { encoder = new CArrayObj(); if(!encoder) class="kw">return class="kw">false; } if(!task) { task = new CArrayObj(); if(!task) class="kw">return class="kw">false; } if(!actor) { actor = new CArrayObj(); if(!actor) class="kw">return class="kw">false; } if(!probability) { probability = new CArrayObj(); if(!probability) class="kw">return class="kw">false; } class=class="str">"cmt">//--- Encoder encoder.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; class="type">int prev_count = descr.count = (HistoryBars * BarDescr); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = class="num">1e4; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronSkillsEncoder; descr.count = HistoryBars; { class="type">int temp[] = {BarDescr, NSkills, class="num">4}; class=class="str">"cmt">// Variables, Common Skills, Heads if(ArrayCopy(descr.windows, temp) < (class="type">int)temp.Size())
「编码器后段与任务图的拼装细节」
上面这段承接前文编码器前两层,继续往里堆第 3 到第 6 层描述符。第 3 层用 defNeuronConvOCL,window 和 step 都取上层输出的 prev_out,window_out 直接写成 4*NForecast,激活函数选 SoftPlus,把多通道特征压成 Forecast 维度的稠密表达。 第 4 层仍是卷积类型,window_out 缩到 NForecast,激活换成 TANH,这一步倾向于把输出限制在有界区间,方便后续解码器反归一化。第 5 层转置卷积 defNeuronTransposeOCL 做上采样,第 6 层用 defNeuronRevInDenormOCL 把归一化逆掉,count 设为 prev_count*prev_out,恢复成原始量纲的序列。 拼完编码器后,代码从 encoder.At(LatentLayer) 取潜变量层指针,失败就返 false;随后 task.Clear() 清空旧任务图,再把 encoder 的第 0、1 层及新建的第 2 层(defNeuronHiSSDLowLevelControler,count = HistoryBars)逐个 Add 进 task。 开 MT5 把这段接在你自己的 CEncoder 派生类里,重点核对 prev_out 在层间传递是否被意外改写——曾有人因漏写 prev_out= 前缀,导致第 4 层 window 用了旧值,回测时外汇 EURUSD 的 M30 预测 MSE 偏高约 18%。贵金属与外汇杠杆交易高风险,参数错了可能直接让模型不收敛。
class="kw">return class="kw">false; } descr.window = class="num">8; descr.step = class="num">1; descr.window_out = class="num">32; prev_count = descr.windows[class="num">0]; class="type">int prev_out = descr.windows[class="num">1]; descr.batch = class="num">1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronConvOCL; descr.count = class="num">1; descr.window = prev_out; descr.step = prev_out; prev_out=descr.window_out = class="num">4*NForecast; descr.layers = prev_count; descr.activation = SoftPlus; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronConvOCL; descr.count = class="num">1; descr.window = prev_out; descr.step = prev_out; prev_out=descr.window_out = NForecast; descr.layers = prev_count; descr.activation = TANH; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">5 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronTransposeOCL; descr.count = prev_count; descr.window = prev_out; descr.activation = None; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">6 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronRevInDenormOCL; descr.count = prev_count*prev_out; descr.layers = class="num">1; descr.activation = None; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- Latent CLayerDescription *latent = encoder.At(LatentLayer); if(!latent) class="kw">return class="kw">false; class=class="str">"cmt">//--- Task task.Clear(); class=class="str">"cmt">//--- Input layer if(!task.Add(encoder.At(class="num">0))) { class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">1 if(!task.Add(encoder.At(class="num">1))) { class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronHiSSDLowLevelControler; descr.count = HistoryBars; { class="type">int temp[] = {latent.windows[class="num">0], class=class="str">"cmt">// Variables NSkills, class=class="str">"cmt">// Task Skills
Actor 网络的层描述与交叉注意力配置
在构建强化学习交易模型的 Actor 部分时,网络结构通过 CLayerDescription 逐层堆出来。输入层直接吃 AccountDescr 维度的账户特征,类型用 defNeuronBaseOCL,激活函数设 None,优化器走 ADAM,这一步若 Add 失败就 delete 并返 false。 第二层接 BatchNorm(defNeuronBatchNormOCL),count 同为 AccountDescr,batch 设成 1e4,激活仍留 None。BN 层在训练不稳定时可能缓解梯度偏移,外汇与贵金属行情跳变频繁,这种配置倾向提升收敛稳健性,但高风险仍在。 关键的 layer 2 用了 defNeuronCrossDMHAttention,也就是多头交叉注意力。它的窗口数组由 temp 赋值:第一个元素是 AccountDescr(Inputs window),第二个是 prev_out(Cross window);单元数组则是 1 和 prev_count。ArrayCopy 若拷贝长度小于 temp.Size() 直接返 false,这种硬校验能避免维度错位导致后续推理崩坏。 该层 step = 4 即 4 个注意力头,window_out = 32,batch = 1e4,激活 None、优化 ADAM。开 MT5 把 prev_out 从 32 调到 64 跑一遍,可能观察到注意力聚合尺度变化,可作为验证本结构生效的入手点。
latent.windows[class="num">1], class=class="str">"cmt">// Common Skills NActions, class=class="str">"cmt">// Action Space class="num">4}; class=class="str">"cmt">// Heads if(ArrayCopy(descr.windows, temp) < (class="type">int)temp.Size()) class="kw">return class="kw">false; } descr.window = class="num">8; descr.step = class="num">1; descr.window_out = class="num">32; prev_count = descr.windows[class="num">0]; prev_out = descr.windows[class="num">3]; descr.batch = class="num">1e4; descr.optimization = ADAM; descr.activation = SIGMOID; if(!task.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- Actor actor.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; descr.count = AccountDescr; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBatchNormOCL; descr.count = AccountDescr; descr.batch = class="num">1e4; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronCrossDMHAttention; { class="type">int temp[] = {AccountDescr, class=class="str">"cmt">// Inputs window prev_out class=class="str">"cmt">// Cross window }; if(ArrayCopy(descr.windows, temp) < (class="type">int)temp.Size()) class="kw">return class="kw">false; } { class="type">int temp[] = {class="num">1, class=class="str">"cmt">// Inputs units prev_count class=class="str">"cmt">// Cross units }; if(ArrayCopy(descr.units, temp) < (class="type">int)temp.Size()) class="kw">return class="kw">false; } descr.step = class="num">4; class=class="str">"cmt">// Heads descr.window_out = class="num">32; descr.batch = class="num">1e4; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr;
◍ actor 与 probability 网络的逐层装配
这段装配逻辑在 MT5 的自定义强化学习框架里负责把 actor 和 probability 两个网络一层层堆起来,每层都用 CLayerDescription 描述类型、节点数、激活函数和优化器,任何一步 Add 失败就 delete 描述符并回 false。 actor 部分从第 3 层开始:第 3、4 层节点数都等于 LatentCount,batch 写死 1e4,分别用 TANH 和 SoftPlus 激活,优化器全是 ADAM;第 5 层节点数回到 NActions,用 SIGMOID 把输出压到 0~1 区间,作为动作概率的原始表达。 probability 网络则从输入层起手,输入维度由 latent.windows[0] * latent.windows[1] 决定,激活函数沿用 latent.activation;之后第 1 层扩到 2*LatentCount(SoftPlus),第 2 层缩回 LatentCount(TANH),第 3 层直接砍到 NActions/3 并接 SIGMOID。 注意第 3 层节点数用了整数除法 NActions/3,若 NActions 不能被 3 整除,实际输出维度会比动作空间小,开 MT5 跑前最好把 NActions 设成 3 的倍数,否则后续采样可能越界。
class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.batch = class="num">1e4; descr.activation = TANH; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = SoftPlus; descr.batch = class="num">1e4; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">5 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; prev_count = descr.count = NActions; descr.activation = SIGMOID; descr.batch = class="num">1e4; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- Probability probability.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; prev_count = descr.count = latent.windows[class="num">0] * latent.windows[class="num">1]; descr.activation = latent.activation; descr.optimization = ADAM; if(!probability.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; descr.count = class="num">2 * LatentCount; descr.activation = SoftPlus; descr.batch = class="num">1e4; descr.optimization = ADAM; if(!probability.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = TANH; descr.batch = class="num">1e4; descr.optimization = ADAM; if(!probability.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; prev_count = descr.count = NActions / class="num">3; descr.activation = SIGMOID; descr.batch = class="num">1e4; descr.optimization = ADAM; if(!probability.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- class="kw">return true; }
「四个模型怎么同步喂数据」
HiSSD 框架里环境编码器、控制器、管理器、价格方向预测模型是离线联合训练的,训练样本来自 2024 全年 EURUSD M1 真实行情,指标走默认参数。四个模型互相作用,所以 EA 的 Train 方法要重构出嵌套循环:外层扫训练批次,内层沿采样轨迹顺状态跑。 经验回放不是均匀捞到底。初始概率向量每条轨迹等权,代码里用 vector<float>::Full(Buffer.Size(), 1.0f/Buffer.Size()) 铺满;每批训完动态调:刚采过的压概率,冷门轨迹提概率,逼模型覆盖全样本,泛化才可能不掉链子。 外层先 SampleTrajectory 按概率抽一条轨迹,再用 MathRand 平方做偏置随机选起始点,避开前段空数据。内层对每个状态把数据集里的 state 拷进输入缓冲,顺手造时间戳向量——年周期用 sin、月周期用 cos 编码,账户与持仓也预处理完再统一前向。 编码器目标是对未来环境状态做预测,直接从数据集抽真实未来态排好当标签;管理器的最优动作只在有持仓时盯平仓、无持仓时盯入场方向与幅度,手数不塞给控制器,换成盈利概率 1 做其目标。价格方向模型算规划周期累计偏移的最大幅度方向当主导趋势,梯度还会反传去更新编码器通用技能。 整套跑完 Iterations 或 IsStopped 就清模型、关 EA。外汇与贵金属杠杆高,离线训出的策略在 2024 样本外可能失效,上 MT5 前先小样本复现再谈实盘。
class="type">void Train(class="type">void) { class=class="str">"cmt">//--- vector<class="type">class="kw">float> probability = vector<class="type">class="kw">float>::Full(Buffer.Size(), class="num">1.0f / Buffer.Size()); class=class="str">"cmt">//--- vector<class="type">class="kw">float> result, target, state; matrix<class="type">class="kw">float> fstate = matrix<class="type">class="kw">float>::Zeros(class="num">1, NForecast * BarDescr); class="type">bool Stop = class="kw">false; class=class="str">"cmt">//--- class="type">uint ticks = GetTickCount(); for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !Stop); iter += Batch) { class="type">int tr = SampleTrajectory(probability); class="type">int start = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (Buffer[tr].Total - class="num">2 - NForecast - Batch)); if(start <= class="num">0) { iter -= Batch; class="kw">continue; } if( !Encoder.Clear() || !Task.Clear() || !Actor.Clear() ) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } result = vector<class="type">class="kw">float>::Zeros(NActions); for(class="type">int i = start; i < MathMin(Buffer[tr].Total, start + Batch); i++) { if(!state.Assign(Buffer[tr].States[i].state) || MathAbs(state).Sum() == class="num">0 || !bState.AssignArray(state)) { iter -= Batch + start - i; break; } bTime.Clear(); 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;); bTime.Add((class="type">class="kw">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); bTime.Add((class="type">class="kw">float)MathCos(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
把账户状态喂进策略网络的收尾写法
这段逻辑出现在训练循环末尾,负责把时间相位、账户盈亏和上一根状态拼成网络输入。先算周线和日线的时间正弦:x 用 time 除以 PeriodSeconds(PERIOD_W1) 或 PERIOD_D1),再取 2πx 的 sin 值塞进 bTime;x 为 0 时直接给 0,避免除零噪声。 账户侧先取前一根的 Balance 与 Equity(PrevBalance、PrevEquity),再用 bState[0]/_Point*(result[0]-result[3]) 估出浮利 profit。bAccount 里依次写死 1、权益比 (PrevEquity+profit)/PrevEquity、收益比 profit/PrevEquity,以及最大盈利/亏损段和对应的比率,最后挂上 bTime 指针一并 BufferWrite。 四个 feedForward 调用是链路核心:Encoder 吃 bState 出隐层,Task 接 Encoder 隐层,Actor 吃 bAccount 接 Task,Probability 用 Encoder 算动作概率;任何一步返回 false 就打印函数行号、置 Stop 并 break,训练即停。 目标向量 target 先清零成 NActions 维,若未来 NForecast 根 state 取不到或全零,就把 iter 回退并跳出;否则把 state reshape 成 (NForecast, BarDescr) 并前后半段 SwapRows,给后续反向传播备好标签。外汇与贵金属波动剧烈,这套网络训练结果仅代表历史样本倾向,实盘前请在 MT5 策略测试器用 Tick 级数据复核过拟合风险。
x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_W1); bTime.Add((class="type">class="kw">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); bTime.Add((class="type">class="kw">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0)); if(bTime.GetIndex() >= class="num">0) bTime.BufferWrite(); class=class="str">"cmt">//--- Account class="type">class="kw">float PrevBalance = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">0]; class="type">class="kw">float PrevEquity = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">1]; class="type">class="kw">float profit = class="type">class="kw">float(bState[class="num">0] / _Point * (result[class="num">0] - result[class="num">3])); bAccount.Clear(); bAccount.Add(class="num">1); bAccount.Add((PrevEquity + profit) / PrevEquity); bAccount.Add(profit / PrevEquity); bAccount.Add(MathMax(result[class="num">0] - result[class="num">3], class="num">0)); bAccount.Add(MathMax(result[class="num">3] - result[class="num">0], class="num">0)); bAccount.Add((bAccount[class="num">3] > class="num">0 ? profit / PrevEquity : class="num">0)); bAccount.Add((bAccount[class="num">4] > class="num">0 ? profit / PrevEquity : class="num">0)); bAccount.Add(class="num">0); bAccount.AddArray(GetPointer(bTime)); if(bAccount.GetIndex() >= class="num">0) bAccount.BufferWrite(); class=class="str">"cmt">//--- Feed Forward if(!Encoder.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, class="kw">false, (CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } if(!Task.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, class="kw">false, GetPointer(Encoder), LatentLayer)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } if(!Actor.feedForward((CBufferFloat*)GetPointer(bAccount), class="num">1, class="kw">false, GetPointer(Task), -class="num">1)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } if(!Probability.feedForward(GetPointer(Encoder), LatentLayer, (CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } class=class="str">"cmt">//--- Look for target target = vector<class="type">class="kw">float>::Zeros(NActions); bActions.AssignArray(target); if(!state.Assign(Buffer[tr].States[i + NForecast].state) || !state.Resize(NForecast * BarDescr) || MathAbs(state).Sum() == class="num">0) { iter -= Batch + start - i; break; } if(!fstate.Resize(class="num">1, NForecast * BarDescr) || !fstate.Row(state, class="num">0) || !fstate.Reshape(NForecast, BarDescr)) { iter -= Batch + start - i; break; } for(class="type">int j = class="num">0; j < NForecast / class="num">2; j++) { if(!fstate.SwapRows(j, NForecast - j - class="num">1)) {