如何创建自己的追踪止损

Dmitry Fedoseev | 7 十月, 2013

简介

我认为循规蹈矩是个不错的主意,在我们开始本文的主题前,先再次定义术语“持仓”和“订单”。

图 1. 持仓和订单。

图 1. 持仓和订单。

本文主要介绍持仓的追踪止损水平。对于挂单,该操作毫无意义,因为您可以直接移动到订单价格。什么时候挂单变成持仓(或持仓的一部分),本文将派上用场。

交易持仓不仅可以通过在持仓对话框中按下 "Close"(平仓)按钮来平仓(请参见图 2)。

图 2. 在持仓对话框中使用 "Close"(平仓)按钮平仓。

图 2. 在持仓对话框中使用 "Close"(平仓)按钮平仓。1 - 打开持仓上下文菜单,2 - 选择 "Close position"(平仓)
3 - 单击 "Close"(平仓)按钮。

此外,持仓还可以在价格达到预先设定的利润水平(获利)或损失水平(止损)时自动平仓。与使用 "Close"(平仓)按钮平仓不同,通过止损和获利平仓并非是通过终端(交易人员或“EA 交易”)执行,而是由经纪人完成。因此,平仓获得充分保障,不论是否连接了网络和电源。这使得对止损的使用几乎成为交易人员工作中的强制性元素。

交易人员唯一需要执行的操作就是提交订单,以便经纪人设置保护性止损。换言之,您必须在持仓上设置止损(或以该水平设置开仓)。使用终端的 "Modify"(修改)上下文菜单命令设置止损。在持仓列表中选择一个持仓,右键单击并选择 "Modify or Delete"(修改或删除)。接下来在持仓对话框中,您需要输入必要的止损水平然后单击 "Modify"(修改)(请参见图 3)。

图 3. 设置持仓的止损水平。

图 3. 设置持仓的止损水平。1 - 打开持仓上下文菜单,2 - 单击 "Modify or Delete"(修改或删除),3 - 设置值,4 - 单击 "Modify"(修改)。 

持仓的止损水平和敞口水平显示在价格图表上(请参见图 4)。

图 4. 持仓和止损。水平由红色虚线标示,左边有 sl 标记。

图 4. 持仓和止损。水平由红色虚线标示,左边有 sl 标记。

您不仅能为持仓设置止损,还能定期修改该值。例如,当价格朝向获利的方向变化时,您可以向上拉动止损以减少可能的损失。此类保护性止损的拉动称之为追踪止损。

有多种追踪止损变体:您可以简单地跟随价格以给定的距离拉动止损。您无需立即移动止损,但是当持仓达到一定的获利时,它立即移动至收支平衡的水平。该变体是 MetaTrader 5 客户端的标准内置追踪止损。要使用标准追踪止损,右键单击持仓并选择 "Trailing Stop"(追踪止损)(请参见图 5)。

图 5. 在终端中启用标准追踪止损。

图 5. 在终端中启用标准追踪止损。1 - 打开持仓上下文菜单,2 - 单击 "Trailing Stop"(追踪止损),3 - 选择值(或设置值)。
设置值命令(自定义)
位于上下文菜单的底部,且未在图中显示。

除了管理价格监测,追踪止损还可以基于某些技术指标展开工作。例如,基于移动平均线可不对短期价格变化作出反应;基于云图 (Ichimoku) 指标或更合适的指标;甚至还可以基于抛物线转向 (Parabolic SAR) 指标(停损并转向),虽然最初的设计并不是基于此目的。请参见图 6。

图 6. 抛物线转向指标。

图 6. 抛物线转向指标。

在 MQL4 中,追踪止损的过程编程通常作为单独的函数创建或被集成至其他函数中。例如,MetaTrader 4 包含的 MACD 示例“EA 交易”中,追踪止损功能与订单的闭市功能集成在一起:

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

MQL5 作为面向对象语言,在设计“EA 交易”方面提供了更多的可能性。您可以创建多用途多功能的,随后可简单快速地将其集成至几乎任何“EA 交易”。在这篇文章中,我们将要开发这样的类。


1. 创建追踪止损的基类

如前文所述,存在大量的追踪止损,但它们都具有共同的功能面:

追踪止损的类型仅决定计算的止损水平的值。因此,追踪止损的基本功能性将包含基类中。对于依赖于追踪止损类型的功能性,我们将创建子类。应用至这些子类的方法将通过基类的虚拟方法实现。

由于我们计划使用技术指标,要确保它们的稳定操作,需要向它们提供周期性装置。为此,我们将使用计时器。我们还计划开启/关闭追踪止损(在将类用作手工操作的交易系统的一部分时),并使用图形对象 - 按钮来实现此操作(在将类用作辅助“EA 交易”的一部分时)。根据这些功能要求,基类将具有以下方法集:

class CTrailingStop
  {
protected:
public:
   void CTrailingStop(){};
   void ~CTrailingStop(){};
   void Init(){};                   // 初始化类
   bool StartTimer(){};             // 启动计时器
   void StopTimer(){};              // 停止计时器
   void On(){};                     // 开启追踪止损
   void Off(){};                    // 关闭追踪止损 
   bool DoStoploss(){};             // 控制持仓止损水平的主函数
   void EventHandle(){};            // 处理图标时间的方法(按下按钮开启追踪止损)
   void Deinit(){};                 // 去初始化
   virtual bool Refresh(){};        // 刷新指标
   virtual void Setparameters(){};  // 设置参数和加载指标
   virtual int Trend(){};           // 由指标显示的趋势
   virtual double BuyStoploss(){};  // 买入持仓的止损值
   virtual double SellStoploss(){}; // 卖出持仓的止损值
  };

在调用 Init() 方法时,它将接收不依赖于所使用追踪止损的类型的通用参数。该方法将设置追踪止损模式,并使用一些市场参数来准备变量。

1.1. Init() 方法

Init() 方法是创建类的实例后调用的首个方法。它接收独立于追踪止损类型的通用参数:交易品种、时间表、追踪止损模式(按订单号或按柱)、附加或不附加指标至图表、创建或不创建按钮。接下来,它将接收按钮属性:按钮的 X 坐标、按钮的 Y 坐标、按钮颜色、按钮标题颜色。

后续工作所需的参数存储在类变量中。此外,在 Init() 方法工作时,它将确定止损所需的主要未变更市场参数:逗号后面的位数和点的值。最后,取决于追踪止损的类型,按钮名称及其标题形成。如果设置为使用按钮,将创建按钮。

我们在“保护”部分声明所有需要的变量:

protected:
string m_symbol;             // 交易品种
ENUM_TIMEFRAMES m_timeframe; // 时间框架
bool m_eachtick;             // 在每个tick上运行
bool m_indicator;            // 在图表上显示指标
bool m_button;               // 显示“开/关”按钮
int m_button_x;              // 按钮的x坐标
int m_button_y;              // 按钮的y坐标
color m_bgcolor;             // 按钮颜色
color m_txtcolor;            // 按钮标题颜色
int m_shift;                 // 柱形偏移量
bool m_onoff;                // 开关
int m_handle;                // 指标句柄
datetime m_lasttime;         // 最近一次执行追踪止损的时间
MqlTradeRequest m_request;   // 交易请求结构体
MqlTradeResult m_result;     // 交易请求结果结构体
int m_digits;                // 价格的小数点后的位数
double m_point;              // 点值
string m_objname;            // 按钮名称
string m_typename;           // 追踪止损类型名称
string m_caption;            // 按钮标题

现在我们来编写 Init() 方法本身:

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

您可以看到,在创建按钮名称和标题时使用了 m_typename 变量,该变量未使用任何值进行初始化。子类构造函数将分配一个值给该变量。因此,使用不同追踪止损方法,它将具有对应于所用追踪止损类型的不同的值。

1.2. StartTimer() 方法

StartTimer() 方法用于启动“EA 交易”的通用计时器。  

//--- 开启计时器
bool StartTimer()
  {
   return(EventSetTimer(1));
  };

使用计时器时,您必须将 Refresh() 方法的调用添加至 OnTimer() 函数,以周期性请求指标。

1.3. StopTimer() 方法

StopTimer() 方法用于停止“EA 交易”的计时器。  

//--- 停止计时器
void StopTimer()
  {
   EventKillTimer();
  };

使用该方法,当“EA 交易”结束工作时,该方法将停止计时器。在运行类的 Deinit() 方法时,该方法将被调用。  

1.4. On() 方法

On() 方法用于开启追踪止损。开启通过将值 true 分配给变量 m_onoff 来完成。如果按钮在类的初始化中设置为使用,则按钮按下。

//--- 开启追踪止损
void On()
  {
   m_onoff=true; 
   if(m_button)
     { // 如果使用按钮,它被“按下”
      if(!ObjectGetInteger(0,m_objname,OBJPROP_STATE))
        {
         ObjectSetInteger(0,m_objname,OBJPROP_STATE,true);
        }
     }
  }

1.5. Off() 方法

Off() 方法用于关闭追踪止损。关闭通过将值 false 分配给变量 m_onoff 来完成。如果按钮在类的初始化中设置为使用,则按钮弹起。

//--- 关闭追踪止损
void Off()
  {
   m_onoff=false;
   if(m_button)
     { //如果使用按钮,它未被“按下”
      if(ObjectGetInteger(0,m_objname,OBJPROP_STATE))
        {
         ObjectSetInteger(0,m_objname,OBJPROP_STATE,false);
        }
     }
  }

1.6. EventHandle() 方法

EventHandle() 方法将从 OnChartEvent() 函数中调用,并且相应地,它将接收所有传递至 OnChartEvent() 函数的参数。

//---追踪按钮状态(开/关)的方法
void EventHandle(const int id,const long  &lparam,const double &dparam,const string &sparam)
  {
   if(id==CHARTEVENT_OBJECT_CLICK && sparam==m_objname)
     { // 有一个按钮事件
      if(ObjectGetInteger(0,m_objname,OBJPROP_STATE))
        { // 检查按钮状态
         On(); // 开
        }
      else
        {
         Off(); // 关
        }
     }
  }

如果 CHARTEVENT_OBJECT_CLICK 事件发生,且该事件的发生伴随具有 m_objname 名称的按钮(事件伴随发生的对象名称,在 sparam 变量中传递),则取决于按钮状态,On() 或 Off() 方法将执行。

1.7. Deinit() 方法

当“EA 交易”结束工作时必须调用 Deinit() 方法。该方法将停止计时器、释放指标句柄并删除按钮(如使用的话)。

//--- 去初始化的方法
void Deinit()
  {
   StopTimer();                  // 停止计时器
   IndicatorRelease(m_handle);   // 释放指标句柄
   if(m_button)
     {
      ObjectDelete(0,m_objname); // 删除按钮
      ChartRedraw();             // 重绘图表
     }
  }

Refresh()、SetParameters()、Trend()、BuyStoploss() 和 SellStoploss() 虚拟方法将在下文中讨论 - 届时我们将创建追踪止损子类。现在,我们来考虑基类的主方法 - DoStoploss() 方法。

1.8. DoStoploss() 方法

DoStoploss() 方法是主工作方法,必须针对每个订单号从 OnTick() 函数调用该方法。如果 m_onoff 变量的值为 false(追踪止损关闭),则方法立即结束工作。  

if(!m_onoff)
  {
   return(true);// 如果追踪止损被关闭了
  }

而且,如果追踪止损在按柱模式下工作,则会检查时间 - 将创建的柱的时间和函数上次成功执行的时间进行比较。如果时间匹配,则方法结束工作。

datetime tm[1];
// 在每个bar模式下,获取最近一个价格柱形的时间 
if(!m_eachtick)
  { 
   // 如果没能成功复制时间,结束方法,在下一个tick到来时重复 
   if(CopyTime(m_symbol,m_timeframe,0,1,tm)==-1)
     {
      return(false); 
     }
   // 如果柱形的时间和本方法最后执行时间一致,结束
   if(tm[0]==m_lasttime)
     { 
      return(true);
     }
  }

如果追踪止损开启且时间检查通过,则方法的主要部分执行 - 指标值刷新(Refresh() 方法被调用)。

if(!Refresh())
  { // 获取指标值
   return(false);
  }

然后,取决于 Trend() 方法返回的值,买入或卖出持仓的追踪止损执行。

// 取决于指标显示的趋势,进行多种操作
switch (Trend())
  {
   // 上升趋势
   case 1: 
      // 买入持仓的追踪止损代码
      break;
   // 下降趋势
   case -1: 
      // 卖出持仓的追踪止损代码
      break;
  }

我们以买入持仓为例来考虑它的工作。

if(PositionSelect(m_symbol,1000))
  {   //--- 选择持仓。如果成功,那么持仓存在
   if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY)
     {//--- 如果为买入持仓

      //--- 获取买入持仓的止损值
      sl=BuyStoploss(); 
      //--- 找到买入持仓被允许设置止损的位置
      double minimal=SymbolInfoDouble(m_symbol,SYMBOL_BID)-m_point*SymbolInfoInteger(m_symbol,SYMBOL_TRADE_STOPS_LEVEL);
      //--- 对值进行标准化
      sl=NormalizeDouble(sl,m_digits); 
      //--- 对值进行标准化
      minimal=NormalizeDouble(minimal,m_digits); 
      //---如果不能在由指标获得的水平上设置止损, 
      //   那么止损将被放置在离可能止损位最近的地方
      sl=MathMin(sl,minimal); 
      //--- 持仓的止损值
      double possl=PositionGetDouble(POSITION_SL); 
      //--- 对值进行标准化
      possl=NormalizeDouble(possl,m_digits); 
      if(sl>possl)
        {//--- 如果新的止损值大于当前止损值,
         //    那么将尝试移动止损到新的水平上
         //--- 填充请求结构体
         m_request.sl=sl; 
         //--- 填充请求结构体
         m_request.tp=PositionGetDouble(POSITION_TP); 
         //--- 请求
         OrderSend(m_request,m_result); 
         if(m_result.retcode!=TRADE_RETCODE_DONE)
           {//--- 检查请求结果
            //--- 日志错误信息
            printf("Unable to move Stop Loss of position %s, error #%I64u",m_symbol,m_result.retcode); 
            //--- 未能移动止损,结束
            return(false); 
           }
        }
     }
  }

如果可以选择该持仓,将检查持仓的类型。如果持仓的类型和趋势对应,我们使用 BuyStoploss() 方法获取所需的止损值(放入 sl 变量)。接下来是确定可设置止损的允许水平。如果计算的水平相较允许值过近,调整 sl 变量的值。然后我们获得止损持仓的当前值(放入 possl 变量),并将 sl 变量的值与 possl 变量的值进行比较。如果止损 id 的新值好于当前值 - 修改持仓。

修改前,填充 MqlTradeRequest 结构的 sltp 字段。m_request.sl 变量分配到所需的止损值,m_request.tp 变量分配到获利的现有值(使其保持不变)。剩下的字段在 Init() 方法执行时填充。在结构填充后,OrderSend() 函数调用。

一旦工作完成,m_result.retcode 变量的值将受到检查。如果值不等于 TRADE_RETCODE_DONE,则出于某些原因,它将无法执行 OrderSend() 函数请求的操作。同时显示含有错误数量的日志消息,且方法完成得到执行。如果 OrderSend() 函数成功执行,则记住 DoStoploss() 方法在其上完成最后工作的柱的时间。如果发生错误,即使是在按柱模式下,将在下一订单号尝试重试方法。只要成功完成工作,尝试将一直持续。

以下是 DoStopLoss() 方法的完整代码。

bool DoStoploss()
  {
//--- 如果追踪止损被关闭
   if(!m_onoff)
     {
      return(true);
     }
   datetime tm[1];
//--- 在每个bar模式下,获取最近一个柱形的时间
   if(!m_eachtick)
     {
      //--- 如果没能成功复制时间,结束方法,在下一个tick到来时重复 
      if(CopyTime(m_symbol,m_timeframe,0,1,tm)==-1)
        {
         return(false);
        }
      //--- 如果柱形的时间和本方法最后执行时间一致,结束
      if(tm[0]==m_lasttime)
        {
         return(true);
        }
     }
//--- 获取指标值
   if(!Refresh())
     {
      return(false);
     }
   double sl;
//--- 取决于指标显示的趋势,进行多种操作
   switch(Trend())
     {
      //--- 上升趋势
      case 1:
         //--- 选择持仓如果成功,那么持仓存在
         if(PositionSelect(m_symbol))
           {
            //---如果为买入持仓
            if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY)
              {
               //--- 获取买入持仓的止损值
               sl=BuyStoploss();
               //--- 找到买入持仓被允许设置止损的位置
               double minimal=SymbolInfoDouble(m_symbol,SYMBOL_BID)-m_point*SymbolInfoInteger(m_symbol,SYMBOL_TRADE_STOPS_LEVEL);
               //--- 对值进行标准化
               sl=NormalizeDouble(sl,m_digits);
               //--- 对值进行标准化
               minimal=NormalizeDouble(minimal,m_digits);
               //---如果不能在由指标获得的水平上设置止损, 
               //   那么止损将被放置在离可能止损位最近的地方
               sl=MathMin(sl,minimal);
               //--- 持仓的止损值
               double possl=PositionGetDouble(POSITION_SL);
               //--- 对值进行标准化
               possl=NormalizeDouble(possl,m_digits);
               //--- 如果新的止损值大于当前止损值, 
               //    那么将尝试移动止损到新的水平上
               if(sl>possl)
                 {
                  //--- 填充请求结构体
                  m_request.sl=sl;
                  //--- 填充请求结构体
                  m_request.tp=PositionGetDouble(POSITION_TP);
                  //--- 请求
                  OrderSend(m_request,m_result);
                  //--- 检查请求结果
                  if(m_result.retcode!=TRADE_RETCODE_DONE)
                    {
                     //--- 日志错误信息
                     printf("Unable to move Stop Loss of position %s, error #%I64u",m_symbol,m_result.retcode);
                     //--- 未能移动止损,结束
                     return(false);
                    }
                 }
              }
           }
         break;
         //--- 下降趋势
      case -1:
         if(PositionSelect(m_symbol))
           {
            if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL)
              {
               sl=SellStoploss();
               //--- 加上点差,因为卖单是通过买价平仓的
               sl+=(SymbolInfoDouble(m_symbol,SYMBOL_ASK)-SymbolInfoDouble(m_symbol,SYMBOL_BID));
               double minimal=SymbolInfoDouble(m_symbol,SYMBOL_ASK)+m_point*SymbolInfoInteger(m_symbol,SYMBOL_TRADE_STOPS_LEVEL);
               sl=NormalizeDouble(sl,m_digits);
               minimal=NormalizeDouble(minimal,m_digits);
               sl=MathMax(sl,minimal);
               double possl=PositionGetDouble(POSITION_SL);
               possl=NormalizeDouble(possl,m_digits);
               if(sl<possl || possl==0)
                 {
                  m_request.sl=sl;
                  m_request.tp=PositionGetDouble(POSITION_TP);
                  OrderSend(m_request,m_result);
                  if(m_result.retcode!=TRADE_RETCODE_DONE)
                    {
                     printf("Unable to move Stop Loss of position %s, error #%I64u",m_symbol,m_result.retcode);
                     return(false);
                    }
                 }
              }
           }
         break;
     }
//--- 记住方法的最近一次执行时间
   m_lasttime=tm[0];
   return(true);
  }

注意买入和卖出持仓的代码的不同之处。对于卖出持仓,SellStoploss() 返回的值需要加上点差的值,因为卖出持仓是按买价进行平仓的。相应的,对于买入,止损最小水平的倒计时从卖价开始,对于卖出则是从买价开始。

现在我们已完成了追踪止损基类的创建。接下来我们创建子类。

2. 抛物线转向指标的追踪止损子类

CTrailingStop 类的虚拟方法已向您展示了子类的内容 - SetParameters()、Refresh()、Trend()、BuyStoploss()、SellStoploss() 方法以及类构造函数设置追踪止损的名称。该类将命名为 CParabolicStop。由于该类是 CTrailingStop 的一个子类,这将在它的声明中提及。

class CParabolicStop: public CTrailingStop

通过该声明,调用 CParabolicStop 类的虚拟方法将运行基类的继承方法。  

让我们从细节上考虑子类的所有方法。

2.1. CParabolicStop() 方法

该方法与类本身同名,是一个构造函数。它在类加载时自动执行,比类的任何其他方法都要更早执行。在 CParabolicStop() 方法中,止损名称将分配至 m_typename 变量。该变量用于创建按钮名称和标题(在基类的 Init() 方法中)。

void CParabolicStop()
  {
   m_typename="SAR"; // 设置追踪止损类型的名称
  };

2.2. SetParameters() 方法

当调用 SetParameters() 方法时,它接收指标参数,且指标和这些参数一同加载。如果 m_indicator 参数设置基类的 Init() 方法,则指标将附加至图表(ChartIndicatorAdd() 函数)。

// 设置参数和加载指标的方法
bool SetParameters(double sarstep=0.02,double sarmaximum=0.2)
  {
   m_handle=iSAR(m_symbol,m_timeframe,sarstep,sarmaximum); // 加载指标
   if(m_handle==-1)
     {
      return(false); // 如果未能加载指标,方法返回false
     }
   if(m_indicator)
     {
      ChartIndicatorAdd(0,0,m_handle); // 将指标添加到图标上
     }
    
   return(true);
  }

2.3. Refresh() 方法

Refresh() 方法用于获取新的价格以及刷新指标值。在类的“保护”部分中,有用于价格值的 pricebuf 数组和用于指标值的 indbuf 数组。两个数组都只有一个元素 - 应只有一个价格值和一个来自正在形成或已形成的柱的指标的值(取决于 m_shift 参数,在基类初始化时设置)。

//获取指标值的方法
bool Refresh()
  {
   if(CopyBuffer(m_handle,0,m_shift,1,indbuf)==-1)
     {
      return(false); // 如果不能复制数组值,返回false
     }
    
   if(CopyClose(m_symbol,m_timeframe,m_shift,1,pricebuf)==-1)
     {
      return(false); // 如果不能复制数组值,返回false
     }    
   return(true); 
  }

2.4. Trend() 方法

Trend() 方法用于检查相对于指标线的价格位置。如果价格高于指标线,则为上行趋势,且方法返回值 1。如果价格低于指标线,则为下行趋势,且方法返回值 -1。但也不排除价格等于指标线的情形(很少见,但有可能)。在这种情况下,方法将返回值 0。  

// 找到趋势的方法
int Trend()
  {
   if(pricebuf[0]>indbuf[0])
     { // 价格比指标线高,上升趋势
      return(1);
     }
   if(pricebuf[0]<indbuf[0])
     { // 价格比指标线低,下降趋势
      return(-1);
     }    
   return(0);
  }

2.5. BuyStoploss() 和 SellStoploss() 方法

由于抛物线转向指标仅有一条线,两个方法是等价的。它们返回从 Refresh() 方法获得的值。

// 找到买入持仓止损水平的方法
virtual double BuyStoploss()
  {
   return(indbuf[0]);
  };
// 找到卖出持仓止损水平的方法
virtual double SellStoploss()
  {
   return(indbuf[0]);
  };

到目前为止追踪止损已就绪。虽然它还只有一个类,但已经可以使用。将其作为单独的包含文件保存在 .\MQL5\Include 文件夹中,文件名为 Sample_TrailingStop.mqh(文件已附于本文)。

3. 将抛物线追踪止损添加至“EA 交易”

让我们尝试将追踪止损添加至某些“EA 交易”,比如出自针对初学者以 MQL5 编写“EA 交易”的分步指南一文的 My_First_EA。

3.1. 在 MetaEditor 中打开 My_First_EA“EA 交易”,将其另存为 My_First_EA_SARTrailing。

3.2. 包含追踪止损文件。在“EA 交易”代码的上部(在外部变量的声明之前则更佳)添加代码行:

#include <Sample_TrailingStop.mqh> // 包含追踪止损类

3.3. 在外部变量后创建一个名为 Trailing 的 CParabolicStop 类的实例。

CParabolicStop Trailing; // 创建类的实例

 3.4. 在 OnInit() 函数中初始化类并设置它的参数。首先,使用指标参数声明外部变量:

input double TrailingSARStep=0.02;
input double TrailingSARMaximum=0.2;

然后添加代码至 OnInit() 函数。

Trailing.Init(_Symbol,PERIOD_CURRENT,true,true,false); //初始化(设置基本参数)
if(!trailing.setparameters(TrailingSARStep,TrailingSARMaximum))
  { // 设置所使用的追踪止损类型的参数
   Alert("trailing error");
   return(-1);
  } 
Trailing.StartTimer(); // 启动计时器
Trailing.On();         // 激活

3.5. 在“EA 交易”代码中找到 OnTimer() 函数。OnTimer() 函数未在 My_First_EA 中使用,因此将其添加,并添加 Refresh() 的调用。

void OnTimer()
  {
   Trailing.Refresh();
  }

3.6. 在 OnTick() 函数的顶部添加 DoStoploss() 方法的调用。

3.7. 编译并尝试测试“EA 交易”。“EA 交易”的测试结果在图 7 中(无追踪止损)和图 8(带追踪止损)中显示。

图 7. 无追踪止损的“EA 交易”的测试结果。

图 7. 无追踪止损的“EA 交易”的测试结果。   

图 8. 带追踪止损的“EA 交易”的测试结果。

图 8. 带追踪止损的“EA 交易”的测试结果。   

使用追踪止损的效果显著。

My_First_EA_SARTrailing.mq5 文件已附于本文。

4. NRTR 的追踪止损子类

NRTR 指标 (Nick Rypock Trailing Reverse) 的名称和外观(请参见图 9)引发了人们在其上尝试创建追踪止损的兴趣。

图 9. NRTR 指标。

图 9. NRTR 指标。

指标将绘制基线(支撑线或阻力线)和目标线。当价格超出目标线时,基线以价格移动的方向迁移,价格的轻微振荡将被忽略。当价格与基线相交时,它将被视为趋势的改变,因而将根据价格改变基线和目标线的位置。上行的支撑线和目标线以蓝色标示,下行则以红色标示。

现在为 NRTR 指标创建其他追踪止损。

声明其他类 CNRTRStop 包含在 CNRTRStop 基类中。

class CNRTRStop: public CTrailingStop

除了构造函数 - 现在它将被命名为 CNRTRStop() - NRTR 子类的追踪止损将具有和抛物线追踪止损相同的方法集。  

4.1. CNRTRStop() 方法

根据所用指标,现在在类构造函数中 m_typename 变量将分配到值 NRTR。

void CNRTRStop()
  {
   m_typename="NRTR"; // 设置追踪止损类型的名称
  };

4.2. SetParameters() 方法

当调用 SetParameters() 方法时,它接收指标参数并加载。然后,取决于追踪止损的基本参数,指标将被附加至图表。

// 设置参数和加载指标的方法
bool SetParameters(int period,double k)
  {
   m_handle=iCustom(m_symbol,m_timeframe,"NRTR",period,k); // 加载指标
   if(m_handle==-1)
     { // 如果未能加载指标,方法返回false
      return(false); 
     }
   if(m_indicator)
     {
       
      ChartIndicatorAdd(0,0,m_handle); // 将指标添加到图标上
     }
   return(true);
  }

4.3. Refresh() 方法

Refresh() 方法复制 NRTR 指标的两个缓冲区 - 支撑线缓冲区和阻力线缓冲区。  

 //获取指标值的方法
bool Refresh()
  {
   if(CopyBuffer(m_handle,0,m_shift,1,sup)==-1)
     {
      return(false); // 如果不能复制数组值,返回false
     }
    
   if(CopyBuffer(m_handle,1,m_shift,1,res)==-1)
     {
      return(false); // 如果不能复制数组值,返回false
     }
    
   return(true);
  }

在类的“保护”部分,有两个声明的数组:double sup[] 和 double res[]

protected:
double sup[1]; // 支撑位的值
double res[1]; // 阻力位的值

4.4. Trend() 方法

Trend() 方法用于检查当前存在的线。如果为支撑线,意味着指标显示上行趋势。方法本身将返回值 1.如果为阻力线,则方法返回 -1。

// 找到趋势的方法
int Trend()
  {
   if(sup[0]!=0)
     { // 若有支撑线,那么是上升趋势
      return(1);
     }
   if(res[0]!=0)
     { // 若有阻力线,那么是下降趋势
      return(-1);
     }
    
   return(0);
  }

4.5. BuyStoploss() 方法

BuyStoploss() 方法用于返回支撑线的值。

// 找到买入持仓止损水平的方法
double BuyStoploss()
  {
   return(sup[0]);
  }

4.6. SellStoploss() 方法

SellStoploss() 方法用于返回阻力线的值。  

// 找到卖出持仓止损水平的方法
double SellStoploss()
  {
   return(res[0]);
  }

现在,我们已完成了追踪止损类。

5. 将 NRTR 追踪止损添加至“EA 交易”

和处理抛物线追踪止损时一样,让我们将 NRTR My_First_EA 追踪止损添加至“EA 交易”。

5.1. 在 MetaEditor 中打开 My_First_EA_SARTrailing“EA 交易”,将其另存为 My_First_EA_NRTRTrailing。

5.2. 使用 NRTR 追踪止损的参数替换抛物线追踪止损的外部参数。

input int TrailingNRTRPeriod = 40;
input double TrailingNRTRK   =  2;

5.3. 创建 CNRTRStop 类的实例而不是 CParabolicStop 类的实例。代码位于外部变量后面。

CNRTRStop Trailing; // 创建类的实例 

5.4. 在 OnInit() 中使用 NRTR 参数替换 SetParameters() 方法调用的参数。

Trailing.SetParameters(TrailingNRTRPeriod,TrailingNRTRK)

5.5. 编译并尝试测试“EA 交易”。

图 10. 带 NRTR 追踪止损的“EA 交易”的测试结果。

图 10. 带 NRTR 追踪止损的“EA 交易”的测试结果。   

比较带追踪止损策略的“EA 交易”的工作结果(请参见图 10)和无追踪止损策略的“EA 交易”的工作结果(请参见图 7),我们发现结果几乎没有变化。经证明,对于该“EA 交易”,抛物线追踪止损的使用更有效。我们可以得出结论,部分追踪止损在开发“EA 交易”时极其有效 - 进行尝试,并找出最合适的追踪止损类型。  

My_First_EA_NRTRTrailing.mq5 文件已附于本文。

6. 辅助“EA 交易”

追踪止损基类的创建旨在通过按钮控制追踪止损开启/关闭。让我们创建一个辅助“EA 交易”,以追踪具有不同追踪止损类型的各种交易品种的持仓。该“EA 交易”将不会建立而仅仅是追踪持仓。

6.1. 在 MetaEditor 中创建名为 Sample_TrailingStop 的新的“EA 交易”。

6.2. 包含 Sample_TrailingStop.mqh 文件。

#include <Sample_TrailingStop.mqh> // 包含追踪止损类

6.3. 为指标声明外部参数。

input double SARStep=0.02;     // 抛物线指标的步长
input double SARMaximum=0.02;  //  抛物线指标的最大值
input int NRTRPeriod=40;       // NRTR 周期
input double NRTRK=2;          // NRTR 因子

6.4. 声明“EA 交易”将能够在其上工作的交易品种数组。

string Symbols[]={"EURUSD","GBPUSD","USDCHF","USDJPY"};

6.5. 声明用于加载类的数组。

CParabolicStop *SARTrailing[];
CNRTRStop *NRTRTrailing[];

6.6. 在 OnInit() 函数中,根据 Symbols 数组的大小改变用于加载类的数组的大小。

ArrayResize(SARTrailing,ArraySize(Symbols));  //  根据用到的交易品种的数量,重新设置数组大小
ArrayResize(NRTRTrailing,ArraySize(Symbols)); // 根据用到的交易品种的数量,重新设置数组大小

6.7.循环中,为数组的每个元素加载类实例。

for(int i=0;i<ArraySize(Symbols);i++)
  { // 针对所有交易品种
   SARTrailing[i]=new CParabolicStop(); // 创建CParabolicStop类的实例
   SARTrailing[i].Init(Symbols[i],PERIOD_CURRENT,false,true,true,5,15+i*17,Silver,Blue);    // 初始化CParabolicStop类的实例
   if(!SARTrailing[i].SetParameters(SARStep,SARMaximum))
     { // 设置CParabolicStop类实例的参数
      Alert("trailing error");
      return(-1);
     }
   SARTrailing[i].StartTimer();         // 启动计时器
//----
   NRTRTrailing[i]=new CNRTRStop();     // 创建CNRTRStop类的实例
   NRTRTrailing[i].Init(Symbols[i],PERIOD_CURRENT,false,true,true,127,15+i*17,Silver,Blue); // 初始化CNRTRStop类的实例
   if(!NRTRTrailing[i].SetParameters(NRTRPeriod,NRTRK))
     { //  设置CNRTRcStop类实例的参数
      Alert("trailing error");
      return(-1);
     }
   NRTRTrailing[i].StartTimer();        // 启动计时器 
  }

注意:在调用 Init() 方法时,将计算按钮的坐标。左边将为抛物线追踪止损的电源开关,右边则为 NRTR 追踪止损的电源开关。  

6.8. 在 OnTick() 函数中,为追踪止损的每个实例添加 DoStoploss() 方法的调用。

for(int i=0;i<ArraySize(Symbols);i++)
  {
   SARTrailing[i].DoStoploss();
   NRTRTrailing[i].DoStoploss();
  }

6.9. 添加图表事件句柄。

void OnChartEvent(const int         id,
                  const long   &lparam,
                  const double &dparam,
                  const string &sparam 
                  )
  {
   for(int i=0;i<ArraySize(Symbols);i++)
     {
      SARTrailing[i].EventHandle(id,lparam,dparam,sparam);
      NRTRTrailing[i].EventHandle(id,lparam,dparam,sparam);
     }
    
  }

6.10. 在 Deinit() 函数中,取消所有类实例的初始化并将其删除

for(int i=0;i<ArraySize(Symbols);i++)
  {
   SARTrailing[i].Deinit(); 
   NRTRTrailing[i].Deinit();
   delete(SARTrailing[i]);  // 删除对象
   delete(NRTRTrailing[i]); // 删除对象
  }

编译,然后将“EA 交易”附加至图表。指标和按钮出现在图表上(请参见图 11)- “EA 交易”此时工作就绪。  

图 11. 启动 Sample_TrailingStop 后图表上的按钮和指标。

图 11. 启动 Sample_TrailingStop 后图表上的按钮和指标。

开仓后,只需按下按钮以追踪相应的持仓。

Sample_TrailingStop.mq5 文件已附于本文。

总结

让我们回顾一下在创建手工操作的交易系统时 CTrailingStop 类的使用顺序:

1. 包含 Sample_TrailingStop.mqh 文件。

2. 使用所用追踪止损的指标参数声明外部变量。

3. 创建类实例。

4. 从 OnInit() 函数添加 Init()、SetParameters()、StartTimer() 和 On() 方法的调用。

5. 从 OnTimer() 函数添加 Refresh() 方法的调用。

6. 从 OnTick() 函数添加 DoStopLoss() 方法的调用。

7. 从 OnDeinit() 函数添加 Deinit() 方法的调用。


仅需七个步骤,用时不到 5 分钟,您的“EA 交易”便拥有了追踪止损功能!