使用 OpenCL 测试烛形形态·综合运用
(3/3)·从 MQL5 基准到 OpenCL 内核,三币种优化实测看清并行回测的真实收益边界
「用 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 五分钟图一晚扫出几十个信号的概率分布,外汇和贵金属杠杆高,信号仅作概率参考。
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] 是否等于你的订单总数,就能验证映射有没有漏柱。
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)价差,能立刻看到千级订单的并行退出分布。
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 跑出的漂亮曲线仅作概率参考。
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 决定。
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 回测仍属高风险验证,结果仅作概率参考。
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 参数就能复验。
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 并行推进。外汇与贵金属杠杆高,这类并行回测只解决速度,不预示任何实盘胜率。
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 并行的历史回测只反映过去概率,实盘信号失效可能突发,参数请先在策略测试器用历史数据验证。
__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 记录上传时间序列的耗时,方便你对比不同品种的数据吞吐。
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 阶段失败概率,外汇与贵金属回测本身属高风险验证,结果仅作技术参考。
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 非零就打印错误位置并退出,否则把净利润、总笔数等六项打印到日志。外汇与贵金属品种用这套跑优化,参数过拟合风险高,实盘前务必换时段交叉验证。
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,把这串打印接在测试结束处,比凭感觉调参实在。
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 溢出那坑就行。
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; }