神经网络在交易中的应用:市场异常的自适应检测(终篇)·进阶篇
(2/3)· 接上篇理论,我们动手搭自适应瓶颈模块,看它如何实时调压缩级别揪出异常盘口
「编码器后段的层定义与窗口配置」
下面这段 MT5 神经网络编码器从第 4 层到第 8 层的定义,展示了卷积与自适应批归一化层如何串接。第 4 层用 defNeuronConvOCL,窗口取 HistoryBars / Segments,步长等于窗口,输出维度锁在 EmbeddingSize,上一层输出 prev_wout 被记录为后续层的输入窗口。 第 6 层是重点:类型 defNeuronAdaBN,window_out 写死 256,step=3 对应 Top K 选择;内部用长度为 15 的临时数组 temp,循环填入 (i+1)*8,也就是 8,16,24…120,再拷进 descr.windows。batch 设 1e4、优化器 ADAM,激活函数为 None。 第 7、8 层继续卷积,count 乘上 BarDescr 扩维,window_out 分别从 EmbeddingSize/2 降到 HistoryBars/Segments,激活在 SoftPlus 与 TANH 间切换。任何一层 Add 失败都 delete descr 并 return false,避免内存泄漏。 开 MT5 把 HistoryBars、Segments、EmbeddingSize 代入,能直接算出第 4 层 descr.count = (HistoryBars + HistoryBars/Segments - 1) / (HistoryBars/Segments),外汇与贵金属品种上跑这套结构前务必认清过拟合与实盘滑点风险。
if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronTransposeOCL; descr.window = BarDescr; prev_count = descr.count = HistoryBars; descr.activation = SoftPlus; 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 = defNeuronConvOCL; descr.window = HistoryBars / Segments; prev_count = descr.count = (HistoryBars + descr.window - class="num">1) / descr.window; descr.step = descr.window; descr.layers = BarDescr; descr.activation = SoftPlus; class="type">int 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">5 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConvOCL; descr.count = prev_count; descr.window = prev_wout; descr.step = prev_wout; prev_wout = descr.window_out = EmbeddingSize; descr.layers = BarDescr; descr.activation = TANH; if(!encoder.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 = defNeuronAdaBN; descr.window = prev_wout; descr.count = prev_count; descr.window_out = class="num">256; descr.step = class="num">3; class=class="str">"cmt">// Top K descr.layers = BarDescr; class=class="str">"cmt">// Variables { class="type">int temp[class="num">15]; for(class="type">uint i = class="num">0; i < temp.Size(); i++) temp[i] = class="type">int(i + class="num">1) * class="num">8; if(ArrayCopy(descr.windows, temp) < (class="type">int)temp.Size()) class="kw">return false; } descr.batch = class="num">1e4; descr.activation = None; descr.optimization = ADAM; if(!encoder.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 = defNeuronConvOCL; descr.count = prev_count * BarDescr; descr.window = prev_wout; descr.step = prev_wout; prev_wout = descr.window_out = EmbeddingSize / class="num">2; descr.activation = SoftPlus; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">8 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConvOCL; descr.count = prev_count * BarDescr; descr.window = prev_wout; descr.step = prev_wout; prev_wout = descr.window_out = HistoryBars / Segments; descr.activation = TANH; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; }
编码器尾部与策略网络的层定义
编码器在第九、第十层收口:第九层用转置神经元把 BarDescr 个特征按 HistoryBars 窗口铺开,激活函数设为 None,不做非线性扭曲;第十层接反归一化层,节点数直接拉到 HistoryBars * BarDescr,把编码空间还原回原始量纲。这两层都不参与梯度激活,纯粹做张量形变。 Latent 指针从 encoder.At(LatentLayer) 取出后,actor 网络清空重搭。输入层取账户描述维度 AccountDescr,优化器指定 ADAM,但激活仍为 None——原始账户状态不急着压缩。 紧接着的批归一化层把 prev_count 个节点做归一,batch 设成 1e4,意味着训练时按一万条样本估算均值方差,能缓解账户特征量纲差异导致的梯度偏移。 拼接层把 LatentCount 个隐变量按 step = latent.count * latent.window * latent.layers 的跨度缝进网络,激活用 SoftPlus 保非负;后面三层 BaseOCL 全用 TANH 把输出压到 [-1,1],batch 统一 1e4。外汇与贵金属杠杆高,这类网络权重若过拟合历史样本,实盘信号失效概率会明显上升,建议先在 MT5 策略测试器用自有数据跑前向验证。
} class=class="str">"cmt">//--- layer class="num">9 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronTransposeOCL; descr.count = BarDescr; descr.window = HistoryBars; descr.activation = None; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">10 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronRevInDenormOCL; descr.count = HistoryBars * BarDescr; descr.layers = class="num">1; descr.activation = None; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- Latent CLayerDescription *latent = encoder.At(LatentLayer); if(!latent) 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; 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 = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = class="num">1e4; 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">2 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConcatenate; descr.count = LatentCount; descr.window = prev_count; descr.step = latent.count * latent.window * latent.layers; descr.batch = class="num">1e4; descr.activation = SoftPlus; 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 = 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 false; } class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = TANH; descr.batch = class="num">1e4; 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()))
◍ 策略网络的概率头堆叠方式
这段构建逻辑把 actor 与 probability 两个网络头分开挂接,actor 先以 defNeuronBaseOCL 类型、SIGMOID 激活、batch=1e4、ADAM 优化器加入 NActions 个输出节点,失败即释放描述符并返回 false。 probability 网络从输入层开始:节点数 = latent.count * latent.window * latent.layers,激活函数沿用 latent.activation,同样走 ADAM。往后三层隐藏层均设 LatentCount 个节点、TANH 激活、batch=1e4,属于对称编码结构。 第四层把节点数压到 NActions/3,换用 SoftPlus 激活,输出恒非负,适合表达动作概率的偏置。末层用 defNeuronSoftMaxOCL 做归一化,step=1、激活 None,把前层输出收成总和为 1 的概率分布。 在 MT5 里把 LatentCount 与 NActions 调小(例如 32 与 9),能明显压低显存占用;外汇与贵金属品种波动剧烈,这类网络推理仅作概率参考,实盘仍属高风险。
class="kw">return 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 false; } probability.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = latent.count * latent.window * latent.layers; descr.activation = latent.activation; descr.optimization = ADAM; if(!probability.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; 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 false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return 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 false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = NActions / class="num">3; descr.activation = SoftPlus; descr.batch = class="num">1e4; descr.optimization = ADAM; if(!probability.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 = defNeuronSoftMaxOCL; prev_count = descr.count = prev_count; descr.step = class="num">1; descr.activation = None; descr.batch = class="num">1e4; descr.optimization = ADAM; if(!probability.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; }
「三模型并行训练的内在逻辑」
训练算法放在 EA 的 Train 方法里,因为要同时跑环境编码器、执行者和方向判别三个模型,逻辑上得改造成并行结构。外层按批次迭代,每批从经验回放缓冲区随机抽一条轨迹,再选个起始点;内层沿连续状态推进,这种做法跟常规随机状态训练不同,轨迹动作由 EA 基于后续状态信息直接生成,指导更精确。 重播缓冲区的好处是除最后一条外都有后续状态数据,但代价是丢了未平仓位信息。框架里模型无循环结构,为了让模型既会开仓又会找最优平仓点,训练时须拼出小批次和「最优」仓位一起喂。 代码里先用 probability 向量做均匀抽样权重,大小是 Buffer.Size(),均值 1/Size;外层循环步长 Batch,用 MathRand 平方归一化挑起始点,避开头尾 NForecast+Batch 个位置。时间特征把账户时间戳相对 2023 全年秒数做正弦编码,再除月线秒数加另一周期项,这是给编码器塞周期律。 训练顺序固定:编码器前馈→执行者逼近最优操作→第三模型按下一柱颜色判方向,每轮记进度。全部迭代完吐日志并自关 EA。外汇与贵金属训练涉及杠杆与滑点,回测轨迹再理想也只代表历史概率,实盘可能显著偏离。
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 = 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() || !Actor.Clear()) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; class="kw">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; class="kw">break; } class=class="str">"cmt">//--- 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 缓冲:用 time 除以 PERIOD_W1、PERIOD_D1 的秒数得到 x,再取 2πx 的 sin 值入列,x 为 0 时直接落 0,避免除零噪声。紧接着把上一根的状态余额 PrevBalance、权益 PrevEquity 取出,用当前 result 与 _Point 算出一跳 profit,再向 bAccount 压入 9 个特征——含权益比、盈利占比、最大浮盈浮亏等,最后挂上 bTime 指针一并 BufferWrite。 前向传播分三步:Encoder 吃 bState 做编码,Actor 以 bAccount 加 Encoder 隐层输出做决策,Probability 再基于 Encoder 隐层算概率分布。任一步 feedForward 返回 false 就打印函数名加行号、置 Stop 并 break,说明这套自编码+策略网络在实盘回放里对中间层断层是零容忍的。 你在 MT5 里接这段时,重点核对 bAccount 第 3、4 维(最大多头/空头回撤幅度)是否为非负——原代码用 MathMax(...,0) 兜底,若你的行情源 result[0]-result[3] 符号反了,这两个特征会恒为 0,Actor 学到的风险偏好就会偏向裸奔。外汇与贵金属杠杆高,这类隐层特征错位可能让概率输出失真,上真实账户前务必用历史 tick 复跑一遍前向链路。
bTime.Add((class="type">class="kw">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); 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, false, (CBufferFloat*)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; class="kw">break; } if(!Actor.feedForward((CBufferFloat*)GetPointer(bAccount), class="num">1, false, GetPointer(Encoder), LatentLayer)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; class="kw">break; } if(!Probability.feedForward(GetPointer(Encoder), LatentLayer, (CBufferFloat*)NULL))