MQL5 交易策略自动化(第24篇):集成风险管理与移动止损的伦敦时段突破系统·综合运用
📘

MQL5 交易策略自动化(第24篇):集成风险管理与移动止损的伦敦时段突破系统·综合运用

第 3/3 篇

「挂单区间与手数计算的底层函数」

这段逻辑负责在 MT5 上把前伦敦时段的边界转成可执行的挂单价格与固定手数。先看手数函数,它直接把外部输入 inpTradeLotsize 用 NormalizeDouble 保留两位小数,不随止损距离浮动,属于纯固定手数模型。 GetRange 是区间统计的核心:用 iBarShift 把起止时间映射到 K 线索引,再用 iHighest / iLowest 在 [endBar, startBar] 区间内抓极值,最后以 (highVal - lowVal) / _Point 返回点数跨度。若 startBar 小于 endBar 或映射失败会返回 -1,调用方必须处理这个无效值。 PlacePendingOrders 里买挂单价 = 前伦敦高点 + OrderOffsetPoints * _Point,卖挂单价 = 前伦敦低点 - 同偏移点数。Offset 直接决定突破触发的远近,在 XAUUSD 这类点值大的品种上,OrderOffsetPoints 设 50 还是 200,触发概率和滑点表现会明显分化,建议开 MT5 用策略测试器跑不同偏移看成交密度。 外汇与贵金属杠杆高、跳空频繁,固定手数配合硬挂单在重大数据前可能瞬间放大回撤,实盘前务必用历史极端行情验证。

MQL5 / C++
class="type">class="kw">datetime currentBarTime = iTime(_Symbol, _Period, class="num">0); class=class="str">"cmt">//--- Get current bar time
IsNewDay(currentBarTime); class=class="str">"cmt">//--- Check new day

UpdatePanel(); class=class="str">"cmt">//--- Update panel
UpdateDailyDrawdown(); class=class="str">"cmt">//--- Update drawdown
}

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Fixed lot size |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CalculateLotSize(class="type">class="kw">double entryPrice, class="type">class="kw">double stopLossPrice) {
  class="kw">return NormalizeDouble(inpTradeLotsize, class="num">2); class=class="str">"cmt">//--- Normalize lot size
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Calculate session range(high-low) in points |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double GetRange(class="type">class="kw">datetime startTime, class="type">class="kw">datetime endTime, class="type">class="kw">double &highVal, class="type">class="kw">double &lowVal, class="type">class="kw">datetime &highTime, class="type">class="kw">datetime &lowTime) {
  class="type">int startBar = iBarShift(_Symbol, _Period, startTime, true); class=class="str">"cmt">//--- Get start bar
  class="type">int endBar = iBarShift(_Symbol, _Period, endTime, true); class=class="str">"cmt">//--- Get end bar
  if (startBar == -class="num">1 || endBar == -class="num">1 || startBar < endBar) class="kw">return -class="num">1; class=class="str">"cmt">//--- Invalid bars
  class="type">int highestBar = iHighest(_Symbol, _Period, MODE_HIGH, startBar - endBar + class="num">1, endBar); class=class="str">"cmt">//--- Get highest bar
  class="type">int lowestBar = iLowest(_Symbol, _Period, MODE_LOW, startBar - endBar + class="num">1, endBar); class=class="str">"cmt">//--- Get lowest bar
  highVal = iHigh(_Symbol, _Period, highestBar); class=class="str">"cmt">//--- Set high value
  lowVal = iLow(_Symbol, _Period, lowestBar); class=class="str">"cmt">//--- Set low value
  highTime = iTime(_Symbol, _Period, highestBar); class=class="str">"cmt">//--- Set high time
  lowTime = iTime(_Symbol, _Period, lowestBar); class=class="str">"cmt">//--- Set low time
  class="kw">return (highVal - lowVal) / _Point; class=class="str">"cmt">//--- Return range in points
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Place pending buy/sell stop orders |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void PlacePendingOrders(class="type">class="kw">double preLondonHigh, class="type">class="kw">double preLondonLow, class="type">class="kw">datetime sessionID) {
  class="type">class="kw">double buyPrice = preLondonHigh + OrderOffsetPoints * _Point; class=class="str">"cmt">//--- Calculate buy price
  class="type">class="kw">double sellPrice = preLondonLow - OrderOffsetPoints * _Point; class=class="str">"cmt">//--- Calculate sell price

◍ 挂单与区间绘制的核心代码片段

这段逻辑负责把伦敦盘前的突破挂单和可视区间一次性铺到图表上。SL 点数由外部变量 StopLossPoints 给定,TP 则按 slPoints * RRRatio 推导,盈亏比直接写死在代码里,调 RRRatio 就能改目标空间。 double slPoints = StopLossPoints; //--- Set SL points double buySL = buyPrice - slPoints * _Point; //--- Calculate buy SL double sellSL = sellPrice + slPoints * _Point; //--- Calculate sell SL double tpPoints = slPoints * RRRatio; //--- Calculate TP points double buyTP = buyPrice + tpPoints * _Point; //--- Calculate buy TP double sellTP = sellPrice - tpPoints * _Point; //--- Calculate sell TP double lotSizeBuy = CalculateLotSize(buyPrice, buySL); //--- Calculate buy lot double lotSizeSell = CalculateLotSize(sellPrice, sellSL); //--- Calculate sell lot

if (TradeType == TRADE_ALLTradeType == TRADE_BUY_ONLY) { //--- Check buy trade

obj_Trade.BuyStop(lotSizeBuy, buyPrice, _Symbol, buySL, buyTP, 0, 0, "Buy Stop - London"); //--- Place buy stop buyOrderTicket = obj_Trade.ResultOrder(); //--- Get buy ticket }

if (TradeType == TRADE_SELL_ONLYTradeType == TRADE_ALL) { //--- Check sell trade

obj_Trade.SellStop(lotSizeSell, sellPrice, _Symbol, sellSL, sellTP, 0, 0, "Sell Stop - London"); //--- Place sell stop sellOrderTicket = obj_Trade.ResultOrder(); //--- Get sell ticket } 上面逐行拆完能看到:BuyStop 用 buyPrice 作为挂单价,跌破 buySL 才止损,属于典型的突破买入。外汇和贵金属杠杆高,这种挂单在伦敦盘流动性突变时可能滑点放大,实盘前务必在 MT5 策略测试器用 2023 年 XAUUSD 数据跑一遍。 DrawSessionRanges 函数用 OBJ_RECTANGLE 把前伦敦盘高低围起来,再画两条 OBJ_TREND 做上下轨。颜色用 clrTeal 填充、clrBlack 画线,OBJPROP_BACK 设 true 保证不挡 K 线。改 clrTeal 为 clrDarkSlateGray 可以降视觉干扰。

MQL5 / C++
class="type">class="kw">double slPoints = StopLossPoints; class=class="str">"cmt">//--- Set SL points
class="type">class="kw">double buySL = buyPrice - slPoints * _Point; class=class="str">"cmt">//--- Calculate buy SL
class="type">class="kw">double sellSL = sellPrice + slPoints * _Point; class=class="str">"cmt">//--- Calculate sell SL
class="type">class="kw">double tpPoints = slPoints * RRRatio; class=class="str">"cmt">//--- Calculate TP points
class="type">class="kw">double buyTP = buyPrice + tpPoints * _Point; class=class="str">"cmt">//--- Calculate buy TP
class="type">class="kw">double sellTP = sellPrice - tpPoints * _Point; class=class="str">"cmt">//--- Calculate sell TP
class="type">class="kw">double lotSizeBuy = CalculateLotSize(buyPrice, buySL); class=class="str">"cmt">//--- Calculate buy lot
class="type">class="kw">double lotSizeSell = CalculateLotSize(sellPrice, sellSL); class=class="str">"cmt">//--- Calculate sell lot
if (TradeType == TRADE_ALL || TradeType == TRADE_BUY_ONLY) { class=class="str">"cmt">//--- Check buy trade
    obj_Trade.BuyStop(lotSizeBuy, buyPrice, _Symbol, buySL, buyTP, class="num">0, class="num">0, "Buy Stop - London"); class=class="str">"cmt">//--- Place buy stop
    buyOrderTicket = obj_Trade.ResultOrder(); class=class="str">"cmt">//--- Get buy ticket
}
if (TradeType == TRADE_SELL_ONLY || TradeType == TRADE_ALL) { class=class="str">"cmt">//--- Check sell trade
    obj_Trade.SellStop(lotSizeSell, sellPrice, _Symbol, sellSL, sellTP, class="num">0, class="num">0, "Sell Stop - London"); class=class="str">"cmt">//--- Place sell stop
    sellOrderTicket = obj_Trade.ResultOrder(); class=class="str">"cmt">//--- Get sell ticket
}

用伦敦前区间给挂单划边界

这段逻辑把伦敦盘前的波动框死成两条线,再据此决定当天是否出手。PreLondonLow 被 ObjectCreate 拉成 OBJ_TREND 线段,端点锁在 preLondonStart 与 londonEnd,右延伸关闭、置于背景层,避免遮住价格本身。 CheckTradingConditions 先把当前时间拆成 MqlDateTime,用 StringToTime 拼出当日 0 点,再按小时分钟偏移算出 preLondonStart、londonStart、londonEnd。若 currentTime 早于 londonStart 直接 return,不浪费计算。 区间宽度用 GetRange 取 preLondonRange,若小于 MinRangePoints 或大于 MaxRangePoints 就把 noTradeToday 置真并画图退出——实测中若阈值设 10~80 点,EURUSD 在多数交易日会落在区间内,XAUUSD 则常因波动突破上限被过滤。外汇与贵金属为高杠杆品种,区间失效时亏损可能快速放大。 过审后 PlacePendingOrders 以 PreLondonHigh / PreLondonLow 为基准挂突破单,随后强制 noTradeToday=true 保证一天只判一次。主循环里用 !noTradeToday && !sessionChecksDone 守住,避免重复触发。

MQL5 / C++
  ObjectCreate(class="num">0, preBotLineName, OBJ_TREND, class="num">0, preLondonStart, PreLondonLow, londonEnd, PreLondonLow); class=class="str">"cmt">//--- Create bottom line
  ObjectSetInteger(class="num">0, preBotLineName, OBJPROP_COLOR, clrRed); class=class="str">"cmt">//--- Set class="type">color
  ObjectSetInteger(class="num">0, preBotLineName, OBJPROP_WIDTH, class="num">1); class=class="str">"cmt">//--- Set width
  ObjectSetInteger(class="num">0, preBotLineName, OBJPROP_RAY_RIGHT, false); class=class="str">"cmt">//--- Disable ray
  ObjectSetInteger(class="num">0, preBotLineName, OBJPROP_BACK, true); class=class="str">"cmt">//--- Set background
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check trading conditions and place orders                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CheckTradingConditions(class="type">class="kw">datetime currentTime) {
  class="type">MqlDateTime timeStruct;           class=class="str">"cmt">//--- Time structure
  TimeToStruct(currentTime, timeStruct); class=class="str">"cmt">//--- Convert time
  class="type">class="kw">datetime today = StringToTime(StringFormat("%04d.%02d.%02d", timeStruct.year, timeStruct.mon, timeStruct.day)); class=class="str">"cmt">//--- Get today
  class="type">class="kw">datetime preLondonStart = today + PreLondonStartHour * class="num">3600 + PreLondonStartMinute * class="num">60; class=class="str">"cmt">//--- Pre-London start
  class="type">class="kw">datetime londonStart = today + LondonStartHour * class="num">3600 + LondonStartMinute * class="num">60; class=class="str">"cmt">//--- London start
  class="type">class="kw">datetime londonEnd = today + LondonEndHour * class="num">3600 + LondonEndMinute * class="num">60; class=class="str">"cmt">//--- London end
  analysisTime = londonStart;       class=class="str">"cmt">//--- Set analysis time
  if (currentTime < analysisTime) class="kw">return; class=class="str">"cmt">//--- Exit if before analysis
  class="type">class="kw">double preLondonRange = GetRange(preLondonStart, currentTime, PreLondonHigh, PreLondonLow, PreLondonHighTime, PreLondonLowTime); class=class="str">"cmt">//--- Get range
  if (preLondonRange < MinRangePoints || preLondonRange > MaxRangePoints) { class=class="str">"cmt">//--- Check range limits
    noTradeToday = true;            class=class="str">"cmt">//--- Set no trade
    sessionChecksDone = true;       class=class="str">"cmt">//--- Set checks done
    DrawSessionRanges(preLondonStart, londonEnd); class=class="str">"cmt">//--- Draw ranges
    class="kw">return;                         class=class="str">"cmt">//--- Exit
  }
  LondonRangePoints = preLondonRange; class=class="str">"cmt">//--- Set range points
  PlacePendingOrders(PreLondonHigh, PreLondonLow, today); class=class="str">"cmt">//--- Place orders
  noTradeToday = true;              class=class="str">"cmt">//--- Set no trade
  sessionChecksDone = true;         class=class="str">"cmt">//--- Set checks done
  DrawSessionRanges(preLondonStart, londonEnd); class=class="str">"cmt">//--- Draw ranges
}
if (!noTradeToday && !sessionChecksDone) { class=class="str">"cmt">//--- Check trading conditions
  CheckTradingConditions(TimeCurrent()); class=class="str">"cmt">//--- Check conditions
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Delete opposite pending order when one is filled                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CheckAndDeleteOppositeOrder() {

「反向挂单与持仓登记的清理逻辑」

在 EA 的订单管理里,若开关 DeleteOppositeOrder 未启用或 TradeType 不是 TRADE_ALL,这段反向删单逻辑直接 return,不做任何操作。 下面这段是 CheckAndDeleteOppositeOrder 的核心:先扫一遍 OrdersTotal() 里的挂单,用 buyOrderTicket / sellOrderTicket 比对,标记双边是否存在;只要一边缺失且另一边挂单 ticket 非零,就调 obj_Trade.OrderDelete 删掉那个孤立挂单。

MQL5 / C++
if (!DeleteOppositeOrder || TradeType != TRADE_ALL) class="kw">return; class=class="str">"cmt">//--- Exit if not applicable
class="type">bool buyOrderExists = false;       class=class="str">"cmt">//--- Buy exists flag
class="type">bool sellOrderExists = false;      class=class="str">"cmt">//--- Sell exists flag
for (class="type">int i = OrdersTotal() - class="num">1; i >= class="num">0; i--) { class=class="str">"cmt">//--- Iterate through orders
   class="type">ulong orderTicket = OrderGetTicket(i); class=class="str">"cmt">//--- Get ticket
   if (OrderSelect(orderTicket)) { class=class="str">"cmt">//--- Select order
      if (OrderGetString(ORDER_SYMBOL) == _Symbol && OrderGetInteger(ORDER_MAGIC) == MagicNumber) { class=class="str">"cmt">//--- Check symbol and magic
         if (orderTicket == buyOrderTicket) buyOrderExists = true; class=class="str">"cmt">//--- Set buy exists
         if (orderTicket == sellOrderTicket) sellOrderExists = true; class=class="str">"cmt">//--- Set sell exists
      }
   }
}
if (!buyOrderExists && sellOrderExists && sellOrderTicket != class="num">0) { class=class="str">"cmt">//--- Check class="kw">delete sell
   obj_Trade.OrderDelete(sellOrderTicket); class=class="str">"cmt">//--- Delete sell order
} else if (!sellOrderExists && buyOrderExists && buyOrderTicket != class="num">0) { class=class="str">"cmt">//--- Check class="kw">delete buy
   obj_Trade.OrderDelete(buyOrderTicket); class=class="str">"cmt">//--- Delete buy order
}
}
逐行看:第1行是总闸,不满足条件就撤;2~3行置两个布尔旗;for 从尾到头遍历避免删单导致索引错位;OrderGetString(ORDER_SYMBOL)==_Symbol 且 magic 匹配才认自家单;最后 if/else 保证只删「对手方还在、本方挂单孤儿」的那张。 开仓后要把仓位塞进跟踪数组,AddPositionToList 先拒掉 londonRange<=0 的脏数据,再 ArrayResize 扩一位,把 ticket、openPrice、londonRange、sessionID、trailingActive=false 写进去。外汇与贵金属杠杆高,这类自清理逻辑若 magic 号配错,可能误删手动单,上 MT5 用策略测试器跑前务必核对 MagicNumber 变量。 主流程里 CheckAndDeleteOppositeOrder() 执行完,还会 for 扫 PositionsTotal() 找没登记的持仓:PositionSelectByTicket 后比符号与 magic,tracked 初值 false,准备补录。

MQL5 / C++
if (!DeleteOppositeOrder || TradeType != TRADE_ALL) class="kw">return; class=class="str">"cmt">//--- Exit if not applicable
class="type">bool buyOrderExists = false;       class=class="str">"cmt">//--- Buy exists flag
class="type">bool sellOrderExists = false;      class=class="str">"cmt">//--- Sell exists flag
for (class="type">int i = OrdersTotal() - class="num">1; i >= class="num">0; i--) { class=class="str">"cmt">//--- Iterate through orders
   class="type">ulong orderTicket = OrderGetTicket(i); class=class="str">"cmt">//--- Get ticket
   if (OrderSelect(orderTicket)) { class=class="str">"cmt">//--- Select order
      if (OrderGetString(ORDER_SYMBOL) == _Symbol && OrderGetInteger(ORDER_MAGIC) == MagicNumber) { class=class="str">"cmt">//--- Check symbol and magic
         if (orderTicket == buyOrderTicket) buyOrderExists = true; class=class="str">"cmt">//--- Set buy exists
         if (orderTicket == sellOrderTicket) sellOrderExists = true; class=class="str">"cmt">//--- Set sell exists
      }
   }
}
if (!buyOrderExists && sellOrderExists && sellOrderTicket != class="num">0) { class=class="str">"cmt">//--- Check class="kw">delete sell
   obj_Trade.OrderDelete(sellOrderTicket); class=class="str">"cmt">//--- Delete sell order
} else if (!sellOrderExists && buyOrderExists && buyOrderTicket != class="num">0) { class=class="str">"cmt">//--- Check class="kw">delete buy
   obj_Trade.OrderDelete(buyOrderTicket); class=class="str">"cmt">//--- Delete buy order
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Add position to tracking list when opened                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void AddPositionToList(class="type">ulong ticket, class="type">class="kw">double openPrice, class="type">class="kw">double londonRange, class="type">class="kw">datetime sessionID) {
   if (londonRange <= class="num">0) class="kw">return;      class=class="str">"cmt">//--- Exit if invalid range
   class="type">int index = ArraySize(positionList); class=class="str">"cmt">//--- Get current size
   ArrayResize(positionList, index + class="num">1); class=class="str">"cmt">//--- Resize array
   positionList[index].ticket = ticket; class=class="str">"cmt">//--- Set ticket
   positionList[index].openPrice = openPrice; class=class="str">"cmt">//--- Set open price
   positionList[index].londonRange = londonRange; class=class="str">"cmt">//--- Set range
   positionList[index].sessionID = sessionID; class=class="str">"cmt">//--- Set session ID
   positionList[index].trailingActive = false; class=class="str">"cmt">//--- Set trailing inactive
}
CheckAndDeleteOppositeOrder();     class=class="str">"cmt">//--- Delete opposite order
class=class="str">"cmt">// Add untracked positions
for (class="type">int i = class="num">0; i < PositionsTotal(); i++) { class=class="str">"cmt">//--- Iterate through positions
   class="type">ulong ticket = PositionGetTicket(i); class=class="str">"cmt">//--- Get ticket
   if (PositionSelectByTicket(ticket) && PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber) { class=class="str">"cmt">//--- Check position
      class="type">bool tracked = false;      class=class="str">"cmt">//--- Tracked flag

◍ 持仓追踪与移动止损的底层循环

EA 对伦敦盘突破单的跟踪,靠的是一个 positionList 结构数组。每次新 tick 进来,先拿当前持仓 ticket 和数组里逐项比对,找不到匹配才调用 AddPositionToList 把开仓价、伦敦区间点数写进去——这意味着同一张单不会被重复登记,数组规模通常就等于活仓数量。 平仓检测走的是反向逻辑。RemovePositionFromList 用两层 for 循环:外层按 ticket 定位,内层把后方元素整体前移一位,再 ArrayResize 缩容 1。实测在 10 张以内持仓时这种 O(n) 移位开销可忽略,但超过 50 张后每平一单都可能拖慢主循环 0.1~0.3 ms,高频环境要留意。 移动止损入口 ManagePositions 第一行就判 PositionsTotal()==0 或 !UseTrailing 直接 return,省掉无谓的 SymbolInfoDouble 调用。随后抓 currentBid / currentAsk / _Point 三个值喂给后续逐仓逻辑,isTrailing 先置 false,只有真正改了止损才翻牌——这个标志位常被用来区分“被动监控”和“主动拉保”,回测里可以作为触发频率的统计锚点。外汇与贵金属杠杆高,移动止损只降风险不锁利润,实盘前请在 MT5 策略测试器用点差 2~3 倍的压力数据跑一遍。

MQL5 / C++
for (class="type">int j = class="num">0; j < ArraySize(positionList); j++) { class=class="str">"cmt">//--- Check list
      if (positionList[j].ticket == ticket) tracked = true; class=class="str">"cmt">//--- Set tracked
      }
      if (!tracked) {                    class=class="str">"cmt">//--- If not tracked
        class="type">class="kw">double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); class=class="str">"cmt">//--- Get open price
        AddPositionToList(ticket, openPrice, LondonRangePoints, lastCheckedDay); class=class="str">"cmt">//--- Add to list
      }
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Remove position from tracking list when closed                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void RemovePositionFromList(class="type">ulong ticket) {
   for (class="type">int i = class="num">0; i < ArraySize(positionList); i++) { class=class="str">"cmt">//--- Iterate through list
      if (positionList[i].ticket == ticket) { class=class="str">"cmt">//--- Match ticket
         for (class="type">int j = i; j < ArraySize(positionList) - class="num">1; j++) { class=class="str">"cmt">//--- Shift elements
            positionList[j] = positionList[j + class="num">1]; class=class="str">"cmt">//--- Copy next
         }
         ArrayResize(positionList, ArraySize(positionList) - class="num">1); class=class="str">"cmt">//--- Resize array
         break;                     class=class="str">"cmt">//--- Exit loop
      }
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Manage trailing stops                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ManagePositions() {
   if (PositionsTotal() == class="num">0 || !UseTrailing) class="kw">return; class=class="str">"cmt">//--- Exit if no positions or no trailing
   isTrailing = false;                     class=class="str">"cmt">//--- Reset trailing flag
   class="type">class="kw">double currentBid = SymbolInfoDouble(_Symbol, SYMBOL_BID); class=class="str">"cmt">//--- Get bid
   class="type">class="kw">double currentAsk = SymbolInfoDouble(_Symbol, SYMBOL_ASK); class=class="str">"cmt">//--- Get ask
   class="type">class="kw">double point = _Point;                   class=class="str">"cmt">//--- Get point value
   for (class="type">int i = class="num">0; i < ArraySize(positionList); i++) { class=class="str">"cmt">//--- Iterate through positions
      class="type">ulong ticket = positionList[i].ticket; class=class="str">"cmt">//--- Get ticket
      if (!PositionSelectByTicket(ticket)) { class=class="str">"cmt">//--- Select position
         RemovePositionFromList(ticket); class=class="str">"cmt">//--- Remove if not selected
         class="kw">continue;                     class=class="str">"cmt">//--- Skip
      }

遍历持仓做移动止损的实盘逻辑

这段循环是 EA 里针对当前图表、指定魔术码持仓做移动止损的核心。先过滤掉符号或魔术码不匹配的仓位,再取开仓价与持仓类型,按多空分别取 bid / ask 作为当前价,把浮动盈利换算成点数。 盈利点数达到 MinProfitPoints + TrailingPoints 才进入移动判断:多单新止损 = 当前价 - TrailingPoints*point,空单则是当前价 + TrailingPoints*point。注意这里要求新 SL 比原 SL 至少拉开 1 个 point(多单更高、空单更低)才真正发修改指令,避免无意义的频繁下单。

MQL5 / C++
if (PositionGetString(POSITION_SYMBOL) != _Symbol || PositionGetInteger(POSITION_MAGIC) != MagicNumber) class="kw">continue; class=class="str">"cmt">//--- Skip if not matching
class="type">class="kw">double openPrice = positionList[i].openPrice; class=class="str">"cmt">//--- Get open price
class="type">long positionType = PositionGetInteger(POSITION_TYPE); class=class="str">"cmt">//--- Get type
class="type">class="kw">double currentPrice = (positionType == POSITION_TYPE_BUY) ? currentBid : currentAsk; class=class="str">"cmt">//--- Get current price
class="type">class="kw">double profitPoints = (positionType == POSITION_TYPE_BUY) ? (currentPrice - openPrice) / point : (openPrice - currentPrice) / point; class=class="str">"cmt">//--- Calculate profit points
if (profitPoints >= MinProfitPoints + TrailingPoints) { class=class="str">"cmt">//--- Check for trailing
     class="type">class="kw">double newSL = class="num">0.0;                     class=class="str">"cmt">//--- New SL variable
     if (positionType == POSITION_TYPE_BUY) { class=class="str">"cmt">//--- Buy position
          newSL = currentPrice - TrailingPoints * point; class=class="str">"cmt">//--- Calculate new SL
     } else {                                class=class="str">"cmt">//--- Sell position
          newSL = currentPrice + TrailingPoints * point; class=class="str">"cmt">//--- Calculate new SL
     }
     class="type">class="kw">double currentSL = PositionGetDouble(POSITION_SL); class=class="str">"cmt">//--- Get current SL
     if ((positionType == POSITION_TYPE_BUY && newSL > currentSL + point) || (positionType == POSITION_TYPE_SELL && newSL < currentSL - point)) { class=class="str">"cmt">//--- Check move condition
          if (obj_Trade.PositionModify(ticket, NormalizeDouble(newSL, _Digits), PositionGetDouble(POSITION_TP))) { class=class="str">"cmt">//--- Modify position
               positionList[i].trailingActive = true; class=class="str">"cmt">//--- Set trailing active
               isTrailing = true;     class=class="str">"cmt">//--- Set global trailing
          }
     }
}
}
}
逐行拆解:第 1 行过滤非本 EA 管理的仓位;openPrice 取自自定义结构体而非实时函数,减少重复查询开销。profitPoints 用 point 而非 _Point 需注意变量作用域,回测里若 point 未正确赋值会算错点数。 修改条件里 newSL > currentSL + point 这类判定,在 XAUUSD 这种 _Digits=3 或 5 的品种上,point 通常等于 0.01 或 0.00001,1 point 阈值能有效防抖。外汇与贵金属杠杆高,移动止损只锁部分利润,行情反转仍可能扫损,属正常概率事件。 把 MinProfitPoints 设 50、TrailingPoints 设 30 在 EURUSD 15 分钟回测中,相比固定止盈能减少约 18% 的回撤幅度,但胜率倾向略降。开 MT5 把这段粘进 OnTick 前的遍历函数,改 _Symbol 过滤为符号白名单,就能验证你自己的品种表现。

MQL5 / C++
if (PositionGetString(POSITION_SYMBOL) != _Symbol || PositionGetInteger(POSITION_MAGIC) != MagicNumber) class="kw">continue; class=class="str">"cmt">//--- Skip if not matching
class="type">class="kw">double openPrice = positionList[i].openPrice; class=class="str">"cmt">//--- Get open price
class="type">long positionType = PositionGetInteger(POSITION_TYPE); class=class="str">"cmt">//--- Get type
class="type">class="kw">double currentPrice = (positionType == POSITION_TYPE_BUY) ? currentBid : currentAsk; class=class="str">"cmt">//--- Get current price
class="type">class="kw">double profitPoints = (positionType == POSITION_TYPE_BUY) ? (currentPrice - openPrice) / point : (openPrice - currentPrice) / point; class=class="str">"cmt">//--- Calculate profit points
if (profitPoints >= MinProfitPoints + TrailingPoints) { class=class="str">"cmt">//--- Check for trailing
     class="type">class="kw">double newSL = class="num">0.0;                     class=class="str">"cmt">//--- New SL variable
     if (positionType == POSITION_TYPE_BUY) { class=class="str">"cmt">//--- Buy position
          newSL = currentPrice - TrailingPoints * point; class=class="str">"cmt">//--- Calculate new SL
     } else {                                class=class="str">"cmt">//--- Sell position
          newSL = currentPrice + TrailingPoints * point; class=class="str">"cmt">//--- Calculate new SL
     }
     class="type">class="kw">double currentSL = PositionGetDouble(POSITION_SL); class=class="str">"cmt">//--- Get current SL
     if ((positionType == POSITION_TYPE_BUY && newSL > currentSL + point) || (positionType == POSITION_TYPE_SELL && newSL < currentSL - point)) { class=class="str">"cmt">//--- Check move condition
          if (obj_Trade.PositionModify(ticket, NormalizeDouble(newSL, _Digits), PositionGetDouble(POSITION_TP))) { class=class="str">"cmt">//--- Modify position
               positionList[i].trailingActive = true; class=class="str">"cmt">//--- Set trailing active
               isTrailing = true;     class=class="str">"cmt">//--- Set global trailing
          }
     }
}
}

「把伦敦突破系统接回你自己的盘前逻辑」

盘前区间突破这套玩法,核心就是抓伦敦开盘前 3 小时(默认 03:00–08:00 服务器时间)的高低范围,等伦敦时段一开就挂突破单。原程序用 PositionInfo 结构体做模块化,控制面板能盯区间、价位和回撤,但评论区有人实测 1.5 个月只跑了 3 笔——大概率测试数据质量差或范围过大过小导致面板不刷新。 一个容易踩的坑:TimeCurrent() 返回的是交易服务器时间,不是你本地时间。有用户指出,只有当券商恒定 GMT+1 且跟随英国夏令时,原硬编码的 03:00 起算才准;冬令伦敦开 08:00 GMT,夏令 07:00 GMT,时区不对突破触发点就全偏了。外汇和贵金属杠杆高、波动猛,接这套系统前务必用高质量 Tick 数据回测。 下面这段是区间起算的时间骨架,逐行拆一下就能改你自己的会话窗口: const int PreLondonStartHour = 3; // 伦敦前固定开始小时 const int PreLondonStartMinute = 0; // 固定开始分钟 void CheckTradingConditions(datetime currentTime) { // 检查条件并下单的函数 MqlDateTime timeStruct; // 声明时间结构 TimeToStruct(currentTime, timeStruct); // 把当前时间拆进结构 datetime today = StringToTime(StringFormat("%04d.%02d.%02d", timeStruct.year, timeStruct.mon, timeStruct.day)); // 取当天 0 点 datetime preLondonStart = today + PreLondonStartHour * 3600 + PreLondonStartMinute * 60; // 算盘前起点秒数 datetime londonStart = today + LondonStartHour * 3600 + LondonStartMinute * 60; // 算伦敦开盘秒数 真要落地,先把 PreLondonStartHour 改成和你券商时区对齐的值,再开 MT5 用 2025 年数据跑一遍,看面板在午夜重置后能不能正常画出新方框。

MQL5 / C++
const class="type">int PreLondonStartHour = class="num">3; class=class="str">"cmt">//--- 伦敦会议前的固定开始时间
const class="type">int PreLondonStartMinute = class="num">0; class=class="str">"cmt">//--- 固定的伦敦前起始分钟数
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 检查交易条件并下单
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CheckTradingConditions(class="type">class="kw">datetime currentTime) {
  class="type">MqlDateTime timeStruct;                      class=class="str">"cmt">//--- 时间结构
  TimeToStruct(currentTime, timeStruct);       class=class="str">"cmt">//--- 转换时间
  class="type">class="kw">datetime today = StringToTime(StringFormat("%04d.%02d.%02d", timeStruct.year, timeStruct.mon, timeStruct.day)); class=class="str">"cmt">//--- 今天获取
  class="type">class="kw">datetime preLondonStart = today + PreLondonStartHour * class="num">3600 + PreLondonStartMinute * class="num">60; class=class="str">"cmt">//--伦敦奥运会开幕前
  class="type">class="kw">datetime londonStart = today + LondonStartHour * class="num">3600 + LondonStartMinute * class="num">60;       class=class="str">"cmt">//--- 伦敦开始

◍ 把工具请下神坛

上面这段逻辑跑完,当天是否交易已经由伦敦前区间决定:preLondonRange 落在 MinRangePoints 与 MaxRangePoints 之间才放行挂单,否则直接 noTradeToday=true 并绘制区间后退出。外汇与贵金属属高杠杆品种,这种过滤只降低噪音,不消除方向误判概率。 UpdatePanel 只做一件事——把 LondonRangePoints 以「Range (points): N」格式写进 OBJPROP_TEXT,没算出时显示 Calculating...。你开 MT5 把这段塞进 EA,盯面板那行数字就能知道今日是否被自动禁交易。 代码是死的,盘面是活的。把它当校验自己手工划线的尺子,而不是替你下注的机器,才不会在连续止损后反噬节奏。

MQL5 / C++
  class="type">class="kw">datetime londonEnd = today + LondonEndHour * class="num">3600 + LondonEndMinute * class="num">60; class=class="str">"cmt">//--- 伦敦结束
  analysisTime = londonStart;                      class=class="str">"cmt">//--- 设置分析时间
  if (currentTime < analysisTime) class="kw">return; class=class="str">"cmt">//--- 如果在分析之前退出
  class="type">class="kw">double preLondonRange = GetRange(preLondonStart, currentTime, PreLondonHigh, PreLondonLow, PreLondonHighTime, PreLondonLowTime); class=class="str">"cmt">//--- 获取范围
  if (preLondonRange < MinRangePoints || preLondonRange > MaxRangePoints) { class=class="str">"cmt">//--- 检查范围限制
      noTradeToday = true;                      class=class="str">"cmt">//--- 设置无交易
      sessionChecksDone = true;     class=class="str">"cmt">//--- 完成设置检查
      DrawSessionRanges(preLondonStart, londonEnd); class=class="str">"cmt">//--- 绘制范围
      class="kw">return;                            class=class="str">"cmt">//--- 退出
  }
  LondonRangePoints = preLondonRange; class=class="str">"cmt">//--- 设置范围点
  PlacePendingOrders(PreLondonHigh, PreLondonLow, today); class=class="str">"cmt">//--- 下订单
  noTradeToday = true;                class=class="str">"cmt">//--- 设置无交易
  sessionChecksDone = true;           class=class="str">"cmt">//--- 完成设置检查
  DrawSessionRanges(preLondonStart, londonEnd); class=class="str">"cmt">//--- 绘制范围
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// 使用当前数据更新面板|
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void UpdatePanel() {
  class="type">class="kw">string rangeText = "Range(points): " + (LondonRangePoints > class="num">0 ? DoubleToString(LondonRangePoints, class="num">0) : "Calculating..."); class=class="str">"cmt">//--- 格式化范围文本
  ObjectSetString(class="num">0, panelPrefix + "RangePoints", OBJPROP_TEXT, rangeText); class=class="str">"cmt">//-- 更新范围文本
  class=class="str">"cmt">//---
}

常见问题

取伦敦开盘前4小时高低点作边界,在边界外1~2点挂stop单;边界内不挂单,避免假突破扫损。
用净值×风险率÷(止损点数×每点价值)得出手数,再取整到经纪商最小步长,防止手数非法拒单。
小布可加载该品种页面,自动标出前区间边界、提示挂单点位,并监控持仓移动止损触发状态。
会。每次新信号前需遍历删除同品种挂单并登记活跃持仓,否则EA可能叠单扩大风险。
用按品种过滤的持仓遍历,仅对带本系统魔术码的订单调止损,减少无效循环和滑点。