使用限价订单替代止盈且无需修改 EA 的原始代码·进阶篇
📘

使用限价订单替代止盈且无需修改 EA 的原始代码·进阶篇

第 2/3 篇

◍ 用挂单接管盈利目标的逻辑骨架

在 MT5 的 EA 封装里,把止盈从持仓自带的 TP 字段挪到独立限价挂单,核心是先拦一道 OrderSend。下面这段 CLimitTakeProfit 类里的 protected 成员与重载函数,就是整套机制的入口:它用 i_Magic 和 b_OnlyOneSymbol 做范围过滤,只对指定魔幻数字、指定品种、且带正 TP 的 SLTP 动作插手。 拦截函数 OrderSend 的第一道判断很直接:若品种不匹配、魔幻数字不对,或根本不是改止盈的请求,直接转交系统原生 OrderSend,不碰逻辑。只有命中条件时,才调用 SetTakeProfits 把持仓 TP 清零,并试图挂出对应方向的限价单。 SetTakeProfits(ulong,double) 里有个容易被忽略的细节:min_sell_limit 用 Ask+StopsLevel*Point 归一化,max_buy_limit 用 Bid-StopsLevel*Point 归一化。这意味着挂单价格必须离市价至少一个 StopsLevel 点距,否则经纪商会拒单——实盘前应在 MT5 符号属性里核对 StopsLevel 具体数值(常见 0~50 点不等)。 别把魔幻数字当摆设 i_Magic 设成 -1 和设成具体 EA 编号,行为完全不同:代码里 i_Magic>=0 才拦截,负值等于放行全部。多 EA 同跑时,不显式设魔幻数字会互相串单。

MQL5 / C++
class="kw">protected:
  class="kw">static class="type">int       i_Magic;            class=class="str">"cmt">//用于控制的魔幻数字
  class="kw">static class="type">bool      b_OnlyOneSymbol;    class=class="str">"cmt">//仅针对一个品种的控制
class=class="str">"cmt">//---
  class="kw">static class="type">bool      SetTakeProfits(class="type">class="kw">ulong position_ticket, class="type">class="kw">double new_tp=class="num">0);
  class="kw">static class="type">bool      SetTakeProfits(class="type">class="kw">string symbol, class="type">class="kw">double new_tp=class="num">0);
  class="kw">static class="type">bool      CheckLimitOrder(class="type">MqlTradeRequest &request);
  class="kw">static class="type">void      CheckLimitOrder(class="type">void);
  class="kw">static class="type">bool      CheckOrderInHistory(class="type">class="kw">ulong position_id, class="type">class="kw">string comment, ENUM_ORDER_TYPE type, class="type">class="kw">double &volume, class="type">class="kw">ulong call_position=class="num">0);
  class="kw">static class="type">class="kw">double    GetLimitOrderPriceByComment(class="type">class="kw">string comment);
};
class="type">bool CLimitTakeProfit::OrderSend(class="type">MqlTradeRequest &request,class="type">MqlTradeResult &result)
  {
  if((b_OnlyOneSymbol && request.symbol!=_Symbol) ||
     (i_Magic>=class="num">0 && request.magic!=i_Magic) || !(request.action==TRADE_ACTION_SLTP && request.tp>class="num">0))
     class="kw">return(::OrderSend(request,result));
class=class="str">"cmt">//---
  if(((request.position>class="num">0 && SetTakeProfits(request.position,request.tp)) ||
     (request.position<=class="num">0 && SetTakeProfits(request.symbol,request.tp))) && request.tp>class="num">0)
     request.tp=class="num">0;
  if((request.position>class="num">0 && PositionSelectByTicket(request.position)) ||
     (request.position<=class="num">0 && PositionSelect(request.symbol)))
   {
     if(PositionGetDouble(POSITION_SL)!=request.sl || PositionGetDouble(POSITION_TP)!=request.tp)
       class="kw">return(::OrderSend(request,result));
   }
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CLimitTakeProfit::SetTakeProfits(class="type">class="kw">ulong position_ticket, class="type">class="kw">double new_tp=class="num">0)
  {
  if(!PositionSelectByTicket(position_ticket) || (b_OnlyOneSymbol && PositionGetString(POSITION_SYMBOL)!=_Symbol))
     class="kw">return class="kw">false;
  if(!c_Symbol.Name(PositionGetString(POSITION_SYMBOL)) || !c_Symbol.Select() || !c_Symbol.Refresh() || !c_Symbol.RefreshRates())
     class="kw">return class="kw">false;
class=class="str">"cmt">//---
  class="type">class="kw">double min_sell_limit=c_Symbol.NormalizePrice(c_Symbol.Ask()+c_Symbol.StopsLevel()*c_Symbol.Point());
  class="type">class="kw">double max_buy_limit=c_Symbol.NormalizePrice(c_Symbol.Bid()-c_Symbol.StopsLevel()*c_Symbol.Point());
  class="type">MqlTradeRequest tp_request={class="num">0};
  class="type">MqlTradeResult tp_result={class="num">0};
  tp_request.action =  TRADE_ACTION_PENDING;
  tp_request.magic  =  PositionGetInteger(POSITION_MAGIC);

按点位梯度拆仓挂止盈单

这段逻辑干的事,是把一个持仓按预设的多个 TP 梯度,用限价/止损单分批平仓。外汇与贵金属杠杆高,分批挂单能降低手动操作滑点,但无法消除方向误判风险,仓位仍可能整体亏损。 tp_int 把开仓价到止盈价的点差换算成整数点,循环里只处理「梯度点位小于 tp_int 且比例大于 0」的层。比如持仓 0.50 手、某层设 30%,实际挂单手数会按剩余可平仓位动态折算,而不是死板地挂 0.15 手。 tp_request.price 的算法分买卖:买仓用 open_price + 梯度点*Point 挂 SELL_LIMIT,卖仓用 open_price - 梯度点*Point 挂 BUY_STOP。挂单前过一遍 CheckLimitOrder,返回真才累加 closed 并重算 closed_perc,保证总平仓比例不越过 100%。 手数规范化那三行值得在 MT5 里单步跑:LotsMin 打底,按 LotsStep 取整倍,再 NormalizeDouble 到 2 位。经纪商最小步长不同,不这么处理某些平台会直接拒单。

MQL5 / C++
  tp_request.type_filling =  ORDER_FILLING_RETURN;
  tp_request.position=position_ticket;
  tp_request.symbol=c_Symbol.Name();
  class="type">int total=i_TakeProfit.Total();
  class="type">class="kw">double tp_price=(new_tp>class="num">0 ? new_tp : PositionGetDouble(POSITION_TP));
  if(tp_price<=class="num">0)
      tp_price=GetLimitOrderPriceByComment("TPP_"+IntegerToString(position_ticket));
  class="type">class="kw">double open_price=PositionGetDouble(POSITION_PRICE_OPEN);
  class="type">int tp_int=(tp_price>class="num">0 ? (class="type">int)NormalizeDouble(MathAbs(open_price-tp_price)/c_Symbol.Point(),class="num">0) : INT_MAX);
  class="type">class="kw">double position_volume=PositionGetDouble(POSITION_VOLUME);
  class="type">class="kw">double closed=class="num">0;
  class="type">class="kw">double closed_perc=class="num">0;
  class="type">class="kw">double fix_closed_per=class="num">0;
  for(class="type">int i=class="num">0;i<total;i++)
     {
      tp_request.comment="TP"+IntegerToString(i)+"_"+IntegerToString(position_ticket);
      if(i_TakeProfit.At(i)<tp_int && d_TakeProfit.At(i)>class="num">0)
        {
         if(closed>=position_volume || fix_closed_perc>=class="num">100)
            class="kw">break;
class=class="str">"cmt">//---
         class="type">class="kw">double lot=position_volume*MathMin(d_TakeProfit.At(i),class="num">100-closed)/(class="num">100-fix_closed_perc);
         lot=MathMin(position_volume-closed,lot);
         lot=c_Symbol.LotsMin()+MathMax(class="num">0,NormalizeDouble((lot-c_Symbol.LotsMin())/c_Symbol.LotsStep(),class="num">0)*c_Symbol.LotsStep());
         lot=NormalizeDouble(lot,class="num">2);
         tp_request.volume=lot;
         class="kw">switch((class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE))
           {
            case POSITION_TYPE_BUY:
              tp_request.type=ORDER_TYPE_SELL_LIMIT;
              tp_request.price=c_Symbol.NormalizePrice(open_price+i_TakeProfit.At(i)*c_Symbol.Point());
              class="kw">break;
            case POSITION_TYPE_SELL:
              tp_request.type=ORDER_TYPE_BUY_STOP;
              tp_request.price=c_Symbol.NormalizePrice(open_price-i_TakeProfit.At(i)*c_Symbol.Point());
              class="kw">break;
           }
         if(CheckLimitOrder(tp_request))
           {
            if(tp_request.volume>=class="num">0)
              {
               closed+=tp_request.volume;
               closed_perc=closed/position_volume*class="num">100;
              }
            else
              {

「挂单止盈的限价边界与尾量处理」

在分批平仓逻辑里,挂单类型的止盈请求必须先按持仓方向相反生成:买持仓对应 SELL_LIMIT,卖持仓对应 BUY_LIMIT,这一点由 PositionGetInteger(POSITION_TYPE) 分支决定。 如果剩余仓位大于已平量且 tp_price 有效,代码会把尾量凑成合规手数——先取 LotsMin() 保底,再按 LotsStep() 向上归一,最后 NormalizeDouble 到 2 位小数。若尾量低于 LotsMin() 直接返回 false,避免 MT5 拒单。 CheckLimitOrder 不通过时,BUY_LIMIT 价格会被 MathMin 压到 max_buy_limit 之下,SELL_LIMIT 则被 MathMax 抬到 min_sell_limit 之上。这套边界钳制能让挂单不至于越过市场允许的限价区,外汇与贵金属波动剧烈,越界挂单在高滑点时段被拒概率明显上升。 普通挂单发送前也走同样的价格钳制:tp_request.price 在 ORDER_TYPE_BUY_LIMIT 时取与 max_buy_limit 的最小值,SELL_LIMIT 时取与 min_sell_limit 的最大值,然后才进 OrderSend。开 MT5 把 max_buy_limit / min_sell_limit 打印出来,就能看到你的尾量止盈到底被压到了什么价位。

MQL5 / C++
fix_closed_per-=tp_request.volume/(position_volume-tp_request.volume)*class="num">100;
}
class="kw">continue;
 }
 class="kw">switch(tp_request.type)
 {
 case ORDER_TYPE_BUY_LIMIT:
 tp_request.price=MathMin(tp_request.price,max_buy_limit);
 class="kw">break;
 case  ORDER_TYPE_SELL_LIMIT:
 tp_request.price=MathMax(tp_request.price,min_sell_limit);
 class="kw">break;
 }
 if(::OrderSend(tp_request,tp_result))
 {
 closed+=tp_result.volume;
 closed_perc=closed/position_volume*class="num">100;
 ZeroMemory(tp_result);
 }
 }
 }
 if(tp_price>class="num">0 && position_volume>closed)
 {
 tp_request.price=tp_price;
 tp_request.comment="TPP_"+IntegerToString(position_ticket);
 tp_request.volume=position_volume-closed;
 if(tp_request.volume<c_Symbol.LotsMin())
 class="kw">return class="kw">false;
 tp_request.volume=c_Symbol.LotsMin()+MathMax(class="num">0,NormalizeDouble((tp_request.volume-c_Symbol.LotsMin())/c_Symbol.LotsStep(),class="num">0)*c_Symbol.LotsStep());
 tp_request.volume=NormalizeDouble(tp_request.volume,class="num">2);
class=class="str">"cmt">//---
 class="kw">switch((class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE))
 {
 case POSITION_TYPE_BUY:
 tp_request.type=ORDER_TYPE_SELL_LIMIT;
 class="kw">break;
 case POSITION_TYPE_SELL:
 tp_request.type=ORDER_TYPE_BUY_LIMIT;
 class="kw">break;
 }
 if(CheckLimitOrder(tp_request) && tp_request.volume>=class="num">0)
 {
 closed+=tp_request.volume;
 closed_perc=closed/position_volume*class="num">100;
 }
 else
 {
 class="kw">switch(tp_request.type)
 {
 case ORDER_TYPE_BUY_LIMIT:
 tp_request.price=MathMin(tp_request.price,max_buy_limit);
 class="kw">break;
 case  ORDER_TYPE_SELL_LIMIT:
 tp_request.price=MathMax(tp_request.price,min_sell_limit);

◍ 挂单参数校验与残留限价单清理

上面这段逻辑紧接部分平仓流程:当挂出的限价单成交量算出来小于等于 0 时,代码会把剩余未平仓位减去已平部分作为挂单量,再按经纪商最小手数 LotsMin() 和步长 LotsStep() 做对齐,最后 NormalizeDouble 到 2 位小数。这一步直接决定你挂的 TP 限价单会不会因手数非法被服务器拒单。 平仓循环结束后,若已平总量 closed 达到原仓位 position_volume,且原持仓 TP 大于 0,会再用 TRADE_ACTION_SLTP 把原仓 TP 清零,只保留 SL。注意这里若 OrderSend 失败直接 return false,意味着整套平仓修饰逻辑中止,实盘里可能留下未清理的挂单。 CheckLimitOrder 函数负责扫挂单:用 StopsLevel()*Point() 算出卖限价单最低价和买限价单最高价边界,再按 comment 匹配自己发的单。若发现同 comment 但体积或类型不对的残留单,直接发 TRADE_ACTION_REMOVE 删除,删成功就 return false 并同步回写 request.volume。外汇与贵金属杠杆高,这类挂单残留若没清干净,遇到跳空可能触发非预期成交。

MQL5 / C++
         class="kw">break;
          }
        if(tp_request.volume<=class="num">0)
          {
          tp_request.volume=position_volume-closed;
          tp_request.volume=c_Symbol.LotsMin()+MathMax(class="num">0,NormalizeDouble((tp_request.volume-c_Symbol.LotsMin())/c_Symbol.LotsStep(),class="num">0)*c_Symbol.LotsStep());
          tp_request.volume=NormalizeDouble(tp_request.volume,class="num">2);
          }
        if(::OrderSend(tp_request,tp_result))
          {
          closed+=tp_result.volume;
          closed_perc=closed/position_volume*class="num">100;
          ZeroMemory(tp_result);
          }
        }
      }
   if(closed>=position_volume && PositionGetDouble(POSITION_TP)>class="num">0)
    {
     ZeroMemory(tp_request);
     ZeroMemory(tp_result);
     tp_request.action=TRADE_ACTION_SLTP;
     tp_request.position=position_ticket;
     tp_request.symbol=c_Symbol.Name();
     tp_request.sl=PositionGetDouble(POSITION_SL);
     tp_request.tp=class="num">0;
     tp_request.magic=PositionGetInteger(POSITION_MAGIC);
     if(!OrderSend(tp_request,tp_result))
       class="kw">return class="kw">false;
    }
   class="kw">return true;
   }
class="type">bool CLimitTakeProfit::CheckLimitOrder(class="type">MqlTradeRequest &request)
  {
   class="type">class="kw">double min_sell_limit=c_Symbol.NormalizePrice(c_Symbol.Ask()+c_Symbol.StopsLevel()*c_Symbol.Point());
   class="type">class="kw">double max_buy_limit=c_Symbol.NormalizePrice(c_Symbol.Bid()-c_Symbol.StopsLevel()*c_Symbol.Point());
   for(class="type">int i=class="num">0;i<total;i++)
     {
      class="type">class="kw">ulong ticket=OrderGetTicket((class="type">uint)i);
      if(ticket<=class="num">0)
        class="kw">continue;
      if(OrderGetString(ORDER_COMMENT)!=request.comment)
        class="kw">continue;
      if(OrderGetDouble(ORDER_VOLUME_INITIAL) != request.volume || OrderGetInteger(ORDER_TYPE)!=request.type)
        {
         class="type">MqlTradeRequest del_request={class="num">0};
         class="type">MqlTradeResult del_result={class="num">0};
         del_request.action=TRADE_ACTION_REMOVE;
         del_request.order=ticket;
         if(::OrderSend(del_request,del_result))
            class="kw">return class="kw">false;
         request.volume=OrderGetDouble(ORDER_VOLUME_INITIAL);
        }

挂单改价与历史单递归核对的实现细节

这段逻辑处理的是限价挂单的改价与成交后历史核对。先判断开仓价与请求价之差是否大于等于一个 Point,若成立就构造 TRADE_ACTION_MODIFY 请求去移动挂单;Buy Limit 改价若高于 max_buy_limit、Sell Limit 改价若低于 min_sell_limit 则直接 return true 不发送,相当于边界保护。 改价请求里复用原单的 magic、symbol 和 type,通过 OrderSend 发出。若持仓 ticket 已选不中(说明仓位不在了),函数也直接 return true,把后续交给历史记录去查。 CheckOrderInHistory 先用 HistorySelectByPosition 拉取某持仓的全部历史成交,再遍历 HistoryDealsTotal 笔。零售对冲账户下(ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)会判断 ORDER_POSITION_ID 是否匹配,不匹配就递归查关联仓位,防止对冲单被漏掉。 外汇和贵金属杠杆高,这类自动改挂单逻辑在多点滑点行情中可能误触边界返回,建议开 MT5 用策略测试器跑一遍 hedge 账户样本验证递归深度。

MQL5 / C++
if(MathAbs(OrderGetDouble(ORDER_PRICE_OPEN)-request.price)>=c_Symbol.Point())
  {
   class="type">MqlTradeRequest mod_request={class="num">0};
   class="type">MqlTradeResult mod_result={class="num">0};
   mod_request.action=TRADE_ACTION_MODIFY;
   mod_request.price=request.price;
   mod_request.magic=request.magic;
   mod_request.symbol=request.symbol;
   class="kw">switch(request.type)
     {
      case ORDER_TYPE_BUY_LIMIT:
        if(mod_request.price>max_buy_limit)
         class="kw">return true;
       class="kw">break;
      case ORDER_TYPE_SELL_LIMIT:
        if(mod_request.price<min_sell_limit)
         class="kw">return true;
       class="kw">break;
     }
   class="type">bool mod=::OrderSend(mod_request,mod_result);
   }
  class="kw">return true;
  }
 if(!PositionSelectByTicket(request.position))
   class="kw">return true;
class=class="str">"cmt">//---
  class="kw">return CheckOrderInHistory(PositionGetInteger(POSITION_IDENTIFIER),request.comment, request.type, request.volume);
  }
class="type">bool CLimitTakeProfit::CheckOrderInHistory(class="type">class="kw">ulong position_id, class="type">class="kw">string comment, ENUM_ORDER_TYPE type, class="type">class="kw">double &volume, class="type">class="kw">ulong call_position=class="num">0)
  {
  if(!HistorySelectByPosition(position_id))
    class="kw">return true;
  class="type">int total=HistoryDealsTotal();
  class="type">bool hedging=(AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
class=class="str">"cmt">//---
  for(class="type">int i=class="num">0;i<total;i++)
    {
     class="type">class="kw">ulong ticket=HistoryDealGetTicket((class="type">uint)i);
     ticket=HistoryDealGetInteger(ticket,DEAL_ORDER);
     if(!HistoryOrderSelect(ticket))
       class="kw">continue;
     if(ticket<=class="num">0)
       class="kw">continue;
     if(hedging && HistoryOrderGetInteger(ticket,ORDER_POSITION_ID)!=position_id && HistoryOrderGetInteger(ticket,ORDER_POSITION_ID)!=call_position)
      {
       if(CheckOrderInHistory(HistoryOrderGetInteger(ticket,ORDER_POSITION_ID),comment,type,volume))
         class="kw">return true;
       if(!HistorySelectByPosition(position_id))
         class="kw">continue;

「用持仓注释反查对冲单并闭环平仓」

在零售对冲账户(ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)下,限价挂单触发后生成的止盈持仓会在注释里写入 'TP_原单ticket' 这类标记。OnTrade 遍历 PositionsTotal 返回的当前持仓数,对每张持仓先按 Magic 与品种过滤,再抓注释做反查。 代码里用 StringFind(comment,"TP")==0 判定这是系统自己开的止盈仓,随后从下划线位置切出被对冲的原始 ticket。若 PositionSelectByTicket 能选中且两边 POSITION_TYPE 相反,就填 MqlTradeRequest 以 TRADE_ACTION_CLOSE_BY 把两仓互平。 这套逻辑实测在 MT5 _build 35xx 零售对冲账户上,能把手动误留的对冲腿自动清掉;外汇与贵金属杠杆高,自动互平若遇点差跳空可能只成交一端,剩余风险敞口需人工复核。

MQL5 / C++
   }
      if(HistoryOrderGetString(ticket,ORDER_COMMENT)!=comment)
         class="kw">continue;
      if(HistoryOrderGetInteger(ticket,ORDER_TYPE)!=type)
         class="kw">continue;
class=class="str">"cmt">//---
      volume=-OrderGetDouble(ORDER_VOLUME_INITIAL);
      class="kw">return true;
   }
   class="kw">return class="kw">false;
   }
class="type">bool CLimitTakeProfit::OnTrade(class="type">void)
  {
   class="type">int total=PositionsTotal();
   class="type">bool result=true;
   class="type">bool hedhing=AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING;
   for(class="type">int i=class="num">0;i<total;i++)
    {
      class="type">class="kw">ulong ticket=PositionGetTicket((class="type">uint)i);
      if(ticket<=class="num">0 || (b_OnlyOneSymbol && PositionGetString(POSITION_SYMBOL)!=_Symbol))
         class="kw">continue;
class=class="str">"cmt">//---
    if(i_Magic>class="num">0)
       {
        if(hedhing && PositionGetInteger(POSITION_MAGIC)!=i_Magic)
           class="kw">continue;
       }
      if(hedhing)
       {
        class="type">class="kw">string comment=PositionGetString(POSITION_COMMENT);
        if(StringFind(comment,"TP")==class="num">0)
          {
           class="type">int start=StringFind(comment,"_");
           if(start>class="num">0)
             {
              class="type">long ticket_by=StringToInteger(StringSubstr(comment,start+class="num">1));
              class="type">long type=PositionGetInteger(POSITION_TYPE);
              if(ticket_by>class="num">0 && PositionSelectByTicket(ticket_by) && type!=PositionGetInteger(POSITION_TYPE))
                {
                 class="type">MqlTradeRequest  request  ={class="num">0};
                 class="type">MqlTradeResult   trade_result ={class="num">0};
                 request.action=TRADE_ACTION_CLOSE_BY;
                 request.position=ticket;
                 request.position_by=ticket_by;
                 if(::OrderSend(request,trade_result))
                    class="kw">continue;
                 }

常见问题

在持仓浮盈到达目标位前,由外部脚本按梯度挂出限价单接管平仓,原始EA只负责开仓与止损,不碰止盈逻辑。
按点位梯度拆仓:例如50/80/120点各挂一张限价卖单,每张对应持仓的三分之一,尾量单需处理最小手数边界。
小布可实时扫描持仓与挂单注释,识别梯度止盈单是否缺失或残留,并提示你补单或清理,不用自己翻终端。
用持仓注释反查关联挂单,对超过有效期或未触发的中途限价单递归核对后一键平仓闭环,避免占用保证金。
按订单注释递归核对改价记录与历史成交,用反查机制把对冲单闭环平仓,确保账面持仓和挂单完全一致。