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

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

(3/3)·点值止损只是起点,用图表价和CTrail把动态停止逻辑真正写进多平台EA

进阶 第 3/3 篇

很多EA开发者把止损只当成入场价加减固定点数,遇到基于指标输出或OHLC动态算出的价位就卡壳。跨平台项目里这类自定义停止位若处理不当,回测和实盘会出现不该有的滑点缺口。本文收尾整个系列,把自定义停止与演化停止一次讲透。

跟踪止损基类里的价格触发逻辑

做跟踪止损前,先看清基类 CTrailBase 把「激活、失效、实时挂价」三件事拆成了三个虚函数。ActivationPrice 用 m_start 个点作为触发门槛,DeactivationPrice 用 m_end 个点作为失效线,Price 则按当前 Bid/Ask 与 m_trail 算出新止损或止盈位。 Set 方法的默认参数是 step=1、end=0,意味着若你只传 trail 和 st,跟踪步长就是 1 个点、且不设失效价位。在外汇与贵金属上,点值随品种跳变,用 Point() 而不是写死小数,是避免跨品种出错的基本动作,这类品种杠杆高、滑点可能吞掉窄幅跟踪。 ActivationPrice 对 BUY 返回 entry_price+m_start*Point(),SELL 则返回减去;返回 0 代表类型不匹配。DeactivationPrice 里 m_end==0 时直接回 0,说明 0 被当作「不失效」的哨兵值,不是价格 0。 Refresh 每次用 symbol 名比对,指针不对就向 m_symbol_man 重新取对象。CheckPointer 失败会返回 false,调用方若忽略这个返回值,后续 Point() 访问可能直接崩 EA。

MQL5 / C++
class="type">void CTrailBase::Set(class="kw">const class="type">class="kw">double trail,class="kw">const class="type">class="kw">double st,class="kw">const class="type">class="kw">double step=class="num">1,class="kw">const class="type">class="kw">double end=class="num">0)
  {
   m_trail=trail;
   m_start=st;
   m_end=end;
   m_step=step;
  }
class="type">bool CTrailBase::Refresh(class="kw">const class="type">class="kw">string symbol)
  {
   if(!CheckPointer(m_symbol) || StringCompare(m_symbol.Name(),symbol)!=class="num">0)
      m_symbol=m_symbol_man.Get(symbol);
   class="kw">return CheckPointer(m_symbol);
  }
class="type">class="kw">double CTrailBase::ActivationPrice(class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double entry_price)
  {
   if(type==ORDER_TYPE_BUY)
      class="kw">return entry_price+m_start*m_symbol.Point();
   else if(type==ORDER_TYPE_SELL)
      class="kw">return entry_price-m_start*m_symbol.Point();
   class="kw">return class="num">0;
  }
class="type">class="kw">double CTrailBase::DeactivationPrice(class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double entry_price)
  {
   if(type==ORDER_TYPE_BUY)
      class="kw">return m_end==class="num">0?class="num">0:entry_price+m_end*m_symbol.Point();
   else if(type==ORDER_TYPE_SELL)
      class="kw">return m_end==class="num">0?class="num">0:entry_price-m_end*m_symbol.Point();
   class="kw">return class="num">0;
  }
class="type">class="kw">double CTrailBase::Price(class="kw">const ENUM_ORDER_TYPE type)
  {
   if(type==ORDER_TYPE_BUY)
     {
      if(m_target==TRAIL_TARGET_STOPLOSS)
         class="kw">return m_symbol.Bid()-m_trail*m_symbol.Point();
      else if(m_target==TRAIL_TARGET_TAKEPROFIT)
         class="kw">return m_symbol.Ask()+m_trail*m_symbol.Point();
     }
   else if(type==ORDER_TYPE_SELL)
     {
      if(m_target==TRAIL_TARGET_STOPLOSS)
         class="kw">return m_symbol.Ask()+m_trail*m_symbol.Point();

「跟踪止损与止盈的触发判定逻辑」

这段基类代码把跟踪止损(TRAIL_TARGET_STOPLOSS)和跟踪止盈(TRAIL_TARGET_TAKEPROFIT)两套规则塞进了同一个 Check 方法,靠 m_target 枚举切换。外汇与贵金属杠杆高,跟踪触发一旦写错,滑点扩大时可能直接吞掉已有浮盈,建议先在策略测试器跑历史 tick 验证。 对于 BUY 单的跟踪止损:当 m_step>0 且价格越过激活线(activation - m_trail*point)并满足新价较前高抬升至少 m_step 个点时,next_stop 取新价;否则退回 activation - m_trail*point 的静态拖尾。最后用 next_stop<price 兜底,保证止损不高于当前价。 SELL 单的对称逻辑里,条件反转为 price<=activation+m_trail*point 与 new_price<price-m_step*point,且兜底判断变成 next_stop>price 才修正。注意 deactivation 非零时,BUY 止损要求 next_stop>=deactivation 才返回,止盈则要求 next_stop<=deactivation,方向完全相反。 实际调参时把 m_start、m_trail、m_step 打印到日志,观察黄金 XAUUSD 在美盘波动 15 点内是否被反复触发,再决定是否放宽 m_step。

MQL5 / C++
   else if(m_target==TRAIL_TARGET_TAKEPROFIT)
      class="kw">return m_symbol.Bid()-m_trail*m_symbol.Point();
   }
   class="kw">return class="num">0;
}
enum ENUM_TRAIL_TARGET
  {
   TRAIL_TARGET_STOPLOSS,
   TRAIL_TARGET_TAKEPROFIT
  };
class="type">class="kw">double CTrailBase::Check(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(!Active())
      class="kw">return class="num">0;
   if(!Refresh(symbol))
      class="kw">return class="num">0;
   if(m_start==class="num">0 || m_trail==class="num">0)
      class="kw">return class="num">0;
   class="type">class="kw">double next_stop=class="num">0.0,activation=class="num">0.0,deactivation=class="num">0.0,new_price=class="num">0.0,point=m_symbol.Point();
   activation=ActivationPrice(type,entry_price);
   deactivation=DeactivationPrice(type,entry_price);
   new_price=Price(type);
   if(type==ORDER_TYPE_BUY)
    {
     if (m_target==TRAIL_TARGET_STOPLOSS)
     {
      if(m_step>class="num">0 && (activation==class="num">0.0 || price>=activation-m_trail*point) && (new_price>price+m_step*point))
       next_stop=new_price;
      else next_stop=activation-m_trail*point;
      if(next_stop<price)
       next_stop=price;
      if((deactivation==class="num">0) || (deactivation>class="num">0 && next_stop>=deactivation && next_stop>class="num">0.0))
       if(next_stop<=new_price)
        class="kw">return next_stop;
     }
     else if (m_target==TRAIL_TARGET_TAKEPROFIT)
     {
      if(m_step>class="num">0 && ( activation==class="num">0.0 || price>=activation) && (new_price>price+m_step*point))
       next_stop=new_price;
      else next_stop=activation+m_trail*point;
      if(next_stop<price)
       next_stop=price;
      if((deactivation==class="num">0) || (deactivation>class="num">0 && next_stop<=deactivation && next_stop>class="num">0.0))
       if(next_stop>=new_price)
        class="kw">return next_stop;
     }
    }
   if(type==ORDER_TYPE_SELL)
    {
     if (m_target==TRAIL_TARGET_STOPLOSS)
     {
      if(m_step>class="num">0 && (activation==class="num">0.0 || price<=activation+m_trail*point) && (new_price<price-m_step*point))
       next_stop=new_price;
      else next_stop=activation+m_trail*point;
      if(next_stop>price)
       next_stop=price;

◍ 跟踪止盈分支的触发与回退逻辑

上面这段是跟踪止损类模块里针对 TRAIL_TARGET_TAKEPROFIT 模式的分支。它的核心动作是:当 m_step 大于 0,且激活价 activation 为 0 或当前价格已跌破 activation,同时新价较前一价回落超过 m_step 个 point 时,把 next_stop 直接设为 new_price;否则退回用 activation 减去 m_trail*point 作为候选止损。 若算出的 next_stop 反而高于当前 price,则强制拉平到 price,避免止损挂到市价上方这种无效单。外汇与贵金属波动剧烈,这种边界修正能少踩返异常报价的坑。 最后一层判断处理 deactivation:未设置(0)或候选止损已穿破 deactivation 且为正值时,才在 next_stop 小于等于 new_price 的条件下 return 出去。整个函数拿不到合法止损就返回 0,调用方需自行判别。

MQL5 / C++
   if((deactivation==class="num">0) || (deactivation>class="num">0 && next_stop<=deactivation && next_stop>class="num">0.0))
         if(next_stop>=new_price)
            class="kw">return next_stop;
   }
   else if (m_target==TRAIL_TARGET_TAKEPROFIT)
   {
      if(m_step>class="num">0 && (activation==class="num">0.0 || price<=activation) && (new_price<price-m_step*point))
            next_stop=new_price;
      else next_stop=activation-m_trail*point;
      if(next_stop>price)
            next_stop=price;   
      if((deactivation==class="num">0) || (deactivation>class="num">0 && next_stop<=deactivation && next_stop>class="num">0.0))
         if(next_stop<=new_price)
            class="kw">return next_stop;
   }
 }
 class="kw">return class="num">0;
}

多尾随停止的容器如何择优返回

CTrails 作为 CArrayObj 的派生容器,负责持有多个 CTrail 实例指针,并在 CStop 与具体尾随算法之间做桥接。当容器内存在多个 CTrail 指针时,一次 Check 调用会遍历全部实例,但只把最接近当前市价的那个数值作为最终输出。 这种“取最优”的行为并非写死。基类 CTrailBase 把所有计算逻辑下沉,CTrails 只做存储与调度,因此你可以通过继承 CTrails 改写汇总规则,比如改成加权平均或按优先级覆盖。 下面这段基类定义和 Check 实现值得在 MT5 里直接建个类验证:m_active 控制总开关,GetContainer/SetContainer 绑定 CStop;Check 里先判 Active,再按 ENUM_TRAIL_TARGET 过滤,买单的止损类尾随取最大值、卖单止损类取最小值,其余组合同理反向比较。外汇与贵金属杠杆高,尾随逻辑改错可能瞬间放大回撤,上线前务必用历史 tick 跑一遍边界。

MQL5 / C++
class CTrailsBase : class="kw">public CArrayObj
{
class="kw">protected:
  class="type">bool              m_active;
  CEventAggregator *m_event_man;
  CStop            *m_stop;
class="kw">public:
                     CTrailsBase(class="type">void);
                    ~CTrailsBase(class="type">void);
  class="kw">virtual class="type">int       Type(class="type">void) class="kw">const {class="kw">return CLASS_TYPE_TRAILS;}
  class=class="str">"cmt">//--- 初始化
  class="kw">virtual class="type">bool      Init(CSymbolManager*,CEventAggregator*);
  class="kw">virtual CStop    *GetContainer(class="type">void);
  class="kw">virtual class="type">void      SetContainer(CStop*stop);
  class="kw">virtual class="type">bool      Validate(class="type">void) class="kw">const;
  class=class="str">"cmt">//--- 取值和赋值
  class="type">bool              Active(class="type">void) class="kw">const;
  class="type">void              Active(class="kw">const class="type">bool activate);
  class=class="str">"cmt">//--- 检查
  class="kw">virtual class="type">class="kw">double    Check(class="kw">const class="type">class="kw">string,class="kw">const ENUM_ORDER_TYPE,class="kw">const class="type">class="kw">double,class="kw">const class="type">class="kw">double,class="kw">const ENUM_TRAIL_TARGET);
 };
class="type">class="kw">double CTrailsBase::Check(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(!Active())
      class="kw">return class="num">0;  
   class="type">class="kw">double val=class="num">0.0,ret=class="num">0.0;
   for(class="type">int i=class="num">0;i<Total();i++)
    {
     CTrail *trail=At(i);
     if(!CheckPointer(trail))
       class="kw">continue;
     if(!trail.Active())
       class="kw">continue;
     class="type">int trail_target=trail.TrailTarget();
     if(mode!=trail_target)
       class="kw">continue;
     val=trail.Check(symbol,type,entry_price,price,mode);
     if((type==ORDER_TYPE_BUY && trail_target==TRAIL_TARGET_STOPLOSS) || (type==ORDER_TYPE_SELL && trail_target==TRAIL_TARGET_TAKEPROFIT))
     {
       if(val>ret || ret==class="num">0.0)
          ret=val;
     }        
     else if((type==ORDER_TYPE_SELL && trail_target==TRAIL_TARGET_STOPLOSS) || (type==ORDER_TYPE_BUY && trail_target==TRAIL_TARGET_TAKEPROFIT))
     {
       if(val<ret || ret==class="num">0.0)

「把极值缓存塞进返回值里」

这段 MQL5 片段把遍历得到的极值通过局部变量 ret 暂存,只在满足条件时更新 ret=val,最后统一 return ret。它避开了在循环内多次直接返回,方便调用方拿到一个确定的极值引用而不是中途断点。 实际在 MT5 里跑时,你可以把这段放进自定义指标的取值函数,观察在 1 分钟图上连续 500 根 K 线里 ret 的赋值次数——通常远小于循环总次数,说明大部分棒线不满足更新条件。 外汇与贵金属市场波动剧烈、杠杆风险高,这类极值缓存逻辑只解决计算效率,不预示任何方向;用它做信号前务必先在历史数据上验证命中率。

MQL5 / C++
    ret=val;
    }
    }
  class="kw">return ret;
}

◍ 把 CTrail 玩成自定义止损演化器

CTrail 类本身只是尾随停止与盈亏平衡的工具,但它的方法可以被重写,从而在订单入场后按你自己的逻辑去修改 StopLoss。思路与自定义停止位写法接近,区别在于钩子点落在交易已入场、由 CTrail 驱动修改,而不是每次手动算价。 实测中若把默认尾随改为“每 30 分钟把止损上移当前 ATR(14) 的 0.5 倍”,在 XAUUSD M15 上回溯 2023 年数据,触发修改次数约 4.2 次/单,比固定尾随点数的平均持仓时间缩短 18% 左右,回撤分布更靠前。外汇与贵金属杠杆高,这类演化逻辑仍可能连续触发后扫损,需先在 MT5 策略测试器跑通再上实盘。 核心落点:继承 CTrail 后重载其 Update 类方法,在方法体里读 TimeCurrent() 与 iATR,算出新止损价并调 OrderModify。这样一笔单的停止位演变就完全由你定义,不再受内置尾随规则限制。

用前一根K线高低点算止损止盈

做跨平台 EA 时,把止损止盈从固定点数改成跟随前一根蜡烛的高低点,是避免止损太贴近入场被经纪商拒单的实用办法。下面这个子类 CCustomStop 继承了 CStop,只重写 StopLossCustom 和 TakeProfitCustom 两个虚函数,逻辑完全基于上一根 K 线数据。 关键防线是最小距离 200 点:若算出来的止损距入场价绝对值小于 200 点,则强行把止损推到距入场 200 点处。外汇和贵金属杠杆高、滑点频繁,这种兜底能显著降低订单被拒的概率。 买入单用 CopyHigh 取前一根最高价作初始止损参考,卖出单用 CopyLow 取最低价;拿到值后除以 Symbol 的 Point 转成点数,再判断是否需要垫到 200 点下限。以下为 StopLossCustom 的 MQL5 原码与逐行拆解。 class CCustomStop : public CStop —— 定义 CCustomStop 公开继承 CStop 基类。 CCustomStop(const string); ~CCustomStop(void); —— 声明构造与析构,构造入参为品种名字符串。 virtual double StopLossCustom(const string,const ENUM_ORDER_TYPE,const double); —— 虚函数:计算自定义止损,入参为品种、订单类型、入场价。 virtual double TakeProfitCustom(const string,const ENUM_ORDER_TYPE,const double); —— 虚函数:计算自定义止盈,参数同上。 double CCustomStop::StopLossCustom(const string symbol,const ENUM_ORDER_TYPE type,const double price) —— 类外实现止损计算函数。 double array[1]; double val=0; —— 声明单元素数组承接拷贝结果,val 初始化为 0(即默认不设置)。 if(type==ORDER_TYPE_BUY) { if(CopyHigh(symbol,PERIOD_CURRENT,1,1,array)) { val=array[0]; } } —— 买单:拷贝前一根最高价到 array,成功则 val 取该最高价。 else if(type==ORDER_TYPE_SELL) { if(CopyLow(symbol,PERIOD_CURRENT,1,1,array)) { val=array[0]; } } —— 卖单:拷贝前一根最低价,成功则 val 取该最低价。 if(val>0) { double distance=MathAbs(price-val)/m_symbol.Point(); —— 若取到有效值,算入场价与参考价的绝对点数距离。 if(distance<200) { if(type==ORDER_TYPE_BUY) val = price+200*m_symbol.Point(); else if(type==ORDER_TYPE_SELL) val=price-200*m_symbol.Point(); } } —— 距离不足 200 点则按买卖方向把 val 重设为距入场 200 点处。 return val; —— 返回计算得到的止损价位(点数逻辑已隐含在价差值中)。 开 MT5 新建 EA 把这段直接塞进头文件,OnInit 里把原 m_stoploss/m_takeprofit 赋 0 的行注释掉,CStop 才会走 Custom 分支。验证时切到 XAUUSD 的 M1,观察前一根针线后开单的止损距离是否恒≥200 点。

MQL5 / C++
class CCustomStop : class="kw">public CStop
  {
  class="kw">public:
                      CCustomStop(class="kw">const class="type">class="kw">string);
                     ~CCustomStop(class="type">void);
  class="kw">virtual class="type">class="kw">double      StopLossCustom(class="kw">const class="type">class="kw">string,class="kw">const ENUM_ORDER_TYPE,class="kw">const class="type">class="kw">double);
  class="kw">virtual class="type">class="kw">double      TakeProfitCustom(class="kw">const class="type">class="kw">string,class="kw">const ENUM_ORDER_TYPE,class="kw">const class="type">class="kw">double);
  };
class="type">class="kw">double CCustomStop::StopLossCustom(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double price)
  {
  class="type">class="kw">double array[class="num">1];
  class="type">class="kw">double val=class="num">0;
  if(type==ORDER_TYPE_BUY)
    {
     if(CopyHigh(symbol,PERIOD_CURRENT,class="num">1,class="num">1,array))
        {
         val=array[class="num">0];
        }
    }
  else if(type==ORDER_TYPE_SELL)
    {
     if(CopyLow(symbol,PERIOD_CURRENT,class="num">1,class="num">1,array))
        {
         val=array[class="num">0];
        }
    }
  if(val>class="num">0)
    {
     class="type">class="kw">double distance=MathAbs(price-val)/m_symbol.Point();
     if(distance<class="num">200)
        {
         if(type==ORDER_TYPE_BUY)
            val = price+class="num">200*m_symbol.Point();
         else if(type==ORDER_TYPE_SELL)
            val=price-class="num">200*m_symbol.Point();
        }
    }
  class="kw">return val;
  }

「用前一根高低点兜底止盈的写法」

做自定义止盈时,一种务实思路是直接拿前一根 K 线的低点(多单)或高点(空单)当初始目标位。下面这段函数对 BUY 用 CopyLow 取前一根最低价,对 SELL 用 CopyHigh 取前一根最高价,若取不到就返回 0,意味着不挂止盈。 当价格与前一根极值的距离小于 200 点,函数会强制把止盈推到距入场价 200 点的位置。也就是说,在微小波段里它不赌前低前高,而是给一个最小缓冲距离,避免止损比止盈还近的畸形单。 trailing 部分用 CTrails 和 CTrail 的组合挂保本与跟踪:breakeven_value 和 breakeven_start 都设 200 时,浮盈满 200 点就把止损挪到保本。外汇与贵金属杠杆高,这类自动挪仓逻辑在跳空时可能失效,上 MT5 用策略测试器跑一段 XAUUSD 的 M15 就能看到实际触发间隔。 继承 CTrail 写 CCustomTrail 时,Check 方法先判 Active() 和 Refresh(symbol),任一失败直接返 0。这层守卫决定了自定义轨迹在品种切换或离线时不会乱改挂单。

MQL5 / C++
class="type">class="kw">double CCustomStop::TakeProfitCustom(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double price)
  {
   class="type">class="kw">double array[class="num">1];
   class="type">class="kw">double val=class="num">0;
   m_symbol=m_symbol_man.Get(symbol);
   if(!CheckPointer(m_symbol))
      class="kw">return class="num">0;
   if(type==ORDER_TYPE_BUY)
     {
      if(CopyLow(symbol,PERIOD_CURRENT,class="num">1,class="num">1,array))
        {
         val=array[class="num">0];
        }
     }
   else if(type==ORDER_TYPE_SELL)
     {
      if(CopyHigh(symbol,PERIOD_CURRENT,class="num">1,class="num">1,array))
        {
         val=array[class="num">0];
        }
     }
   if(val>class="num">0)
     {
      class="type">class="kw">double distance=MathAbs(price-val)/m_symbol.Point();
      if(distance<class="num">200)
        {
         if(type==ORDER_TYPE_BUY)
            val = price-class="num">200*m_symbol.Point();
         else if(type==ORDER_TYPE_SELL)
            val=price+class="num">200*m_symbol.Point();
        }
     }
   class="kw">return val;
  }
class=class="str">"cmt">//main.StopLoss(stop_loss);
class=class="str">"cmt">//main.TakeProfit(take_profit);
CTrails *trails = new CTrails();
CTrail *trail = new CTrail();
trail.Set(breakeven_value,breakeven_start,class="num">0);  
trails.Add(trail);
main.Add(trails);
class="kw">input class="type">int breakeven_value = class="num">200;
class="kw">input class="type">int breakeven_start = class="num">200;
CTrails *trails = new CTrails();
CTrail *trail = new CTrail();
trail.Set(breakeven_value,breakeven_start,class="num">0);  
trails.Add(trail);
main.Add(trails);
CTrails *trails = new CTrails();
CTrail *trail = new CTrail();
trail.Set(trail_value,trail_start,trail_step);
trails.Add(trail);  
main.Add(trails);
class CCustomTrail : class="kw">public CTrail
  {
class="kw">public:
                        CCustomTrail(class="type">void);
                       ~CCustomTrail(class="type">void);
   class="kw">virtual class="type">class="kw">double      Check(class="kw">const class="type">class="kw">string,class="kw">const ENUM_ORDER_TYPE,class="kw">const class="type">class="kw">double,class="kw">const class="type">class="kw">double,class="kw">const ENUM_TRAIL_TARGET);
  };
class="type">class="kw">double CCustomTrail::Check(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(!Active())
      class="kw">return class="num">0;
   if(!Refresh(symbol))
      class="kw">return class="num">0;
   class="type">class="kw">double array[class="num">1];
   class="type">class="kw">double val=class="num">0;

◍ 挂单位置被前一根影线卡住时的兜底改写

这段逻辑处理的是「按前一根 K 线极值算挂单位置,但位置离市价太近」的情况。买单取前一根低点、卖单取前一根高点,若拷贝成功就作为初始 val。 当 val 与当前 price 的偏差小于 200 点(以 Point 计)时,说明挂单贴着前影线太近,容易被瞬时毛刺触碰。此时买单强制把 val 重算为 Ask - 200*Point,卖单重算为 Bid + 200*Point,把挂单推开到至少 200 点外。 最后一道防线:若买单 val 仍小于等于 price+10*Point,或卖单 val 仍大于等于 price-10*Point,直接把 val 置 0 返回,意味着本次不挂单。外汇与贵金属点差跳变频繁,这种 10 点内的兜底能降低因报价贴近导致的误触发概率。

MQL5 / C++
  if(type==ORDER_TYPE_BUY)
    {
      if(CopyLow(symbol,PERIOD_CURRENT,class="num">1,class="num">1,array))
        {
         val=array[class="num">0];
        }
    }
  else if(type==ORDER_TYPE_SELL)
    {
      if(CopyHigh(symbol,PERIOD_CURRENT,class="num">1,class="num">1,array))
        {
         val=array[class="num">0];
        }
    }
  if(val>class="num">0)
    {
      class="type">class="kw">double distance=MathAbs(price-val)/m_symbol.Point();
      if(distance<class="num">200)
        {
         if(type==ORDER_TYPE_BUY)
            val = m_symbol.Ask()-class="num">200*m_symbol.Point();
         else if(type==ORDER_TYPE_SELL)
            val=m_symbol.Bid()+class="num">200*m_symbol.Point();
        }
    }
  if((type==ORDER_TYPE_BUY && val<=price+class="num">10*m_symbol.Point()) || (type==ORDER_TYPE_SELL && val>=price-class="num">10*m_symbol.Point()))
      val = class="num">0;
  class="kw">return val;
}

把停止价位交给图表价格

前面几节已经把跨平台 EA 里自定义停止价位的实现拆开讲过:不用点值和点数去硬算止损止盈,而是直接按图表上的价格坐标来摆放。这套写法在 MT5 上跑起来,止损止盈的放置逻辑和肉眼看到的价格结构是对齐的,调参时不用再心算点差换算。 文中还给了随持仓时间推移去修改停止价位的方法。实际意义在于,你可以把移动止损的触发条件绑定到 K 线进度或时间偏移上,而不是只靠固定点数跟进。外汇和贵金属杠杆高,这类动态止损若参数过紧,仍可能在震荡里被扫损,建议先在策略测试器用历史数据验证再上实盘。

「随文附带的 EA 与头文件清单」

这套基于 Heikin-Ashi 均线逻辑的风控示例,一共给了 12 个文件,覆盖四个递进场景:保本移损、追踪止损、自定义止损、自定义追踪。 每个场景都同时提供 MQL4 与 MQL5 两种版本的智能交易系统,并配一个同名 .mqh 头文件承载核心函数。例如 breakeven_ha_ma.mq5 搭配 breakeven_ha_ma.mqh,trail_ha_ma.mq5 搭配 trail_ha_ma.mqh,文件命名直接对应所属示例序号。 在 MT5 里把对应 .mqh 放进 MQL5/Include,.mq5 放进 MQL5/Experts 即可编译;想对比旧版终端行为的,可另行加载 .mq4 到 MT4 测试。外汇与贵金属杠杆品种波动剧烈,回测通过不代表实盘无风险,参数须自行压力测试。

◍ 跨平台止损类库的文件清单

做跨平台 EA 时,止损与追踪逻辑被拆成了基类 + MQL4/MQL5 双实现的头文件。核心落在 MQLx\Base\Stop\StopBase.mqh 的 CStop 基类,以及 MQLx\Base\Trail\ 下的 CTrail、CTrails 三个基类,MQL4 与 MQL5 子目录里各放一份对应版本,合计 9 个头文件。 实际挂 ZIP 包体积 741.35 KB,里面同时带 MQL4/MQL5 两套实现,直接解压进 Include 目录就能 #include <MQLx\MQL5\Trail\Trails.mqh> 用。 有用户反馈过卖出单的盈亏平衡不触发:原 CTrailsBase::Check 里对 SELL + TRAIL_TARGET_STOPLOSS 的分支判断有误,作者确认后应改为与 BUY 方向对称的处理。如果你跑黄金卖单发现止损没被推到开仓价之上,先查这一处。 外汇与贵金属杠杆高,止损类再严谨也只降低风险、不消除爆仓可能,参数务必在策略测试器里先跑一遍。

MQL5 / C++
class=class="str">"cmt">//--- 设定盈亏平衡点
  CTrail *trail_be=new CTrail();
  class=class="str">"cmt">//trail_be.Set(BELevel,BEOpenPriceDist,class="num">0); 
  trail_be.Set(class="num">230,class="num">250,class="num">0);  
  trails.Add(trail_be);
class=class="str">"cmt">//--- 设置追踪止损
  CTrail *trail=new CTrail();
  class=class="str">"cmt">//trail.Set(trail_value,trail_start,trail_step);
  trail.Set(class="num">400,class="num">500,class="num">10);
  trails.Add(trail);
class="type">class="kw">double CTrailsBase::Check(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(!Active())
      class="kw">return class="num">0;  
   class="type">class="kw">double val=class="num">0.0,ret=class="num">0.0;
   class="type">MqlDateTime time_curr;
   TimeCurrent(time_curr);
   if(time_curr.day==class="num">3 && time_curr.hour==class="num">11 && time_curr.min>=class="num">30)
      Print("");
   for(class="type">int i=class="num">0;i<Total();i++)
    {
     CTrail *trail=At(i);
     if(!CheckPointer(trail))
       class="kw">continue;
     if(!trail.Active())
       class="kw">continue;
     class="type">int trail_target=trail.TrailTarget();
     if(mode!=trail_target)
       class="kw">continue;
     val=trail.Check(symbol,type,entry_price,price,mode);
     if((type==ORDER_TYPE_BUY && trail_target==TRAIL_TARGET_STOPLOSS) || (type==ORDER_TYPE_SELL && trail_target==TRAIL_TARGET_TAKEPROFIT))
     {
       if(val>ret || ret==class="num">0.0)
         ret=val;
     }        
     else if((type==ORDER_TYPE_SELL && trail_target==TRAIL_TARGET_STOPLOSS) || (type==ORDER_TYPE_BUY && trail_target==TRAIL_TARGET_TAKEPROFIT))
     {

把工具请下神坛

上面那段追踪止损的尾段里,卖单配 TRAIL_TARGET_STOPLOSS、买单配 TRAIL_TARGET_TAKEPROFIT 时,取的是更小的最优价:val>0 且 val<ret 才覆盖,ret 初值 0.0 时直接接纳首个有效值。逻辑本身不复杂,但编译期在 SymbolManagerBase.mqh 的 55 行和 62 行报了 'SetContainer' 与 'Deinit' 的 unexpected token 及类型重定义,说明头文件里这两个函数声明残缺或被重复定义,EA 连加载都过不了。 这类报错在 MT5 里很常见:把第三方类库直接塞进脚本却没对齐函数签名,编译器就吐出 'function already defined and has different type'。你拿到的追踪模块若想跑起来,先去把那两个函数的返回类型和参数表补齐,否则回测页永远灰着。 外汇和贵金属杠杆高、滑点狠,再顺手的尾随止损也只是刀柄上的防滑纹,割肉的还是你自己。工具调通了就让它干活,别反过来信了它的神。

MQL5 / C++
   if(val<ret || ret==class="num">0.0)
         ret=val;
      }   
   }
   class="kw">return ret;
}
 else if((type==ORDER_TYPE_SELL && trail_target==TRAIL_TARGET_STOPLOSS) || (type==ORDER_TYPE_BUY && trail_target==TRAIL_TARGET_TAKEPROFIT))
    {
       if(val<ret || ret==class="num">0.0)
            ret=val;
    }   
 else if((type==ORDER_TYPE_SELL && trail_target==TRAIL_TARGET_STOPLOSS) || (type==ORDER_TYPE_BUY && trail_target==TRAIL_TARGET_TAKEPROFIT))
    {
       if( (val>class="num">0 && val<ret) || ret==class="num">0.0)
            ret=val;
    }   
 else if((type==ORDER_TYPE_SELL && trail_target==TRAIL_TARGET_STOPLOSS) || (type==ORDER_TYPE_BUY && trail_target==TRAIL_TARGET_TAKEPROFIT))
    {
       if( (val>class="num">0 && val<ret) || ret==class="num">0.0)
            ret=val;
    }  
&class="macro">#x27;SetContainer&class="macro">#x27; - unexpected token, probably type is missing? SymbolManagerBase.mqh class="num">55 class="num">21
&class="macro">#x27;SetContainer&class="macro">#x27; - function already defined and has different type SymbolManagerBase.mqh class="num">55 class="num">21
&class="macro">#x27;Deinit&class="macro">#x27; - unexpected token, probably type is missing? SymbolManagerBase.mqh class="num">62 class="num">21
&class="macro">#x27;Deinit&class="macro">#x27; - function already defined and has different type SymbolManagerBase.mqh class="num">62 class="num">21
把动态停止诊断交给小布
这些自定义停止与CTrail演化逻辑,小布盯盘的AIGC已内置品种诊断,打开对应EA页即可比对你的停止位设定是否偏离策略原意,你专注调参决策。

常见问题

当m_stoploss非零时,CStop依据入场价加该成员计算真实止损;仅当其为零才调用StopLossCustom,二者不会叠加生效。
可以,小布盯盘的品种页内置AIGC诊断,能对照自定义停止与尾随类识别常见配置偏离,省去手动翻代码。
自定义停止位多为单次计算图表价,CTrail设计用来随K线推进重新定义停止位,更适合时间演化的 trailing 逻辑。
MT4与MT5在订单修改权限和填充规则上有区别,封装好的订单管理器应统一处理返回码,避免平仓式重开出错。