您应当知道的 MQL5 向导技术(第 17 部分):多币种交易·进阶篇
(2/3)·单一品种 EA 遇多币震荡就裸奔?向导类改两处即可并行拆杠杆
「EA 初始化时的周期与品种校验逻辑」
在 MT5 的 EA 基类里,Init 函数第一道关卡就是周期一致性:若传入的 period 与当前图表 Period() 不符,直接 PrintFormat 报错并返回 false,EA 不会在错误周期上跑。 如果启用了定时器(m_on_timer_process 为真),还会调用 EventSetTimer(PeriodSeconds(period));设失败同样返回 false。这两步决定了 EA 能否在预期时间粒度上运转。 品种对象 m_symbol 为 NULL 时才 new 一个 CSymbolInfo,随后用 Name(symbol) 绑定品种,绑定失败即退出。外汇与贵金属波动剧烈、杠杆高风险大,周期或品种绑错可能让策略信号完全失真。 派生类 CExfolio 覆写 Init 时,先调 CExpert::Init 做基类校验,再用 symbol!=_Symbol 做二次判断——这种双层检查能避免挂错品种还悄无声息地开仓。
class="type">bool CExpert::Init(class="type">class="kw">string symbol,ENUM_TIMEFRAMES period,class="type">bool every_tick,class="type">class="kw">ulong magic) { class=class="str">"cmt">//--- returns false if the EA is initialized on a timeframe different from the current one if(period!=::Period()) { PrintFormat(__FUNCTION__+": wrong timeframe(must be: %s)",EnumToString(period)); class="kw">return(false); } if(m_on_timer_process && !EventSetTimer(PeriodSeconds(period))) { PrintFormat(__FUNCTION__+": cannot set timer at: ",EnumToString(period)); class="kw">return(false); } class=class="str">"cmt">//--- initialize common information if(m_symbol==NULL) { if((m_symbol=new CSymbolInfo)==NULL) class="kw">return(false); } if(!m_symbol.Name(symbol)) class="kw">return(false); class=class="str">"cmt">//--- ok class="kw">return(true); }
定时器初始化失败的排查点
在 EA 的 OnInit 流程里,若想靠 OnTimer 做周期轮询,必须先确认 EventSetTimer 真的挂上了。上面这段把 Reinit 判断和定时器设置串在一起:只有 Reinit 返回 true 才把 _init 置真,随后调用 CExpert::OnTimerProcess(true) 准备计时逻辑。 紧接着那行 if(CExpert::m_on_timer_process && !EventSetTimer(PeriodSeconds(_Period))) 是硬关卡——如果内部计时开关已开,但 EventSetTimer 用当前图表周期秒数去注册却返回 false,就会打印「cannot set timer at: 周期枚举」并强制 _init=false,EA 直接起不来。 实盘里遇到 OnInit 返回 INIT_FAILED 且日志出现这句,优先怀疑 MT5 终端在该图表上定时器资源被占用,或 PeriodSeconds(_Period) 取到的数值因离线图表为 0。开 MT5 用 __FUNCTION__ 配合 EnumToString(period) 打印,能直接看到是哪个周期枚举注册失败。外汇与贵金属品种波动剧烈,EA 初始化异常可能错过建仓窗口,属高风险操作环境。
if(Reinit(symbol,period,every_tick,magic)){ _init=true; } } } CExpert::OnTimerProcess(true); if(CExpert::m_on_timer_process && !EventSetTimer(PeriodSeconds(_Period))) { printf(__FUNCTION__+": cannot set timer at: ",EnumToString(period)); _init=false; } class=class="str">"cmt">//--- ok class="kw">return(_init); }
◍ 用相关性过滤多品种信号
在向导生成的 EA 里做多货种交易,核心不是重写信号,而是加一个 Select 函数当过滤器。它依赖组合内品种收盘价的相关系数,只在存在跨行情机会时放行,本质是把第一篇的 RSI 主信号再筛一层。 先得手动开定时交易——默认居然是关的,在 OnInit 里补一行 EventSetTimer 即可。否则后面 OnTimer 里的逻辑根本不会跑。 Select 分两块:无持仓时,遍历组合算各品种两段 30 根 K 线的 CorrCoef,取最大正相关系数者;再看其最新收盘相对 29 根前是涨是跌,定多空。我们只要正相关性最大的,因为目标是组合里趋势最强的标的。 有单之后逻辑反转:若初始仓缩水,就在剩余品种里找与当前持仓相关性绝对值最高者,顺着它的趋势开“对冲”仓。下面这段是无持仓分支的代码骨架,能直接抄进 EA 类里改。 向导类得把 Refresh 和 Process 从 protected 挪到 public,否则 OnTimer 里没法手调。另外自定义品种测试时,组合数组里的名字必须带后缀,不然策略测试器同步不了数据——这是实测坑点。 最后把 OnTick 里的 OnProcess 禁掉,计时器里单独调 OnTimerProcess(true);撰写时 ExpertFolio 的 OnTimer 内 Process 不会自执行,只能手工调且自己声明/注销定时器。
class=class="str">"cmt">// ExtExpert[f].OnTimerProcess(true); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Symbol Selector via Correlation | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool Select(class="type">class="kw">double Direction, class="type">int &Index, class="type">ENUM_POSITION_TYPE &Type) { if(PositionsTotal() == class="num">0) { class="type">class="kw">double _max = class="num">0.0; class="type">int _index = -class="num">1; Type = INVALID_HANDLE; for(class="type">int f = class="num">0; f < __FOLIO; f++) { vector _v_0, _v_1; _v_0.CopyRates(__F[f], Period(), class="num">8, class="num">0, class="num">30); _v_1.CopyRates(__F[f], Period(), class="num">8, class="num">30, class="num">30); class="type">class="kw">double _corr = _v_0.CorrCoef(_v_1); if(_max < _corr && ((Direction > class="num">0.0 && _v_0[class="num">0] > _v_0[class="num">29]) || (Direction < class="num">0.0 && _v_0[class="num">0] < _v_0[class="num">29]))) { _max = _corr; _index = f; if(_v_0[class="num">0] > _v_0[class="num">29]) { Type = POSITION_TYPE_BUY; } else if(_v_0[class="num">0] < _v_0[class="num">29]) { Type = POSITION_TYPE_SELL; } } } Index = _index; class="kw">return(true); } else if(PositionsTotal() == class="num">1) { class=class="str">"cmt">//... class=class="str">"cmt">//... } class="kw">return(false); } class=class="str">"cmt">//+------------------------------------------------------------------+
「持仓浮亏时按相关性挑下一个标的」
当账户里只有一笔持仓且当前浮亏(PositionGetDouble(POSITION_PROFIT) < 0.0)时,这段逻辑会去组合里找和它走势最像的另一个品种接单。它先通过 ArrayBsearch 定位当前持仓品种在 __F 数组里的下标,再遍历其余 __FOLIO 个品种逐一算相关系数。 相关系数用 vector 的 CorrCoef 算,两边各取 Period() 周期下最近 30 根 K 线(CopyRates 第 4、5 参数 0 和 30),取绝对值 fabs 后比较。只有比已记录的最大值 _max 更大才更新候选,所以最终选出的是与亏损单相关性最高的那个标的。 方向判定很直接:候选品种 _v_1[0](最新收盘)大于 _v_1[29](30 根前收盘)就给 POSITION_TYPE_BUY,反之下破给 POSITION_TYPE_SELL,并把下标写进 Index、类型写进 Type 返回 true。外汇与贵金属杠杆高,相关性会随流动性骤变而断裂,这种接单逻辑只降低盲选概率,不消除爆仓风险,实盘前务必在 MT5 策略测试器用真实点差回测。
class="type">bool Select(class="type">class="kw">double Direction, class="type">int &Index, class="type">ENUM_POSITION_TYPE &Type) { if(PositionsTotal() == class="num">0) { class=class="str">"cmt">//... class=class="str">"cmt">//... } else if(PositionsTotal() == class="num">1) { class="type">class="kw">ulong _ticket = PositionGetTicket(class="num">0); if(PositionSelectByTicket(_ticket)) { class="type">class="kw">double _float = PositionGetDouble(POSITION_PROFIT); class="type">ENUM_POSITION_TYPE _type = (class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); class="type">int _index = ArrayBsearch(__F, PositionGetString(POSITION_SYMBOL)); class="type">class="kw">double _max = class="num">0.0; Type = INVALID_HANDLE; for(class="type">int f = class="num">0; f < __FOLIO; f++) { if(f == _index) { class="kw">continue; } else { vector _v_0, _v_1; _v_0.CopyRates(__F[_index], Period(), class="num">8, class="num">0, class="num">30); _v_1.CopyRates(__F[f], Period(), class="num">8, class="num">0, class="num">30); class="type">class="kw">double _corr = fabs(_v_0.CorrCoef(_v_1)); if(_float < class="num">0.0 && _max < _corr) { _max = _corr; Index = f; if(_v_1[class="num">0] > _v_1[class="num">29]) { Type = POSITION_TYPE_BUY; } else if(_v_1[class="num">0] < _v_1[class="num">29]) { Type = POSITION_TYPE_SELL; } } } } } class="kw">return(true); } class="kw">return(false); }