通用智能交易系统:自定义追踪止损(第六章)·进阶篇
🔗

通用智能交易系统:自定义追踪止损(第六章)·进阶篇

(2/3)· 当追踪止损逻辑塞进基类,代码可读性与扩展性双双崩坏,本文给出干净解法

含代码示例偏理论 第 2/3 篇
把追踪止损写成 CStrategy 里的固定方法,是多数自建 EA 最常见的耦合错误。一旦算法变多,基类迅速膨胀,缺失该函数还会拖累整条交易流水线。外部化、组合式挂载才是可持续的扩展路径。

◍ 每个头寸配独立追踪实例才不串味

把 CTrailing 指针塞进 CPosition,头寸就能像管自己一样管止损。但真正落地时,CStrategy 层若只挂一个默认 Trailing 指针,所有头寸会共享同一个实例——这是坑。 共享实例会在内部保存计算变量,下一次循环传入的头寸变了,逻辑就错乱,出现难以排查的诡异 bug。解法是给每个头寸 Copy() 出独立实例,生命周期内不变。 Copy() 必须写在 CTrailing 派生类里。下面这段 CTrailingClassic::Copy 建新对象、搬参数、返指针,CStrategy 靠它自动给未平仓头寸配副本。 若你只在 BuySupport / SellSupport 里手动调 Trailing,不重写 Copy 也行;但想让 CStrategy 自动托管,就必须重写,否则 EA 跑起来管不了仓。 CallSupport 里有个细节:Modify 之后立刻查 pos.IsActive(),头寸若在追踪中途被平,循环直接跳下一单,不崩。这意味着任何平仓逻辑都能伪装成追踪函数用——在特定条件平掉仓位,策略照常跑。外汇与贵金属杠杆高,这类自动托管逻辑先开 MT5 用 demo 验一遍再上实盘。

MQL5 / C++
class CPosition
  {
class="kw">public:
  CTrailing* Trailing;    class=class="str">"cmt">// 追踪止损模块
  };
class="type">void CMovingAverage::SupportBuy(class="kw">const MarketEvent &event,CPosition *pos)
  {
   pos.Trailing.Modify();
  }
class CStrategy
  {
class="kw">public:
  CTrailing* Trailing;   class=class="str">"cmt">// 用于所有头寸的追踪止损模块
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 调用头寸管理逻辑,如果交易状态不等于                               |
class=class="str">"cmt">//| TRADE_WAIT                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CStrategy::CallSupport(class="kw">const MarketEvent &event)
  {
  m_trade_state=m_state.GetTradeState();
  if(m_trade_state == TRADE_WAIT)class="kw">return;
  SpyEnvironment();
  for(class="type">int i=ActivePositions.Total()-class="num">1; i>=class="num">0; i--)
   {
    CPosition *pos=ActivePositions.At(i);
    if(pos.ExpertMagic()!=m_expert_magic)class="kw">continue;
    if(pos.Symbol()!=ExpertSymbol())class="kw">continue;
    if(CheckPointer(Trailing)!=POINTER_INVALID)
     {
      if(CheckPointer(Trailing)==POINTER_INVALID)
        pos.Trailing=Trailing.Copy();
      pos.Trailing.Modify();
      if(!pos.IsActive())
        class="kw">continue;
      }
    if(pos.Direction()==POSITION_TYPE_BUY)
      SupportBuy(event,pos);
    else
      SupportSell(event,pos);
    if(m_trade_state==TRADE_STOP && pos.IsActive())
      ExitByStopRegim(pos);
   }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回一个实例的副本                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+  
CTrailing *CTrailingClassic::Copy(class="type">void)
  {
  CTrailingClassic *tral=new CTrailingClassic();
  tral.SetDiffExtremum(m_diff_extremum);
  tral.SetStepModify(m_step_modify);
  tral.SetPosition(m_position);
  class="kw">return tral;
  }

「经典算法怎么管住止损乱跳」

经典追踪止损的逻辑很直白:多头盯着新高价、空头盯着新低价去推止损,价格回撤时止损原地不动,始终和极值保持一段由参数决定的距离。这么做的目的是让利润跑、把风险钉死在回撤边界上。 但有个坑得提前堵:如果不加限制,价格每晃一下 EA 就可能去改一次止损。在 FORTS 这类交易所,频繁改单却很少真实成交会被判定为“低效交易”并加收手续费。所以实例里引入了 StepModify——新止损和旧止损的最小变动点数,不到这个距离就不动单。 下面这段是基于 CTrailing 派生出的第一个实例,核心计算落在 Modify 和 FindExtremum 两个方法里。FindExtremum 负责按持仓方向捞到最高/最低价,因此即便 EA 重启或空闲,止损也能算回正确位置,不会丢状态。 代码里用 SHOW_TRAILING_CLASSIC_PARAMS 宏和 input 参数把差值距离(默认 0.00200)与最小步长(默认 0.00005)暴露给面板,具体怎么接进 EA 设置后面单说。外汇和贵金属波动大,这类追踪止损在高波动段可能频繁触及 StepModify 边界,实盘前建议在 MT5 策略测试器用历史数据验证改单频率。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 TrailingClassic.mqh |
class=class="str">"cmt">//|                         Copyright class="num">2016, Vasiliy Sokolov. |
class=class="str">"cmt">//|                                            [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2016, Vasiliy Sokolov."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include "Trailing.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 将追踪止损参数集成在EA的参数列表中                               |
class=class="str">"cmt">//|                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#ifdef SHOW_TRAILING_CLASSIC_PARAMS
class="kw">input class="type">class="kw">double PointsModify=class="num">0.00200;
class="kw">input class="type">class="kw">double StepModify=class="num">0.00005;
class="macro">#endif
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 一个经典的追踪止损算法                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CTrailingClassic : class="kw">public CTrailing
  {
class="kw">private:
   class="type">class="kw">double            m_diff_extremum;   class=class="str">"cmt">// 价格到达的极值和止损位置之间的距离点数
   class="type">class="kw">double            m_step_modify;     class=class="str">"cmt">// 修改止损的最小差异点数
   class="type">class="kw">double            FindExtremum(CPosition *pos);
class="kw">public:
                     CTrailingClassic(class="type">void);
   class="type">void              SetDiffExtremum(class="type">class="kw">double points);
   class="type">class="kw">double            GetDiffExtremum(class="type">void);
   class="type">void              SetStepModify(class="type">class="kw">double points_step);
   class="type">class="kw">double            GetStepModify(class="type">void);
   class="kw">virtual class="type">bool      Modify(class="type">void);
   class="kw">virtual CTrailing *Copy(class="type">void);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造函数初始化默认参数                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
CTrailingClassic::CTrailingClassic(class="type">void) : m_diff_extremum(class="num">0.0),

经典追踪止损类的接口与执行逻辑

CTrailingClassic 把经典追踪止损封装成可复制的对象。Copy() 会 new 一个同型实例,把 m_diff_extremum、m_step_modify 和 m_position 逐一搬过去,返回指针——这意味着你在 EA 里可以随时克隆一套独立参数跑对照测试。 两个setter很直白:SetDiffExtremum(double points) 存的是价格极值到止损位的点数距离,SetStepModify(double points_step) 存的是触发修改的最小点数差。GetDiffExtremum() 与 GetStepModify() 原样回读,方便在面板或日志里核对实参。 真正下单前卡了两道闸。Modify() 先查 m_position 指针是否有效,再查 m_diff_extremum 是否大于 0.0,任意一项不成立就写一条 MESSAGE_WARNING 并返回 false,不会动止损。 过闸后由 FindExtremum(m_position) 取极值,若为 0.0 直接退出;买仓用 extremum - m_diff_extremum 算新止损,卖仓走 else 分支(原文截断)。外汇与贵金属杠杆高,追踪止损只是概率性保护,极端跳空时仍可能大幅滑点。

MQL5 / C++
 m_step_modify(class="num">0.0)
{
class="macro">#ifdef SHOW_TRAILING_CLASSIC_PARAMS
 m_diff_extremum=PointsModify;
 m_step_modify=StepModify;
class="macro">#endif
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回实例的副本                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
CTrailing *CTrailingClassic::Copy(class="type">void)
 {
 CTrailingClassic *tral=new CTrailingClassic();
 tral.SetDiffExtremum(m_diff_extremum);
 tral.SetStepModify(m_step_modify);
 tral.SetPosition(m_position);
 class="kw">return tral;
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置价格极值同止损位置的距离点数                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTrailingClassic::SetDiffExtremum(class="type">class="kw">double points)
 {
 m_diff_extremum=points;
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置修改止损的最小差异点数                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTrailingClassic::SetStepModify(class="type">class="kw">double points_step)
 {
 m_step_modify=points_step;
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回距离价格极值的点数                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CTrailingClassic::GetDiffExtremum(class="type">void)
 {
 class="kw">return m_diff_extremum;
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回最小修改点数的值                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CTrailingClassic::GetStepModify(class="type">void)
 {
 class="kw">return m_step_modify;
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 根据经典追踪止损算法按修改止损位置                             |
class=class="str">"cmt">//|                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTrailingClassic::Modify(class="type">void)
 {
 if(CheckPointer(m_position)==POINTER_INVALID)
   {
    class="type">class="kw">string text="Invalid position for current trailing-stop. Set position with &class="macro">#x27;SetPosition&class="macro">#x27; method";
    CMessage *msg=new CMessage(MESSAGE_WARNING,__FUNCTION__,text);
    Log.AddMessage(msg);
    class="kw">return false;
   }
 if(m_diff_extremum<=class="num">0.0)
   {
    class="type">class="kw">string text="Set points trailing-stop with &class="macro">#x27;SetDiffExtremum&class="macro">#x27; method";
    CMessage *msg=new CMessage(MESSAGE_WARNING,__FUNCTION__,text);
    Log.AddMessage(msg);
    class="kw">return false;
   }
 class="type">class="kw">double extremum=FindExtremum(m_position);
 if(extremum == class="num">0.0)class="kw">return false;
 class="type">class="kw">double n_sl = class="num">0.0;
 if(m_position.Direction()==POSITION_TYPE_BUY)
    n_sl=extremum-m_diff_extremum;
 else

◍ 用持仓周期极值重挂止损

经典跟踪止损类里,止损位不是盯一根K线,而是从开仓时刻拉到当前,找这段区间里的极端价。多头取 M1 周期自开仓以来的最高价,空头取最低价,再叠加上一段算出的偏移量 m_diff_extremum 得到 n_sl。 下面这段是极值查找的实现核心。若当前止损已不等于算出的 n_sl,就调用 StopLossValue(n_sl) 重设并返回;否则返回 false 表示无需改动。 FindExtremum 先按持仓方向分流:买仓用 CopyHigh 把 pos.TimeOpen() 到 TimeCurrent() 的 M1 高价拷进数组,只要拷贝到的柱数大于 1(即不止当前未闭合的一根),就用 ArrayMaximum 取最高价;卖仓对称走 CopyLow 与 ArrayMinimum 取最低价。拷贝失败或不足两根则返回 0.0,调用方需自行处理这种边界。 实盘里这种写法对外汇与贵金属都有效,但 M1 重算频率高,点差扩大时滑点可能让重挂失败概率上升,属高风险操作,建议先在 MT5 策略测试器用历史数据验证极值窗口是否符合你的持仓逻辑。

MQL5 / C++
  n_sl=extremum+m_diff_extremum;
  if(n_sl!=m_position.StopLossValue())
    class="kw">return m_position.StopLossValue(n_sl);
  class="kw">return false;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 当持有头寸时,返回价格极值                                |
class=class="str">"cmt">//| 对于多头头寸,将返回最高价。  |
class=class="str">"cmt">//| 对于空头头寸,返回最低价。                                      |
class=class="str">"cmt">//| |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CTrailingClassic::FindExtremum(CPosition *pos)
  {
  class="type">class="kw">double prices[];
  if(pos.Direction()==POSITION_TYPE_BUY)
    {
    if(CopyHigh(pos.Symbol(),PERIOD_M1,pos.TimeOpen(),TimeCurrent(),prices)>class="num">1)
      class="kw">return prices[ArrayMaximum(prices)];
    }
  else
    {
    if(CopyLow(pos.Symbol(),PERIOD_M1,pos.TimeOpen(),TimeCurrent(),prices)>class="num">1)
      class="kw">return prices[ArrayMinimum(prices)];
    }
  class="kw">return class="num">0.0;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「把均线平仓换成经典追踪止损」

早先的 CImpulse 用移动平均做头寸管理:柱线开盘价跌破均线平多、升破均线平空,逻辑写死在 SupportBuy / SupportSell 里。这套规则在震荡市里容易被毛刺洗掉,实战中持仓体验偏脆。 直接把那两段基于均线的平仓代码从策略类里删掉,构造函数改成挂一个 CTrailingClassic 实例,默认 diff_extremum 设为 0.001。改名 CImpulseTrailingAuto 后,平仓完全交给追踪止损:从价格极值回撤 0.001 才触发,不再看均线脸色。 下面这段是原文里被替换掉的老逻辑和新的构造写法,逐行拆一下你能看清差异在哪。外汇与贵金属杠杆高,追踪止损只是缩小回撤概率,不等于锁定利润,实盘前请在 MT5 策略测试器用历史数据跑一遍。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 根据移动平均管理多头头寸                                      &nbsp;&nbsp; |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CImpulse::SupportBuy(class="kw">const MarketEvent &event,CPosition *pos)
{
&nbsp;&nbsp; if(!IsTrackEvents(event))class="kw">return;
&nbsp;&nbsp; ENUM_ACCOUNT_MARGIN_MODE mode = (ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
&nbsp;&nbsp; if(mode != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;class="type">class="kw">double target = Bid() - Bid()*(m_percent/class="num">100.0);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if(target < Moving.OutValue(class="num">0))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pos.StopLossValue(target);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pos.StopLossValue(class="num">0.0);
&nbsp;&nbsp; }
&nbsp;&nbsp; if(Bid() < Moving.OutValue(class="num">0))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pos.CloseAtMarket();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 根据移动平均管理空头头寸                                       &nbsp;&nbsp;|
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CImpulse::SupportSell(class="kw">const MarketEvent &event,CPosition *pos)
{
&nbsp;&nbsp; if(!IsTrackEvents(event))class="kw">return;
&nbsp;&nbsp; ENUM_ACCOUNT_MARGIN_MODE mode = (ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
&nbsp;&nbsp; if(mode != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;class="type">class="kw">double target = Ask() + Ask()*(m_percent/class="num">100.0);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if(target > Moving.OutValue(class="num">0))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pos.StopLossValue(target);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pos.StopLossValue(class="num">0.0);
&nbsp;&nbsp; }
&nbsp;&nbsp; if(Ask() > Moving.OutValue(class="num">0))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pos.CloseAtMarket();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 策略初始化并在启动时配置追踪止损                          &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|
class=class="str">"cmt">//| &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;           &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|
class=class="str">"cmt">//+------------------------------------------------------------------+
CImpulseTrailing::CImpulseTrailing(class="type">void)
{
&nbsp;&nbsp; CTrailingClassic* classic = new CTrailingClassic();
&nbsp;&nbsp; classic.SetDiffExtremum(class="num">0.00100);
&nbsp;&nbsp; Trailing = classic;
}

用条件编译收住追踪止损的参数歧义

经典追踪止损模块本身好用,但参数总得在策略里手动接一次。麻烦在于:即便这次根本不打算开追踪止损,那几个 input 参数还是会挂在 EA 设置窗口里,交易者容易误以为生效了。 解法是在头文件里套一层条件编译。定义宏 SHOW_TRAILING_CLASSIC_PARAMS 时,PointsModify 与 StepModify 才作为 input 编译进 EA;宏被注释掉,参数直接从设置里消失,界面干净,也不会产生「开了还是没开」的歧义。 宏不只控制显隐,还会在 CTrailingClassic 构造函数里把这两个值直接喂给成员变量。也就是说,只要策略编译时带了这个宏,EA 设置窗口里填的数在对象创建那一刻就已经落进算法内部,不用再写额外的传参代码。 外汇与贵金属波动剧烈,追踪止损只是风控的一环,实际触发概率和滑点损耗都随行情变化,参数请结合回测与实盘验证再定。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  TrailingClassic.mqh |
class=class="str">"cmt">//|                                                 Copyright class="num">2016, Vasiliy Sokolov. |
class=class="str">"cmt">//|                                                                    [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2016, Vasiliy Sokolov."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include "Trailing.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 将追踪止损参数集成在EA的参数列表中                               |
class=class="str">"cmt">//|                                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#ifdef SHOW_TRAILING_CLASSIC_PARAMS
class="kw">input class="type">class="kw">double PointsModify = class="num">0.00100;
class="kw">input class="type">class="kw">double StepModify =   class="num">0.00005;
class="macro">#endif
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 一个经典的追踪止损算法                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CTrailingClassic : class="kw">public CTrailing
  {
  ...
class="kw">public:
                        CTrailingClassic(class="type">void);
  ...
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造函数。初始化参数                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
CTrailingClassic::CTrailingClassic(class="type">void) : m_diff_extremum(class="num">0.0),
                                          m_step_modify(class="num">0.0)
  {
  class="macro">#ifdef SHOW_TRAILING_CLASSIC_PARAMS
  m_diff_extremum = PointsModify;
  m_step_modify = StepModify;
  class="macro">#endif
  }
把追踪止损回测交给小布
不同 CTrailing 子类在历史行情上的止损表现对比,小布盯盘的 AIGC 已内置,打开对应品种页即可看到回测摘要,你只需判断哪类逻辑贴合自己的持仓周期。

常见问题

追踪止损对交易引擎是外部功能,缺失它不应影响策略基础运作;放进基类会让代码膨胀且降低可读性,组合模式可让其独立挂载。
CPosition 负责头寸状态与基础操作,CTrailing 只计算止损移动目标,二者通过组合交互,避免把算法堆进头寸类。
前者止损参考均线位移,对趋势平滑度更敏感;后者依赖价格极值距离,反应更快但可能被毛刺触发,实盘概率上各有适应市况。
可以,将类结构说明贴入小布的品种页,AIGC 会按历史波动给出该类触发频率与回撤分布的粗略估计,辅助你调参而非替代决策。
主要新增独立 trailing 文件并在策略初始化处组合挂载,同时暴露参数到 EA 设置面板,不必改动引擎核心循环。