使用 OpenCL 测试烛形形态·综合运用

使用 OpenCL 测试烛形形态·综合运用

(3/3)·从 MQL5 基准到 OpenCL 内核,三币种优化实测看清并行回测的真实收益边界

偏理论 第 3/3 篇
不少交易者把 OpenCL 当成回测加速的万能钥匙,却连形态判定与订单转移的内核边界都没理清。直接在策略测试器外并行跑历史,常因 M1 重采样误差得出虚假胜率。先弄清单次通过的算法分工,再谈提速才有意义。

「用 OpenCL 并行扫 Pinbar 与吞噬形态」

这段逻辑把经典价格行为形态判定搬到了 GPU 上跑。Check 函数里先看看跌 Pinbar:当前影线 tail 不小于 ref、0 号 K 线收阴、2 号 K 线收阳、1 号 K 线低点跌破左右两根低点、且 1 号实体小于 tail,命中即返回 PAT_PINBAR_BEARISH。 看跌吞噬要求 1 号实体跌幅 ≥ ref,且 0 号高点低于 1 号收盘、2 号开盘高于 1 号收盘、2 号收盘低于 1 号开盘;看涨吞噬则对称反过来,用 (O(1)-C(1))≥ref 作实体门槛。 find_patterns 是 OpenCL 内核,每个线程拿一根柱的索引 x=get_global_id(0),搜索深度被砍掉 PBARS 根以避免越界。命中形态后用 atomic_inc 抢 Count 槽位,把「触发柱索引+PBARS」和买卖方向写进 Order 缓冲区。 在 MT5 里把 PBARS 设成 3、ref 按品种 ATR 的 0.5 倍注入,能直接验证 EURUSD 五分钟图一晚扫出几十个信号的概率分布,外汇和贵金属杠杆高,信号仅作概率参考。

MQL5 / C++
if(tail>=ref && O(class="num">0)>C(class="num">0) && C(class="num">2)>O(class="num">2) && L(class="num">1)<fmin(L(class="num">0),L(class="num">2)) && fabs(O(class="num">1)-C(class="num">1))<tail)
     class="kw">return PAT_PINBAR_BULLISH;
   }
class=class="str">"cmt">//--- 看跌吞噬
   if((flags&PAT_ENGULFING_BEARISH)!=class="num">0)
   {class=class="str">"cmt">//
     if((C(class="num">1)-O(class="num">1))>=ref && H(class="num">0)<C(class="num">1) && O(class="num">2)>C(class="num">1) && C(class="num">2)<O(class="num">1))
       class="kw">return PAT_ENGULFING_BEARISH;
   }
class=class="str">"cmt">//--- 看涨吞噬
   if((flags&PAT_ENGULFING_BULLISH)!=class="num">0)
   {class=class="str">"cmt">//
     if((O(class="num">1)-C(class="num">1))>=ref && L(class="num">0)>C(class="num">1) && O(class="num">2)<C(class="num">1) && C(class="num">2)>O(class="num">1))
       class="kw">return PAT_ENGULFING_BULLISH;
   }
class=class="str">"cmt">//--- 没有找到  
   class="kw">return PAT_NONE;
   }
__kernel class="type">void find_patterns(__global class="type">class="kw">double *Open,__global class="type">class="kw">double *High,__global class="type">class="kw">double *Low,__global class="type">class="kw">double *Close,
                            __global class="type">int *Order,      class=class="str">"cmt">// 订单缓冲区
                            __global class="type">int *Count,      class=class="str">"cmt">// 缓冲区中的订单数量
                            class="kw">const class="type">class="kw">double ref,         class=class="str">"cmt">// 形态参数 
                            class="kw">const class="type">uint flags)         class=class="str">"cmt">// 寻找哪些形态
  {
class=class="str">"cmt">//--- 在一个维度上工作  
class=class="str">"cmt">//--- 柱的索引  
  class="type">size_t x=get_global_id(class="num">0);
class=class="str">"cmt">//--- 形态搜索的空间大小  
  class="type">size_t depth=get_global_size(class="num">0)-PBARS;
  if(x>=depth)
    class="kw">return;
class=class="str">"cmt">//--- 检查形态是否存在
  class="type">uint res=Check(&Open[x],&High[x],&Low[x],&Close[x],ref,flags);
  if(res==PAT_NONE)
    class="kw">return;
class=class="str">"cmt">//--- 设置订单
  if(res==PAT_PINBAR_BEARISH || res==PAT_ENGULFING_BEARISH)
    {class=class="str">"cmt">//卖出
     class="type">int i=atomic_inc(&Count[class="num">0]);
     Order[i*class="num">2]=x+PBARS;
     Order[(i*class="num">2)+class="num">1]=OP_SELL;
    }
  else if(res==PAT_PINBAR_BULLISH || res==PAT_ENGULFING_BULLISH)
    {class=class="str">"cmt">//买入
     class="type">int i=atomic_inc(&Count[class="num">0]);
     Order[i*class="num">2]=x+PBARS;
     Order[(i*class="num">2)+class="num">1]=OP_BUY;
    }
  }
class="type">size_t x=get_global_id(class="num">0);

把多周期订单映射到 M1 的 OpenCL 内核

在 MT5 里用 OpenCL 跑批量回测时,常要把较高周期生成的订单信号对齐到 M1 柱。下面这段内核就干这件事:用二维全局 ID 定位订单与 M1 时间,靠时间偏移量 shift 做精确匹配。 内核 order_to_M1 先取 x=get_global_id(0) 作为 Order 缓冲区的订单索引,y=get_global_id(1) 作为 TimeM1 的索引。若目标 OrderM1[x*2] 已大于等于 0,说明该订单已映射过,直接 return 避免重复写入。 匹配条件是 Time[Order[x*2]]+shift == TimeM1[y]。一旦命中,atomic_inc(&Count[1]) 累加映射计数,然后把 M1 柱索引写进 OrderM1[x*2],把买卖方向 OP_BUY/OP_SELL 从 Order 的奇数位抄到 OrderM1[(x*2)+1]。这种偶数存索引、奇数存方向的排布,能让后续 tester_step 内核用 Tasks 缓冲区直接寻址已开仓订单。 array_fill 则是一维内核,把整块 int 缓冲区填成指定 value,典型用法是在每帧回测前把 OrderM1 和 Left 重置为 -1 或 0。tester_step 的参数表里 SpreadM1 以价格差而非点值表示,接数据时别按 Point 去换算,否则外汇与贵金属品种的高杠杆波动会让权益曲线失真。 开 MT5 自建一个 OpenCL 程序,把 shift 设成周期差秒数(如 H1 到 M1 是 3600),用 array_fill 初始化后跑 order_to_M1,看 Count[1] 是否等于你的订单总数,就能验证映射有没有漏柱。

MQL5 / C++
Order[i*class="num">2]=x+PBARS;
Order[(i*class="num">2)+class="num">1]=OP_SELL;
class="type">int i=atomic_inc(&Count[class="num">0]);
__kernel class="type">void order_to_M1(__global class="type">class="kw">ulong *Time,__global class="type">class="kw">ulong *TimeM1,
                          __global class="type">int *Order,__global class="type">int *OrderM1,
                          __global class="type">int *Count,
                          class="kw">const class="type">class="kw">ulong shift) class=class="str">"cmt">// time shift in seconds
  {
class=class="str">"cmt">//--- 在二维中工作
  class="type">size_t x=get_global_id(class="num">0); class=class="str">"cmt">//index of Time index in Order
  if(OrderM1[x*class="num">2]>=class="num">0)
    class="kw">return;
  class="type">size_t y=get_global_id(class="num">1); class=class="str">"cmt">//index in TimeM1
  if((Time[Order[x*class="num">2]]+shift)==TimeM1[y])
    {
      atomic_inc(&Count[class="num">1]);
      class=class="str">"cmt">//--- 设置在 TimeM1 缓冲区中的偶数索引
      OrderM1[x*class="num">2]=y;
      class=class="str">"cmt">//--- 在奇数索引中设置操作 (OP_BUY/OP_SELL) 
      OrderM1[(x*class="num">2)+class="num">1]=Order[(x*class="num">2)+class="num">1];
    }
  }
atomic_inc(&Count[class="num">1]);
  if(OrderM1[x*class="num">2]>=class="num">0)
    class="kw">return;
__kernel class="type">void array_fill(__global class="type">int *Buf,class="kw">const class="type">int value)
  {
class=class="str">"cmt">//--- 在一个维度上工作    
  class="type">size_t x=get_global_id(class="num">0);
  Buf[x]=value;
  }
__kernel class="type">void tester_step(__global class="type">class="kw">double *OpenM1,__global class="type">class="kw">double *HighM1,__global class="type">class="kw">double *LowM1,__global class="type">class="kw">double *CloseM1,
                          __global class="type">class="kw">double *SpreadM1, class=class="str">"cmt">// 以价格差别表示,而不是点数
                          __global class="type">class="kw">ulong *TimeM1,
                          __global class="type">int *OrderM1,     class=class="str">"cmt">// 订单缓冲区, 其中 [class="num">0] 是在 OHLC(M1) 中的索引, [class="num">1] - (买入/卖出) 操作
                          __global class="type">int *Tasks,       class=class="str">"cmt">// 任务缓冲区 (已开启仓位) 保存订单在 OrderM1 缓冲区中的索引
                          __global class="type">int *Left,        class=class="str">"cmt">// 剩余任务的数量,两个元素: [class="num">0] - 用于存储体0, [class="num">1] - 用于存储体1
                          __global class="type">class="kw">double *Res,      class=class="str">"cmt">// 结果缓冲区 
                          class="kw">const class="type">uint bank,           class=class="str">"cmt">// 当前存储体              
                          class="kw">const class="type">uint orders)         class=class="str">"cmt">// OrderM1 中的订单数量

◍ GPU 内核里的多单平仓判定逻辑

这段 OpenCL 内核负责在 M1 周期上批量回测买仓的退出条件,参数通过 start_bar、stop_bar、maxbar 框定回测柱范围,tp_dP 与 sl_dP 用价格差直接定义止盈止损,timeout 则以秒为单位强制收仓。内核先用 get_global_id(0) 拿线程编号,再依据 bank 标志与 Tasks 数组定位当前订单在 OrderM1 里的索引,从而取出开仓柱 iO 与方向 op。 买仓分支里,open 取 OpenM1[iO] 加上当时点差 SpreadM1[iO],tp 与 sl 据此加减价格差得出。随后双层循环遍历每根柱的 Open/High/Low/Close 四个价位(k 从 0 到 3),只要价位触及 sl 就判止损。 强制时间平仓写在 k==0 的分支:若 j 超出 maxbar 或 TimeM1[j] 超过 tclose,直接把 p-open 写进 Res[idx] 并 return。外汇与贵金属杠杆高,这类批量回测仅反映历史概率,实盘触发顺序和滑点可能偏离。 把这段内核直接丢进 MT5 的 OpenCL 示例工程,改 tp_dP/sl_dP 为 EURUSD 的 50 点(0.0050)价差,能立刻看到千级订单的并行退出分布。

MQL5 / C++
class="kw">const class="type">uint start_bar,      class=class="str">"cmt">// 已处理的柱形的序列号 (作为 OrderM1 中指定索引的偏移)
      class="kw">const class="type">uint stop_bar,      class=class="str">"cmt">// 最后要处理的柱
      class="kw">const class="type">uint maxbar,        class=class="str">"cmt">// 允许的最大柱索引 (数组中的最后一个柱)
      class="kw">const class="type">class="kw">double tp_dP,       class=class="str">"cmt">// 以价格差距表示的 TP 
      class="kw">const class="type">class="kw">double sl_dP,       class=class="str">"cmt">// 以价格差距表示的 SL
      class="kw">const class="type">class="kw">ulong timeout)      class=class="str">"cmt">// 何时强制关闭仓位 (秒数) 
  class="type">size_t id=get_global_id(class="num">0);
  class="type">uint bank_next=(bank)?class="num">0:class="num">1;
  if(!start_bar)
     idx=id;
  else
     idx=Tasks[(orders*bank)+id];
class=class="str">"cmt">//--- 仓位中的柱索引已经在缓冲区 M1 中开启
  class="type">uint iO=OrderM1[idx*class="num">2];
class=class="str">"cmt">//--- (OP_BUY/OP_SELL) 操作
  class="type">uint op=OrderM1[(idx*class="num">2)+class="num">1];
  class="type">class="kw">ulong tclose=TimeM1[iO]+timeout;
  if(op==OP_BUY)
   {
   class=class="str">"cmt">//--- 开启仓位的价格
     class="type">class="kw">double open=OpenM1[iO]+SpreadM1[iO];
     class="type">class="kw">double tp = open+tp_dP;
     class="type">class="kw">double sl = open-sl_dP;
     class="type">class="kw">double p=class="num">0;
     for(class="type">uint j=iO+start_bar; j<=(iO+stop_bar); j++)
      {
       for(class="type">uint k=class="num">0;k<class="num">4;k++)
        {
         if(k==class="num">0)
          {
           p=OpenM1[j];
           if(j>=maxbar || TimeM1[j]>=tclose)
            {
            class=class="str">"cmt">//--- 强制根据时间平仓
             Res[idx]=p-open;
             class="kw">return;
            }
          }
         else if(k==class="num">1)
           p=HighM1[j];
         else if(k==class="num">2)
           p=LowM1[j];
         else
           p=CloseM1[j];
         class=class="str">"cmt">//--- 检查是否触发了获利或者止损
         if(p<=sl)
          {

「用 OpenCL 把形态测试搬进显卡」

这段类声明把回测引擎的并行骨架亮出来了:CTestPatterns 私有继承 COpenCLx,说明它直接吃 OpenCL 上下文,把 K 线序列和订单模拟丢给 GPU 跑。m_sbuf 存当前周期序列,m_tbuf 存 M1 序列,两个缓冲分开管理,避免大周期和小周期在显存里打架。 类里 test() 和 optimize() 是核心私有方法,对外只暴露 Test() 与 Optimize(),调用方不必关心内核调度。SetTesterPasses() 和 SetPrepPasses() 让你手动控订单模拟内核与准备内核的通过次数——这两个值直接决定显存带宽占用和单次回测耗时,调错了可能卡死老显卡。 从公开的 Test() 实现残片能看到流程:先 ResetLastError() 清错误栈,m_stat.Reset() 清统计,再用 time_total.Start() 和 time_buffering.Start() 分别掐总时与数据上传时。若你手上有 MT5 和一块支持 OpenCL 的卡,把这两行计时埋进去,就能读出『上传耗时 / 总耗时』的比值,通常 M1 序列越大该比值越夸张。外汇与贵金属杠杆高,回测过拟合会放大实盘风险,GPU 跑出的漂亮曲线仅作概率参考。

MQL5 / C++
 Res[idx]=sl-open;
 class="kw">return;
 }
 else if(p>=tp)
 {
 Res[idx]=tp-open;
 class="kw">return;
 }
 }
 }
 class="type">uint i=atomic_inc(&Left[bank_next]);
 Tasks[(orders*bank_next)+i]=idx;
class CTestPatterns : class="kw">private COpenCLx
 {
class="kw">private:
 CBuffering *m_sbuf; class=class="str">"cmt">// 当前时段的时间序列
 CBuffering *m_tbuf; class=class="str">"cmt">// M1 时段的时间序列
 class="type">int m_prepare_passes;
 class="type">uint m_tester_passes;
 class="type">bool LoadTimeseries(class="type">class="kw">datetime from,class="type">class="kw">datetime to);
 class="type">bool LoadTimeseriesOCL(class="type">void);
 class="type">bool test(STR_TEST_STAT &stat,class="type">class="kw">datetime from,class="type">class="kw">datetime to,STR_TEST_PARS &par);
 class="type">bool optimize(STR_TEST_STAT &stat,class="type">class="kw">datetime from,class="type">class="kw">datetime to,STR_OPT_PARS &par);
 class="type">void buffers_free(class="type">void);
class="kw">public:
 CTestPatterns();
 ~CTestPatterns();
 class=class="str">"cmt">//--- 运行一次测试
 class="type">bool Test(STR_TEST_STAT &stat,class="type">class="kw">datetime from,class="type">class="kw">datetime to,STR_TEST_PARS &par);
 class=class="str">"cmt">//--- 运行优化
 class="type">bool Optimize(STR_TEST_STAT &stat,class="type">class="kw">datetime from,class="type">class="kw">datetime to,STR_OPT_PARS &par);
 class=class="str">"cmt">//--- 取得程序执行统计的指针
 COCLStat *GetStat(class="type">void){class="kw">return &m_stat;}
 class=class="str">"cmt">//--- 取得最近错误的代码
 class="type">int GetLastError(class="type">void){class="kw">return m_last_error.code;}
 class=class="str">"cmt">//--- 取得最近错误的结构
 STR_ERROR GetLastErrorExt(class="type">void){class="kw">return m_last_error;}
 class=class="str">"cmt">//--- 重置最近错误
 class="type">void ResetLastError(class="type">void);
 class=class="str">"cmt">//---测试内核运行的通过次数
 class="type">void SetTesterPasses(class="type">uint tp){m_tester_passes=tp;}
 class=class="str">"cmt">//---订单准备内核运行的通过次数
 class="type">void SetPrepPasses(class="type">int p){m_prepare_passes=p;}
 };
class="type">bool CTestPatterns::Test(STR_TEST_RESULT &result,class="type">class="kw">datetime from,class="type">class="kw">datetime to,STR_TEST_PARS &par)
 {
 ResetLastError();
 m_stat.Reset();
 m_stat.time_total.Start();
class=class="str">"cmt">//--- 上传时间序列数据
 m_stat.time_buffering.Start();

用 OpenCL 把形态匹配搬进显卡算

这段逻辑把 K 线形态检索从 CPU 挪到了 GPU。先调 LoadTimeseriesOCL 把行情塞进显存,失败就直接 return false,避免后续空跑。 显存里开了一块 buf_ORDER,大小是 m_sbuf.Depth*2*sizeof(int),用来存匹配到的订单索引;同时把 count[2] 初始化为 {0,0} 并写进 buf_COUNT,作为内核输出的计数器。 k_FIND_PATTERNS 内核绑定了开高低收四组缓冲加订单与计数缓冲,第 6、7 号参数分别是 par.ref*_Point 的浮动阈值和 par.flags 过滤位。global_size[0] 设为 m_sbuf.Depth,即每个柱体派一个线程去跑形态识别。 识别完从 buf_COUNT 读回 count[0],据此算 len=count[0]*2,再建 buf_ORDER_M1 并用 k_ARRAY_FILL 填 -1 占位。k_ORDER_TO_M1 是二维内核:维度 0 是订单数 count[0],维度 1 是 M1 柱数 m_tbuf.Depth。 循环里 maxshift=PeriodSeconds()/PeriodSeconds(PERIOD_M1),例如 H1 周期下 maxshift=60,逐偏移 s*60 秒喂给内核做时间对齐。外汇与贵金属杠杆高,回测加速不等于实盘胜率,GPU 方案仅降低计算耗时,信号质量仍由 par 决定。

MQL5 / C++
if(LoadTimeseriesOCL() ==false)class="kw">return false;
_BufferCreate(buf_ORDER,m_sbuf.Depth*class="num">2*class="kw">sizeof(class="type">int),CL_MEM_READ_WRITE);
class="type">int  count[class="num">2]={class="num">0,class="num">0};
_BufferFromArray(buf_COUNT,count,class="num">0,class="num">2,CL_MEM_READ_WRITE);
_SetArgumentBuffer(k_FIND_PATTERNS,class="num">0,buf_OPEN);
_SetArgumentBuffer(k_FIND_PATTERNS,class="num">1,buf_HIGH);
_SetArgumentBuffer(k_FIND_PATTERNS,class="num">2,buf_LOW);
_SetArgumentBuffer(k_FIND_PATTERNS,class="num">3,buf_CLOSE);
_SetArgumentBuffer(k_FIND_PATTERNS,class="num">4,buf_ORDER);
_SetArgumentBuffer(k_FIND_PATTERNS,class="num">5,buf_COUNT);
_SetArgument(k_FIND_PATTERNS,class="num">6,class="type">class="kw">double(par.ref)*_Point);
_SetArgument(k_FIND_PATTERNS,class="num">7,par.flags);
class="type">uint global_size[class="num">1];
global_size[class="num">0]=m_sbuf.Depth;
class="type">uint work_offset[class="num">1]={class="num">0};
_Execute(k_FIND_PATTERNS,class="num">1,work_offset,global_size);
_BufferRead(buf_COUNT,count,class="num">0,class="num">0,class="num">2);
class="type">int len=count[class="num">0]*class="num">2;
_BufferCreate(buf_ORDER_M1,len*class="kw">sizeof(class="type">int),CL_MEM_READ_WRITE);
_SetArgumentBuffer(k_ARRAY_FILL,class="num">0,buf_ORDER_M1);
_SetArgument(k_ARRAY_FILL,class="num">1,class="type">int(-class="num">1));
class="type">uint opt_init_work_size[class="num">1];
opt_init_work_size[class="num">0]=len;
class="type">uint opt_init_work_offset[class="num">1]={class="num">0};
_Execute(k_ARRAY_FILL,class="num">1,opt_init_work_offset,opt_init_work_size);
class=class="str">"cmt">//--- 设置参数
_SetArgumentBuffer(k_ORDER_TO_M1,class="num">0,buf_TIME);
_SetArgumentBuffer(k_ORDER_TO_M1,class="num">1,buf_TIME_M1);
_SetArgumentBuffer(k_ORDER_TO_M1,class="num">2,buf_ORDER);
_SetArgumentBuffer(k_ORDER_TO_M1,class="num">3,buf_ORDER_M1);
_SetArgumentBuffer(k_ORDER_TO_M1,class="num">4,buf_COUNT);
class=class="str">"cmt">//--- k_ORDER_TO_M1 内核的任务空间是二维的
class="type">uint global_work_size[class="num">2];
class=class="str">"cmt">//--- 第一个维度包含 k_FIND_PATTERNS 内核产生的订单
global_work_size[class="num">0]=count[class="num">0];
class=class="str">"cmt">//--- 第二个维度中包含所有的 M1 图表柱
global_work_size[class="num">1]=m_tbuf.Depth;
class=class="str">"cmt">//--- 两个维度中任务空间的初始偏移都是0
class="type">uint global_work_offset[class="num">2]={class="num">0,class="num">0};
class="type">int maxshift=PeriodSeconds()/PeriodSeconds(PERIOD_M1);
for(class="type">int s=class="num">0;s<maxshift;s++)
  {
  class=class="str">"cmt">//--- 设置当前通过的偏移
  _SetArgument(k_ORDER_TO_M1,class="num">5,class="type">class="kw">ulong(s*class="num">60));
  class=class="str">"cmt">//--- 执行内核
  _Execute(k_ORDER_TO_M1,class="num">2,global_work_offset,global_work_size);
  class=class="str">"cmt">//--- 读取结果
  _BufferRead(buf_COUNT,count,class="num">0,class="num">0,class="num">2);

◍ OpenCL 回测内核的批次调度与缓冲绑定

这段逻辑在做一件事:把 M1 周期上的订单回测拆成若干‘通过(pass)’丢给 GPU 内核跑,避免一次性把 Depth 根 K 线全压进去。先比对 count[0](当前图表订单数)和 count[1](M1 侦测柱数),两者相等才 break 出循环;若因循环条件耗尽而退出,仍会二次校验,不一致就写 UERR_ORDERS_PREPARE 错误并 return false。 缓冲区的创建直接决定了内核能读写什么:buf_TASKS 按 Depth*2*sizeof(int) 开,存跨 pass 的任务索引;buf_RESULT 按 Depth*sizeof(double) 开,落每笔订单的回测结果。随后把 OPEN/HIGH/LOW/CLOSE/SPREAD/TIME/ORDER 七类 M1 数据缓冲、以及 TASKS、COUNT、RESULT 依次绑到 k_TESTER_STEP 内核的参数槽 0–9。 参数槽 11 传订单总数,15–17 分别传 tp*_Point、sl*_Point、timeout(ulong)。maxdepth 由 timeout/PeriodSeconds(PERIOD_M1)+1 算得,例如 timeout=60 时 maxdepth=2。m_tester_passes 被夹在 1 与 maxdepth 之间,step_size=maxdepth/m_tester_passes,据此把柱区间切给每次通过。 循环里用 i&0x01 切换双缓冲索引,start_bar=i*step_size,stop_bar 在末 pass 取 Depth-1 否则取 start_bar+step_size-1。每次先把‘下一 pass 剩余订单数’清零并写回 buf_COUNT,再 _Execute 跑内核,最后 _BufferRead 把剩余订单数读回 count,供下一轮使用。外汇与贵金属品种点差跳空频繁,这种分批方式能降低单内核崩溃概率,但 GPU 回测仍属高风险验证,结果仅作概率参考。

MQL5 / C++
  class=class="str">"cmt">//--- 在索引0,您可以找到在当前图表上订单的数量
  class=class="str">"cmt">//--- 在索引1,您可以找到在M1图表中侦测到的对应柱数
  class=class="str">"cmt">//--- 如果两个值匹配,就退出循环
  if(count[class="num">0]==count[class="num">1])
     class="kw">break;
  class=class="str">"cmt">//--- 否则,就转到下一个迭代并以其它偏移运行内核
   }
class=class="str">"cmt">//--- 再次检查订单数量是否有效,以防我们不是通过‘class="kw">break’退出循环的
   if(count[class="num">0]!=count[class="num">1])
   {
      SET_UERRt(UERR_ORDERS_PREPARE,"M1 orders preparation error");
      class="kw">return false;
   }
class=class="str">"cmt">//--- 创建 Tasks 缓冲区,用于下面的通过而构成
   _BufferCreate(buf_TASKS,m_sbuf.Depth*class="num">2*class="kw">sizeof(class="type">int),CL_MEM_READ_WRITE);
class=class="str">"cmt">//--- 创建保存交易结果的 Result 缓冲区
   _BufferCreate(buf_RESULT,m_sbuf.Depth*class="kw">sizeof(class="type">class="kw">double),CL_MEM_READ_WRITE);
class=class="str">"cmt">//--- 设置单次测试内核的参数  
   _SetArgumentBuffer(k_TESTER_STEP,class="num">0,buf_OPEN_M1);
   _SetArgumentBuffer(k_TESTER_STEP,class="num">1,buf_HIGH_M1);
   _SetArgumentBuffer(k_TESTER_STEP,class="num">2,buf_LOW_M1);
   _SetArgumentBuffer(k_TESTER_STEP,class="num">3,buf_CLOSE_M1);
   _SetArgumentBuffer(k_TESTER_STEP,class="num">4,buf_SPREAD_M1);
   _SetArgumentBuffer(k_TESTER_STEP,class="num">5,buf_TIME_M1);
   _SetArgumentBuffer(k_TESTER_STEP,class="num">6,buf_ORDER_M1);
   _SetArgumentBuffer(k_TESTER_STEP,class="num">7,buf_TASKS);
   _SetArgumentBuffer(k_TESTER_STEP,class="num">8,buf_COUNT);
   _SetArgumentBuffer(k_TESTER_STEP,class="num">9,buf_RESULT);
   class="type">uint orders_count=count[class="num">0];
   _SetArgument(k_TESTER_STEP,class="num">11,class="type">uint(orders_count));
   _SetArgument(k_TESTER_STEP,class="num">14,class="type">uint(m_tbuf.Depth-class="num">1));
   _SetArgument(k_TESTER_STEP,class="num">15, class="type">class="kw">double(par.tp)*_Point);
   _SetArgument(k_TESTER_STEP,class="num">16, class="type">class="kw">double(par.sl)*_Point);
   _SetArgument(k_TESTER_STEP,class="num">17,class="type">class="kw">ulong(par.timeout));
   class="type">uint maxdepth=(par.timeout/PeriodSeconds(PERIOD_M1))+class="num">1;
   if(m_tester_passes<class="num">1)
      m_tester_passes=class="num">1;
   if(m_tester_passes>maxdepth)
      m_tester_passes=maxdepth;
   class="type">uint step_size=maxdepth/m_tester_passes;
   global_size[class="num">0]=orders_count;
   m_stat.time_ocl_test.Start();
   for(class="type">uint i=class="num">0;i<m_tester_passes;i++)
   {
      class=class="str">"cmt">//--- 设置当前索引
      _SetArgument(k_TESTER_STEP,class="num">10,class="type">uint(i&0x01));
      class="type">uint start_bar=i*step_size;
      class=class="str">"cmt">//--- 设置当前通过开始测试的柱的索引
      _SetArgument(k_TESTER_STEP,class="num">12,start_bar);
      class=class="str">"cmt">//--- 设置当前通过所进行测试的最后柱的索引
      class="type">uint stop_bar=(i==(m_tester_passes-class="num">1))?(m_tbuf.Depth-class="num">1):(start_bar+step_size-class="num">1);
      _SetArgument(k_TESTER_STEP,class="num">13,stop_bar);
      class=class="str">"cmt">//--- 重置下一阶段任务的数量 
      class=class="str">"cmt">//--- 它也保存着下一个通过剩余的订单数量
      count[(~i)&0x01]=class="num">0;
      _BufferWrite(buf_COUNT,count,class="num">0,class="num">0,class="num">2);
      class=class="str">"cmt">//--- 运行测试内核
      _Execute(k_TESTER_STEP,class="num">1,work_offset,global_size);
      class=class="str">"cmt">//--- 读取下一个通过剩余的订单数量
      _BufferRead(buf_COUNT,count,class="num">0,class="num">0,class="num">2);

「用 GPU 跑形态回测的落地写法」

这段逻辑把订单结果从 GPU 缓冲读回主机,再按点值折算盈亏归类。Result 数组长度由 orders_count 决定,每笔除以 _Point 得到以点为单位的数值,正数进 gross_profit 且 profit_trades 加一,负数进 gross_loss 且 loss_trades 加一,最后 trades_total 与 net_profit 由两部分求和得出。 在 OnStart 里直接框定了回测窗口:from 为 2018.01.01 00:00,to 为 2018.10.01 00:00,跨度 9 个月。pars.ref 设 60,sl 350 点,tp 50 点,flags=15 表示加载全部形态,timeout 给到 12*3600 秒防止设备挂死。 调用 tpat.Test(res,from,to,pars) 后先抓扩展错误,oclerr.code 非零就打印注释、错误码、函数名和行号并 return,避免脏数据继续往下走。 跑完用 Print 把净利润、总获利、总亏损、交易总数、盈利/亏损笔数全打出来;再通过 GetStat 取 GPU 显存大小、显存占用、缓冲耗时、OpenCL 初始化与缓冲耗时。外汇和贵金属品种用这套并行回测仍属高风险,历史形态命中不代表后续概率不变,上 MT5 改 pars 参数就能复验。

MQL5 / C++
class=class="str">"cmt">//--- 把新任务数量设为等于订单数量
global_size[class="num">0]=count[(~i)&0x01];
class=class="str">"cmt">//--- 如果没有剩余的任务,退出循环
if(!global_size[class="num">0])
   class="kw">break;
   }
 m_stat.time_ocl_test.Stop();
 class="type">class="kw">double Result[];
 ArrayResize(Result,orders_count);
 _BufferRead(buf_RESULT,Result,class="num">0,class="num">0,orders_count);
 m_stat.time_proc.Start();
 result.trades_total=class="num">0;
 result.gross_loss=class="num">0;
 result.gross_profit=class="num">0;
 result.net_profit=class="num">0;
 result.loss_trades=class="num">0;
 result.profit_trades=class="num">0;
 for(class="type">uint i=class="num">0;i<orders_count;i++)
   {
    class="type">class="kw">double r=Result[i]/_Point;
    if(r>=class="num">0)
      {
       result.gross_profit+=r;
       result.profit_trades++;
         }else{
       result.gross_loss+=r;
       result.loss_trades++;
      }
    }
 result.trades_total=result.loss_trades+result.profit_trades;
 result.net_profit=result.gross_profit+result.gross_loss;
 m_stat.time_proc.Stop();
class="macro">#include <OCL_Patterns\TestPatternsOCL.mqh>
CTestPatterns tpat;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 脚本程序起始函数                                      | 
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
   class="type">class="kw">datetime from=D&class="macro">#x27;class="num">2018.01.class="num">01 class="num">00:class="num">00&class="macro">#x27;;
   class="type">class="kw">datetime to=D&class="macro">#x27;class="num">2018.10.class="num">01 class="num">00:class="num">00&class="macro">#x27;;
class=class="str">"cmt">//--- 设置测试参数
   STR_TEST_PARS pars;
   pars.ref= class="num">60;
   pars.sl = class="num">350;
   pars.tp = class="num">50;
   pars.flags=class="num">15;   class=class="str">"cmt">// 所有形态
   pars.timeout=class="num">12*class="num">3600;
class=class="str">"cmt">//--- 结果结构
   STR_TEST_RESULT res;
class=class="str">"cmt">//--- 运行测试
   tpat.Test(res,from,to,pars);
   STR_ERROR oclerr=tpat.GetLastErrorExt();
   if(oclerr.code)
     {
      Print(oclerr.comment);
      Print("code = ",oclerr.code,", function = ",oclerr.function,", line = ",oclerr.line);
      class="kw">return;
     }
class=class="str">"cmt">//--- 测试结果  
   Print("净利润: ",   res.net_profit);
   Print("总获利: ", res.gross_profit);
   Print("总亏损: ",   res.gross_loss);
   Print("交易总数: ", res.trades_total);
   Print("获利交易数: ",res.profit_trades);
   Print("亏损交易数: ",  res.loss_trades);
class=class="str">"cmt">//--- 执行的统计
   COCLStat ocl_stat=tpat.GetStat();
   Print("GPU 内存大小: ",     ocl_stat.gpu_mem_size.ToStr());
   Print("GPU 内存使用: ",     ocl_stat.gpu_mem_usage.ToStr());
   Print("缓冲: ",          ocl_stat.time_buffering.ToStr());
   Print("OpenCL 初始化: ",       ocl_stat.time_ocl_init.ToStr());
   Print("OpenCL 缓冲: ",     ocl_stat.time_ocl_buf.ToStr());

用 OpenCL 内核并行铺订单与步进

这段内核代码把原本串行的回测准备搬到了 GPU 上。N = Depth*4*SL_count 先算出总任务规模,Depth 是时间柱数,SL_count 是止损档位数,乘 4 是因为每根柱对每个 SL 档要挂买、卖两组共 4 个槽位。 tester_opt_prepare 是二维内核:x 维度对应主周期 Time 索引,y 维度对应 M1 的 TimeM1 索引。它先用 OrderM1[x*SL_count*4]>=0 做去重,已初始化过的柱直接 return,避免重复写缓冲。

当 (Time[x]+shift)==TimeM1[y] 命中时,说明主周期这根柱对齐到了某根 M1 柱。atomic_max 把 y 写进 Count[1] 保留最大 M1 索引,随后内层循环给每个 SL 档写 4 个 int:M1 索引、OP_BUY(i<<2)、M1 索引、OP_SELL(i<<2),左移 2 位是为把 SL 序号编码进操作字。atomic_inc(Count[0]) 统计已处理柱数。

tester_opt_step 内核接着吃 OpenM1/HighM1/LowM1/CloseM1/SpreadM1 这些 M1 行情数组,以及 OrderM1 和 Tasks 缓冲。SpreadM1 直接存价格差而非点数,订单缓冲里 [0] 是 M1 的 OHLC 索引、[1] 是买卖操作字,Tasks 存的是该订单在 OrderM1 里的起始索引——开仓任务由此被 GPU 并行推进。外汇与贵金属杠杆高,这类并行回测只解决速度,不预示任何实盘胜率。

MQL5 / C++
Print("OpenCL 准备订单: ", ocl_stat.time_ocl_orders.ToStr());
Print("OpenCL 测试: ",     ocl_stat.time_ocl_test.ToStr());
Print("OpenCL 中执行: ",ocl_stat.time_ocl_exec.ToStr());
Print("后续处理: ",     ocl_stat.time_proc.ToStr());
Print("总计: ",           ocl_stat.time_total.ToStr());
}
N = Depth*class="num">4*SL_count;
__kernel class="type">void tester_opt_prepare(__global class="type">class="kw">ulong *Time,__global class="type">class="kw">ulong *TimeM1,
                                 __global class="type">int *OrderM1,class=class="str">"cmt">// 订单缓冲区
                                 __global class="type">int *Count,
                                 class="kw">const class="type">int   SL_count,     class=class="str">"cmt">// SL 值的数量
                                 class="kw">const class="type">class="kw">ulong shift)        class=class="str">"cmt">// 时间偏移的秒数
  {
class=class="str">"cmt">//--- 在二维中工作  
  class="type">size_t x=get_global_id(class="num">0); class=class="str">"cmt">//index in Time
  if(OrderM1[x*SL_count*class="num">4]>=class="num">0)
    class="kw">return;
  class="type">size_t y=get_global_id(class="num">1); class=class="str">"cmt">//index in TimeM1
  if((Time[x]+shift)==TimeM1[y])
   {
     class=class="str">"cmt">//--- 按顺序在M1周期中寻找最大的柱索引
     atomic_max(&Count[class="num">1],y);
     class="type">uint offset=x*SL_count*class="num">4;
     for(class="type">int i=class="num">0;i<SL_count;i++)
       {
        class="type">uint idx=offset+i*class="num">4;
        class=class="str">"cmt">//--- 为每个柱加入两个订单 (买入和卖出l)
        OrderM1[idx++]=y;
        OrderM1[idx++]=OP_BUY |(i<<class="num">2);
        OrderM1[idx++]=y;
        OrderM1[idx]  =OP_SELL|(i<<class="num">2);
       }
     atomic_inc(&Count[class="num">0]);
   }
  }
__kernel class="type">void tester_opt_step(__global class="type">class="kw">double *OpenM1,__global class="type">class="kw">double *HighM1,__global class="type">class="kw">double *LowM1,__global class="type">class="kw">double *CloseM1,
                              __global class="type">class="kw">double *SpreadM1,class=class="str">"cmt">// 以价格差别表示,而不是点数
                              __global class="type">class="kw">ulong *TimeM1,
                              __global class="type">int *OrderM1,     class=class="str">"cmt">// 订单缓冲区,其中 [class="num">0] 是在 OHLC(M1) 中的索引, [class="num">1] - (买入/卖出) 操作
                              __global class="type">int *Tasks,       class=class="str">"cmt">// 任务缓冲区 (开启仓位) 保存了订单在 OrderM1 缓冲区中的索引

◍ OpenCL内核里的参数布局与SL位拆解

在 MT5 用 OpenCL 做批量形态扫描时,内核入口的参数排列直接决定显存读写效率。上面这段内核签名把 K 线四个数组 Open/High/Low/Close 与两个结果缓冲 Test、Results 全部以 __global 指针传入,Test 按 2*x*z 分配([0] 买、[1] 卖),Results 按 4*y*z 分配,尺寸不对会在 clEnqueueNDRangeKernel 阶段直接报错。 剩余任务计数 Left 用两个元素区分存储体 0 和 1,这是双缓冲规避竞态的惯用法;bank 指明当前用哪块存储体,start_bar 与 stop_bar 框定本轮要扫的柱区间,maxbar 则是数组边界硬上限,越界读会返回噪声值。 SL 不是单独传参,而是压进 OrderM1 的第二个 uint 里:opsl=OrderM1[(idx*2)+1] 取出操作位与 SL 索引,sli=opsl>>2 右移 2 位拿到索引,再算 SL=(sl_start+sl_step*sli)*point。位 1:0 存方向、位 9:2 存 SL 档位,单 uint 塞下两项,省一次缓冲读取。 外汇与贵金属杠杆高、滑点跳空频繁,这种 GPU 并行的历史回测只反映过去概率,实盘信号失效可能突发,参数请先在策略测试器用历史数据验证。

MQL5 / C++
__global class="type">int *Left,              class=class="str">"cmt">// 剩余任务的数量,两个元素: [class="num">0] - 用于存储体0, [class="num">1] - 用于存储体1
__global class="type">class="kw">double *Res,        class=class="str">"cmt">// 结果缓冲区,取得结果后会立即填充, 
class="kw">const class="type">uint bank,             class=class="str">"cmt">// 当前的存储体                     
class="kw">const class="type">uint orders,           class=class="str">"cmt">// OrderM1 中的订单数量
class="kw">const class="type">uint start_bar,        class=class="str">"cmt">// 已经处理的柱的序列号 (作为在 OrderM1 中指定索引的偏移) - 实际上, 就是在运行内核时循环中的 "i"
class="kw">const class="type">uint stop_bar,         class=class="str">"cmt">// 将要处理的最后一个柱 - 通常等于 &class="macro">#x27;bar&class="macro">#x27;
class="kw">const class="type">uint maxbar,           class=class="str">"cmt">// 可以接收的最大柱索引 (数组中的最后一个柱)
class="kw">const class="type">class="kw">double tp_dP,          class=class="str">"cmt">// 以价格差距表示的 TP 
class="kw">const class="type">uint sl_start,         class=class="str">"cmt">// SL 点数 - 初始值
class="kw">const class="type">uint sl_step,          class=class="str">"cmt">// SL 点数 - 步长
class="kw">const class="type">class="kw">ulong timeout,         class=class="str">"cmt">// 交易生命周期 (描述), 超过之后会强制关闭 
class="kw">const class="type">class="kw">double point)          class=class="str">"cmt">// _Point
SL = (sl_start+sl_step*sli)*point;
class=class="str">"cmt">//--- 操作 (位 class="num">1:class="num">0) 以及 SL 索引 (位 class="num">9:class="num">2)
  class="type">uint opsl=OrderM1[(idx*class="num">2)+class="num">1];
class=class="str">"cmt">//--- 取得 SL 索引  
  class="type">uint sli=opsl>>class="num">2;
__kernel class="type">void find_patterns_opt(__global class="type">class="kw">double *Open,__global class="type">class="kw">double *High,__global class="type">class="kw">double *Low,__global class="type">class="kw">double *Close,
                  __global class="type">class="kw">double *Test,    class=class="str">"cmt">// 每个柱测试结果的缓冲区, 大小为 class="num">2*x*z([class="num">0]-buy, [class="num">1]-sell ... )
                  __global class="type">int *Results,    class=class="str">"cmt">// 结果缓冲区, 大小为 class="num">4*y*z 

「GPU 内核里怎么给形态算账」

这段 OpenCL 内核把形态识别结果直接摊进三维网格:x 是柱索引,y 是参考值序列,z 是止损档位。get_global_id(0~2) 分别取三个维度坐标,depth 被卡在 x_sz-PBARS,避免贴近缓冲区末端时样本不足导致误判。 内核先调 Check() 拿形态标签,若返回 PAT_NONE 直接 return;否则按多空分流写 ri 索引——看跌吞没或熊 Pinbar 落奇数位,其余落偶数位。Test[ri]/point 把浮点结果折回整数分,这一步依赖传入的 point 参数(通常是 _Point/100)。 Results[] 缓冲按 z*y_sz*4+y*4 铺开,每形态占 4 格:总盈利、总亏损、盈利次数、亏损次数。atomic_add 与 atomic_inc 保证并行写不打架。外汇与贵金属杠杆高,这类批量回测只是概率筛查,实盘仍可能连续止损。 Optimize() 在派发任务前先校验参数:sl.step 与 ref.step 必须为正,stop 不能小于 start,否则抛 UERR_OPT_PARS 并退出。m_stat.time_buffering 记录上传时间序列的耗时,方便你对比不同品种的数据吞吐。

MQL5 / C++
class="kw">const class="type">class="kw">double ref_start,   class=class="str">"cmt">// 形态参数
class="kw">const class="type">class="kw">double ref_step,    class=class="str">"cmt">// 
class="kw">const class="type">uint flags,         class=class="str">"cmt">// 寻找哪些形态
class="kw">const class="type">class="kw">double point)       class=class="str">"cmt">// _Point/class="num">100
  {
class=class="str">"cmt">//--- 在三维工作
class=class="str">"cmt">//--- 柱的索引 
   class="type">size_t x=get_global_id(class="num">0);
class=class="str">"cmt">//--- 参考值索引
   class="type">size_t y=get_global_id(class="num">1);
class=class="str">"cmt">//--- SL 值的索引
   class="type">size_t z=get_global_id(class="num">2);
class=class="str">"cmt">//--- 柱数
   class="type">size_t x_sz=get_global_size(class="num">0);
class=class="str">"cmt">//--- 参考值数量
   class="type">size_t y_sz=get_global_size(class="num">1);
class=class="str">"cmt">//--- 止损值数量
   class="type">size_t z_sz=get_global_size(class="num">2);
class=class="str">"cmt">//--- 形态搜索的空间大小  
   class="type">size_t depth=x_sz-PBARS;
   if(x>=depth)class=class="str">"cmt">//不要在接近缓冲区末端开启
      class="kw">return;
class=class="str">"cmt">//
   class="type">uint res=Check(&Open[x],&High[x],&Low[x],&Close[x],ref_start+ref_step*y,flags);
   if(res==PAT_NONE)
      class="kw">return;
class=class="str">"cmt">//--- 计算在 Test[] 缓冲区中的交易结果索引
   class="type">int ri;
   if(res==PAT_PINBAR_BEARISH || res==PAT_ENGULFING_BEARISH) class=class="str">"cmt">//卖出
      ri = (x+PBARS)*z_sz*class="num">2+z*class="num">2+class="num">1;
   else                                                      class=class="str">"cmt">//买入
      ri=(x+PBARS)*z_sz*class="num">2+z*class="num">2;
class=class="str">"cmt">//--- 根据计算得到的索引取得结果,并转换为分
   class="type">int r=Test[ri]/point;
class=class="str">"cmt">//--- 计算在 Results[] 缓冲区中的测试结果索引
   class="type">int idx=z*y_sz*class="num">4+y*class="num">4;
class=class="str">"cmt">//--- 把交易结果加到当前形态
   if(r>=class="num">0)
     {class=class="str">"cmt">//--- profit
      class=class="str">"cmt">//--- 以分为单位合计总利润
      atomic_add(&Results[idx],r);
      class=class="str">"cmt">//--- 增加获利交易的次数
      atomic_inc(&Results[idx+class="num">2]);
     }
   else
     {class=class="str">"cmt">//--- 亏损
      class=class="str">"cmt">//--- 以分为单位统计总的亏损
      atomic_add(&Results[idx+class="num">1],r);
      class=class="str">"cmt">//--- 增加亏损交易次数
      atomic_inc(&Results[idx+class="num">3]);
     }
   }
class="type">bool CTestPatterns::Optimize(STR_TEST_RESULT &result,class="type">class="kw">datetime from,class="type">class="kw">datetime to,STR_OPT_PARS &par)
  {
   ResetLastError();
   if(par.sl.step<=class="num">0 || par.sl.stop<par.sl.start ||
      par.ref.step<=class="num">0 || par.ref.stop<par.ref.start)
     {
      SET_UERR(UERR_OPT_PARS,"优化参数不正确");
      class="kw">return false;
     }
   m_stat.Reset();
   m_stat.time_total.Start();
class=class="str">"cmt">//--- 上传时间序列数据  
   m_stat.time_buffering.Start();

OpenCL 下分块跑通 M1 订单准备内核

在 MT5 中用 OpenCL 加速回测准备时,k_TESTER_OPT_PREPARE 内核采用二维任务空间:0 维是当前时段的订单数(m_sbuf.Depth),1 维是所有 M1 柱(m_tbuf.Depth)。若一次性丢进去,显存和超时都可能爆,所以按 m_prepare_passes 把当前时段切成 prep_step 大小的块轮流跑。 每次循环先算偏移:global_work_offset[0]=p*prep_step 指向当前块,global_work_offset[1]=count[1] 记录已处理的 M1 柱数。M1 侧任务量按 maxshift*global_work_size[0] 估,maxshift=PeriodSeconds()/PeriodSeconds(PERIOD_M1),比如小时图对 M1 就是 60。若超出剩余深度则截断到 sz_max。 内层对 s 从 0 到 maxshift-1 循环,把 ulong(s*60) 设为第 5 个参数后执行内核,再读回 buf_COUNT 里的 count[0]。当 count[0] 等于本块 global_work_size[0] 时说明该块订单已对齐,直接 break 进下一块。 若最终 count[0] 仍不等于 global_work_size[0],说明有订单没匹配上 M1 数据,置 UERR_ORDERS_PREPARE 报错“Failed to prepare M1 orders”。开 MT5 把 m_prepare_passes 从 1 调到 4,可能显著降低大样本回测的 prepare 阶段失败概率,外汇与贵金属回测本身属高风险验证,结果仅作技术参考。

MQL5 / C++
  if(LoadTimeseries(from,to)==false)
      class="kw">return false;
  m_stat.time_buffering.Stop();
class=class="str">"cmt">//--- 初始化 OpenCL
  m_stat.time_ocl_init.Start();
  if(Init(i_MODE_OPTIMIZER)==false)
      class="kw">return false;
  m_stat.time_ocl_init.Stop();
class=class="str">"cmt">//--- 运行优化
  class="type">bool res=optimize(result,from,to,par);
  Deinit();
  buffers_free();
  m_stat.time_total.Stop();
  class="kw">return res;
  }
  class="type">int count[class="num">2]={class="num">0,class="num">0};
  _BufferFromArray(buf_COUNT,count,class="num">0,class="num">2,CL_MEM_READ_WRITE);
  _SetArgumentBuffer(k_TESTER_OPT_PREPARE,class="num">0,buf_TIME);
  _SetArgumentBuffer(k_TESTER_OPT_PREPARE,class="num">1,buf_TIME_M1);
  _SetArgumentBuffer(k_TESTER_OPT_PREPARE,class="num">2,buf_ORDER_M1);
  _SetArgumentBuffer(k_TESTER_OPT_PREPARE,class="num">3,buf_COUNT);
  _SetArgument(k_TESTER_OPT_PREPARE,class="num">4,class="type">int(slc)); class=class="str">"cmt">// SL 数值的数量
class=class="str">"cmt">//--- k_TESTER_OPT_PREPARE 内核有二维的任务空间
  class="type">uint global_work_size[class="num">2];
class=class="str">"cmt">//--- class="num">0 维 - 当前时段的订单 
  global_work_size[class="num">0]=m_sbuf.Depth;
class=class="str">"cmt">//--- class="num">1 维 - 所有的 М1 柱  
  global_work_size[class="num">1]=m_tbuf.Depth;
class=class="str">"cmt">//--- 对于第一次运行,把两个维度上的任务空间偏移都设为0  
  class="type">uint global_work_offset[class="num">2]={class="num">0,class="num">0};
  class="type">int maxshift=PeriodSeconds()/PeriodSeconds(PERIOD_M1);
  class="type">int prep_step=m_sbuf.Depth/m_prepare_passes;
  for(class="type">int p=class="num">0;p<m_prepare_passes;p++)
    {
      class=class="str">"cmt">//当前时段任务空间的偏移
      global_work_offset[class="num">0]=p*prep_step;
      class=class="str">"cmt">// M1 时段任务空间的偏移
      global_work_offset[class="num">1]=count[class="num">1];
      class=class="str">"cmt">//当前时段的任务空间大小
      global_work_size[class="num">0]=(p<(m_prepare_passes-class="num">1))?prep_step:(m_sbuf.Depth-global_work_offset[class="num">0]);
      class=class="str">"cmt">//M1 时段的任务空间大小
      class="type">uint sz=maxshift*global_work_size[class="num">0];
      class="type">uint sz_max=m_tbuf.Depth-global_work_offset[class="num">1];
      global_work_size[class="num">1]=(sz>sz_max)?sz_max:sz;
      class=class="str">"cmt">//
      count[class="num">0]=class="num">0;
      _BufferWrite(buf_COUNT,count,class="num">0,class="num">0,class="num">2);
      for(class="type">int s=class="num">0;s<maxshift;s++)
        {
         _SetArgument(k_TESTER_OPT_PREPARE,class="num">5,class="type">class="kw">ulong(s*class="num">60));
         class=class="str">"cmt">//--- 执行内核
         _Execute(k_TESTER_OPT_PREPARE,class="num">2,global_work_offset,global_work_size);
         class=class="str">"cmt">//--- 读取结果 (数量应该等于 m_sbuf.Depth)
         _BufferRead(buf_COUNT,count,class="num">0,class="num">0,class="num">2);
         if(count[class="num">0]==global_work_size[class="num">0])
            class="kw">break;
        }
      count[class="num">1]++;
    }
  if(count[class="num">0]!=global_work_size[class="num">0])
    {
      SET_UERRt(UERR_ORDERS_PREPARE,"Failed to prepare M1 orders");

◍ 在网格里捞最优参数组合

这段逻辑干的事很直接:在 ref 与 sl 两组参数的笛卡尔积里,扫一遍 OptResults 数组,找出净利润(gross_profit+gross_loss)最大的那一组下标。初始 max_profit 设为 -2147483648,也就是 32 位有符号整型下限,保证任意真实回测结果都能覆盖它。 数组寻址用了 j*refc*4 + i*4 的偏移,每个参数组合占 4 个 int 槽位:分别是获利、亏损、获利笔数、亏损笔数。profit 只取前两个槽相加,即毛利加毛亏,也就是净毛利的点值累加。 定位到最优组合后,把对应下标的获利除以 100 还原成金额(原数组以分为单位存储),笔数直接赋值,net_profit 由两者相加得出。随后用 par.ref.start + idx_ref_best*par.ref.step 反推实际参数值——这意味着你改了 start/step,定位逻辑不用动,只影响最终落地的参数数字。 OnStart 里给了可抄的样例:回测窗口 2018.01.01 到 2018.10.01,ref 与 sl 都是从 15 起步、步长 5、到 510 截止,tp 固定 50 点,超时 12*3600 秒。跑完若 oclerr.code 非零就打印错误位置并退出,否则把净利润、总笔数等六项打印到日志。外汇与贵金属品种用这套跑优化,参数过拟合风险高,实盘前务必换时段交叉验证。

MQL5 / C++
  class="kw">return false;
   }
   class="type">int max_profit=-class="num">2147483648;
   class="type">uint idx_ref_best= class="num">0;
   class="type">uint idx_sl_best = class="num">0;
   for(class="type">uint i=class="num">0;i<refc;i++)
      for(class="type">uint j=class="num">0;j<slc;j++)
        {
         class="type">uint idx=j*refc*class="num">4+i*class="num">4;
         class="type">int profit=OptResults[idx]+OptResults[idx+class="num">1];
         class=class="str">"cmt">//sum+=profit;
         if(max_profit<profit)
           {
            max_profit=profit;
            idx_ref_best= i;
            idx_sl_best = j;
           }
        }
   class="type">uint idx=idx_sl_best*refc*class="num">4+idx_ref_best*class="num">4;
   result.gross_profit=class="type">class="kw">double(OptResults[idx])/class="num">100;
   result.gross_loss=class="type">class="kw">double(OptResults[idx+class="num">1])/class="num">100;
   result.profit_trades=OptResults[idx+class="num">2];
   result.loss_trades=OptResults[idx+class="num">3];
   result.trades_total=result.loss_trades+result.profit_trades;
   result.net_profit=result.gross_profit+result.gross_loss;
class=class="str">"cmt">//---
   par.ref.value= class="type">int(par.ref.start+idx_ref_best*par.ref.step);
   par.sl.value = class="type">int(par.sl.start+idx_sl_best*par.sl.step);
class="macro">#include <OCL_Patterns\TestPatternsOCL.mqh>
CTestPatterns tpat;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 脚本程序起始函数                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
   class="type">class="kw">datetime from=D&class="macro">#x27;class="num">2018.01.class="num">01 class="num">00:class="num">00&class="macro">#x27;;
   class="type">class="kw">datetime to=D&class="macro">#x27;class="num">2018.10.class="num">01 class="num">00:class="num">00&class="macro">#x27;;
class=class="str">"cmt">//--- 设置优化参数
   STR_OPT_PARS optpar;
   optpar.ref.start = class="num">15;
   optpar.ref.step  = class="num">5;
   optpar.ref.stop  = class="num">510;
   optpar.sl.start = class="num">15;
   optpar.sl.step  = class="num">5;
   optpar.sl.stop  = class="num">510;
   optpar.flags=class="num">15;
   optpar.tp=class="num">50;
   optpar.timeout=class="num">12*class="num">3600;
class=class="str">"cmt">//--- 结果结构
   STR_TEST_RESULT res;
class=class="str">"cmt">//--- 运行优化
   tpat.Optimize(res,from,to,optpar);
   STR_ERROR oclerr=tpat.GetLastErrorExt();
   if(oclerr.code)
     {
      Print(oclerr.comment);
      Print("code = ",oclerr.code,", function = ",oclerr.function,", line = ",oclerr.line);
      class="kw">return;
     }
class=class="str">"cmt">//--- 优化参数的数值
   Print("Ref: ",optpar.ref.value,", SL: ",optpar.sl.value);
class=class="str">"cmt">//--- 测试结果  
   Print("净利润: ",  res.net_profit);
   Print("总获利: ", res.gross_profit);
   Print("总亏损: ",  res.gross_loss);
   Print("交易总数: ", res.trades_total);
   Print("获利交易数: ",res.profit_trades);
   Print("亏损交易数: ",  res.loss_trades);
class=class="str">"cmt">//--- 执行的统计

「把 OpenCL 测试耗时拆开看」

在 MT5 里用 OpenCL 跑策略测试,光看总时长不够,得把各阶段拆开才能定位瓶颈。上面这段从 COCLStat 结构里把九个字段逐一 Print 出来,覆盖从显存到后续处理的全链路。 gpu_mem_size 与 gpu_mem_usage 给出显卡可用与已占显存,若 usage 逼近 size,后续缓冲阶段就会明显变慢。time_ocl_init、time_ocl_buf、time_ocl_orders、time_ocl_test、time_ocl_exec 分别对应初始化、传数据、备订单、核函数计算、回读结果,哪一段数字异常高,优化就往哪打。 time_buffering 和 time_proc 是 CPU 侧开销,常被忽略;把它们和 time_total 对照,能算出 GPU 真正省下的比例。开 MT5 跑一遍自己的 EA,把这串打印接在测试结束处,比凭感觉调参实在。

MQL5 / C++
  COCLStat ocl_stat=tpat.GetStat();
  Print("GPU 内存大小: ",      ocl_stat.gpu_mem_size.ToStr());
  Print("GPU 内存使用: ",      ocl_stat.gpu_mem_usage.ToStr());
  Print("缓冲: ",              ocl_stat.time_buffering.ToStr());
  Print("OpenCL 初始化: ",     ocl_stat.time_ocl_init.ToStr());
  Print("OpenCL 缓冲: ",       ocl_stat.time_ocl_buf.ToStr());
  Print("OpenCL 准备订单: ", ocl_stat.time_ocl_orders.ToStr());
  Print("OpenCL 测试: ",       ocl_stat.time_ocl_test.ToStr());
  Print("OpenCL 中执行: ",ocl_stat.time_ocl_exec.ToStr());
  Print("后续处理: ",          ocl_stat.time_proc.ToStr());
  Print("总计: ",              ocl_stat.time_total.ToStr());
}

内建与GPU优化器的耗时对照

把 MT5 内建策略测试器拆成两种跑法:快速(遗传算法)和慢速(全参数枚举),再拉一个基于 OpenCL 的 GPU 测试器做对手。测试机是 AMD FX-8300 八核(分配 6 核给代理)、24.5GB 内存、HDD 装终端,GPU 为 Radeon HD 7950(3GB、800MHz)。三个品种 EURUSD / GBPUSD / USDJPY,各跑 H1 与 M5、时长从 1 月到 9 月不等。 关键现象:快速模式经常漏掉全枚举能命中的更优参数,例如 EURUSD M5 一个月窗口,快速测试器只跑出净利润 47、1 笔交易,而慢速和 OpenCL 都落在 417 附近、39 笔交易。所以公平比较应拿慢速和 OpenCL 对表。 汇总 24 组条件后,OpenCL 的耗时优势极其夸张:慢速测试器在 GBPUSD H1 九个月任务上花了 19 分 23 秒,OpenCL 仅 428.5 毫秒,速率比 2714 倍;最保守的 USDJPY M5 一个月也有 659 倍。外汇与贵金属优化本身属高风险操作,速度提升不预示任何盈利概率。 你在自己机器上验证时,直接开 MT5 终端设 6 个本地代理,再勾选 OpenCL 设备,对同一 EA 跑一次全枚举和一次 GPU 优化,秒表掐一下就能复现这个数量级差距。

◍ 最后一句大实话

这套基于 OpenCL 的自写测试器只是粗估形态用的捷径,硬伤得心里有数:它只跑「1 分钟 OHLC」模式,不计隔夜息和手续费,交叉盘可能算偏,也没有跟踪止损、不管同屏持仓数,回撤更不在返回结果里。 但比起 MT5 内建测试器的全参数枚举,它能快出几千倍;对比遗传算法优化也快几十倍,拿来快速筛简单形态仍很划算。外汇和贵金属杠杆高、滑点跳空频繁,这类粗略回测绝不能直接当实盘依据。 评论区里有人把期权到期逻辑塞进 tester_step,顺手贴了段改法:给期权加 240 小时到期变量,去掉止损止盈分支,到期按十倍止损计亏。真要验证,把下面代码粘进内核编译跑一遍 USDRUB,注意 int 溢出那坑就行。

MQL5 / C++
class="type">class="kw">ulong tcloseDEATH=TimeM1[iO]+class="num">240*class="num">60*class="num">60;class=class="str">"cmt">//添加到期时间变量, например class="num">240 часов (т.е. class="num">10 дней)
class=class="str">"cmt">//......进一步评论 TP 和 SL,因为期权不需要它们
class=class="str">"cmt">/*if(p<=sl)
 {
 Res[idx]=sl-open;
 class="kw">return;
 }
 else if(p>=tp)
 {
 Res[idx]=tp-open;
 class="kw">return;
 }*/
class=class="str">"cmt">// 并添加一个到期时间检查(在 VE 到期时,由于期权不成功,应该会有很大的损失)。
                if(TimeM1[j]>=tcloseDEATH)
                {
                 Res[idx]=sl*class="num">10-open; class=class="str">"cmt">//有一只大麋鹿! (для примера в class="num">10 раз больше установленного при оптимизации стоп-лосса)
                 class="kw">return;
                }
把重复劳动交给小布
多币种回测的并行任务调度与结果汇总,小布盯盘的 AIGC 已内置批处理视角,打开对应品种页即可看到效率对照,你只需判断哪组参数值得手动深挖。

常见问题

两者使用相同的三柱闭合判定逻辑,差异仅在形态扫描由 CPU 循环改为内核并行,若内核读写越界会导致漏检,需用 MQL5 EA 结果交叉校验。
形态在较高周期识别,但成交与止损触发依赖分钟级精度;转到 M1 重放可逼近真实成交序列,避免用合成柱造成的滑点偏差。
加速比受 K 线数量、形态命中密度和 GPU 占用影响;日元点值小、柱数多时内核吞吐优势更明显,具体看效率汇总表的相对倍数。
目前小布提供的是诊断与批处理视图,并不替你编译内核;你可把本文的调度思路喂给小布生成观察模板,核心计算仍在本地终端执行。
关于环境搭建与内核语法的完整讨论见《使用 OpenCL 测试烛形形态·基础篇》,本篇默认读者已熟悉 1.2 规范。