交易中的神经网络:时间序列的分段线性表示·进阶篇
📉

交易中的神经网络:时间序列的分段线性表示·进阶篇

(2/3)· 面对满屏 tick 数据算到卡顿?这篇进阶篇讲清双向分段线性表示怎么压缩时间序列

新手友好 第 2/3 篇

接上篇,我们继续深挖时间序列的紧凑表达。当你把整段 EURUSD 的逐笔报价直接喂给异常检测模型,内存和时延往往先崩,信号反而被淹。本篇聚焦 BPLR 算法,看它如何把长序列切成几段直线,留住结构、丢掉噪声。

◍ 转折点之间的线性斜率怎么算

在 GPU 内核里标记出当前柱为转折点(bttp=true)之后,先要回数前面已经出现过几个转折点,并定位上一个转折点的位置 prev_ttp 与索引 prev_in。代码用 pos 从 0 累加,每碰到一个 isttp 为真的单元就 pos++,这段循环在 transpose 开关下用不同的内存排布取 current_in,但逻辑一致。 只有当 pos 落在 1 到 lenth/3 之间(不含边界)时,才对 prev_ttp 到当前 i 这段距离做最小二乘线性回归。dist = i - prev_ttp,遍历 p 从 0 到 dist 累加 x、y、xy、xx,斜率 slope = (dist*sum_xy - sum_x*sum_y) / (dist*sum_xx - sum_x*sum_x),截距 intercept 同步算出,并把归一化长度 dist/lenth 写进 outputs 第三槽。 若 pos 恰好等于 lenth/3,则改用 dist = lenth - prev_ttp 跑同样的求和,覆盖从最后一个转折点到序列末尾的全段拟合。外汇与贵金属行情跳变频繁,这种局部斜率仅反映历史片段倾向,实盘需警惕过拟合与滑点风险。

MQL5 / C++
bttp = true;
class=class="str">"cmt">//---
  isttp[shift_in] = (class="type">int)bttp;
  outputs[shift_in] = class="num">0;
  barrier(CLK_LOCAL_MEM_FENCE);
class=class="str">"cmt">//--- calc position
  class="type">int pos = -class="num">1;
  class="type">int prev_in = class="num">0;
  class="type">int prev_ttp = class="num">0;
  if(bttp)
    {
     pos = class="num">0;
     for(class="type">int p = class="num">0; p < i; p++)
       {
        class="type">int current_in = ((class="type">bool)transpose ? (p * variables + v) : (v * lenth + p));
        if((class="type">bool)isttp[current_in])
          {
           pos++;
           prev_ttp = p;
           prev_in = current_in;
          }
       }
    }
class=class="str">"cmt">//--- cacl tendency
  if(pos > class="num">0 && pos < (lenth / class="num">3))
    {
     class="type">class="kw">float sum_x = class="num">0;
     class="type">class="kw">float sum_y = class="num">0;
     class="type">class="kw">float sum_xy = class="num">0;
     class="type">class="kw">float sum_xx = class="num">0;
     class="type">int dist = i - prev_ttp;
     for(class="type">int p = class="num">0; p < dist; p++)
       {
        class="type">class="kw">float x = (class="type">class="kw">float)(p);
        class="type">class="kw">float y = inputs[prev_in + p * step_in];
        sum_x += x;
        sum_y += y;
        sum_xy += x * y;
        sum_xx += x * x;
       }
     class="type">class="kw">float slope = (dist * sum_xy - sum_x * sum_y) / (dist > class="num">1 ? (dist * sum_xx - sum_x * sum_x) : class="num">1);
     class="type">class="kw">float intercept = (sum_y - slope * sum_x) / dist;
     class="type">int shift_out = ((class="type">bool)transpose ? ((pos - class="num">1) * class="num">3 * variables + v) : (v * lenth + (pos - class="num">1) * class="num">3));
     outputs[shift_out] = slope;
     outputs[shift_out + class="num">1 * step_in] = intercept;
     outputs[shift_out + class="num">2 * step_in] = ((class="type">class="kw">float)dist) / lenth;
    }
  else
    {
     if(pos == (lenth / class="num">3))
       {
        class="type">class="kw">float sum_x = class="num">0;
        class="type">class="kw">float sum_y = class="num">0;
        class="type">class="kw">float sum_xy = class="num">0;
        class="type">class="kw">float sum_xx = class="num">0;
        class="type">int dist = lenth - prev_ttp;
        for(class="type">int p = class="num">0; p < dist; p++)
          {
           class="type">class="kw">float x = (class="type">class="kw">float)(p);
           class="type">class="kw">float y = inputs[prev_in + p * step_in];
           sum_x += x;
           sum_y += y;
           sum_xy += x * y;
           sum_xx += x * x;

分段线性回归的反向梯度怎么算

上面这段 OpenCL 内核是分段线性回归(PLR)层在训练时用的反向传播梯度函数,核心任务是把输出端的斜率、截距梯度,按当前样本落在哪一段回推到输入梯度。 先通过 do-while 定位:用 outputs 里存的第 3 个值(段长占比 × 序列长)累加 prev_in,直到覆盖当前全局下标 i 的那一段 pos 被找到;dist 至少取 1,避免除零。 接着只对该段长度 dist 预累加 sum_x 与 sum_xx(x 从 0 到 dist-1),不再遍历全序列,所以单点梯度计算量是 O(dist) 而非 O(lenth),在段数多时明显省算力。 梯度合并处用 grad_slope -= sum_x/dist*grad_intercept 先消去截距耦合,再除以 fmax(dist*sum_xx - sum_x*sum_x, 1) 得到纯斜率贡献;最终 grad 由截距项 grad_intercept/dist 加位置项 (dist*(i-prev_in)-sum_x)*grad_slope 合成,isnan/isinf 直接归零防止核崩溃。 在 MT5 里接自己的 OCL 网络时,若某段 dist 过短(如 1),分母保护会令 grad_slope 趋零,这时该段几乎只传截距梯度——调小分段阈值可能让更多短段出现,反向信号会变平滑但也可能丢细节。外汇与贵金属价格序列用这类层做特征提取时,高风险在于过拟合历史波动结构,实盘前务必用不同品种周期验证。

MQL5 / C++
__kernel class="type">void PLRGradient(__global class="type">class="kw">float *inputs_gr,
__global const class="type">class="kw">float *outputs,
__global const class="type">class="kw">float *outputs_gr,
const class="type">int transpose
)
{
 const class="type">size_t i = get_global_id(class="num">0);
 const class="type">size_t lenth = get_global_size(class="num">0);
 const class="type">size_t v = get_global_id(class="num">1);
 const class="type">size_t variables = get_global_size(class="num">1);
class=class="str">"cmt">//--- constants
 const class="type">int shift_in = ((class="type">bool)transpose ? (i * variables + v) : (v * lenth + i));
 const class="type">int step_in = ((class="type">bool)transpose ? variables : class="num">1);
 const class="type">int shift_out = ((class="type">bool)transpose ? v : (v * lenth));
 const class="type">int step_out = class="num">3 * step_in;
class=class="str">"cmt">//--- calc position
 class="type">int pos = -class="num">1;
 class="type">int prev_in = class="num">0;
 class="type">int dist = class="num">0;
 do
  {
   pos++;
   prev_in += dist;
   dist = (class="type">int)fmax(outputs[shift_out + pos * step_out + class="num">2 * step_in] * lenth, class="num">1);
  }
 class="kw">while(!(prev_in <= i && (prev_in + dist) > i));
class=class="str">"cmt">//--- calc constants
 class="type">class="kw">float sum_x = class="num">0;
 class="type">class="kw">float sum_xx = class="num">0;
 for(class="type">int p = class="num">0; p < dist; p++)
  {
   class="type">class="kw">float x = (class="type">class="kw">float)(p);
   sum_x += x;
   sum_xx += x * x;
  }
class=class="str">"cmt">//--- get output gradient
 class="type">class="kw">float grad_slope = outputs_gr[shift_out + pos * step_out];
 class="type">class="kw">float grad_intercept = outputs_gr[shift_out + pos * step_out + step_in];
class=class="str">"cmt">//--- calc gradient
 grad_slope -= sum_x / dist * grad_intercept;
 grad_slope /= fmax(dist * sum_xx - sum_x * sum_x, class="num">1);
 class="type">class="kw">float grad = grad_intercept / dist;
 grad += (dist * (i - prev_in) - sum_x) * grad_slope;
 if(isnan(grad) || isinf(grad))
    grad = class="num">0;
class=class="str">"cmt">//--- save result
 inputs_gr[shift_in] = grad;
}

「PLR 神经元的 OpenCL 初始化与前向传播骨架」

CNeuronPLROCL 是 MT5 中基于 OpenCL 的多项式线性回归神经元封装,类内先声明了 icIsTTP、iVariables、iCount 等整型成员,分别用于标记转置缓冲、输入窗口长度与单元数量,并预留了 feedForward、calcInputGradients 等虚函数接口,其中 updateInputWeights 直接返回 true,说明该层权重不在反向阶段更新。 构造函数仅把 bTranspose 置为 false,真正的资源申请在 Init 里完成:它先调用基类 CNeuronBaseOCL::Init,传入 window_in * units_count 作为展开后的输入维度,再把 iVariables 和 iCount 分别赋值为窗口与单元数,最后通过 OpenCL.AddBuffer 申请一块 sizeof(int) * Neurons() 大小的可读写缓冲,失败则返回 false。 feedForward 函数负责把上一层输出搬进 PLR 核。它先校验 OpenCL 指针与上游神经元输出缓冲非空,然后定义二维工作组:global_work_size 为 {iCount, iVariables},local_work_size 为 {iCount, 1},这意味着每个输入变量沿单元维度并行。接着用 SetArgumentBuffer 把上游输出索引与自身输出索引绑到 def_k_PLR 核的参数槽,任一绑定失败就打印错误码并退出。 在 MT5 终端里打开含此类的 EA 源码,把 window_in 从默认 12 改成 24 后重编译,能直接观察到 AddBuffer 申请的内存放翻约一倍,显存占用随窗口线性增长,外汇与贵金属模型训练属高风险,参数放大可能触发 CL_MEM 分配失败。

MQL5 / C++
class="type">int                icIsTTP;
class="type">int                iVariables;
class="type">int                iCount;
class=class="str">"cmt">//---
class="kw">virtual class="type">bool       feedForward(CNeuronBaseOCL *NeuronOCL);
class=class="str">"cmt">//---
class="kw">virtual class="type">bool       calcInputGradients(CNeuronBaseOCL *prevLayer);
class="kw">virtual class="type">bool       updateInputWeights(CNeuronBaseOCL *NeuronOCL)   { class="kw">return true; }
class="kw">public:
                      CNeuronPLROCL(class="type">void)  : bTranspose(class="kw">false) {};
                     ~CNeuronPLROCL(class="type">void) {};
class=class="str">"cmt">//---
class="kw">virtual class="type">bool       Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                         class="type">uint window_in, class="type">uint units_count, class="type">bool transpose,
                         ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
class=class="str">"cmt">//---
class="kw">virtual class="type">int        Type(class="type">void)  const   {  class="kw">return defNeuronPLROCL;  }
class=class="str">"cmt">//---
class="kw">virtual class="type">bool       Save(class="type">int const file_handle);
class="kw">virtual class="type">bool       Load(class="type">int const file_handle);
class="kw">virtual class="type">void       SetOpenCL(COpenCLMy *obj);
};
class="type">bool CNeuronPLROCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                         class="type">uint window_in, class="type">uint units_count, class="type">bool transpose,
                         ENUM_OPTIMIZATION optimization_type, class="type">uint batch
  )
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window_in * units_count, optimization_type, batch))
      class="kw">return class="kw">false;
   iVariables = (class="type">int)window_in;
   iCount = (class="type">int)units_count;
   bTranspose = transpose;
   icIsTTP = OpenCL.AddBuffer(class="kw">sizeof(class="type">int) * Neurons(), CL_MEM_READ_WRITE);
   if(icIsTTP < class="num">0)
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   class="kw">return true;
  }
class="type">bool CNeuronPLROCL::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!OpenCL || !NeuronOCL || !NeuronOCL.getOutput())
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   class="type">uint global_work_offset[class="num">2] = {class="num">0};
   class="type">uint global_work_size[class="num">2] = {iCount, iVariables};
   class="type">uint local_work_size[class="num">2] = {iCount, class="num">1};
   ResetLastError();
   if(!OpenCL.SetArgumentBuffer(def_k_PLR, def_k_plr_inputs, NeuronOCL.getOutputIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      class="kw">return class="kw">false;
     }
   if(!OpenCL.SetArgumentBuffer(def_k_PLR, def_k_plr_outputs, getOutputIndex()))
     {

◍ 往 OpenCL 内核塞参数与搭编码器骨架

在 MT5 里跑 GPU 加速的行情模型,先得把内核参数一个个绑好。下面这段把缓冲区和标量往 def_k_PLR 内核灌:icIsTTP 缓冲、是否转置的 int、以及写死的步长 0.3(float)。任何一步 SetArgument/SetArgumentBuffer 失败就 printf 出函数名、错误码和行号并 return false,方便你直接在 Experts 日志里定位是哪一行 OpenCL 调用炸了。 参数绑完之后用 OpenCL.Execute(def_k_PLR, 2, global_work_offset, global_work_size, local_work_size) 真正派发计算,维度数传 2。若执行返回 false,同样打印错误后退出——外汇与贵金属杠杆品种上跑这套,显存或驱动异常都可能让 Execute 失败,属于高风险调试环节。 编码器网络结构用 CArrayObj 挂 CLayerDescription 来拼。输入层节点数 = HistoryBars * BarDescr,类型 defNeuronBaseOCL、激活 None、优化 ADAM;随后接一层 defNeuronBatchNormOCL,batch 设 1e4,节点数沿用上层;再接 defNeuronTransposeOCL,count=HistoryBars、window=BarDescr 做维度翻转。每层 new 出来加不进数组就 delete 并返回 false,避免内存泄漏。 开 MT5 把这段贴进你的 EA 源码,改 HistoryBars 和 BarDescr 两个宏,看日志里 0.3 步长下 PLR 内核是否一次过,就能验证你本地 OpenCL 环境通不通。

MQL5 / C++
if(!OpenCL.SetArgumentBuffer(def_k_PLR, def_k_plt_isttp, icIsTTP))
  {
    printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
    class="kw">return class="kw">false;
  }
 if(!OpenCL.SetArgument(def_k_PLR, def_k_plr_transpose, (class="type">int)bTranspose))
  {
    printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
    class="kw">return class="kw">false;
  }
 if(!OpenCL.SetArgument(def_k_PLR, def_k_plr_step, (class="type">class="kw">float)class="num">0.3))
  {
    printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
    class="kw">return class="kw">false;
  }
class=class="str">"cmt">//---
 if(!OpenCL.Execute(def_k_PLR, class="num">2, global_work_offset, global_work_size, local_work_size))
  {
    printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
    class="kw">return class="kw">false;
  }
class=class="str">"cmt">//---
 class="kw">return true;
}
class="type">bool CreateEncoderDescriptions(CArrayObj *encoder)
  {
class=class="str">"cmt">//---
  CLayerDescription *descr;
class=class="str">"cmt">//---
  if(!encoder)
    {
      encoder = new CArrayObj();
      if(!encoder)
        class="kw">return class="kw">false;
    }
class=class="str">"cmt">//---
  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 = defNeuronTransposeOCL;
  descr.count = HistoryBars;
  descr.window = BarDescr;
  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;

编码器后段卷积与反归一化的堆叠

上面这段是编码器从第 4 层到第 9 层的实际装配逻辑,每层都用 new CLayerDescription() 建描述符,再交给 encoder.Add() 入栈,任何一步失败就 delete 并回 false。 第 4 层用 defNeuronConvOCL,window 取 HistoryBars、step 也等于 HistoryBars,window_out 压到 LatentCount,激活走 LReLU;第 5 层同样卷积但 window 与 step 均为 LatentCount,输出维度不变,激活换 SIGMOID。 第 6 层继续卷积,window_out 直接指向 NForecast,激活用 TANH 把输出限到 [-1,1],这一步基本决定了预测值的物理边界。第 7 层转置 defNeuronTransposeOCL 做形状还原,无激活。 第 8 层 defNeuronRevInDenormOCL 负责反归一化,count 写成 BarDescr*NForecast、layers=1,把网络内部尺度拉回价格量纲;第 9 层 defNeuronFreDFOCL 做频域处理,step 为 int(true),probability 设 0.7f,意味着约七成概率走该分支。 外汇与贵金属市场高杠杆、高波动,这类网络结构只是特征抽取框架,实盘信号倾向需结合样本外验证,复制代码到 MT5 跑一遍层数与维度打印最能看清张量怎么流动。

MQL5 / C++
  descr.type = defNeuronPLROCL;
  descr.count = HistoryBars;
  descr.window = BarDescr;
  descr.step = class="type">int(class="kw">false);
  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">4
  if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
  descr.type = defNeuronConvOCL;
  descr.count = BarDescr;
  descr.window = HistoryBars;
  descr.step = HistoryBars;
  descr.window_out = LatentCount;
  descr.activation = LReLU;
  descr.optimization = ADAM;
  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 = defNeuronConvOCL;
  descr.count = BarDescr;
  descr.window = LatentCount;
  descr.step = LatentCount;
  descr.window_out = LatentCount;
  descr.optimization = ADAM;
  descr.activation = SIGMOID;
  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 = defNeuronConvOCL;
  descr.count = BarDescr;
  descr.window = LatentCount;
  descr.step = LatentCount;
  descr.window_out = NForecast;
  descr.optimization = ADAM;
  descr.activation = TANH;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return class="kw">false;
    }
class=class="str">"cmt">//--- layer class="num">7
  if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
  descr.type = defNeuronTransposeOCL;
  descr.count = BarDescr;
  descr.window = NForecast;
  descr.activation = None;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return class="kw">false;
    }
class=class="str">"cmt">//--- layer class="num">8
  if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
  descr.type = defNeuronRevInDenormOCL;
  descr.count = BarDescr*NForecast;
  descr.activation = None;
  descr.optimization = ADAM;
  descr.layers=class="num">1;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return class="kw">false;
    }
class=class="str">"cmt">//--- layer class="num">9
  if(!(descr = new CLayerDescription()))
      class="kw">return class="kw">false;
  descr.type = defNeuronFreDFOCL;
  descr.window = BarDescr;
  descr.count =  NForecast;
  descr.step = class="type">int(true);
  descr.probability = class="num">0.7f;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return class="kw">false;
    }
class=class="str">"cmt">//---
  class="kw">return true;
  }
把降维后的线段交给小布盯盘
这些分段与相似度诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到压缩后的走势骨架,你只需判断哪段偏离了常态。

常见问题

普通 PLR 多按单向误差切段,BPLR 双向兼顾前后上下文拟合,对集体异常的保留更完整,适合金融序列里的群组偏离。
太小会抹掉关键拐点,太大则降维失效、计算量回到原点。实际可按品种波动节律先粗调再微调。
可以,小布盯盘在品种页后台完成分段线性压缩与异常评分,你看到的是已提炼的线段与警示,不必自己写 MQL5 脚本。
单根影线刺穿支撑是上下文异常,一段连续收盘偏离通道才是集体异常,后者常由资金面或数据市引发,概率上更值得追踪。
它用分段积分思路比对形状面积差,避免逐点计算,开销低且对平移不敏感,贵金属跳空后仍能比出形态相似。