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

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

(3/3)· 当价格异动藏在频谱细节里,传统阈值告警往往慢半拍,频域分块能把隐蔽风险提前暴露

进阶 第 3/3 篇
多数交易者只看时域里的尖峰和缺口,却忽略了资产间相关性在低频段的缓慢漂移。把行情做傅里叶变换后,那些‘看起来平静’的异常反而更清晰。频域分块不是炫技,是给多变量序列换一种肉眼可见的尺度。

OpenCL 核函数里的注意力归约与梯度回传

这段 OpenCL 内核展示了在本地内存做分段归约、再把注意力分数写回 scores 缓冲区的典型手法。外层 for 按 kunits 步长 ls 切块,每个工作项只在自己所属区间累加 score 到 temp[k % ls].x,随后用 barrier(CLK_LOCAL_MEM_FENCE) 保证本地内存可见性。 归约阶段用 do-while 把 count 从 ls 每次 (count+1)/2 折半,k<ls 的工作项把 temp[k+count].x 加进来并将后者清零,循环到 count>1 不成立为止。实测在 ls=64、kunits=256 时,该折半只需 6 次迭代即可完成全和。 temp[0].x 若大于 0,就把 score 归一化为 score/temp[0].x 并存入 scores[shift_s];否则保持原值。下游输出循环对 dimension 个头逐一做 ComplexMul,同样走一遍本地归约,最终 k==0 的工作项把 temp[0] 写进 out[shift_q+d]。 紧接的 MaskAttentionGradientsComplex 内核声明了 q/q_g、kv/kv_g、scores、mask/mask_g 与 gradient 等全局指针,说明前向的分数与掩码会在反向阶段复用,梯度张量直接以 float2 复数形态传入。外汇与贵金属行情下用此类 GPU 核做序列注意力,显存带宽和 barrier 次数会显著影响回测耗时,属高风险算力投入。

MQL5 / C++
  for(class="type">int i = class="num">0; i < kunits; i += ls)
    {
      if(k >= i && k < (i + ls))
        temp[k % ls].x = (i == class="num">0 ? class="num">0 : temp[k % ls].x) + score;
      barrier(CLK_LOCAL_MEM_FENCE);
    }
  class="type">uint count = ls;
class="macro">#pragma unroll
  do
    {
      count = (count + class="num">1) / class="num">2;
      if(k < ls)
        temp[k].x += (k < count && (k + count) < kunits ? temp[k + count].x : class="num">0);
      if(k + count < ls)
        temp[k + count].x = class="num">0;
      barrier(CLK_LOCAL_MEM_FENCE);
    }
  class="kw">while(count > class="num">1);
class=class="str">"cmt">//--- score
  if(temp[class="num">0].x > class="num">0)
    score = score / temp[class="num">0].x;
  scores[shift_s] = score;
class=class="str">"cmt">//--- out
class="macro">#pragma unroll
  for(class="type">int d = class="num">0; d < dimension; d++)
    {
      float2 val = (score > class="num">0 ? ComplexMul(kv[shift_v + d], (float2)(score,class="num">0)) : (float2)class="num">0);
class="macro">#pragma unroll
      for(class="type">int i = class="num">0; i < kunits; i += ls)
        {
         if(k >= i && k < (i + ls))
           temp[k % ls] = (i == class="num">0 ? (float2)class="num">0 : temp[k % ls]) + val;
         barrier(CLK_LOCAL_MEM_FENCE);
        }
      class="type">uint count = ls;
class="macro">#pragma unroll
      do
        {
         count = (count + class="num">1) / class="num">2;
         if(k < ls)
           temp[k] += (k < count && (k + count) < kunits ? temp[k + count] : (float2)class="num">0);
         if((k + count) < ls)
           temp[k + count] = (float2)class="num">0;
         barrier(CLK_LOCAL_MEM_FENCE);
        }
      class="kw">while(count > class="num">1);
      class=class="str">"cmt">//---
      if(k == class="num">0)
        out[shift_q + d] = temp[class="num">0];
      barrier(CLK_LOCAL_MEM_FENCE);
    }
  }
__kernel class="type">void MaskAttentionGradientsComplex(__global class="kw">const float2 *q, __global float2 *q_g,
                                            __global class="kw">const float2 *kv, __global float2 *kv_g,
                                            __global class="kw">const class="type">class="kw">float *scores,
                                            __global class="kw">const class="type">class="kw">float *mask, __global class="type">class="kw">float *mask_g,
                                            __global class="kw">const float2 *gradient,

「反向传播里 Value 与 Query 梯度的并行拆解」

在 MT5 的 OpenCL 内核里做注意力机制反向传播,最容易被忽略的是多头之间 KV 头复用带来的索引错位。上面这段内核以 get_global_id(0/1/2) 分别取 query 序号、维度、头号,heads_kv 小于 heads 时,必须用 h % heads_kv 把当前头映射到真实的 KV 头,否则梯度会写错通道。 Value 梯度计算只在 h < heads_kv 的线程里跑,循环步长用 qunits 做并行分片:每个线程负责一段 v,内层再按 heads_kv 步长累加各 query 头 hq 的贡献。这里 scores 数组用 shift_score + g * step_score 寻址,step_score = kunits * heads,若 scores 出现 NaN/Inf 会被 IsNaNOrInf 夹成 0,避免污染 kv_g。 Query 梯度则走另一条路:先取输出梯度 out_g,若模长为 0 直接跳过整个 k 循环省算力;否则对 kunits 个 key 做 scores 的共轭乘加。shift_val 与 shift_key 分别偏移到 (heads_kv + h_kv) 与 h_kv 段,说明 Key/Value 在 kv_g 里是连续排布、各占 heads_kv * dimension。 把这段直接丢进 MT5 的 OpenCL 调试内核,把 heads 设成 8、heads_kv 设成 4,能直观看到只有一半线程在写 Value 梯度——外汇与贵金属行情序列短、维度低,这种复用能压掉约 30% 冗余计算,但 GPU 占用率波动大,属于高风险低延迟实验。

MQL5 / C++
class="kw">const class="type">int kunits, 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 d = get_global_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 dimension = get_global_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) + d;
   class="kw">const class="type">int shift_s = (q_id * heads + h) * kunits;
   class="kw">const class="type">int shift_g = h * dimension + d;
   float2 koef = (float2)(fmax(sqrt((class="type">class="kw">float)dimension), (class="type">class="kw">float)class="num">1), class="num">0);
class=class="str">"cmt">//--- Calculating Value&class="macro">#x27;s gradients
   class="type">int step_score = kunits * heads;
   if(h < heads_kv)
    {
class="macro">#pragma unroll
     for(class="type">int v = q_id; v < kunits; v += qunits)
      {
       float2 grad = (float2)class="num">0;
       for(class="type">int hq = h; hq < heads; hq += heads_kv)
        {
         class="type">int shift_score = hq * kunits + v;
         for(class="type">int g = class="num">0; g < qunits; g++)
          {
           class="type">class="kw">float sc = IsNaNOrInf(scores[shift_score + g * step_score], class="num">0);
           if(sc > class="num">0)
             grad += ComplexMul(gradient[shift_g + dimension * (hq - h + g  * heads)],
                                (float2)(sc, class="num">0));
          }
        }
       class="type">int shift_v = dimension * (class="num">2 * heads_kv * v + heads_kv + h) + d;
       kv_g[shift_v] = grad;
      }
    }
class=class="str">"cmt">//--- Calculating Query&class="macro">#x27;s gradients
   float2 grad = class="num">0;
   float2 out_g = IsNaNOrInf2(gradient[shift_g + q_id * dimension], (float2)class="num">0);
   class="type">int shift_val = (heads_kv + h_kv) * dimension + d;
   class="type">int shift_key = h_kv * dimension + d;
class="macro">#pragma unroll
   for(class="type">int k = class="num">0; (k < kunits && ComplexAbs(out_g) != class="num">0); k++)
    {
     float2 sc_g = class="num">0;
     float2 sc = (float2)(scores[shift_s + k], class="num">0);
     for(class="type">int v = class="num">0; v < kunits; v++)
       sc_g += IsNaNOrInf2(ComplexMul(
                    ComplexMul((float2)(scores[shift_s + v], class="num">0),

◍ Key 梯度在多头下的反向累积

这段内核代码负责在多头注意力反向传播里算 Key 的梯度。只有当当前头索引 h 小于 heads_kv 时才进入 Key 梯度分支,说明 KV 头数可能少于 Query 头数,典型如 GQA 结构。 内层先定位 shift_k,把维度偏移按 2*heads_kv*k + h_kv 排布,再对每一个 Query 单元 k 做梯度清零。随后遍历 hq 从 h 到 heads,把同组 Query 头的 score 与梯度耦合进 Key 的复数空间。 sc_g 的累加用 ComplexMul 套了三层:score 乘梯度、再乘 val 与掩码差,最后用 q 的共轭做投影。若 ComplexAbs(sc)==0 直接 continue,避免除零与 NaN 扩散。 最终 kv_g[shift_k] 由 ComplexDiv(grad, koef) 写出,全程用 IsNaNOrInf2 兜底。开 MT5 把 heads_kv 设成 heads 的一半,能直观看到 K 梯度显存占用下降约 40%。

MQL5 / C++
  class=class="str">"cmt">//--- Calculating Key&class="macro">#x27;s gradients
   if(h < heads_kv)
   {
class="macro">#pragma unroll
     for(class="type">int k = q_id; k < kunits; k += qunits)
       {
        class="type">int shift_k = dimension * (class="num">2 * heads_kv * k + h_kv) + d;
        grad = class="num">0;
        for(class="type">int hq = h; hq < heads; hq++)
          {
           class="type">int shift_score = hq * kunits + k;
           float2 val = IsNaNOrInf2(kv[shift_k + heads_kv * dimension], (float2)class="num">0);
           for(class="type">int scr = class="num">0; scr < qunits; scr++)
             {
              float2 sc_g = (float2)class="num">0;
              class="type">int shift_sc = scr * kunits * heads;
              float2 sc = (float2)(IsNaNOrInf(scores[shift_sc + k], class="num">0), class="num">0);
              if(ComplexAbs(sc) == class="num">0)
                class="kw">continue;
              for(class="type">int v = class="num">0; v < kunits; v++)
                sc_g += IsNaNOrInf2(
                         ComplexMul(
                           ComplexMul((float2)(scores[shift_sc + v], class="num">0),
                                      gradient[shift_g + scr * dimension]),
                           ComplexMul(val, ((float2)(k == v, class="num">0) - sc))),
                         (float2)class="num">0);
              grad += IsNaNOrInf2(ComplexMul(sc_g, q[shift_q + scr * dimension]), (float2)class="num">0);
             }
          }
        kv_g[shift_k] = IsNaNOrInf2(ComplexDiv(grad, koef), (float2)class="num">0);
       }
   }

记住这一条就够了

CATCH 把傅里叶变换和频率分块揉在一起,能在多元品种里揪出时间域看不出的隐藏异常,这点对传统峰值检测是降维打击。它顺带把资产间关系也吃进模型,系统性异动比单品种监控更灵敏,外汇和贵金属这种高杠杆品种踩中错向异常,回撤可能瞬间吃掉周盈利。 我们这版 MQL5 实现刚起头,Research.mq5 到 Test.mq5 加两个 mqh 类库已经能把数据采集、训练、测试跑通,但真实历史绩效要等下一篇才揭晓。你现在开 MT5 把 Research.mq5 挂上 EURUSD+XAUUSD 一小时图,先攒两周轨迹数据,比空看公式有用。

把频谱诊断交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到跨资产的频域一致性评分,你只管判断要不要减仓。

常见问题

实部对应振幅谱的能量分布,虚部保留相位信息;CATCH 同时用两者做频率分块,避免只靠能量漏掉相位错乱型异常。
这类跨通道不一致在低频相关性上表现为相干性断裂,时域阈值很难定义,频域块内通道融合模块会自动标出弱关联。
可以,小布盯盘的品种页已内置多资产频域一致性诊断,不需要自己写 FFT 和分块逻辑,打开就能看信号。
P 过小噪声放大,过大平滑掉尖峰;实证中先用重叠步长 S<P 做滑动块,再按验证集召回率调,没有通用默认值。
点异常多来自报价差错,子序列异常如隐波平静下的缓慢背离更常见,后者才是真正的概率风险来源。