神经网络在交易中的应用:市场异常的自适应检测(终篇)(基础篇)
「用神经网络盯住市场异常」
传统指标大多假设价格序列平稳,遇到跳空、流动性骤降或突发事件驱动的非典型波动,往往滞后甚至失效。把神经网络接进 MT5,核心目的不是预测点位,而是让模型自己从行情里学出“正常”长什么样,再标记偏离。 这篇文章是系列第一篇,先铺底:后面会围绕自适应异常检测展开,用 MQL5 把推理逻辑跑在实时行情上。外汇与贵金属杠杆高、滑点随机,任何异常信号都只是概率提示,不能直接当入场指令。 一个可落地的起点是,先用历史 Tick 或 M1 数据训练一个自编码器,重构误差超过阈值即判为异常。MT5 的 OnTick 里调用模型输出,比人工扫盘快几个数量级,但训练集必须覆盖至少一次真实流动性危机,否则模型会把异常当常态。
DADA 的三块拼图怎么拼
DADA 框架把异常检测拆成三块能各自调参的组件,不是端到端黑箱。自适应瓶颈模块会按实时行情动态改压缩率,和传统模型写死压缩参数不同,它尽量保住市场数据里最管用的特征,把干扰信息砍掉。 对抗解码器是一对:一个重建正常市况、摸清典型波动轮廓,另一个专门盯异常片段,两边对着练,误报比单解码器设置明显更少,模型也更扛造。 分块加随机掩码是处理时间序列的关键。数据被切成小段后,模型抓局部模式更利索;随机遮掉一部分逼模型自己补出来,潜在依赖关系就学到了,顺带防过拟合。这三样叠起来,异常识别准头上去,面对突发行情也不容易崩。 最实用的点是自适应:行情一变,传统算法往往得重训,DADA 自己改参数。高频场景里决策窗口就毫秒级,这种免重训的韧性直接决定能不能用。外汇和贵金属波动剧烈、杠杆风险高,拿这类模型做辅助筛选也得随时人工复核。 上篇我们落了个 CNeuronMultiWindowsConvOCL 多窗口卷积层,它虽不在 DADA 原版描述里,但在我们实现中顶替了自适应瓶颈的动态压缩职能。
◍ 用自编码器堆出自适应瓶颈
自适应瓶颈模块的核心,是把一组潜在空间压缩级别不同的自编码器当作并行小模型来用。它继承自混合专家(MoE)对象,复用其动态门控选前 k 个专家的机制,但内部不再塞通用专家,而是换成一串为时序异常检测定制的自编码器。 实际写代码时只重写了 Init 方法。父类 CNeuronMoE 里已有 cGates(选前 k 专家)和 cExperts(专家指针数组),我们跳过直接父类的初始化以免误建专家池,转而手动初始化继承组件,并按 bottlenecks 数组大小决定专家总数。 Init 里先调基础全连接层初始化,再初始化门控模块,随后用循环累加所有自编码器潜在状态的总大小。关键一步是首层卷积:卷积核数量被设为所有自编码器潜在维度之和,每个核独立把相同输入压成单值,相当于一堆微型模型并行压缩。后续多窗口卷积按各自窗口重构到目标大小,再接转置层与按自编码器数量分权重的卷积层,恢复数据布局即完成。 这套结构让每个自编码器专注子任务、动态入选,对复杂时间序列的噪声抑制和异常模式检出倾向更稳。外汇与贵金属行情高波动、高风险,这类模型只作概率性辅助,实盘前务必在 MT5 用历史数据回测验证。
class CNeuronAdaBN : class="kw">public CNeuronMoE { class="kw">public: CNeuronAdaBN(class="type">void) {}; ~CNeuronAdaBN(class="type">void) {}; class="kw">virtual class="type">bool Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_out, class="type">uint units_count, class="type">uint &bottlenecks[], class="type">uint top_k, class="type">uint variables, ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) class="kw">override const { class="kw">return defNeuronAdaBN; } }; class CNeuronMoE : class="kw">public CNeuronBaseOCL { class="kw">protected: CNeuronTopKGates cGates; CLayer cExperts; class=class="str">"cmt">//--- .......... .......... .......... }; class="type">bool CNeuronAdaBN::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_out, class="type">uint units_count, class="type">uint &bottlenecks[], class="type">uint top_k, class="type">uint variables, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count * variables, optimization_type, batch)) class="kw">return false; class="type">int index = class="num">0; if(!cGates.Init(class="num">0, index, OpenCL, window, units_count * variables, bottlenecks.Size(), top_k, optimization, iBatch))
「搭建多窗口卷积与转置重构层」
这段初始化逻辑在 MT5 的 OpenCL 神经网络类里很典型:先清空专家容器并绑定计算设备,再依次压入卷积、多窗口卷积和转置重建层。每一层都靠 index 自增来占位,任一 Init 失败就删对象并回 false,保证栈不漏内存。
瓶颈层尺寸 bn_size 由 bottlenecks 数组累加得到,循环里 bn_size += bottlenecks[i] 把各分支通道数求和,作为首层卷积的输出通道。首层 CNeuronConvOCL 用 window×window 核映射到 bn_size,激活选 SoftPlus;随后 CNeuronMultiWindowsConvOCL 用 bottlenecks 做多尺度窗口,输出宽 window_out。
转置层 CNeuronTransposeRCDOCL 第一次把 units_count*variables 拉回 bottlenecks.Size() 通道、宽 window_out,激活直接复用前面 conv 的 Activation()。第二轮卷积把 window_out 收回到 window,激活设 None,再做一次转置还原到 window 宽。
在 MT5 里跑这套,先确认 OpenCL 设备可用,否则 Init 全返 false。外汇与贵金属行情下用此类网络做特征提取属高风险实验,过拟合概率偏高,建议先用 EURUSD 的 M1 历史做离线校验。
class="kw">return false; cExperts.Clear(); cExperts.SetOpenCL(OpenCL); CNeuronConvOCL *conv = NULL; CNeuronMultiWindowsConvOCL *mwconv = NULL; CNeuronTransposeRCDOCL *transp = NULL; class="type">uint bn_size = class="num">0; for(class="type">uint i = class="num">0; i < bottlenecks.Size(); i++) bn_size += bottlenecks[i]; index++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, index, OpenCL, window, window, bn_size, units_count, variables, optimization, iBatch) || !cExperts.Add(conv)) { class="kw">delete conv; class="kw">return false; } conv.SetActivationFunction(SoftPlus); index++; mwconv = new CNeuronMultiWindowsConvOCL(); if(!mwconv || !mwconv.Init(class="num">0, index, OpenCL, bottlenecks, window_out, units_count, variables, optimization, iBatch) || !cExperts.Add(mwconv)) { class="kw">delete conv; class="kw">return false; } mwconv.SetActivationFunction(SoftPlus); transp = new CNeuronTransposeRCDOCL(); index++; if(!transp || !transp.Init(class="num">0, index, OpenCL, units_count * variables, bottlenecks.Size(), window_out, optimization, iBatch) || !cExperts.Add(transp)) { class="kw">delete transp; class="kw">return false; } transp.SetActivationFunction((ENUM_ACTIVATION)conv.Activation()); index++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, index, OpenCL, window_out, window_out, window, units_count * variables, bottlenecks.Size(), optimization, iBatch) || !cExperts.Add(conv)) { class="kw">delete conv; class="kw">return false; } conv.SetActivationFunction(None); transp = new CNeuronTransposeRCDOCL(); index++; if(!transp || !transp.Init(class="num">0, index, OpenCL, bottlenecks.Size(), units_count * variables, window, optimization, iBatch) || !cExperts.Add(transp)) { class="kw">delete transp; class="kw">return false; } transp.SetActivationFunction((ENUM_ACTIVATION)conv.Activation()); class=class="str">"cmt">//--- class="kw">return true; }
三模型线性装配而非整体封装
这一版没有把整套分析框架塞进一个黑盒对象,而是复用库里已验证的组件,把先前开发的『自适应瓶颈』拼进一条可训练的线性链路。好处是模块能单独换或调,不必重写整个代码库,编码器和解码器的结构限制也消失了,做任务特化实验更自由。 实际跑的是三个并行训练模型:环境状态编码器、Actor、价格概率预测器。编码器走经典自编码器路线,从潜在空间以最小损失重建输入,顺带挖出隐藏依赖;Actor 接管原框架里的异常解码器位置,负责识别趋势、逮反转点、下单决策;第三个模型只做未来方向概率预测,给 Actor 的信号做二次过滤。 编码器输入是终端原始多模态数据,变量分布不一,所以先接全连接层再上批量归一化把量纲拉平。随后按框架做分块掩码:20% 随机掩码直接用 Dropout 层(probability=0.2),数据先转置以适配序列处理。两个并行卷积层对非重叠片段编码,自适应瓶颈里建了 15 个潜在维为 8 倍数的小自编码器,每片段挑 3 个最合适的编码,解码器用两个连续卷积层 + tanh 激活还原。 用 tanh 不是随手选的。批归一化后数据零中心、方差近 1,按三西格玛法则约 68% 值落 [-1,1],正好卡在 tanh 值域里,异常值被自然抑住。Actor 输入账户状态向量,过全连接后与编码器潜在表示拼接,经三层全连接出动作;概率模型最简,只吃压缩环境表示,三层全连接后接 SoftMax 转概率。 所有架构在 CreateDescriptions 里用三个动态缓冲区指针定义,开头校验指针为空就 new 实例,任一分配失败直接返回 false。下面这段是编码器前几层的真实描述代码,注意批归一化 batch 设了 1e4、Dropout 概率 0.2 这两个硬参数,开 MT5 把 HistoryBars*BarDescr 换成你的样本维度就能复跑。
class="type">bool CreateDescriptions(CArrayObj *&encoder, CArrayObj *&actor, CArrayObj *&probability) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!encoder) { encoder = new CArrayObj(); if(!encoder) class="kw">return false; } if(!actor) { actor = new CArrayObj(); if(!actor) class="kw">return false; } if(!probability) { probability = new CArrayObj(); if(!probability) class="kw">return false; } class=class="str">"cmt">//--- Encoder encoder.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 = (HistoryBars * BarDescr); 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">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(!encoder.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 = defNeuronDropoutOCL; descr.count = prev_count; descr.probability = class="num">0.2f; descr.activation = None; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3