创建动态多货币对EA(第二部分):投资组合多元化与优化·进阶篇
🧩

创建动态多货币对EA(第二部分):投资组合多元化与优化·进阶篇

(2/3)·单货币对扛不住宏观拐弯?这篇把不相关资产与超买超卖过滤塞进同一套EA逻辑

偏理论 第 2/3 篇
把杠杆堆在高度相关的几个货币对上,表面是分散,实则一跌全跌。真正降低回撤的办法是在不相关市场里用同一套突破规则跑,再用振荡器把假突破挡在门外。

多品种初始区间与随机指标句柄的挂载

EA 初始化阶段先按品种数组逐个算初始波动区间,再为每个品种建一个随机指标句柄。这里用的是 iStochastic(品种, PERIOD_CURRENT, KPeriod, 1, 3, MODE_SMA, STO_LOWHIGH),也就是慢速随机、K 线周期参数由外部 KPeriod 控制、 slowing 取 1、价格字段平滑 3、SMA 模式、基于高低价。 若任一 handle 返回 INVALID_HANDLE,直接 Alert 并 return INIT_FAILED,避免后续 OnTick 空跑。止损距离按 SymbolInfoDouble(品种, SYMBOL_POINT) * TrailingStop 计算,点值乘外部跟踪点数,外汇与贵金属杠杆高,点值波动可能让实际止损跳变明显。 OnDeinit 里只做一件事:遍历 handles 数组,非 INVALID_HANDLE 就 IndicatorRelease 释放,防止 MT5 终端退出时指标句柄泄漏。 OnTick 的核心是先存 prevTick 再取 SymbolInfoTick 刷新 currTick,随后判断 tick 时间是否落在 rangeArray[i] 的 start_time 与 end_time 内:落在区间内就置 b_entry=true,并分别用 ask 刷新 high、bid 刷新 low。区间重算触发条件有四个,满足任一就调 CalculateRange(i, 品种),最后统一跑 checkBreak 做突破判定。

MQL5 / C++
  class=class="str">"cmt">// Calculate initial ranges for each symbol
  for (class="type">int i = class="num">0; i < ArraySize(Formatted_Symbs); i++) {
      CalculateRange(i, Formatted_Symbs[i]);  class=class="str">"cmt">// Pass the symbol index
      handles[i] = iStochastic(Formatted_Symbs[i], PERIOD_CURRENT, KPeriod, class="num">1, class="num">3, MODE_SMA, STO_LOWHIGH);
      
      if(handles[i] == INVALID_HANDLE){
         Alert("Failed to create indicator handle");
         class="kw">return INIT_FAILED;
      }
      
      StopLoss = SymbolInfoDouble(Formatted_Symbs[i], SYMBOL_POINT)*TrailingStop;
  }
  class="kw">return(INIT_SUCCEEDED);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason){
   for(class="type">int i = class="num">0; i < ArraySize(Formatted_Symbs); i++){
      if(handles[i] != INVALID_HANDLE){
         IndicatorRelease(handles[i]);
      } 
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick(){
   for(class="type">int i = class="num">0; i < ArraySize(Formatted_Symbs); i++){
      class="type">class="kw">string symbol = Formatted_Symbs[i];
      prevTick[i] = currTick[i];
      SymbolInfoTick(symbol, currTick[i]);
      
      class=class="str">"cmt">// Range Cal
      if(currTick[i].time > rangeArray[i].start_time && currTick[i].time < rangeArray[i].end_time){
         class=class="str">"cmt">// flag
         rangeArray[i].b_entry = true;
         
         class=class="str">"cmt">// high
         if(currTick[i].ask > rangeArray[i].high){
            rangeArray[i].high = currTick[i].ask;
         }
         
         class=class="str">"cmt">// low
         if(currTick[i].bid < rangeArray[i].low){
            rangeArray[i].low = currTick[i].bid;
         }
      }
         
      class=class="str">"cmt">// now calculate range
      if(((RangeClose >= class="num">0 && currTick[i].time >= rangeArray[i].close_time)
         || (rangeArray[i].b_high_breakout && rangeArray[i].b_low_breakout)
         || (rangeArray[i].end_time == class="num">0)
         || (rangeArray[i].end_time != class="num">0 && currTick[i].time > rangeArray[i].end_time && !rangeArray[i].b_entry))){
         CalculateRange(i, Formatted_Symbs[i]);
      }
      checkBreak(i, Formatted_Symbs[i]);
   }
   
}

◍ 区间起止与收盘时间的滚动避开周末逻辑

这段函数负责给每个监控品种算出当日区间的三条时间线:start_time、end_time、close_time,并把区间状态变量先清零。low 初始化成 999999、high 成 0.0,是为了后续用极值比较自然覆盖,不用额外判断首根 K 线。 start_time 以 86400 秒(一天)为周期取整,再加 RangeStart*60 偏移。紧接着一个 8 次循环做「跳过」:若当前 tick 时间已晚于算出的起点,或那天是周六(6)/周日(0),就把起点往后推一天。最多推 8 次,足以跨过周五收盘到下周一所产生的周末空档。 end_time 直接等于 start_time + RangeDuration*60,再跑 2 次周末检查;close_time 则按 end_time 所在日零点加 RangeClose*60,用 3 次循环保证不落在区间结束前或周末。外汇与贵金属周末无连续报价,这种滚动偏移能避免把休市时段误算进突破统计,实盘前建议在 MT5 用 Print 把三个时间打印出来核对时区。 下面这段 CLots 只是仓位接口壳子:固定取 In_Lot,再交给 CHLots 做风控裁剪,返回是否成功。真正手数约束在别处,这里不展开。

MQL5 / C++
class="type">void CalculateRange(class="type">int index, class="type">class="kw">string symbol) {
   for(index = class="num">0; index < ArraySize(Formatted_Symbs); index++){
      symbol = Formatted_Symbs[index];
      
      class=class="str">"cmt">// Reset all the range variables
      rangeArray[index].start_time = class="num">0;
      rangeArray[index].end_time = class="num">0;
      rangeArray[index].close_time = class="num">0;
      rangeArray[index].high = class="num">0.0;
      rangeArray[index].low = class="num">999999;
      rangeArray[index].b_entry = false;
      rangeArray[index].b_high_breakout = false;
      rangeArray[index].b_low_breakout = false;
      
      class=class="str">"cmt">// Calculate range start time
      class="type">int time_cycle = class="num">86400;
      rangeArray[index].start_time = (currTick[index].time - (currTick[index].time % time_cycle)) + RangeStart * class="num">60;
      for(class="type">int i = class="num">0; i < class="num">8; i++){
         class="type">MqlDateTime tmp;
         TimeToStruct(rangeArray[index].start_time, tmp);
         class="type">int dotw = tmp.day_of_week;
         if(currTick[index].time >= rangeArray[index].start_time || dotw == class="num">6 || dotw == class="num">0){
            rangeArray[index].start_time += time_cycle;
         }
      }

      class=class="str">"cmt">// Calculate range end time
      rangeArray[index].end_time = rangeArray[index].start_time + RangeDuration * class="num">60;
      for(class="type">int i = class="num">0 ; i < class="num">2; i++){
         class="type">MqlDateTime tmp;
         TimeToStruct(rangeArray[index].end_time, tmp);
         class="type">int dotw = tmp.day_of_week;
         if(dotw == class="num">6 || dotw == class="num">0){
            rangeArray[index].end_time += time_cycle;
         }
      }
      
      class=class="str">"cmt">// Calculate range close
      rangeArray[index].close_time = (rangeArray[index].end_time - (rangeArray[index].end_time % time_cycle)) + RangeClose * class="num">60;
      for(class="type">int i = class="num">0; i < class="num">3; i++){
         class="type">MqlDateTime tmp;
         TimeToStruct(rangeArray[index].close_time, tmp);
         class="type">int dotw = tmp.day_of_week;
         if(rangeArray[index].close_time <= rangeArray[index].end_time || dotw == class="num">6 || dotw == class="num">0){
            rangeArray[index].close_time += time_cycle;
         }
      }
      
   }
}
class="type">bool CLots(class="type">class="kw">double sl, class="type">class="kw">double &lots){
   lots = In_Lot;
   if(!CHLots(lots)){class="kw">return false;}
   class="kw">return true;
}

「手批手数合规与区间突破触发」

多品种 EA 里最容易被忽略的是手数越界。下面这段 CHLots 会遍历已格式化品种数组,逐个取 SYMBOL_VOLUME_MIN / MAX / STEP,若传入 lots 小于最小手数直接拉回最小值并返回 true,大于最大手数则拒绝开仓返回 false;落在范围内则用 MathFloor(lots/step)*step 把手数吸附到合法步长。

MQL5 / C++
class="type">bool CHLots(class="type">class="kw">double &lots){
  for(class="type">int i = class="num">0; i < ArraySize(Formatted_Symbs); i++){
    class="type">class="kw">string symbol = Formatted_Symbs[i];
    class="type">class="kw">double min = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
    class="type">class="kw">double max = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
    class="type">class="kw">double step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
    if(lots < min){
      lots = min;
      class="kw">return true;
    }
    if(lots > max){
      class="kw">return false;
    }
    lots = (class="type">int)MathFloor(lots / step) * step;
  }
  class="kw">return true;
}
上面代码逐行拆解:第 1 行定义函数,引用传递 lots 以便回写;第 2 行遍历品种;3–5 行分别抓最小、最大、步长三个成交量限制;6–10 行手数过小则修正为最小值并放行;11–13 行手数超上限直接驳回;第 15 行把任意手数向下取整到步长整数倍,避免经纪商拒绝非标准手数。 突破检测另有一套 checkBreak。它先 CopyBuffer 取两个柱的指标缓冲,若返回值不等于 2 就打印失败并退出;随后读 SYMBOL_TRADE_STOPS_LEVEL 与 SYMBOL_SPREAD,再取实时 Bid/Ask。当当前 tick 时间已过区间结束时间、且区间允许入场时,计算区间高度,并用 bufferM[0] 与 bufferM[1] 的穿越关系判断上破或下破——比如 upperBreak 要求本柱指标值 >= 上轨而前柱 < 上轨。外汇与贵金属杠杆高,滑点和止损位不足都可能让这类突破信号失效,实盘前请在 MT5 策略测试器用真实点差验证。
MQL5 / C++
class="type">void checkBreak(class="type">int i, class="type">class="kw">string symbol) {
  for (i = class="num">0; i < ArraySize(Formatted_Symbs); i++) {
    symbol = Formatted_Symbs[i];
    if(CopyBuffer(handles[i], class="num">0, class="num">1, class="num">2, bufferM) != class="num">2){
      Print("Failed to get indicator values");
      class="kw">return;
    }
    class="type">int stopLevel = (class="type">int)SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL);
    class="type">int spread = (class="type">int)SymbolInfoInteger(symbol, SYMBOL_SPREAD);
    class="type">class="kw">double Bid = SymbolInfoDouble(symbol, SYMBOL_BID);
    class="type">class="kw">double Ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
    if (currTick[i].time >= rangeArray[i].end_time && rangeArray[i].end_time > class="num">0 && rangeArray[i].b_entry) {
      class="type">class="kw">double rangeSize = rangeArray[i].high - rangeArray[i].low;
      class="type">bool upperBreak = bufferM[class="num">0] >= upprer_level && bufferM[class="num">1] < upprer_level;
      class="type">bool lowerBreak = bufferM[class="num">0] <= (class="num">100 - upprer_level) && bufferM[class="num">1] > (class="num">100 - upprer_level);
      class="type">bool HighSigType,LowSigType;
      if(BreakOutMode == Normal_Signal){
        HighSigType = upperBreak;
      }else{HighSigType = lowerBreak;}
      if (!rangeArray[i].b_high_breakout && currTick[i].ask >= rangeArray[i].high && HighSigType) {
        rangeArray[i].b_high_breakout = true;
这段里 CopyBuffer 的 1,2 参数表示从索引 1 开始取 2 个值,也就是倒数第二和倒数第一根柱,用前后柱对比才能识别“刚突破”而非“早已在轨外”。stopLevel 单位通常是点,若其值大于 spread,挂单可能直接被经纪商拒,这是多品种监控时容易踩的坑。

MQL5 / C++
class="type">bool CHLots(class="type">class="kw">double &lots){
  for(class="type">int i = class="num">0; i < ArraySize(Formatted_Symbs); i++){
    class="type">class="kw">string symbol = Formatted_Symbs[i];
    class="type">class="kw">double min = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
    class="type">class="kw">double max = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
    class="type">class="kw">double step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
    if(lots < min){
      lots = min;
      class="kw">return true;
    }
    if(lots > max){
      class="kw">return false;
    }
    lots = (class="type">int)MathFloor(lots / step) * step;
  }
  class="kw">return true;
}

class="type">void checkBreak(class="type">int i, class="type">class="kw">string symbol) {
  for (i = class="num">0; i < ArraySize(Formatted_Symbs); i++) {
    symbol = Formatted_Symbs[i];
    if(CopyBuffer(handles[i], class="num">0, class="num">1, class="num">2, bufferM) != class="num">2){
      Print("Failed to get indicator values");
      class="kw">return;
    }
    class="type">int stopLevel = (class="type">int)SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL);
    class="type">int spread = (class="type">int)SymbolInfoInteger(symbol, SYMBOL_SPREAD);
    class="type">class="kw">double Bid = SymbolInfoDouble(symbol, SYMBOL_BID);
    class="type">class="kw">double Ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
    if (currTick[i].time >= rangeArray[i].end_time && rangeArray[i].end_time > class="num">0 && rangeArray[i].b_entry) {
      class="type">class="kw">double rangeSize = rangeArray[i].high - rangeArray[i].low;
      class="type">bool upperBreak = bufferM[class="num">0] >= upprer_level && bufferM[class="num">1] < upprer_level;
      class="type">bool lowerBreak = bufferM[class="num">0] <= (class="num">100 - upprer_level) && bufferM[class="num">1] > (class="num">100 - upprer_level);
      class="type">bool HighSigType,LowSigType;
      if(BreakOutMode == Normal_Signal){
        HighSigType = upperBreak;
      }else{HighSigType = lowerBreak;}
      if (!rangeArray[i].b_high_breakout && currTick[i].ask >= rangeArray[i].high && HighSigType) {
        rangeArray[i].b_high_breakout = true;

突破信号的双向开仓与移动止损钩子

向上突破时,引擎在 Ask 加 100 点挂买价,止损直接取区间下沿 rangeArray[i].low,止盈按 TakeProfit 点数推算。CLots 函数用 entry 与 sl 的差额反推手数,仓位计算失败就 continue 跳过,不硬开仓。 向下突破镜像处理:Bid 减 100 点作为卖单入场,止损取区间上沿 high,tp 向下减 TakeProfit 点。两种单子都带注释字符串「High Breakout / Low Breakout」,实盘里在 MT5 订单备注栏能直接区分信号来源。 BreakOutMode 决定 LowSigType 取上破还是下破标记,Normal_Signal 走 upperBreak,其余走 lowerBreak,这套分支决定卖单是否被激活。 Trailler() 是移动止损的总开关,开头一句 if(!TrailYourStop) return; 直接放弃跟踪。把 TrailYourStop 设为 false,回测时就能隔离 trailing 对胜率的干扰,单独看突破逻辑本身成色。外汇与贵金属波动剧烈,参数未经验证前任何收益都只是概率。

MQL5 / C++
class="type">class="kw">double entry = NormalizeDouble(Ask + class="num">100 * _Point, _Digits);
class="type">class="kw">double sl = rangeArray[i].low;
class=class="str">"cmt">//sl = NormalizeDouble(sl, true);
class="type">class="kw">double tp = entry + TakeProfit * _Point;
class="type">class="kw">double lots;
if (!CLots(entry - sl, lots)) class="kw">continue;
if (!trade.PositionOpen(symbol, ORDER_TYPE_BUY, lots, currTick[i].ask, sl, tp, "High Breakout"))
   Print("Buy Order Failed: ", GetLastError());

if(BreakOutMode == Normal_Signal){
   LowSigType = upperBreak;
}else{LowSigType = lowerBreak; }
class=class="str">"cmt">// Low Breakout(SELL)
if (!rangeArray[i].b_low_breakout && currTick[i].bid <= rangeArray[i].low && LowSigType) {
   rangeArray[i].b_low_breakout = true;
   class="type">class="kw">double entry = NormalizeDouble(Bid - class="num">100 * _Point, _Digits);
   class="type">class="kw">double sl = rangeArray[i].high;
   class=class="str">"cmt">//sl = NormalizeDouble(sl,true);
   class="type">class="kw">double tp = entry - TakeProfit * _Point;
   class="type">class="kw">double lots;
   if (!CLots(sl - entry, lots)) class="kw">continue;
   if (!trade.PositionOpen(symbol, ORDER_TYPE_SELL, lots, currTick[i].bid, sl, tp, "Low Breakout"))
      Print("Sell Order Failed: ", GetLastError());
}

class="type">void Trailler(){
   if(!TrailYourStop) class="kw">return;
让小布替你扫跨品种相关性
小布盯盘的 AIGC 已内置多品种相关性视图,打开对应品种页即可看到哪些货币对在同向共振,避免你以为分散了其实还是一棵藤上的瓜。

常见问题

该时段覆盖伦敦开盘后的早期波动,形成的高低点反映日内初始流动性边界,后续突破更可能带真实动量,而非隔夜 thin market 的噪音。
会,但概率上过滤掉的是超买延伸后的假突破。趋势强到 oscillator 不回落也能持续新高时,本模型主动放弃,换取更干净的风险收益比。
不能直接跑EA,但小布的相关性面板和超买超卖提示可辅助你验证组合里各货币对的振荡器状态,省去手动逐个切图。
没有固定数,取决于杠杆与单笔风险。常见做法是从 4–6 个低相关系数品种起步,过度堆数量会稀释每个信号的注意力并抬升运维成本。