本文讨论如何在跨平台智能交易系统中设置自定义停止价位。它还讨论了一种紧密相关的方法, 即随着时间的推移, 定义停止位的演化。(基础篇)
📘

本文讨论如何在跨平台智能交易系统中设置自定义停止价位。它还讨论了一种紧密相关的方法, 即随着时间的推移, 定义停止位的演化。(基础篇)

第 1/3 篇

自定义止损位的演化逻辑

在 MT5 的智能交易系统里,停止价位不一定非要在下单那一刻写死。更实用的做法是把它当成一个随时间推移而演化的变量:开仓后根据价格行为逐步移动止损或止盈,而不是依赖平台自带的固定距离尾随。 原文给出的示例发布于 2017 年 10 月 24 日,累计浏览 6420 次、评论 15 条,说明这类自定义停止位的需求在 MQL5 社区一直有真实交易量级的使用场景。 具体实现上,作者把停止位拆成「自定义停止位」「修改停止价位」「盈亏平衡」「尾随止损」「尾随止盈」几个层次,并用 CTrails 容器统一管理多条 trail 的实例。对外汇与贵金属这类高波动品种,这种演化式止损可能比固定点数止损更贴合波段结构,但杠杆交易风险极高,任何止损都只是概率保护而非保本承诺。

◍ 用图表价格而非点距来设止损

上一节演示了用 CStop 类按点值或点数设置止损止盈,但实盘里止损位往往不是按入场价加减固定距离算出来的。很多策略的止损/止盈直接来自其他计算逻辑,并以图表上的具体价格呈现,而不是相对入场价的偏移量。 CStop 类本身就支持以图表价格直接指定停止位,不必绕回点距换算。本文要展开的就是这套用法,以及如何用配套函数库在持仓过程中修改这些停止位。 另一个重点是新增的 CTrail 类。它和自定义停止位写法很像,但定位不同:CTrail 专门处理随时间移动的跟踪停止位,而不是一次性写死的价位。外汇与贵金属杠杆高、滑点突发行情多,用价格级停止位前要自己在 MT5 里跑一遍验证逻辑。

「动态停止位怎么在 CStop 里落地」

很多策略的止损止盈不能写死成固定点数,得跟着 OHLC 或指标输出走。CStop 里留了两个扩展入口:StopLossCustom 和 TakeProfitCustom,程序员可以重写它们来算出任意想要的停止价位。 这两个方法其实是一对重载,简化版本只返回布尔值,用来判断类成员 m_stoploss 或 m_takeprofit 是否为零。为零就意味着走自定义计算,非零就按入场价叠加成员值。 CStop 的计算顺序是:m_stoploss 为零则用 StopLossCustom,非零则按成员值算真实止损;止盈对 m_takeprofit 同理。主停止位在订单管理器 TradeOpen 里调用,其余附加停止位在每笔 COrder 实例内处理。 在 MT4 与 MT5 对冲模式里,主停止位要随初始请求发给经纪商,所以 EA 只在发单前需要它。净持模式下不能这么干——止损会作用到整个持仓而不是单笔交易量,因此代码里会把 sl、tp 重置为 0,改由挂单模拟。 附加的多停止位必须等主交易进场后才创建,否则没有意义。COrder 的 CreateStops 会为每个 CStop 实例生成 COrderStop,但仅在主单成功入场后生效。外汇和贵金属杠杆高,自定义停止位算错可能直接放大回撤,上 MT5 前先单步跟一遍返回值。

MQL5 / C++
class="type">bool CStopBase::StopLossCustom(class="type">void)
  {
   class="kw">return m_stoploss==class="num">0;
  }
class="type">bool CStopBase::TakeProfitCustom(class="type">void)
  {
   class="kw">return m_takeprofit==class="num">0;
  }
class="type">bool CStopBase::StopLossCustom(class="type">void)
  {
   class="kw">return m_stoploss==class="num">0;
  }
class="type">bool CStopBase::TakeProfitCustom(class="type">void)
  {
   class="kw">return m_takeprofit==class="num">0;
  }
class="type">bool COrderManager::TradeOpen(class="kw">const class="type">class="kw">string symbol,ENUM_ORDER_TYPE type,class="type">class="kw">double price,class="type">bool in_points=true)
  {
   class="type">int trades_total = TradesTotal();
   class="type">int orders_total = OrdersTotal();
   m_symbol=m_symbol_man.Get(symbol);
   if(!CheckPointer(m_symbol))
      class="kw">return class="kw">false;
   if(!IsPositionAllowed(type))
      class="kw">return true;
   if(m_max_orders>orders_total && (m_max_trades>trades_total || m_max_trades<=class="num">0))
    {
      ENUM_ORDER_TYPE ordertype=type;
      if(in_points)
         price=PriceCalculate(type);
      class="type">class="kw">double sl=class="num">0,tp=class="num">0;
      if(CheckPointer(m_main_stop)==POINTER_DYNAMIC)
        {
         sl = m_main_stop.StopLossCustom()?m_main_stop.StopLossCustom(symbol,type,price):m_main_stop.StopLossCalculate(symbol,type,price);
         tp = m_main_stop.TakeProfitCustom()?m_main_stop.TakeProfitCustom(symbol,type,price):m_main_stop.TakeProfitCalculate(symbol,type,price);
        }
      class="type">class="kw">double lotsize=LotSizeCalculate(price,type,sl);
      if(CheckPointer(m_main_stop)==POINTER_DYNAMIC)
      {
         if (!m_main_stop.Broker())
         {
            sl = class="num">0;
            tp = class="num">0;
         }
      }
      class="type">int ticket=(class="type">int)SendOrder(type,lotsize,price,sl,tp);
      if(ticket>class="num">0)
       {
         if(OrderSelect(ticket,SELECT_BY_TICKET))
          {

从挂单回补到开仓风控的订单封装

这段逻辑把已有订单和新建订单统一塞进 COrder 对象,靠 CheckPointer 判断内存对象是否真实存在,避免空指针去读写票据号、品种和魔术码。 COrder *order = m_orders.NewOrder(OrderTicket(),OrderSymbol(),OrderMagicNumber(), (ENUM_ORDER_TYPE)::OrderType(),::OrderLots(),::OrderOpenPrice()); if (CheckPointer(order)) { LatestOrder(GetPointer(order)); return true; } 上面这段是把历史挂单转成本地订单缓存的标准写法,OrderTicket() 取票据、OrderLots() 取手数、OrderOpenPrice() 取开价,六个参数顺序不能错。 TradeOpen 里先卡两道闸:IsPositionAllowed 拒绝不允许的订单类型,m_max_orders>orders_total 且 m_max_trades>trades_total(或 m_max_trades<=0 表示不限制)才放行进单。外汇与贵金属杠杆高,这类计数闸能直接压住过度交易的概率。 若 m_main_stop 是动态指针,SL/TP 会走自定义或计算函数;但若 Broker() 返回假或不是对冲账户,代码强制把 sl、tp 归 0,等于放弃止损止盈发单。实盘前应在 MT5 策略测试器里打印 m_main_stop.Broker() 返回值,确认经纪商配置没悄悄吞掉你的止损。 lotsize=LotSizeCalculate(price,type,m_main_stop==NULL?0:m_main_stop.StopLossCalculate(symbol,type,price)); 这行把止损距离也喂给仓位计算,若止损计算返回 0,仓位模型会按无止损情形放大手数,风险敞口可能倾向陡增。

MQL5 / C++
COrder *order = m_orders.NewOrder(OrderTicket(),OrderSymbol(),OrderMagicNumber(),
(ENUM_ORDER_TYPE)::OrderType(),::OrderLots(),::OrderOpenPrice());
if (CheckPointer(order))
{
   LatestOrder(GetPointer(order));
   class="kw">return true;
}
class="type">bool COrderManager::TradeOpen(class="kw">const class="type">class="kw">string symbol,ENUM_ORDER_TYPE type,class="type">class="kw">double price,class="type">bool in_points=true)
  {
   class="type">bool ret=class="kw">false;
   class="type">class="kw">double lotsize=class="num">0.0;
   class="type">int trades_total =TradesTotal();
   class="type">int orders_total = OrdersTotal();
   m_symbol=m_symbol_man.Get(symbol);
   if(!IsPositionAllowed(type))
      class="kw">return true;
   if(m_max_orders>orders_total && (m_max_trades>trades_total || m_max_trades<=class="num">0))
     {
      if(in_points)
         price=PriceCalculate(type);
      class="type">class="kw">double sl=class="num">0.0,tp=class="num">0.0;
      if(CheckPointer(m_main_stop)==POINTER_DYNAMIC)
        {
         sl = m_main_stop.StopLossCustom()?m_main_stop.StopLossCustom(symbol,type,price):m_main_stop.StopLossCalculate(symbol,type,price);
         tp = m_main_stop.TakeProfitCustom()?m_main_stop.TakeProfitCustom(symbol,type,price):m_main_stop.TakeProfitCalculate(symbol,type,price);
        }
      lotsize=LotSizeCalculate(price,type,m_main_stop==NULL?class="num">0:m_main_stop.StopLossCalculate(symbol,type,price));
      if(CheckPointer(m_main_stop)==POINTER_DYNAMIC)
      {
         if (!m_main_stop.Broker() || !IsHedging())
          {
            sl = class="num">0;
            tp = class="num">0;
          }
       }
      ret=SendOrder(type,lotsize,price,sl,tp);
      if(ret)
      {
         COrder *order = m_orders.NewOrder((class="type">int)m_trade.ResultOrder(),m_trade.RequestSymbol(),(class="type">int)m_trade.RequestMagic(),
m_trade.RequestType(),m_trade.ResultVolume(),m_trade.ResultPrice());
         if (CheckPointer(order))
          {
            LatestOrder(GetPointer(order));
            class="kw">return true;
          }
       }
     }
   class="kw">return ret;
  }
if(CheckPointer(m_main_stop)==POINTER_DYNAMIC)
{
   if (!m_main_stop.Broker())
   {
      sl = class="num">0;
      tp = class="num">0;
   }
}
if(CheckPointer(m_main_stop)==POINTER_DYNAMIC)
{

◍ 对冲账户下止损止盈的强制清零

在 MT5 的订单基类里,若当前账户不支持对冲(IsHedging() 返回 false)或主止损对象未通过 Broker() 校验,代码会直接把 sl 和 tp 置为 0。这意味着即便上层策略算出了止损位,在该条件下也不会真正挂出保护单,外汇与贵金属杠杆交易下这类静默失效可能放大回撤风险。 CreateStops 方法负责把外部传入的 CStops 集合逐个注册到订单止损管理器。它先判断指针有效性,再遍历 stops.Total() 个对象;任一子止损指针无效就跳过,有效则通过 NewOrderStop 绑定到当前订单实例。 开 MT5 把这段逻辑塞进自己的 EA 订单模块,打印 IsHedging() 与 Broker() 返回值,能立刻验证不同账户类型下 sl/tp 是否被吞掉。

MQL5 / C++
  if (!m_main_stop.Broker() || !IsHedging())
  {
      sl = class="num">0;
      tp = class="num">0;
  }
}
class="type">void COrderBase::CreateStops(CStops *stops)
  {
   if(!CheckPointer(stops))
      class="kw">return;
   if(stops.Total()>class="num">0)
     {
       for(class="type">int i=class="num">0;i<stops.Total();i++)
         {
          CStop *stop=stops.At(i);
          if(CheckPointer(stop)==POINTER_INVALID)
             class="kw">continue;
          m_order_stops.NewOrderStop(GetPointer(this),stop);
         }
     }
  }

「停止位修改的调用链落在哪一层」

CStop 在扩展交易类里持有独立的 CTradeManager 实例,和管入场的订单管理器分开跑。实际改停止价不是订单管理器的事,而是 CStop 自己接手;但调用必须由 COrder 里的交易发起,因为修改按预订顺序执行,乱序会破坏持仓逻辑。 监视起点是 COrder::CheckStops,它只转调 COrderStops::Check。后者在容器里逐个跑 COrderStop 实例,至少做五件事:看是否已平单、按 trailing 实例更新、Update 刷新、检查行情是否触价、MT4 下核对逐笔 ID 变更。MT5 没有第五项,部分平仓时单号不变,逻辑更干净。 尾随真正改值发生在 COrderStop::CheckTrailing,它先排除已平或挂起,再向 CStop 要新 sl/tp。下面这段是核心调用: void COrderBase::CheckStops(void) { m_order_stops.Check(m_volume); } COrderStopsBase::Check(double &volume) { if(!Active()) return; for(int i=0;i<Total();i++) { COrderStop *order_stop=(COrderStop *)At(i); if(CheckPointer(order_stop)) { if(order_stop.IsClosed()) continue; order_stop.CheckTrailing(); order_stop.Update(); order_stop.Check(volume); if(!CheckNewTicket(order_stop)) return; } } } bool COrderStopBase::CheckTrailing(void) {

if(!CheckPointer(m_stop)m_order.IsClosed()m_order.IsSuspended()

(m_stoploss_closed && m_takeprofit_closed)) return false; double stoploss=0,takeprofit=0; string symbol=m_order.Symbol(); ENUM_ORDER_TYPE type=m_order.OrderType(); double price=m_order.Price(); double sl = StopLoss(); double tp = TakeProfit(); if(!m_stoploss_closed) stoploss=m_stop.CheckTrailing(symbol,type,price,sl,TRAIL_TARGET_STOPLOSS); if(!m_takeprofit_closed) takeprofit=m_stop.CheckTrailing(symbol,type,price,tp,TRAIL_TARGET_TAKEPROFIT); if(!IsStopLossValid(stoploss)) stoploss=0; if(!IsTakeProfitValid(takeprofit)) takeprofit=0; return Modify(stoploss,takeprofit); } double CStopBase::CheckTrailing(const string symbol,const ENUM_ORDER_TYPE type,const double entry_price,const double price,const ENUM_TRAIL_TARGET mode) { if(!CheckPointer(m_trails)) return 0; 逐行拆一下:CheckStops 只把成交量丢给 m_order_stops.Check;Check 里先判 Active,不活跃直接退,再遍历所有停止实例,已平的 skip,活的跑尾随、更新、触价检查、MT4 单号核对。CheckTrailing 先卡掉无效指针和平仓挂起状态,取品种、类型、开价、当前 sl/tp,分别问 CStop 算新值,无效就归零,最后 Modify。CStop::CheckTrailing 开头验 m_trails 指针,空就返 0,说明尾随容器没挂上就别想改价。 Modify 之前会用 IsStopLossValid / IsTakeProfitValid 卡一道,建议值等于当前值也判无效重置为 0。经纪商端停止可一次改完,挂单和虚拟停止必须分两次单独处理——虚拟的只动内部数据,不碰服务器。COrderStop 自己没交易对象指针,任何改价最终都绕回 CStop 里的方法。 开 MT5 把这段塞进自己的 EA 框架,先打印 m_trails 指针是否为空,能直接看出尾随为什么没生效。外汇和贵金属杠杆高,虚拟停止位断网就失效,真触价可能滑到你设的价位之外。

MQL5 / C++
class="type">void COrderBase::CheckStops(class="type">void)
  {
   m_order_stops.Check(m_volume);
  }
COrderStopsBase::Check(class="type">class="kw">double &volume)
  {
   if(!Active())
     class="kw">return;
   for(class="type">int i=class="num">0;i<Total();i++)
     {
      COrderStop *order_stop=(COrderStop *)At(i);
      if(CheckPointer(order_stop))
        {
         if(order_stop.IsClosed())
           class="kw">continue;
         order_stop.CheckTrailing();
         order_stop.Update();
         order_stop.Check(volume);
         if(!CheckNewTicket(order_stop))
           class="kw">return;
        }
     }
  }
class="type">bool COrderStopBase::CheckTrailing(class="type">void)
  {
   if(!CheckPointer(m_stop) || m_order.IsClosed() || m_order.IsSuspended() ||
      (m_stoploss_closed && m_takeprofit_closed))
     class="kw">return class="kw">false;
   class="type">class="kw">double stoploss=class="num">0,takeprofit=class="num">0;
   class="type">class="kw">string symbol=m_order.Symbol();
   ENUM_ORDER_TYPE type=m_order.OrderType();
   class="type">class="kw">double price=m_order.Price();
   class="type">class="kw">double sl = StopLoss();
   class="type">class="kw">double tp = TakeProfit();
   if(!m_stoploss_closed)
     stoploss=m_stop.CheckTrailing(symbol,type,price,sl,TRAIL_TARGET_STOPLOSS);
   if(!m_takeprofit_closed)
     takeprofit=m_stop.CheckTrailing(symbol,type,price,tp,TRAIL_TARGET_TAKEPROFIT);
   if(!IsStopLossValid(stoploss))
     stoploss=class="num">0;
   if(!IsTakeProfitValid(takeprofit))
     takeprofit=class="num">0;
   class="kw">return Modify(stoploss,takeprofit);
  }
class="type">class="kw">double CStopBase::CheckTrailing(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double entry_price,class="kw">const class="type">class="kw">double price,class="kw">const ENUM_TRAIL_TARGET mode)
  {
   if(!CheckPointer(m_trails))
     class="kw">return class="num">0;

常见问题

在开仓或挂单时把止损价直接赋为图表上的关键价位(如最近波段低点),而非用开仓价加减点数计算,这样止损会随结构走而不是死板固定。
停止位的修改调用链通常放在订单管理封装层,由定时检查逻辑触发 CStop 类去更新止损价,不要在报价tick里直接硬改。
小布可以监控对冲类账户的开仓回报,一旦发现止损止盈被平台强制清空就推送提醒,你再手动重设即可降低裸奔风险。
挂单成交后需立即用统一订单封装函数补写止损止盈,避免回补单继承空止损,最好在成交事件里强制走一遍风控赋值。
对冲账户在某些品种或条件下会强制清零止损止盈,开仓前应先读账户属性判断,再决定用挂单预埋还是成交后补设。