创建动态多货币对EA(第二部分):投资组合多元化与优化·进阶篇
(2/3)·单货币对扛不住宏观拐弯?这篇把不相关资产与超买超卖过滤塞进同一套EA逻辑
多品种初始区间与随机指标句柄的挂载
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 做突破判定。
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 做风控裁剪,返回是否成功。真正手数约束在别处,这里不展开。
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 把手数吸附到合法步长。
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;
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 对胜率的干扰,单独看突破逻辑本身成色。外汇与贵金属波动剧烈,参数未经验证前任何收益都只是概率。
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;