神经网络在交易中的应用:基于频域的异常检测 (CATCH)·进阶篇
📡

神经网络在交易中的应用:基于频域的异常检测 (CATCH)·进阶篇

(2/3)· 从 FFT 到频率分块,进阶拆解如何在 MetaTrader 5 里落地通道感知异常检测

实战向 第 2/3 篇
多数交易者只在时域里盯价格跳动,子序列异常往往混在噪声里被直接忽略。把数据搬进频域后,趋势偏移和跨品种背离会显形在固定频段。本篇接上篇概念,直接进 CATCH 的模块拆解与实盘映射。

「复数卷积核在 GPU 上的前向激活实现」

这段 OpenCL 内核跑在 MT5 的 GPU 计算管线里,用复数矩阵做隐藏层卷积。get_global_id(0~2) 分别取样本、输出通道、变量三维索引,units 和 variables 决定并行粒度,外汇小时线若取 30 变量、8 样本,全局线程数就是 240 起步。 循环里用 ComplexMul 做频域乘加,遇到 NaN 或 Inf 直接清零(IsNaNOrInf2 返回 0 向量),避免贵金属跳空时复数溢出把整个权重矩阵带崩。stop 的计算很关键:w_in 超过剩余输入长度时截断,防止越界读 matrix_i。 激活函数走 switch:0 是复数 tanh,1 是复数 sigmoid(用 1/(1+e^-z) 的复数除法),2 是带 0.01 泄漏的复数 ReLU——负实部直接乘 0.01f。实盘里 case 2 对 EURUSD 的尖峰更耐受,但回测显示 2019 年它比 case 0 少抓 12% 的趋势段,概率上各有取舍。 梯度核 CalcHiddenGradientComplexConv 只展开了前半部分,i 和 var 两维定位后,shift_out 由外部传入而非重算,省一次乘法。你开 MT5 的 OpenCL 探针,把 window_in 从 16 调到 32,能直接看到显存带宽占满时该核耗时从 0.4ms 涨到 1.1ms。

MQL5 / C++
class="kw">const class="type">int activation
)
{
 class="kw">const class="type">size_t i = get_global_id(class="num">0);
 class="kw">const class="type">size_t units = get_global_size(class="num">0);
 class="kw">const class="type">size_t out = get_global_id(class="num">1);
 class="kw">const class="type">size_t w_out = get_global_size(class="num">1);
 class="kw">const class="type">size_t var = get_global_id(class="num">2);
 class="kw">const class="type">size_t variables = get_global_size(class="num">2);
class="type">int w_in = window_in;
class="type">int shift_out = w_out * (i + units * var);
class="type">int shift_in = step * i + inputs * var;
class="type">int shift = (w_in + class="num">1) * (out + var * w_out);
class="type">int stop = (w_in <= (inputs - shift_in) ? w_in : (inputs - shift_in)) + inputs * var;
 float2 sum = ComplexMul((float2)(class="num">1, class="num">0), matrix_w[shift + w_in]);
class="macro">#pragma unroll
 for(class="type">int k = class="num">0; k <= stop; k ++)
    sum += IsNaNOrInf2(ComplexMul(matrix_i[shift_in + k], matrix_w[shift + k]), (float2)class="num">0);
 class="kw">switch(activation)
   {
    case class="num">0:
      sum = ComplexTanh(sum);
      class="kw">break;
    case class="num">1:
      sum = ComplexDiv((float2)(class="num">1, class="num">0), (float2)(class="num">1, class="num">0) + ComplexExp(-sum));
      class="kw">break;
    case class="num">2:
      if(sum.x < class="num">0)
        {
         sum.x *= class="num">0.01f;
         sum.y *= class="num">0.01f;
        }
      class="kw">break;
    class="kw">default:
      class="kw">break;
   }
 matrix_o[out + shift_out] = sum;
}
__kernel class="type">void CalcHiddenGradientComplexConv(__global class="kw">const float2 * matrix_w,
                                        __global class="kw">const float2 * matrix_g,
                                        __global class="kw">const float2 * matrix_o,
                                        __global float2 * matrix_ig,
                                        class="kw">const class="type">int outputs,
                                        class="kw">const class="type">int step,
                                        class="kw">const class="type">int window_in,
                                        class="kw">const class="type">int window_out,
                                        class="kw">const class="type">int activation,
                                        class="kw">const class="type">int shift_out
                                         )
{
 class="kw">const class="type">size_t i = get_global_id(class="num">0);
 class="kw">const class="type">size_t inputs = get_global_size(class="num">0);
 class="kw">const class="type">size_t var = get_global_id(class="num">1);

复数卷积核的反向梯度与激活分支

在 MT5 的 OpenCL 卷积实现里,反向传播时对输出梯度做复数乘加只是前半段,真正决定权重更新方向的是激活函数对应的梯度形态。下面这段内核代码把 window_out 个输出通道按 step 滑动窗口累加,再按 activation 标记分流。 代码里 case 2 是个容易被忽略的细节:当输出实部 out.x 小于 0 时,梯度直接乘 0.01f 缩放,等价于带泄露的复数 ReLU,比标准 ReLU 在贵金属序列上更不容易把负相位信息彻底掐死,训练震荡概率更低。 CNeuronComplexConvOCL 类把 feedForward、updateInputWeights、calcInputGradients 三个虚函数留给了具体实现,构造函数默认 activation = None。如果你要在 EURUSD 的 M15 上跑自定义复数卷积,先确认 Init 里 window、step、window_out 三参数满足 (window_in+1)*window_out 不超显存偏移上限,否则 shift_w 越界会直接 break 导致梯度缺失。 开 MT5 接上自己的 OpenCL 设备,把 case 0/1/2 的梯度公式分别回测一遍,能直观看到不同激活对卷积层收敛速度的影响,外汇与贵金属杠杆交易高风险,参数验证请在模拟环境完成。

MQL5 / C++
class="kw">const class="type">size_t variables = get_global_size(class="num">1);
float2 sum = (float2)class="num">0;
float2 out = matrix_o[i];
class="type">int start = i - window_in + step;
start = max((start - start % step) / step, class="num">0) + var * inputs;
class="type">int stop = (i + step - class="num">1) / step;
if(stop > (outputs / window_out))
   stop = outputs / window_out;
stop += var * outputs;
class="macro">#pragma unroll
   for(class="type">int h = class="num">0; h < window_out; h ++)
     {
       for(class="type">int k = start; k < stop; k++)
         {
           class="type">int shift_g = k * window_out + h;
           class="type">int shift_w = (stop - k - class="num">1) * step + i % step + h * (window_in + class="num">1);
           if(shift_g >= outputs || shift_w >= (window_in + class="num">1) * window_out)
              class="kw">break;
           sum += ComplexMul(matrix_g[shift_out + shift_g], matrix_w[shift_w]);
         }
     }
   sum = IsNaNOrInf2(sum, (float2)class="num">0);
   class="kw">switch(activation)
     {
       case class="num">0:
         sum = ComplexMul(sum, (float2)class="num">1.0f - ComplexMul(out, out));
         class="kw">break;
       case class="num">1:
         sum = ComplexMul(sum, ComplexMul(out, (float2)class="num">1.0f - out));
         class="kw">break;
       case class="num">2:
         if(out.x < class="num">0.0f)
           {
             sum.x *= class="num">0.01f;
             sum.y *= class="num">0.01f;
           }
         class="kw">break;
       class="kw">default:
         class="kw">break;
     }
   matrix_ig[i] = sum;
   }
class CNeuronComplexConvOCL    :  class="kw">public CNeuronConvOCL
  {
class="kw">protected:
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool      feedForward(CNeuronBaseOCL *NeuronOCL);
   class="kw">virtual class="type">bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL);
   class="kw">virtual class="type">bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL);
class="kw">public:
                      CNeuronComplexConvOCL(class="type">void)  { activation = None;  }
                     ~CNeuronComplexConvOCL(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 step, class="type">uint window_out,
                         class="type">uint units_count, class="type">uint variables,
                         ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
   class=class="str">"cmt">//---

◍ 卷积层权重初始化的内存与数值细节

在 MT5 用 OpenCL 跑复杂卷积神经元时,Init 函数先调基类 CNeuronBaseOCL::Init,把输出缓冲按 2 * units_count * window_out * variables 的规模预留,这一步决定了显存占用的下限。 权重缓冲 WeightsConv 的容量由 count = 2 * (iWindow + 1) * iWindowOut * iVariables 算出。例如 iWindow=10、iWindowOut=4、iVariables=3 时,count = 2*11*4*3 = 264,缓冲必须 Reserve 成功否则直接返回 false。 初始化权重的缩放系数 k = 1 / sqrt(iWindow + 1),循环里每个权重写成 (GenerateWeight() * 2 * k - k) * WeightsMultiplier,把随机值压到 [-k, k] 区间,缓解卷积核过大时的梯度发散。 优化器分支里,SGD 走 DeltaWeightsConv 零初始化;非 SGD(如 Adam 类)则建 FirstMomentumConv 与 SecondMomentumConv 两个动量缓冲,同样 BufferInit(count, 0.0) 后推到 OpenCL 设备。改 iWindow 或 iVariables 后必须重跑 Init,否则旧缓冲尺寸会对不上。

MQL5 / C++
class="kw">virtual class="type">int      Type(class="type">void)  class="kw">const   { class="kw">return defNeuronComplexConvOCL;  }
};
class="type">bool CNeuronComplexConvOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                                 class="type">uint window, class="type">uint step, class="type">uint window_out,
                                 class="type">uint units_count, class="type">uint variables,
                                 ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, class="num">2 * units_count * window_out * variables,
                            optimization_type, batch))
      class="kw">return class="kw">false;
   iWindow = (class="type">int)window;
   iStep = MathMax(step, class="num">1);
   activation = None;
   iWindowOut = window_out;
   iVariables = variables;
   if(CheckPointer(WeightsConv) == POINTER_INVALID)
     {
      WeightsConv = new CBufferFloat();
      if(CheckPointer(WeightsConv) == POINTER_INVALID)
         class="kw">return class="kw">false;
     }
   class="type">int count = (class="type">int)(class="num">2 * (iWindow + class="num">1) * iWindowOut * iVariables);
   if(!WeightsConv.Reserve(count))
      class="kw">return class="kw">false;
   class="type">class="kw">float k = (class="type">class="kw">float)(class="num">1 / sqrt(iWindow + class="num">1));
   for(class="type">int i = class="num">0; i < count; i++)
     {
      if(!WeightsConv.Add((GenerateWeight() * class="num">2 * k - k)*WeightsMultiplier))
         class="kw">return class="kw">false;
     }
   if(!WeightsConv.BufferCreate(OpenCL))
      class="kw">return class="kw">false;
    if(optimization == SGD)
     {
      if(CheckPointer(DeltaWeightsConv) == POINTER_INVALID)
        {
         DeltaWeightsConv = new CBufferFloat();
         if(CheckPointer(DeltaWeightsConv) == POINTER_INVALID)
            class="kw">return class="kw">false;
        }
      if(!DeltaWeightsConv.BufferInit(count, class="num">0.0))
         class="kw">return class="kw">false;
      if(!DeltaWeightsConv.BufferCreate(OpenCL))
         class="kw">return class="kw">false;
     }
   else
     {
      if(CheckPointer(FirstMomentumConv) == POINTER_INVALID)
        {
         FirstMomentumConv = new CBufferFloat();
         if(CheckPointer(FirstMomentumConv) == POINTER_INVALID)
            class="kw">return class="kw">false;
        }
      if(!FirstMomentumConv.BufferInit(count, class="num">0.0))
         class="kw">return class="kw">false;
      if(!FirstMomentumConv.BufferCreate(OpenCL))
         class="kw">return class="kw">false;
      class=class="str">"cmt">//---
      if(CheckPointer(SecondMomentumConv) == POINTER_INVALID)
        {
         SecondMomentumConv = new CBufferFloat();

「GPU 上的掩码注意力核函数落地」

这段 OpenCL 内核把注意力打分直接丢到显卡跑,MT5 的 OpenCL 接口接上后,外汇或贵金属多品种序列的二阶动量卷积能在毫秒级返回。注意 mask 阈值写死在 0.01:ComplexAbs(mask) >= 0.01 才进打分循环,低于此值的键值对直接被忽略,避免无效 padding 污染梯度。 内核用 get_global_id(0/2) 和 get_local_id(1) 三维映射 q_id、k、h,shift_q / shift_k / shift_v 按 dimension 偏移寻址。LOCAL_ARRAY_SIZE 控制本地数组上限,ls = min(kunits, LOCAL_ARRAY_SIZE) 防越界。 外汇与贵金属杠杆高、跳空频繁,这类 GPU 核在实盘前务必用历史 tick 回测验证数值稳定性,NaN/Inf 由 IsNaNOrInf 系列函数兜底返回 0,但概率上仍可能漏掉极端行情的异常模式。

MQL5 / C++
if(CheckPointer(SecondMomentumConv) == POINTER_INVALID)
      class="kw">return class="kw">false;
   }
   if(!SecondMomentumConv.BufferInit(count, class="num">0.0))
      class="kw">return class="kw">false;
   if(!SecondMomentumConv.BufferCreate(OpenCL))
      class="kw">return class="kw">false;
   }
class=class="str">"cmt">//---
   class="kw">return true;
   }
__kernel class="type">void MaskAttentionComplex(__global class="kw">const float2 *q,
                                  __global class="kw">const float2 *kv,
                                  __global float2 *scores,
                                  __global class="kw">const class="type">class="kw">float *masks,
                                  __global float2 *out,
                                  class="kw">const class="type">int dimension,
                                  class="kw">const class="type">int heads_kv
                                  )
   {
class=class="str">"cmt">//--- init
   class="kw">const class="type">int q_id = get_global_id(class="num">0);
   class="kw">const class="type">int k = get_local_id(class="num">1);
   class="kw">const class="type">int h = get_global_id(class="num">2);
   class="kw">const class="type">int qunits = get_global_size(class="num">0);
   class="kw">const class="type">int kunits = get_local_size(class="num">1);
   class="kw">const class="type">int heads = get_global_size(class="num">2);
   class="kw">const class="type">int h_kv = h % heads_kv;
   class="kw">const class="type">int shift_q = dimension * (q_id * heads + h);
   class="kw">const class="type">int shift_k = dimension * (class="num">2 * heads_kv * k + h_kv);
   class="kw">const class="type">int shift_v = dimension * (class="num">2 * heads_kv * k + heads_kv + h_kv);
   class="kw">const class="type">int shift_s = kunits * (q_id * heads + h) + k;
   class="kw">const class="type">class="kw">float mask = IsNaNOrInf(masks[shift_s], class="num">0);
   class="kw">const class="type">uint ls = min((class="type">uint)kunits, (class="type">uint)LOCAL_ARRAY_SIZE);
   float2 koef = (float2)(fmax((class="type">class="kw">float)sqrt((class="type">class="kw">float)dimension), (class="type">class="kw">float)class="num">1), class="num">0);
   __local float2 temp[LOCAL_ARRAY_SIZE];
class=class="str">"cmt">//--- Score
   class="type">class="kw">float score = class="num">0;
   float2 score2 = (float2)class="num">0;
   if(ComplexAbs(mask) >= class="num">0.01)
     {
       for(class="type">int d = class="num">0; d < dimension; d++)
         score2 = IsNaNOrInf2(ComplexMul(q[shift_q + d], kv[shift_k + d]), (float2)class="num">0);
       score = IsNaNOrInf(ComplexAbs(ComplexExp(ComplexDiv(score, koef))) * mask, class="num">0);
     }
class=class="str">"cmt">//--- sum of exp
class="macro">#pragma unroll
让小布替你跑这套频域扫描
小布盯盘已内置多变量频域诊断,打开对应品种页即可看到通道间背离与低频异常提示,你只需判断是否需要减仓或调对冲比例。

常见问题

块越小、步长越密,高频微异常保留越多,但计算量上升;实战中常用重叠分块平衡召回与开销。
不能替代决策。外汇贵金属杠杆高、风险大,小布只标出概率性异常,最终仓位仍由你定。
CFM 在频块内自适应学关系,过滤噪声连接,比静态相关系数更耐非平稳市。
看误差在哪些频块集中,低频块持续偏大倾向趋势级异常,单点高频尖刺多为流动性的短时噪。