跟踪止损

使用改变保护价格水平能力的最常见的任务之一是随着有利趋势的继续,在更好的价格上按顺序转移 Stop Loss。这就是跟踪止损。我们使用前几节中的新结构体 MqlTradeRequestSyncMqlTradeResultSync 来实现跟踪止损。

为了能够将该机制连接到任何 EA 交易,我们将其声明为 Trailing Stop 类(参见文件 TrailingStop.mqh)。我们将存储受控仓位的编号、其交易品种和价格点的大小、止损水平与当前价格的所需距离以及该类单个变量中的水平变化步长。

#include <MQL5Book/MqlTradeSync.mqh>
   
class TrailingStop
{
   const ulong ticket;  // ticket of controlled position
   const string symbol// position symbol
   const double point;  // symbol price pip size
   const uint distance// distance to the stop in points
   const uint step;     // movement step (sensitivity) in points
   ...

只有基础类提供的标准仓位跟踪算法才需要距离。派生类能够根据其他原则移动保护水平,如移动平均线、通道、SAR 指标等。在熟悉了基础类之后,我们将给出一个带有移动平均线的派生类示例。

我们为当前止损价格水平创建 level 变量。在 ok 变量中,我们将保持该仓位的当前状态: true 如果该仓位仍然存在,则为 true;如果出现错误且该仓位已平仓,则为 false

protected:
   double level;
   bool ok;
   virtual double detectLevel() 
   {
      return DBL_MAX;  
   }

虚方法 detectLevel 用于在子类中重写,其中止损价格应根据任意算法来计算。在这个实现中,会返回一个特殊值 DBL_MAX,表示根据标准算法所做的工作(见下文)。

在构造函数中,用相应参数的值填充所有字段。函数 PositionSelectByTicket 用于检查具有给定订单号的仓位是否存在,并在程序环境中分配它,以便 PositionGetString 的后续调用可返回其具有交易品种名称的字符串特性。

public:
   TrailingStop(const ulong tconst uint dconst uint s = 1) :
      ticket(t), distance(d), step(s),
      symbol(PositionSelectByTicket(t) ? PositionGetString(POSITION_SYMBOL) : NULL),
      point(SymbolInfoDouble(symbolSYMBOL_POINT))
   {
      if(symbol == NULL)
      {
         Print("Position not found: " + (string)t);
         ok = false;
      }
      else
      {
         ok = true;
      }
   }
   
   bool isOK() const
   {
      return ok;
   }

现在我们考虑 trail 类的主要公共方法。MQL 程序需要在每个分时报价上或通过计时器调用该方法跟踪仓位。当仓位存在时,该方法返回 true

   virtual bool trail()
   {
      if(!PositionSelectByTicket(ticket))
      {
         ok = false;
         return false// position closed
      }
   
      // find out prices for calculations: current quote and stop level
      const double current = PositionGetDouble(POSITION_PRICE_CURRENT);
      const double sl = PositionGetDouble(POSITION_SL);
      ...

此处和下面,我们使用仓位特性来读取函数。仓位特性将在 单独的章节中详细讨论。特别是,我们需要找出交易方向(买入和卖出)才能知道应在哪个方向设置止损水平。

      // POSITION_TYPE_BUY  = 0 (false)
      // POSITION_TYPE_SELL = 1 (true)
      const bool sell = (bool)PositionGetInteger(POSITION_TYPE);
      TU::TradeDirection dir(sell);
      ...

对于计算和检查,我们应使用辅助类 TU::TradeDirection 及其对象 dir。例如,其 negative 方法允许在亏损方向上计算位于当前价格指定距离处的价格,而无需考虑操作的类型。这样简化了代码,否则就必须为买入和卖出进行“镜像”计算。

      level = detectLevel();
      // we can't trail without a level: removing the stop level must be done by the calling code
      if(level == 0return true;
      // if there is a default value, make a standard offset from the current price
      if(level == DBL_MAXlevel = dir.negative(currentpoint * distance);
      level = TU::NormalizePrice(levelsymbol);
      
      if(!dir.better(currentlevel))
      {
         return true// you can't set a stop level on the profitable side<
      }
      ...

TU::TradeDirection 类的 better 方法用于检查收到的止损水平是否位于价格的右侧。如果没有这个方法,我们将需要再次写入两次检查(针对买入和卖出)。

我们可能会得到一个不正确的止损水平值,因为 detectLevel 方法可以在派生类中被重写。使用标准计算,这个问题就可以消除,因为水平是由 dir 对象计算的。

最后,在计算水平时,需要将其应用到仓位上。如果仓位还没有止损,任何有效水平均可以。如果已经设置了止损,那么新的值应比以前的值更好,并且相差超过指定的步长。

      if(sl == 0)
      {
         PrintFormat("Initial SL: %f"level);
         move(level);
      }
      else
      {
         if(dir.better(levelsl) && fabs(level - sl) >= point * step)
         {
            PrintFormat("SL: %f -> %f"sllevel);
            move(level);
         }
      }
      
      return true// success
   }

发送仓位修改请求是在 move 方法中实现的,该方法使用 MqlTradeRequestSync 结构体的常见 adjust 方法(参见 修改止损和/或止盈水平一节)。

   bool move(const double sl)
   {
      MqlTradeRequestSync request;
      request.position = ticket;
      if(request.adjust(sl0) && request.completed())
      {
         Print("OK Trailing: "TU::StringOf(sl));
         return true;
      }
      return false;
   }
};

现在一切准备就绪,可以向测试 EA 交易 TrailingStop.mq5 添加跟踪了。在输入参数中,你可以指定交易方向、到止损点的距离以及步长。默认情况下,TrailingDistance 参数等于 0,这意味着自动计算每日报价范围,并使用其一半作为距离。

#include <MQL5Book/MqlTradeSync.mqh>
#include <MQL5Book/TrailingStop.mqh>
   
enum ENUM_ORDER_TYPE_MARKET
{
   MARKET_BUY = ORDER_TYPE_BUY,   // ORDER_TYPE_BUY
   MARKET_SELL = ORDER_TYPE_SELL  // ORDER_TYPE_SELL
};
   
input int TrailingDistance = 0;   // Distance to Stop Loss in points (0 = autodetect)
input int TrailingStep = 10;      // Trailing Step in points
input ENUM_ORDER_TYPE_MARKET Type;
input string Comment;
input ulong Deviation;
input ulong Magic = 1234567890;

启动时,EA 交易将查找当前交易品种上是否存在具有指定 Magic 号的仓位,如果不存在,将创建该仓位。

跟踪将由 TrailingStop 类的对象执行(包装在智能指针 AutoPtr中)。由于后者,当需要新的跟踪对象来替换该对象以获得正在创建的新仓位时,我们不需要手动删除旧对象。当新对象被赋予给智能指针时,旧对象会被自动删除。注意,取消对智能指针的引用,即访问存储在其中的工作对象,是使用重载的 [] 运算符完成的。

#include <MQL5Book/AutoPtr.mqh>
   
AutoPtr<TrailingStoptr;

OnTick 处理程序中,我们会检查是否有对象。如果有,检查仓位是否存在(从 trail 方法返回其属性)。程序启动后,如果对象不存在,则指针为空。在这种情况下,你应创建一个新的仓位,或者找到一个已经打开的仓位,并为其创建一个 Trailing Stop 对象。这是由 Setup 函数完成的。在 OnTick 的后续调用中,对象开始并继续跟踪,防止程序在仓位“活动”时进入 if 块。

void OnTick()
{
   if(tr[] == NULL || !tr[].trail())
   {
      // if there is no trailing yet, create or find a suitable position
      Setup();
   }
}

这就是 Setup 函数。

void Setup()
{
   int distance = 0;
   const double point = SymbolInfoDouble(_SymbolSYMBOL_POINT);
   
   if(trailing distance == 0// auto-detect the daily range of prices
   {
      distance = (int)((iHigh(_SymbolPERIOD_D11) - iLow(_SymbolPERIOD_D11))
         / point / 2);
      Print("Autodetected daily distance (points): "distance);
   }
   else
   {
      distance = TrailingDistance;
   }
   
   // process only the position of the current symbol and our Magic
   if(GetMyPosition(_SymbolMagic))
   {
      const ulong ticket = PositionGetInteger(POSITION_TICKET);
      Print("The next position found: "ticket);
      tr = new TrailingStop(ticketdistanceTrailingStep);
   }
   else // there is no our position
   {
      Print("No positions found, lets open it...");
      const ulong ticket = OpenPosition();
      if(ticket)
      {
         tr = new TrailingStop(ticketdistanceTrailingStep);
      }
   }
   
   if(tr[] != NULL)
   {
      // Execute trailing for the first time immediately after creating or finding a position
      tr[].trail();
   }
}

GetMyPosition 函数中实现对合适的开仓仓位的搜索,并且通过 OpenPosition 函数完成对新仓位的开仓。二者都在下文中介绍。无论如何,我们可以得到一张仓位订单号,并为其创建一个跟踪对象。

bool GetMyPosition(const string sconst ulong m)
{
   for(int i = 0i < PositionsTotal(); ++i)
   {
      if(PositionGetSymbol(i) == s && PositionGetInteger(POSITION_MAGIC) == m)
      {
         return true;
      }
   }
   return false;
}

该算法的目的和一般含义从内置函数的名称中就能看得很清楚。在所有未结仓位 (PositionsTotal) 的循环中,我们可使用 PositionGetSymbol 依次选择每个仓位并获取其交易品种。如果该交易品种与请求的交易品种匹配,我们可读取仓位特性 POSITION_MAGIC 并与传递的 "magic" 进行比较。与处理仓位相关的所有函数将在 单独的章节中讨论。

一旦找到第一个匹配仓位,该函数将返回 true。同时,在终端的交易环境中,该仓位将保持选中状态,这使得代码的其余部分可以在必要时读取其相关特性。

我们已经知道了开仓的算法。

ulong OpenPosition()
{
   MqlTradeRequestSync request;
   
   // default values
   const bool wantToBuy = Type == MARKET_BUY;
   const double volume = SymbolInfoDouble(_SymbolSYMBOL_VOLUME_MIN);
   // optional fields are filled directly in the structure
   request.magic = Magic;
   request.deviation = Deviation;
   request.comment = Comment;
   ResetLastError();
   // execute the selected trade operation and wait for its confirmation
   if((bool)(wantToBuy ? request.buy(volume) : request.sell(volume))
      && request.completed())
   {
      Print("OK Order/Deal/Position");
   }
   
   return request.position// non-zero value - sign of success
}

为了清楚起见,我们看看这个程序是如何在测试程序中以可视模式工作的。

编译之后,我们打开终端中的策略测试程序面板,在 Review 选项卡上,选择第一个选项:Single test

Settings 选项卡中,选择以下选项:

  • 在下拉菜单 Expert Advisor 中:MQL5Book\p6\TralingStop
  • Symbol:EURUSD
  • Timeframe:H1
  • Interval:去年、上个月或自定义
  • Forward:否
  • Delays:禁用
  • Modeling:基于真实或生成的分时报价
  • Optimization:禁用
  • Visual mode:启用

一旦按下 Start,将在一个单独的测试程序窗口中看到类似这样的内容:

测试程序中的标准跟踪止损

测试程序中的标准跟踪止损

日志将显示如下条目:

2022.01.10 00:02:00 Autodetected daily distance (points): 373

2022.01.10 00:02:00 No positions found, let's open it...

2022.01.10 00:02:00 instant buy 0.01 EURUSD at 1.13612 (1.13550 / 1.13612 / 1.13550)

2022.01.10 00:02:00 deal #2 buy 0.01 EURUSD at 1.13612 done (based on order #2)

2022.01.10 00:02:00 deal performed [#2 buy 0.01 EURUSD at 1.13612]

2022.01.10 00:02:00 order performed buy 0.01 at 1.13612 [#2 buy 0.01 EURUSD at 1.13612]

2022.01.10 00:02:00 Waiting for position for deal D=2

2022.01.10 00:02:00 OK Order/Deal/Position

2022.01.10 00:02:00 Initial SL: 1.131770

2022.01.10 00:02:00 position modified [#2 buy 0.01 EURUSD 1.13612 sl: 1.13177]

2022.01.10 00:02:00 OK Trailing: 1.13177

2022.01.10 00:06:13 SL: 1.131770 -> 1.131880

2022.01.10 0:06:13 position modified [#2 buy 0.01 EURUSD 1.13612 sl: 1.13188]

2022.01.10 0:06:13 OK Trailing: 1.13188

2022.01.10 0:09:17 SL: 1.131880 -> 1.131990

2022.01.10 0:09:17 position modified [#2 buy 0.01 EURUSD 1.13612 sl: 1.13199]

2022.01.10 0:09:17 OK Trailing: 1.13199

2022.01.10 0:09:26 SL: 1.131990 -> 1.132110

2022.01.10 0:09:26 position modified [#2 buy 0.01 EURUSD 1.13612 sl: 1.13211]

2022.01.10 0:09:26 OK Trailing: 1.13211

2022.01.10 0:09:35 SL: 1.132110 -> 1.132240

2022.01.10 0:09:35 position modified [#2 buy 0.01 EURUSD 1.13612 sl: 1.13224]

2022.01.10 0:09:35 OK Trailing: 1.13224

2022.01.10 10:06:38 stop loss triggered #2 buy 0.01 EURUSD 1.13612 sl: 1.13224 [#3 sell 0.01 EURUSD at 1.13224]

2022.01.10 10:06:38 deal #3 sell 0.01 EURUSD at 1.13221 done (based on order #3)

2022.01.10 10:06:38 deal performed [#3 sell 0.01 EURUSD at 1.13221]

2022.01.10 10:06:38 order performed sell 0.01 at 1.13221 [#3 sell 0.01 EURUSD at 1.13224]

2022.01.10 10:06:38 Autodetected daily distance (points): 373

2022.01.10 10:06:38 No positions found, let's open it...

看看该算法如何在有利的价格变动中提升 SL 水平,直到止损平仓。清算仓位后,该程序会立即打开一个新的仓位。

为了检查使用非标准跟踪机制的可能性,我们实现了一个移动平均方法的示例。为此,我们回到文件 TrailingStop.mqh 并说明派生类 TrailingStopByMA

class TrailingStopByMApublic TrailingStop
{
   int handle;
   
public:
   TrailingStopByMA(const ulong tconst int period,
      const int offset = 1,
      const ENUM_MA_METHOD method = MODE_SMA,
      const ENUM_APPLIED_PRICE type = PRICE_CLOSE): TrailingStop(t01)
   {
      handle = iMA(_SymbolPERIOD_CURRENTperiodoffsetmethodtype);
   }
   
   virtual double detectLevel() override
   {
      double array[1];
      ResetLastError();
      if(CopyBuffer(handle001array) != 1)
      {
         Print("CopyBuffer error: "_LastError);
         return 0;
      }
      return array[0];
   }
};

该类可在构造函数中创建 iMA 指标实例:周期、平均方法和价格类型通过参数传递。

在被改写的 detectLevel 方法中,我们从指标缓冲区读取值,默认情况下,这是以 1 柱线的偏移量完成的,即,该柱线是关闭的,当分时报价到达时,其读数不会改变。如果愿意,可以从零柱线取值,但是这样的信号对于所有价格类型都是不稳定的,除了 PRICE_OPEN 之外。

为了在同一个测试 EA 交易 TrailingStop.mq5 中使用一个新类,我们添加另一个带有移动周期的输入参数 MATrailingPeriod(保持该指标的其他参数不变)。

input int MATrailingPeriod = 0;   // Period for Trailing by MA (0 = disabled)

此参数中的 0 值可禁用跟踪移动平均线。如果启用,TrailingDistance 参数中的距离设置将被忽略。

根据这个参数,我们将创建一个标准的跟踪对象TrailingStop 或一个来自 iMATrailingStopByMA 的派生对象。

      ...
      tr = MATrailingPeriod > 0 ?
         new TrailingStopByMA(ticketMATrailingPeriod) :
         new TrailingStop(ticketdistanceTrailingStep);
      ...

我们看看更新后的程序在测试程序中的表现。在 EA 交易设置中,为 MA 设置一个非零周期,例如 10。

测试程序中移动平均线上的跟踪止损

测试程序中移动平均线上的跟踪止损

请注意,在平均线接近该价格的时刻,有一个频繁止损触发平仓的效应。当平均价格高于报价时,并没有设置保护水平,因为这对于买入是不正确的。这是因为我们的 EA 交易没有任何策略,不管市场情况如何,总是建立相同类型的未结仓位。对于卖出而言,当平均价格低于该价格时,也会偶尔出现相同的矛盾情况,这意味着市场正在增长,而机器人“顽固地”进入空头仓位。

在工作策略中,一般来说,仓位方向的选择要考虑到市场的波动,当移动平均线位于当前价格的右侧时,方可允许设置止损。