MetaTrader 5 中的并行计算·进阶篇
(2/3)·多核不等于自动加速,MT5 里手动切线程才是实盘提速的关键一步
◍ 把相似序列策略改造成多核 EA
选一个适合并行化的思路:取最近 N 根柱构成价格序列,在历史里找最相近的一段,看当时价格往哪偏就开对应方向的单。序列短的时候 MT5 里几秒就算完,但把长度拉到 M1 最近 24 小时(1440 根柱)、回搜约 375,000 根柱的一年历史,单线程会卡很久。 这类搜索天然可切分——按 CPU 核心数把历史分段,每个核心只搜自己那段就行。我们搭的 e-MultiThread EA 靠一个 CM 指标在 OnInit 里跑无限循环做并行计算,EA 和指标之间用终端全局变量传数据,并且 EA 能通过参数切换“普通模式”和“多核指标模式”。 下面这段 EA 源码里,Threads 输入项直接决定用几个核;PatternLen=1440、HistoryLen=375000 就是上面说的压力参数。HistNeed 的计算把各段长度加总,若终端 MaxBars 不够就报错让你改设置重启,这是实盘前必须核对的一点。 [CODE] 里的 OnInit 前半段就是在卡历史长度:Bars 不足就循环 CopyClose 补齐。外汇和贵金属波动剧烈、杠杆高风险大,多核只是提速,不意味信号胜率提升,回测也要防过拟合。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| e-MultiThread.mq5 |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">input</span> <span class="keyword">class="type">int</span> Threads=<span class="number">class="num">1</span>; <span class="comment">class=class="str">"cmt">// 应该使用多少个内核</span> <span class="keyword">input</span> <span class="keyword">class="type">int</span> MagicNumber=<span class="number">class="num">0</span>; <span class="comment">class=class="str">"cmt">// 策略参数</span> <span class="keyword">input</span> <span class="keyword">class="type">int</span> PatternLen = <span class="number">class="num">1440</span>; <span class="comment">class=class="str">"cmt">// 分析序列 (模式) 的长度</span> <span class="keyword">input</span> <span class="keyword">class="type">int</span> PrognozeLen = <span class="number">class="num">60</span>; <span class="comment">class=class="str">"cmt">// 预测长度 (柱数)</span> <span class="keyword">input</span> <span class="keyword">class="type">int</span> HistoryLen = <span class="number">class="num">375000</span>; <span class="comment">class=class="str">"cmt">// 搜索的历史长度</span> <span class="keyword">input</span> <span class="keyword">class="type">class="kw">double</span> Lots=<span class="number">class="num">0.1</span>; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class</span> IndData { <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">int</span> ts,te; <span class="keyword">class="type">class="kw">datetime</span> start_time; <span class="keyword">class="type">class="kw">double</span> prognoze,rating; }; IndData Calc[]; <span class="keyword">class="type">class="kw">double</span> CurPattern[]; <span class="keyword">class="type">class="kw">double</span> Prognoze; <span class="keyword">class="type">int</span> HistPatternBarStart; <span class="keyword">class="type">int</span> ExistsPrognozeLen; <span class="keyword">class="type">uint</span> TicksStart,TicksEnd; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="preprocessor">class="macro">#include <ThreadCalc.mqh></span> <span class="preprocessor">class="macro">#include <Trade\Trade.mqh></span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">int</span> <span class="functions">OnInit</span>() { <span class="keyword">class="type">class="kw">double</span> rates[]; <span class="comment">class=class="str">"cmt">//--- 确保有足够的历史</span> <span class="keyword">class="type">int</span> HistNeed=HistoryLen+Threads+PatternLen+PatternLen+PrognozeLen-<span class="number">class="num">1</span>; <span class="keyword">if</span>(<span class="functions">TerminalInfoInteger</span>(<span class="keyword">TERMINAL_MAXBARS</span>)<HistNeed) { <span class="functions">Print</span>(<span class="class="type">class="kw">string">"修改终端设定 \". 图表上的最大柱数\" 为指定值, 不能小于 "</span>, HistNeed,<span class="class="type">class="kw">string">" 并重新启动终端"</span>); <span class="keyword">class="kw">return</span>(<span class="number">class="num">1</span>); } <span class="keyword">class="kw">while</span>(<span class="functions">Bars</span>(<span class="predefines">_Symbol</span>,<span class="predefines">_Period</span>)<HistNeed) { <span class="functions">Print</span>(<span class="class="type">class="kw">string">"终端中的历史长度不足 ("</span>,<span class="functions">Bars</span>(<span class="predefines">_Symbol</span>,<span class="predefines">_Period</span>),<span class="class="type">class="kw">string">") , 上传中..."</span>); <span class="functions">CopyClose</span>(<span class="predefines">_Symbol</span>,<span class="predefines">_Period</span>,<span class="number">class="num">0</span>,HistNeed,rates); } <span class="functions">Print</span>(<span class="class="type">class="kw">string">"终端中的历史长度: "</span>,<span class="functions">Bars</span>(<span class="predefines">_Symbol</span>,<span class="predefines">_Period</span>)); <span class="comment">class=class="str">"cmt">//--- 为多核模式创建计算指标</span> <span class="keyword">if</span>(Threads><span class="number">class="num">1</span>) { GlobalVarPrefix=<span class="class="type">class="kw">string">"MultiThread_"</span>+<span class="functions">IntegerToString</span>(MagicNumber)+<span class="class="type">class="kw">string">"_"</span>; <span class="functions">GlobalVariablesDeleteAll</span>(GlobalVarPrefix); <span class="functions">ArrayResize</span>(Calc,Threads); <span class="comment">class=class="str">"cmt">// 每个核心的历史长度</span>
「多线程历史匹配的代码骨架」
这段逻辑把历史搜索拆给多个指标线程跑,先按线程数切分历史长度:HistPartLen 用 MathCeil(HistoryLen/Threads) 向上取整,再补上 PatternLen+PrognozeLen-1 得到含边界的 HistPartLenPlus,避免序列头尾被截断。 创建线程指标时有个坑:每个 iCustom 必须挂在非当前图的货币对上,do-while 循环靠 SymbolName 递增跳过 _Symbol,否则 EA 和指标同图会抢数据。句柄若返回 INVALID_HANDLE 直接 return(1) 终止,实战里建议先 Print 出来确认哪一对失败了。 OnTick 里先用 CopyClose 填 CurPattern,不足 PatternLen 就 Sleep(1000) 轮询,再跑 CalcPrognozeLen 和 FindHistoryPrognoze。多线程版 FindHistoryPrognoze 给每个线程 SetParam 发 PrognozeLen 和 Query,然后 Sleep(100) 等 Answer 回包——这里 100 毫秒轮询在高频tick下可能堆积,自己压测时把间隔调到 10~50 看看延迟。 外汇与贵金属杠杆高,多线程历史匹配只是加速近似搜索,匹配结果对后市仅具概率倾向,不能直接当成交依据。
class="type">int HistPartLen=MathCeil(HistoryLen/Threads); class=class="str">"cmt">// 向上取整切分历史长度 class="type">int HistPartLenPlus=HistPartLen+PatternLen+PrognozeLen-class="num">1; class=class="str">"cmt">// 含边界序列长度 class="type">class="kw">string s; class="type">int snum=class="num">0; class=class="str">"cmt">// 创建所有用于计算的指标 for(class="type">int t=class="num">0;t<Threads;t++) { class=class="str">"cmt">// 对于每个指标 - 它们自己的货币对, class=class="str">"cmt">// 它不应该和EA交易相同 do s=SymbolName(snum++,class="kw">false); class="kw">while(s==_Symbol); class="type">int handle=iCustom(s,_Period,"i-Thread", GlobalVarPrefix,t,_Symbol,PatternLen, PatternLen+t*HistPartLen,HistPartLenPlus); if(handle==INVALID_HANDLE) class="kw">return(class="num">1); Print("指标已创建, 货币对 ",s,", 句柄 ",handle); } } class="kw">return(class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { TicksStart=GetTickCount(); class=class="str">"cmt">// 使用最近的柱填充序列 class="kw">while(CopyClose(_Symbol,_Period,class="num">0,PatternLen,CurPattern)<PatternLen) Sleep(class="num">1000); class=class="str">"cmt">// 如果有开启的仓位, 衡量它的 "年龄" class=class="str">"cmt">// 并且为交易的计划剩余时间 class=class="str">"cmt">// 修改预测范围 CalcPrognozeLen(); class=class="str">"cmt">// 寻找历史中最接近的序列 class=class="str">"cmt">// 并且以此为基础预测价格的移动 FindHistoryPrognoze(); class=class="str">"cmt">// 进行必要的交易操作 Trade(); TicksEnd=GetTickCount(); class=class="str">"cmt">// 记录调试信息 PrintReport(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void FindHistoryPrognoze() { Prognoze=class="num">0; class="type">class="kw">double MaxRating; if(Threads>class="num">1) { class=class="str">"cmt">//-------------------------------------- class=class="str">"cmt">// 使用计算指标 class=class="str">"cmt">//-------------------------------------- class=class="str">"cmt">// 遍历所有记录用指标 for(class="type">int t=class="num">0;t<Threads;t++) { class=class="str">"cmt">// 发送计算任务的参数 SetParam(t,"PrognozeLen",ExistsPrognozeLen); class=class="str">"cmt">// "开始计算" 信号 SetParam(t,"Query"); } for(class="type">int t=class="num">0;t<Threads;t++) { class=class="str">"cmt">// 等待结果 class="kw">while(!ParamExists(t,"Answer")) Sleep(class="num">100); DelParam(t,"Answer"); class=class="str">"cmt">// 取得结果
从指标取值到持仓止损的闭环逻辑
这段逻辑把图形识别结果转成可交易的数值:先用 GetParam 取出 Prognoze(方向预测值)、Rating(置信评分)和 PatternStart(形态起点索引),再用 CopyTime 把起点对应的 K 线时间写进 Calc 数组。若当前评分高于已记录的最大值,或这是第一笔候选,就把该评分与预测值暂存为最优解,后续只认最高评分那一套。 当指标未被调用时,EA 会在本地直接跑 FindPrognoze 重新算一遍,把结果塞进数据流,不依赖外部缓存。这里要注意:HistoryLen 和 ExistsPrognozeLen 的取值直接影响回看深度,改小前者可能让形态漏判。 CalcPrognozeLen 负责对齐已开仓的存续时间。若当前有持仓,它用 PositionGetInteger(POSITION_TIME) 取开仓时间,通过两次 CopyTime 数出经过的柱数 poslen;若 poslen 小于 PrognozeLen,则 ExistsPrognozeLen = PrognozeLen - poslen,否则归零。这意味着持仓越久,留给新预测的统计窗口越短。 Trade 函数做反向清仓:持多单且 Prognoze <= 0,或持空单且 Prognoze >= 0 时,调 CTrade.PositionClose 平掉。外汇与贵金属杠杆高,这类机械平仓不保证拐点正确,实盘前请在 MT5 策略测试器用历史数据验证信号反转频率。
class="type">class="kw">double progn = GetParam(t, "Prognoze"); class="type">class="kw">double rating = GetParam(t, "Rating"); class="type">class="kw">datetime time[]; class="type">int start=GetParam(t,"PatternStart"); CopyTime(_Symbol,_Period,start,class="num">1,time); Calc [t].prognoze = progn; Calc [t].rating = rating; Calc [t].start_time = time[class="num">0]; Calc [t].ts = GetParam(t, "TS"); Calc [t].te = GetParam(t, "TE"); class=class="str">"cmt">// 选择最佳结果 if((t==class="num">0) || (rating>MaxRating)) { MaxRating = rating; Prognoze = progn; } } } else { class=class="str">"cmt">//---------------------------- class=class="str">"cmt">// 没有使用的指标 class=class="str">"cmt">//---------------------------- class=class="str">"cmt">// 在EA交易中计算所有内容, 放入数据流中 FindPrognoze(_Symbol,CurPattern,class="num">0,HistoryLen,ExistsPrognozeLen, Prognoze,MaxRating,HistPatternBarStart); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CalcPrognozeLen() { ExistsPrognozeLen=PrognozeLen; class=class="str">"cmt">// 如果有开启的仓位, 确定 class=class="str">"cmt">// 自从开启以来经过了多少个柱 if(PositionSelect(_Symbol)) { class="type">class="kw">datetime postime=PositionGetInteger(POSITION_TIME); class="type">class="kw">datetime curtime,time[]; CopyTime(_Symbol,_Period,class="num">0,class="num">1,time); curtime=time[class="num">0]; CopyTime(_Symbol,_Period,curtime,postime,time); class="type">int poslen=ArraySize(time); if(poslen<PrognozeLen) ExistsPrognozeLen=PrognozeLen-poslen; else ExistsPrognozeLen=class="num">0; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Trade() { class=class="str">"cmt">// 平掉开启的仓位, 假如和预测不符 if(PositionSelect(_Symbol)) { class="type">long type=PositionGetInteger(POSITION_TYPE); class="type">bool close=class="kw">false; if((type == POSITION_TYPE_BUY) && (Prognoze <= class="num">0)) close = true; if((type == POSITION_TYPE_SELL) && (Prognoze >= class="num">0)) close = true; if(close) { CTrade trade; trade.PositionClose(_Symbol);
◍ 无仓时按预测方向开单与回测报告打印
这段逻辑只干一件事:当前账户里没有本符号持仓,且预测值不为零时,才允许进场。Prognoze 大于 0 发 Buy,小于 0 发 Sell,手数由 Lots 控制;若已持仓则直接跳过,避免重复加仓。 PrintReport 函数在 EA 停止或测试结束时输出诊断信息。单线程下会打印「没有使用的指标」并通过 CopyTime 取历史序列起点时间;多线程(Threads>1)则逐指标列出预测值、评分、历史序列起始时间与各线程耗时(毫秒级),方便你核对并行计算的负载分布。 OnDeinit 里给每个指标线程发送 End 命令,确保子进程干净退出,不残留全局变量。外汇与贵金属杠杆高,这类预测开仓逻辑回测胜率随样本漂移可能明显变化,实盘前务必在 MT5 策略测试器用至少 3 个月 Tick 数据验证。 把下面片段直接贴进 EA 的 OnTick 尾段,就能复现「无仓才开」的动作;改 Prognoze 阈值可过滤弱信号。
class=class="str">"cmt">// 如果没有仓位, 根据预测建立仓位 if((Prognoze!=class="num">0) && (!PositionSelect(_Symbol))) { CTrade trade; if(Prognoze > class="num">0) trade.Buy(Lots); if(Prognoze < class="num">0) trade.Sell(Lots); }
「指标常驻监听与EA任务接力」
这段自定义指标不走常规 OnCalculate 算价路线,而是靠 OnInit 里的无限 while 循环常驻内存,专门等 EA 发来的全局变量命令。外汇与贵金属市场跳空频繁,这种异步架构能把形态匹配的重活从 EA 主线程剥离,降低卡顿概率,但长期挂指标也吃 MT5 终端资源,需自行权衡。 输入参数先框定工作范围:DataSymbol 指定货币对,PatternLen 是拿来比对的近期收盘序列长度,BarStart 与 BarCount 决定往历史回看多少根柱。缓冲区 Buffer 与 CurPattern 分别承接输出与当前形态载体,并借 ThreadCalc.mqh 头文件做跨线程参数读写。 循环里先用 ParamExists 探 ThreadNum 下的 End 与 Query 标记:收到 End 就 break 退出;没 Query 就 Sleep(100) 空转继续。一旦拿到 Query,立刻清掉标记、记录 GetTickCount 起点,从 GetParam 取 PrognozeLen,再用 CopyClose 把最近 PatternLen 根收盘塞进 CurPattern,不足就每秒重试。 核心计算交给 FindPrognoze,跑完把 Prognoze、Rating、PatternStart 及起止 ticks 写回全局变量,最后置 Answer 标记通知 EA 取数。OnCalculate 仅返回 0 做空壳,OnDeinit 留了接口处理退出。开 MT5 把这段贴进指标文件,配好 ThreadCalc.mqh,就能看它怎么替 EA 跑后台搜索。
input class="type">class="kw">string DataSymbol; class=class="str">"cmt">// MM-EA 工作的货币对 input class="type">int PatternLen; class=class="str">"cmt">// 分析序列的长度 input class="type">int BarStart; class=class="str">"cmt">// 从历史的哪个柱开始搜索类似序列 input class="type">int BarCount; class=class="str">"cmt">// 在历史中搜索多少个柱 class=class="str">"cmt">//--- 指标缓冲区 class="type">class="kw">double Buffer[]; class=class="str">"cmt">//--- class="type">class="kw">double CurPattern[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <ThreadCalc.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnInit() { SetIndexBuffer(class="num">0,Buffer,INDICATOR_DATA); GlobalVarPrefix=VarPrefix; class=class="str">"cmt">// 无限循环 - 这样指标就会一直 "监听", class=class="str">"cmt">// 为了来自EA的新命令 class="kw">while(true) { class=class="str">"cmt">// 如果有结束命令, 结束指标的工作 if(ParamExists(ThreadNum,"End")) class="kw">break; class=class="str">"cmt">// 等待信号以开始计算 if(!ParamExists(ThreadNum,"Query")) { Sleep(class="num">100); class="kw">continue; } DelParam(ThreadNum,"Query"); class="type">uint TicksStart=GetTickCount(); class=class="str">"cmt">// 取得任务的参数 class="type">int PrognozeLen=GetParam(ThreadNum,"PrognozeLen"); class=class="str">"cmt">// 从最近的柱填充序列 class="kw">while(CopyClose(DataSymbol,_Period,class="num">0,PatternLen,CurPattern) <PatternLen) Sleep(class="num">1000); class=class="str">"cmt">// 进行计算 class="type">int HistPatternBarStart; class="type">class="kw">double Prognoze,Rating; FindPrognoze(DataSymbol,CurPattern,BarStart,BarCount,PrognozeLen, Prognoze,Rating,HistPatternBarStart); class=class="str">"cmt">// 发送计算的结果 SetParam(ThreadNum,"Prognoze",Prognoze); SetParam(ThreadNum,"Rating",Rating); SetParam(ThreadNum,"PatternStart",HistPatternBarStart); SetParam(ThreadNum,"TS",TicksStart); SetParam(ThreadNum,"TE",GetTickCount()); class=class="str">"cmt">// 信号 "万事俱备" SetParam(ThreadNum,"Answer"); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double &price[]) { class=class="str">"cmt">// 事件所需的处理函数 class="kw">return(class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) {