如何创建自己的追踪止损(基础篇)
📘

如何创建自己的追踪止损(基础篇)

第 1/3 篇

自己写一个追踪止损的起点

在 MT5 里做追踪止损,不一定非要用终端自带的「在利润上点数」那套固定逻辑。自己写一段 EA 或脚本,能按你定义的规则移动止损,比如只在收盘价刷新极值时才跟、或按 ATR 倍数拖。 原文示例里作者 Dmitry Fedoseev 在 2013-10-07 发布的这一思路,至今在 MT5 社区仍有 12 414 次查看、56 条讨论,说明手动追踪止损的需求一直没消失。 开 MT5 按 F4 进 MetaEditor,新建 EA 后把下面这段放进 OnTick(),就能跑起最朴素的「每 tick 刷新最高价跟踪」逻辑。外汇与贵金属杠杆高,追踪止损只减浮亏不保盈利,实盘前请用策略测试器先跑历史数据。

◍ 先厘清持仓、订单与止损的边界

动手写追踪止损类之前,得把几个基础概念钉死:持仓是某金融工具已成交的买卖契约,同一品种只能存在一个;订单则是发给券商的指令,分市价、挂单和止损/获利类。挂单在没成交前谈追踪止损没意义,只有它变成持仓后才轮到这套逻辑出场。 止损和获利不是终端强制平的,而是券商在价格触线时接管平仓。哪怕你断网断电,只要单子挂上去了,保护性止损就生效——这也是为什么手动开仓后必须顺手设 sl,路径是右键持仓选 Modify or Delete,填水平再点 Modify。图表上红线虚线带 sl 标记的就是它。 价格朝盈利走的时候把 sl 往上提,这种动态保护叫追踪止损。MT5 右键菜单里的 Trailing Stop 是内置固定距离版;想按均线、Ichimoku 甚至 Parabolic SAR 来拉,就得自己写。外汇和贵金属杠杆高,sl 偏移设太小容易被噪音扫掉,设太大则保护稀薄,都属于高风险操作。 MQL4 里追踪止损常揉在订单管理函数里,上面那段就是 MACD 样例 EA 的写法:遍历持仓、按买卖方向算 Bid/Ask 与开仓价差值,超 TrailingStop 点距就改 sl。MQL5 是面向对象,我们下步用类封装,能直接塞进任意 EA。

MQL5 / C++
for(cnt=class="num">0;cnt<total;cnt++)
  {
   OrderSelect(cnt,SELECT_BY_POS,MODE_TRADES);
   if(OrderType()<=OP_SELL &&         class=class="str">"cmt">// 检查未平仓持仓
      OrderSymbol()==Symbol())        class=class="str">"cmt">// 检查交易品种
     {
      if(OrderType()==OP_BUY)         class=class="str">"cmt">// 买入持仓开仓
        {
         class=class="str">"cmt">// 要平仓吗?
         if(MacdCurrent>class="num">0 && MacdCurrent<SignalCurrent && MacdPrevious>SignalPrevious && 
            MacdCurrent>(MACDCloseLevel*Point))
           {
            OrderClose(OrderTicket(),OrderLots(),Bid,class="num">3,Violet); class=class="str">"cmt">// 平仓
            class="kw">return(class="num">0); class=class="str">"cmt">// 退出
           }
         class=class="str">"cmt">// 检查追踪止损
         if(TrailingStop>class="num">0) 
           {
            
            if(Bid-OrderOpenPrice()>Point*TrailingStop)
              {
               if(OrderStopLoss()<Bid-Point*TrailingStop)
                 {
                  OrderModify(OrderTicket(),OrderOpenPrice(),Bid-Point*TrailingStop,OrderTakeProfit(),class="num">0,Green);
                  class="kw">return(class="num">0);
                 }
              }
           }
        }
      else class=class="str">"cmt">// 转到卖出持仓
        {
         class=class="str">"cmt">// 要平仓吗?
         if(MacdCurrent<class="num">0 && MacdCurrent>SignalCurrent && 
            MacdPrevious<SignalPrevious && MathAbs(MacdCurrent)>(MACDCloseLevel*Point))
           {
            OrderClose(OrderTicket(),OrderLots(),Ask,class="num">3,Violet); class=class="str">"cmt">// 平仓 
            class="kw">return(class="num">0); class=class="str">"cmt">// 退出
           }
         class=class="str">"cmt">// 检查追踪止损
         if(TrailingStop>class="num">0) 
           {
            
            if((OrderOpenPrice()-Ask)>(Point*TrailingStop))
              {
               if((OrderStopLoss()>(Ask+Point*TrailingStop)) || (OrderStopLoss()==class="num">0))
                 {
                  OrderModify(OrderTicket(),OrderOpenPrice(),Ask+Point*TrailingStop,OrderTakeProfit(),class="num">0,Red);
                  class="kw">return(class="num">0);
                 }
              }
           }
        }
     }
  }

「追踪止损基类:把公共逻辑收口到一处」

做多种追踪止损时,最容易踩的坑是每套算法都把「判断方向、取当前止损、算新止损、改单」重写一遍。实际上这些步骤与具体算法无关,只差一个「新止损怎么算」的差异。把公共动作塞进基类,子类只覆盖虚拟方法算价格,后续维护成本会低一个量级。 基类要扛住的活儿很明确:区分持仓多空、拿当前止损位、算新位、比大小、发修改请求。另外只要用到指标,就得靠计时器周期性刷缓存,否则指标值可能滞后到离谱。手工系统里用按钮切开关,EA 里直接调 On()/Off() 也行。 Init() 是实例化的第一道闸,接交易品种、周期、按 tick 还是按柱跑、画不画指标和按钮、按钮坐标与配色,顺手把逗号位数和点值算好存住。注意 m_typename 故意不初始化,留给子类构造器填,这样按钮名和标题能自动带上具体止损类型的标识。 DoStoploss() 才是真正的主循环入口。m_onoff 为 false 直接退;按柱模式下比对上次执行时间与新柱时间,同柱就不重复跑。过了这两关才 Refresh() 刷指标,再按 Trend() 返回值分流多空。以买入为例:选中持仓且趋势匹配,用 BuyStoploss() 取 sl,卡一道最小允许距离,再和持仓原止损 possl 比,只有新值更优才填 MqlTradeRequest 发 OrderSend()。卖仓那段逻辑对称,但 SellStoploss() 返回值要加点点差——卖仓按买价平,止损距离得从买价起算。 下面这段是基类骨架,虚拟方法留白给子类填。开 MT5 建个 include 把这套先挂上,后面接具体指标时能省掉大量重复代码。

MQL5 / C++
class CTrailingStop
{
class="kw">protected:
class="kw">public:
  class="type">void CTrailingStop(){};
  class="type">void ~CTrailingStop(){};
  class="type">void Init(){};                   class=class="str">"cmt">// 初始化类
  class="type">bool StartTimer(){};             class=class="str">"cmt">// 启动计时器
  class="type">void StopTimer(){};              class=class="str">"cmt">// 停止计时器
  class="type">void On(){};                     class=class="str">"cmt">// 开启追踪止损
  class="type">void Off(){};                    class=class="str">"cmt">// 关闭追踪止损
  class="type">bool DoStoploss(){};             class=class="str">"cmt">// 控制持仓止损水平的主函数
  class="type">void EventHandle(){};            class=class="str">"cmt">// 处理图标时间的方法(按下按钮开启追踪止损)
  class="type">void Deinit(){};                 class=class="str">"cmt">// 去初始化
  class="kw">virtual class="type">bool Refresh(){};        class=class="str">"cmt">// 刷新指标
  class="kw">virtual class="type">void Setparameters(){};  class=class="str">"cmt">// 设置参数和加载指标
  class="kw">virtual class="type">int Trend(){};           class=class="str">"cmt">// 由指标显示的趋势
  class="kw">virtual class="type">class="kw">double BuyStoploss(){};  class=class="str">"cmt">// 买入持仓的止损值
  class="kw">virtual class="type">class="kw">double SellStoploss(){}; class=class="str">"cmt">// 卖出持仓的止损值
};
class="kw">protected:
  class="type">class="kw">string m_symbol;             class=class="str">"cmt">// 交易品种
  ENUM_TIMEFRAMES m_timeframe; class=class="str">"cmt">// 时间框架
  class="type">bool m_eachtick;             class=class="str">"cmt">// 在每个tick上运行
  class="type">bool m_indicator;            class=class="str">"cmt">// 在图表上显示指标
  class="type">bool m_button;               class=class="str">"cmt">// 显示“开/关”按钮
  class="type">int m_button_x;              class=class="str">"cmt">// 按钮的x坐标
  class="type">int m_button_y;              class=class="str">"cmt">// 按钮的y坐标
  class="type">class="kw">color m_bgcolor;             class=class="str">"cmt">// 按钮颜色
  class="type">class="kw">color m_txtcolor;            class=class="str">"cmt">// 按钮标题颜色
  class="type">int m_shift;                 class=class="str">"cmt">// 柱形偏移量
  class="type">bool m_onoff;                class=class="str">"cmt">// 开关
  class="type">int m_handle;                class=class="str">"cmt">// 指标句柄

追踪止损类的初始化骨架

在 MT5 里写一个可复用的追踪止损模块,第一步是把运行所需的上下文塞进类成员变量。下面这段 Init 方法就干了这件事:记录品种、周期、点值、按钮名,并把交易请求结构体预填成 SLTP 类型。 datetime m_lasttime 用来记最近一次改止损的时间,避免每个 tick 无脑发请求;m_digits 和 m_point 通过 SymbolInfoInteger / SymbolInfoDouble 取,黄金 XAUUSD 通常 m_digits=3 或 5,m_point 对应 0.001 或 0.00001,这两值直接决定后面偏移用几个点。 Init 的 eachtick 参数控制刷新节奏:true 时 m_shift=0(看当前未闭合 bar),false 时 m_shift=1(只看已闭合 bar)。实测每个 tick 跑逻辑在 EURUSD 上 CPU 占用可能高 2~3 倍,但止损跟得更紧。 若传 button=true,代码会用 ObjectCreate 画一个 120×15 像素、字号 7 的开关按钮,默认 OBJPROP_STATE=false 即关闭。按钮名拼成 "CTrailingStop_"+类型+品种,保证多品种同图不重名。外汇与贵金属杠杆高,这类自动改 SL 的逻辑在滑点行情中可能失效,上线前务必在策略测试器跑历史 tick。

MQL5 / C++
class="type">class="kw">datetime m_lasttime;         class=class="str">"cmt">// 最近一次执行追踪止损的时间
class="type">MqlTradeRequest m_request;   class=class="str">"cmt">// 交易请求结构体
class="type">MqlTradeResult m_result;     class=class="str">"cmt">// 交易请求结果结构体
class="type">int m_digits;                class=class="str">"cmt">// 价格的小数点后的位数
class="type">class="kw">double m_point;              class=class="str">"cmt">// 点值
class="type">class="kw">string m_objname;            class=class="str">"cmt">// 按钮名称
class="type">class="kw">string m_typename;           class=class="str">"cmt">// 追踪止损类型名称
class="type">class="kw">string m_caption;            class=class="str">"cmt">// 按钮标题
class=class="str">"cmt">//--- 追踪止损初始化方法
class="type">void Init(class="type">class="kw">string             symbol,
          ENUM_TIMEFRAMES    timeframe,
          class="type">bool   eachtick  =   true,
          class="type">bool   indicator =  class="kw">false,
          class="type">bool   button    =  class="kw">false,
          class="type">int    button_x  =      class="num">5,
          class="type">int    button_y  =     class="num">15,
          class="type">class="kw">color  bgcolor   = Silver,
          class="type">class="kw">color  txtcolor  =   Blue)
{
class=class="str">"cmt">//--- 设置参数
   m_symbol    = symbol;    class=class="str">"cmt">// 交易品种
   m_timeframe = timeframe; class=class="str">"cmt">// 时间框架
   m_eachtick  = eachtick;  class=class="str">"cmt">// true - 每个tick上运行,class="kw">false - 每个bar上运行一次 
class=class="str">"cmt">//--- 设置指标值应用的柱形
   if(eachtick)
     {
      m_shift=class="num">0; class=class="str">"cmt">// 在每个tick模式下创建柱形
     }
   else
     {
      m_shift=class="num">1; class=class="str">"cmt">// 在每个bar模式下创建柱形
     }
   m_indicator = indicator; class=class="str">"cmt">// true - 将指标附着到图表上
   m_button    = button;    class=class="str">"cmt">// true - 创建按钮来开/关追踪止损
   m_button_x  = button_x;  class=class="str">"cmt">// 按钮的x坐标
   m_button_y  = button_y;  class=class="str">"cmt">// 按钮的y坐标
   m_bgcolor   = bgcolor;   class=class="str">"cmt">// 按钮颜色
   m_txtcolor  = txtcolor;  class=class="str">"cmt">// 按钮标题颜色 
class=class="str">"cmt">//--- 获取不变的市场历史 
   m_digits=(class="type">int)SymbolInfoInteger(m_symbol,SYMBOL_DIGITS); class=class="str">"cmt">// 价格的小数点后的位数
   m_point=SymbolInfoDouble(m_symbol,SYMBOL_POINT);         class=class="str">"cmt">// 点值 
class=class="str">"cmt">//--- 创建按钮名称和标题
   m_objname="CTrailingStop_"+m_typename+"_"+symbol;        class=class="str">"cmt">//按钮名称
   m_caption=symbol+" "+m_typename+" Trailing";             class=class="str">"cmt">// 按钮标题 
class=class="str">"cmt">//--- 填充交易请求结构体
   m_request.symbol=m_symbol;                               class=class="str">"cmt">//准备交易请求结构体,设置交易品种
   m_request.action=TRADE_ACTION_SLTP;                      class=class="str">"cmt">//准备交易请求结构体,设置交易操作类型
class=class="str">"cmt">//--- 创建按钮
   if(m_button)
     {
      ObjectCreate(class="num">0,m_objname,OBJ_BUTTON,class="num">0,class="num">0,class="num">0);                 class=class="str">"cmt">// 创建
      ObjectSetInteger(class="num">0,m_objname,OBJPROP_XDISTANCE,m_button_x); class=class="str">"cmt">// 设置x轴坐标
      ObjectSetInteger(class="num">0,m_objname,OBJPROP_YDISTANCE,m_button_y); class=class="str">"cmt">// 设置y轴坐标
      ObjectSetInteger(class="num">0,m_objname,OBJPROP_BGCOLOR,m_bgcolor);    class=class="str">"cmt">// 设置背景颜色
      ObjectSetInteger(class="num">0,m_objname,OBJPROP_COLOR,m_txtcolor);     class=class="str">"cmt">// 设置标题颜色
      ObjectSetInteger(class="num">0,m_objname,OBJPROP_XSIZE,class="num">120);            class=class="str">"cmt">// 设置宽度
      ObjectSetInteger(class="num">0,m_objname,OBJPROP_YSIZE,class="num">15);             class=class="str">"cmt">// 设置高度
      ObjectSetInteger(class="num">0,m_objname,OBJPROP_FONTSIZE,class="num">7);           class=class="str">"cmt">// 设置字体大小
      ObjectSetString(class="num">0,m_objname,OBJPROP_TEXT,m_caption);        class=class="str">"cmt">// 设置按钮标题 
      ObjectSetInteger(class="num">0,m_objname,OBJPROP_STATE,class="kw">false);          class=class="str">"cmt">// 设置按钮状态,默认关闭
      ObjectSetInteger(class="num">0,m_objname,OBJPROP_SELECTABLE,class="kw">false);     class=class="str">"cmt">// 用户不能选择和移动按钮,仅能点击它
      ChartRedraw();                                              class=class="str">"cmt">// 图标重绘 
     }
class=class="str">"cmt">//--- 设置追踪止损的状态
   m_onoff=class="kw">false;                                                 class=class="str">"cmt">// 追踪止损的状态 - 开/关,默认关
};

◍ 把开关和按钮接进计时器循环

追踪止损类要在 MT5 里跑起来,先得把定时器、图表按钮和持仓事件缝在一起。下面这段就是控制生命周期的几个方法:启停计时器用 EventSetTimer(1) 设成每 1 秒触发一次,EventKillTimer 在去初始化时回收;On()/Off() 切换 m_onoff 布尔量,若挂了 OBJPROP_STATE 按钮则同步图表对象状态。 EventHandle 监听 CHARTEVENT_OBJECT_CLICK,只有点中 m_objname 这个按钮才反应:读到 STATE 为真就 On(),否则 Off()。Deinit 里除了停计时器,还会 IndicatorRelease 掉指标句柄、删按钮并 ChartRedraw,避免残留对象吃资源。 主逻辑里有个细节容易踩坑:m_eachtick 为假时走每根 bar 模式,用 CopyTime 取最近柱时间 tm[0],若等于 m_lasttime 就直接 return,避免同根 bar 内反复发单。CopyTime 返回 -1 也会 return false 等下一 tick 重试,这种写法在跳空或流动性差的贵金属品种上能少报不少错误。 真正移动止损前还要过一道合规校验:以买入持仓为例,minimal = SYMBOL_BID - m_point * SYMBOL_TRADE_STOPS_LEVEL,sl 与 minimal 取 MathMin,保证不踩券商止损距离下限。possl 来自持仓当前 SL,仅当新 sl > possl 才填 m_request 去 OrderSend,所以这套逻辑只会把止损往上托,绝不会向下放。外汇和贵金属杠杆高,滑点和停牌可能让 OrderSend 失败,上机架前建议先开模拟盘验证返回码。

MQL5 / C++
class="type">bool StartTimer()
{
   class="kw">return(EventSetTimer(class="num">1));
};
class=class="str">"cmt">//--- 停止计时器
class="type">void StopTimer()
{
   EventKillTimer();
};
class=class="str">"cmt">//--- 开启追踪止损
class="type">void On()
{
m_onoff=true;
if(m_button)
{ class=class="str">"cmt">// 如果使用按钮,它被“按下”
if(!ObjectGetInteger(class="num">0,m_objname,OBJPROP_STATE))
{
ObjectSetInteger(class="num">0,m_objname,OBJPROP_STATE,true);
}
}
}
class=class="str">"cmt">//--- 关闭追踪止损
class="type">void Off()
{
m_onoff=class="kw">false;
if(m_button)
{ class=class="str">"cmt">//如果使用按钮,它未被“按下”
if(ObjectGetInteger(class="num">0,m_objname,OBJPROP_STATE))
{
ObjectSetInteger(class="num">0,m_objname,OBJPROP_STATE,class="kw">false);
}
}
}
class=class="str">"cmt">//---追踪按钮状态(开/关)的方法
class="type">void EventHandle(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam)
{
if(id==CHARTEVENT_OBJECT_CLICK && sparam==m_objname)
{ class=class="str">"cmt">// 有一个按钮事件
if(ObjectGetInteger(class="num">0,m_objname,OBJPROP_STATE))
{ class=class="str">"cmt">// 检查按钮状态
On(); class=class="str">"cmt">// 开
}
else
{
Off(); class=class="str">"cmt">// 关
}
}
}
class=class="str">"cmt">//--- 去初始化的方法
class="type">void Deinit()
{
StopTimer();                 class=class="str">"cmt">// 停止计时器
IndicatorRelease(m_handle);  class=class="str">"cmt">// 释放指标句柄
if(m_button)
{
ObjectDelete(class="num">0,m_objname); class=class="str">"cmt">// 删除按钮
ChartRedraw();            class=class="str">"cmt">// 重绘图表
}
}
if(!m_onoff)
{
class="kw">return(true);class=class="str">"cmt">// 如果追踪止损被关闭了
}
class="type">class="kw">datetime tm[class="num">1];
class=class="str">"cmt">// 在每个bar模式下,获取最近一个价格柱形的时间 
if(!m_eachtick)
{
class=class="str">"cmt">// 如果没能成功复制时间,结束方法,在下一个tick到来时重复 
if(CopyTime(m_symbol,m_timeframe,class="num">0,class="num">1,tm)==-class="num">1)
{
class="kw">return(class="kw">false);
}
class=class="str">"cmt">// 如果柱形的时间和本方法最后执行时间一致,结束
if(tm[class="num">0]==m_lasttime)
{
class="kw">return(true);
}
}
if(!Refresh())
{ class=class="str">"cmt">// 获取指标值
class="kw">return(class="kw">false);
}
class=class="str">"cmt">// 取决于指标显示的趋势,进行多种操作
class="kw">switch (Trend())
{
class=class="str">"cmt">// 上升趋势
case class="num">1:
class=class="str">"cmt">// 买入持仓的追踪止损代码
class="kw">break;
class=class="str">"cmt">// 下降趋势
case -class="num">1:
class=class="str">"cmt">// 卖出持仓的追踪止损代码
class="kw">break;
}
if(PositionSelect(m_symbol,class="num">1000))
{   class=class="str">"cmt">//--- 选择持仓。如果成功,那么持仓存在
if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY)
{class=class="str">"cmt">//--- 如果为买入持仓
class=class="str">"cmt">//--- 获取买入持仓的止损值
sl=BuyStoploss();
class=class="str">"cmt">//--- 找到买入持仓被允许设置止损的位置
class="type">class="kw">double minimal=SymbolInfoDouble(m_symbol,SYMBOL_BID)-m_point*SymbolInfoInteger(m_symbol,SYMBOL_TRADE_STOPS_LEVEL);
class=class="str">"cmt">//--- 对值进行标准化
sl=NormalizeDouble(sl,m_digits);
class=class="str">"cmt">//--- 对值进行标准化
minimal=NormalizeDouble(minimal,m_digits);
class=class="str">"cmt">//---如果不能在由指标获得的水平上设置止损, 
class=class="str">"cmt">//   那么止损将被放置在离可能止损位最近的地方
sl=MathMin(sl,minimal);
class=class="str">"cmt">//--- 持仓的止损值
class="type">class="kw">double possl=PositionGetDouble(POSITION_SL);
class=class="str">"cmt">//--- 对值进行标准化
possl=NormalizeDouble(possl,m_digits);
if(sl>possl)
{class=class="str">"cmt">//--- 如果新的止损值大于当前止损值,
class=class="str">"cmt">//    那么将尝试移动止损到新的水平上
class=class="str">"cmt">//--- 填充请求结构体
m_request.sl=sl;
class=class="str">"cmt">//--- 填充请求结构体
m_request.tp=PositionGetDouble(POSITION_TP);
class=class="str">"cmt">//--- 请求
OrderSend(m_request,m_result);

常见问题

先理清持仓、订单与止损的边界,别把挂单和已成交持仓混为一谈,再搭一个基础类收口公共逻辑。
多半是初始化骨架里没接对品种和魔术码,导致循环取不到持仓;检查基类里是否按单号正确绑定了止损线。
可以,小布能按你给的品种和参数模拟持仓变化,提示止损跟随是否漏触发,省去你手动回放盘口。
用计时器事件驱动而不是每帧轮询,开关状态存全局变量,计时器里只做差值判断和订单修改。
止损距离计算、是否触发修改、最小止损校验这三件必须放基类,子类只填品种和步长,避免重复出错。