使用 OpenCL 测试烛形形态(基础篇)
📘

使用 OpenCL 测试烛形形态(基础篇)

第 1/3 篇

用 OpenCL 给烛形形态识别提速

在 MT5 里做烛形形态扫描,单线程遍历历史 K 线在品种多、周期细时容易卡顿。把形态判定逻辑丢进 OpenCL 内核,让 GPU 并行比对每根烛线的开盘收口与影线比例,回测吞吐可以明显抬升。 作者 Serhii Shevchuk 在 2019 年 1 月 21 日发布的示例里,原文页面记录 2 945 次浏览与 29 条讨论,说明这套离屏计算思路在社群里有人真去跑过。外汇与贵金属杠杆高,任何形态信号都只是概率倾向,上 GPU 只是省时间,不代表信号更准。 开 MT5 接好 Intel / AMD 的 OpenCL 运行库后,先拿 EURUSD 的 M15 做小样本验证:把吞没形态判定写成内核,对比 CPU 循环耗时可观察帧级差异。

「用 OpenCL 给烛形形态回测提速的切入点」

很多交易者刚摸熟 OpenCL 就卡在一个现实问题上:指标或 EA 开发里很少用到矩阵乘法、大批量排序这类教科书示例,直接上神经网络又得补一堆前置知识,投入产出比不透明。想拿并行计算解决交易里的「基本任务」,反而找不到顺手的入口。 这篇要做的,是用 OpenCL 跑一个朴素但能落地的活儿——在历史数据里搜烛形形态并做单次通过测试,同时在「1分钟OHLC」模式下优化两个参数。最后拿内建策略测试器和 OpenCL 版本比耗时,看后者快多少、快在哪。 阅读前提是你已经懂 OpenCL 基础,不然得先补并行编程的入门资料,本文只聚焦测试器算法本身,不回头讲内核语法。后续章节会先给 MQL5 原生实现,再切到 OpenCL 重写数据更新、形态搜索与结果回收,并在 EURUSD、GBPUSD、USDJPY 上实测效率。外汇与贵金属品种波动剧烈、杠杆风险高,回测提速不等于实盘胜率提升,结论仅反映计算耗时差异。

◍ 用 MQL5 把针杆和吞噬形态写成可回测 EA

要验证 OpenCL 测试器是否靠谱,得先有一个常规路径下能跑的 MQL5 EA 做对照。策略本身不复杂:看跌针杆或看跌吞噬就卖,看涨针杆或看涨吞噬就买,仓位数量不限制,最大持仓时间和固定止盈止损都由用户输入,只在每根新柱出现时回头检查前三根柱是否成形态。 形态判定全部基于柱体几何关系。以看跌针杆为例,第一根柱上影线 tail = High[1]-max(Open[1],Close[1]) 必须 ≥ 参考值,且第零柱收涨、第二柱收跌、第一柱最高价为局部最大、实体小于上影线;看涨针杆把上下影线镜像处理即可。吞噬类则要求第一根趋势柱实体 ≥ 参考值,且第二根柱的开收盘完全反包第一根主体。 代码里用 ENUM_PATTERN 枚举配合位或标志传参,Check() 在被 IsPattern() 调用时逐条比对。下面这段是形态识别核心,H/O/L/C 是宏,分别对应 High/Open/Low/Close 的索引访问: EA 构建时把参考值以点数形式作为输入,四个形态开关用 bool 参数再在 OnInit 里拼成 uint 掩码;持仓时限以小时计,止盈写死 50 点,止损可调。交易走标准库 CTrade,测速用 CDuration 量 OnInit 到 OnDeinit 的毫秒间隔。OnTick 只做两件事:超时就平仓,新柱就查形态并开仓。 实盘前先在 EURUSD M5、2018.1.1–10.1 跑遗传优化,OHLC 1 分钟模式。参考值 60 点、止损 350 点时跑出 1000.50 利润居首,单遍测试耗时记下后,才拿去和 OpenCL 自写测试器比对。外汇与贵金属杠杆高,回测盈利不代表实盘概率同向,参数须重验。

MQL5 / C++
ENUM_PATTERN Check(class="type">MqlRates &r[],class="type">uint flags,class="type">class="kw">double ref)
  {
class=class="str">"cmt">//--- 看跌针杆  
   if((flags&PAT_PINBAR_BEARISH)!=class="num">0)
     {class=class="str">"cmt">//
      class="type">class="kw">double tail=H(class="num">1)-MathMax(O(class="num">1),C(class="num">1));
      if(tail>=ref && C(class="num">0)>O(class="num">0) && O(class="num">2)>C(class="num">2) && H(class="num">1)>MathMax(H(class="num">0),H(class="num">2)) && MathAbs(O(class="num">1)-C(class="num">1))<tail)
        class="kw">return PAT_PINBAR_BEARISH;
     }
class=class="str">"cmt">//--- 看涨针杆
   if((flags&PAT_PINBAR_BULLISH)!=class="num">0)
     {class=class="str">"cmt">//
      class="type">class="kw">double tail=MathMin(O(class="num">1),C(class="num">1))-L(class="num">1);
      if(tail>=ref && O(class="num">0)>C(class="num">0) && C(class="num">2)>O(class="num">2) && L(class="num">1)<MathMin(L(class="num">0),L(class="num">2)) && MathAbs(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))

把形态识别接进EA的开关与超时平仓

这段代码片段展示了一个基于价格行为形态(Pin Bar / 吞没)的EA骨架:用位运算枚举模式,再用输入参数决定开启哪些形态信号。PAT_PINBAR_BULLISH 到 PAT_ENGULFING_BULLISH 分别对应 1<<0 至 1<<3,四个开关可独立组合,p_flags 按勾选做按位或叠加。 宏定义 O(i) H(i) L(i) C(i) 把 MqlRates 数组成员的读写缩写掉,Check() 里直接 O(1)、C(2) 这样比价,回测时改形态阈值只动 ref 不动逻辑。inp_ref 默认 50,意味着以 50 点(_Point 倍数)作为影线/实体的参考尺度,黄金和欧美在同一 EA 下因 _Point 不同实际阈值差出数量级,切品种要重设。 OnTick 里那段是硬止损时间的实现:仓位开仓时间 t0 加上 inp_timeout*3600 秒,超过就 PositionClose。inp_timeout 默认 5 小时,属于偏日内剥头皮的持仓上限,外汇与贵金属杠杆高、跳空频繁,这套超时机制只能控时长、控不住滑点风险。 复制时留意原文重复声明了 inp_bullish_pin_bar 等 input 两次,编译会报重定义,实盘前删掉其中一组。打开 MT5 新建 EA,把这段粘进 OnInit/OnTick,先只勾 bullish engulfing、inp_timeout 设 1,跑模拟盘看触发频率再放大参数。

MQL5 / C++
   class="kw">return PAT_ENGULFING_BULLISH;
    }
class=class="str">"cmt">//--- 没有找到   
   class="kw">return PAT_NONE;
   }
enum ENUM_PATTERN
   {
   PAT_NONE=class="num">0,
   PAT_PINBAR_BEARISH = (class="num">1<<class="num">0),
   PAT_PINBAR_BULLISH = (class="num">1<<class="num">1),
   PAT_ENGULFING_BEARISH = (class="num">1<<class="num">2),
   PAT_ENGULFING_BULLISH = (class="num">1<<class="num">3)
   };
class="macro">#define O(i) (r[i].open)
class="macro">#define H(i) (r[i].high)
class="macro">#define L(i) (r[i].low)
class="macro">#define C(i) (r[i].close)
ENUM_PATTERN IsPattern(class="type">uint flags,class="type">uint ref)
   {
   class="type">MqlRates r[];
   if(CopyRates(_Symbol,_Period,class="num">1,PBARS,r)<PBARS)
      class="kw">return class="num">0;
   ArraySetAsSeries(r,false);
   class="kw">return Check(r,flags,class="type">class="kw">double(ref)*_Point);
   }
class="kw">input class="type">int      inp_ref=class="num">50;
class="kw">input class="type">bool     inp_bullish_pin_bar = true;
class="kw">input class="type">bool     inp_bearish_pin_bar = true;
class="kw">input class="type">bool     inp_bullish_engulfing = true;
class="kw">input class="type">bool     inp_bearish_engulfing = true;
   p_flags = class="num">0;
   if(inp_bullish_pin_bar==true)
      p_flags|=PAT_PINBAR_BULLISH;
   if(inp_bearish_pin_bar==true)
      p_flags|=PAT_PINBAR_BEARISH;
   if(inp_bullish_engulfing==true)
      p_flags|=PAT_ENGULFING_BULLISH;
   if(inp_bearish_engulfing==true)
      p_flags|=PAT_ENGULFING_BEARISH;
class="kw">input class="type">int      inp_timeout=class="num">5;
class="kw">input class="type">bool     inp_bullish_pin_bar = true;
class="kw">input class="type">bool     inp_bearish_pin_bar = true;
class="kw">input class="type">bool     inp_bullish_engulfing = true;
class="kw">input class="type">bool     inp_bearish_engulfing = true;
class="kw">input class="type">class="kw">double   inp_lot_size=class="num">1;
CDuration time;
class="type">int OnInit()
   {
   time.Start();
   class=class="str">"cmt">// ... 
   class="kw">return(INIT_SUCCEEDED);
   }
class="type">void OnDeinit(class="kw">const class="type">int reason)
   {
   time.Stop();
   Print("测试结束 "+time.ToStr());
   }
class="type">void OnTick()
   {
class=class="str">"cmt">//--- 处理开启的仓位
   class="type">int total= PositionsTotal();
   for(class="type">int i=class="num">0;i<total;i++)
    {
      PositionSelect(_Symbol);
      class="type">class="kw">datetime t0=class="type">class="kw">datetime(PositionGetInteger(POSITION_TIME));
      if(TimeCurrent()>=(t0+(inp_timeout*class="num">3600)))
       {
        trade.PositionClose(PositionGetInteger(POSITION_TICKET));
       }
      else
       class="kw">break;
    }

「形态确认后的下单逻辑」

这段执行代码放在 OnTick 里,先卡掉非新 K 线:若 IsNewBar() 返回 false 直接 return,避免一根 bar 内重复触发。 接着调用 IsPattern(p_flags,inp_ref) 判定当前是否出现预设的蜡烛形态,返回 PAT_NONE 同样退出,不浪费后续计算。 拿到 ask 与 bid 后,用位运算 pat&(PAT_ENGULFING_BULLISH|PAT_PINBAR_BULLISH) 判断是否吞没或 Pinbar 看涨;非零就 trade.Buy,SL 设在 ask - inp_sl*_Point,TP 在 ask + inp_tp*_Point,全部 NormalizeDouble 到品种小数位。 否则走 trade.Sell,卖单 SL 在 bid + inp_sl*_Point、TP 在 bid - inp_tp*_Point。外汇与贵金属杠杆高,实盘前务必在 MT5 策略测试器用历史数据验证 inp_sl/inp_tp 点位合理性,形态误判可能造成连续小额止损。

MQL5 / C++
  if(IsNewBar()==false)
      class="kw">return;
class=class="str">"cmt">//--- 检查形态是否存在
   ENUM_PATTERN pat=IsPattern(p_flags,inp_ref);
   if(pat==PAT_NONE)
      class="kw">return;
class=class="str">"cmt">//--- 开启仓位
   class="type">class="kw">double ask=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   class="type">class="kw">double bid=SymbolInfoDouble(_Symbol,SYMBOL_BID);
   if((pat&(PAT_ENGULFING_BULLISH|PAT_PINBAR_BULLISH))!=class="num">0)class=class="str">"cmt">//买入    
      trade.Buy(inp_lot_size,_Symbol,ask,NormalizeDouble(ask-inp_sl*_Point,_Digits),NormalizeDouble(ask+inp_tp*_Point,_Digits),DoubleToString(ask,_Digits));
   elseclass=class="str">"cmt">//卖出
      trade.Sell(inp_lot_size,_Symbol,bid,NormalizeDouble(bid+inp_sl*_Point,_Digits),NormalizeDouble(bid-inp_tp*_Point,_Digits),DoubleToString(bid,_Digits));
   }

◍ 用 COpenCLx 把报错信息榨干

直接调标准库的 COpenCL 在排错时很憋屈:一旦内核创建失败,你只能拿到一个干瘪的索引值。我这边做法是派生一个 COpenCLx 类,把内核、缓冲区全改成枚举类型,这样 EnumToString() 抛出来的错误信息能直接告诉你“是哪个规格的内核、从哪行调进来的”,而不是一个看不懂的数字。 类里用四个宏来接管错误记录:SET_ERR 记最近错误加调用行;SET_ERRx 额外把外层函数名和调用串传进来,内核里报错时你能顺藤摸瓜找到是谁发起的;SET_UERR 和 SET_UERRx 则专门接自定义错误码。对比原生类一行控制台输出,COpenCLx 会多打两行——规格名加调用栈,调试时省一半猜的时间。 封装方法容易让代码肿起来,比如创建缓冲区原本要塞一堆存在性检查、空闲内存检查(几百兆显存得用 CMemsize 盯着),可读性很差。解决办法是把这些封装全收进 _BufferCreate、_KernelCreate 这类宏里,业务代码就剩一行调用。注意 _KernelCreate 宏以 break 结束而非 return false,写多通过程逻辑时得留心。 初始化方法除了 new 出 COpenCL 对象,还会测设备对 'double' 的支持、建内核、探可用显存;mode 参数切优化或单次测试,决定加载哪套内核。内核源码以 cl_tester 资源串附在 OCLInc.mqh,枚举也在那定义。Deinit() 负责拆程序和对象,别漏。

常见问题

把形态匹配计算搬到右值并行设备执行,用 OpenCL 拆分逐根 K 线的判定循环,能明显减轻主线程负担。
容易漏掉形态确认后的下一根开盘过滤,以及影线比例阈值;建议把这两个参数外置,方便回测调优。
可以,小布能按你设定的形态规则在对应品种页直接标注并推送,省去自己搭回测框架的重复劳动。
加一个形态开关状态位,确认后只触发一次下单,并用超时平仓限制持仓时长,防止信号漂移。
用封装好的错误提取接口把内核日志逐条读出来,按报错的全局索引反推对应的烛形序号再排查。