English Русский Español Deutsch 日本語 Português
preview
如何使用抛物线转向(Parabolic SAR)指标设置跟踪止损(Trailing Stop)

如何使用抛物线转向(Parabolic SAR)指标设置跟踪止损(Trailing Stop)

MetaTrader 5示例 | 7 十一月 2024, 11:35
809 0
Artyom Trishkin
Artyom Trishkin

内容



简介

追踪止损对广大交易者而言可谓是耳熟能详。这个函数内置于MetaTrader 5中,并自动调整止损(StopLoss)水平,使其保持在当前价格的一定距离上:

在MetaTrader 5启用追踪止损


追踪止损(Trailing Stop)是一种自动调整止损位置以跟随价格变动的机制,它确保保护性止损始终与价格保持一定距离。这种方法允许交易者在不过早平仓的情况下,保护部分已累积的利润。每当市场价格远离开仓价格时,追踪止损会自动收紧止损位,使其与当前价格保持指定的距离。然而,如果价格接近开仓价格,止损价位则保持不变。这为交易者提供了保护,使其避免遭受由于市场波动而导致的损失。

但是,如果你需要一个更专业的滑动止损程序版本,你可以使用MQL5编写一个函数来扩展标准工具的功能。

有一个程序函数,它接收所需的价格来设置止损价。该程序会检查一些禁止性因素,如“StopLevel”水平——即止损不能设置得比最小允许距离更近,或“FreezeLevel”水平——即在此距离内不能修改持仓或挂单。换句话说,如果价格已经接近持仓订单的止损位置,且距离小于“FreezeLevel”水平,那么将会触发止损,并且禁止修改。追踪止损还有一些其他参数设置,这些参数设置在移动止损水平到指定价格之前也会被检查,例如仓位的交易品种和magic数字(一种用于识别和管理仓位或订单的标识符)。所有这些标准都在将止损位置移动到指定水平之前立即进行检查。

不同类型的追踪止损由不同的算法来计算持仓的止损价格,这些算法被传递给追踪止损函数,并被追踪止损函数所使用。

抛物线转向(Parabolic SAR)指标可完美地作为止损位置的“指示”。



抛物线转向(Parabolic Stop and Reverse,简称SAR)指标是技术分析中的一种流行工具,用于确定当前趋势可能结束并反转的时刻。该指标由威尔斯·怀尔德(Welles Wilder)开发,常用于自动追踪止损。以下是抛物线转向指标在追踪止损方面颇具吸引力的主要原因:

  1. 易于解读:抛物线转向指标易于理解,因为它在图表上以位于价格上方或下方的点来表示。当这些点位于价格下方时,是买入信号;当这些点位于价格上方时,是卖出信号。

  2. 自动跟随价格:抛物线转向指标的主要优势在于其能够自动适应价格变化并随时间移动。这使得它成为设置追踪止损的理想工具,因为它能够随着趋势的移动,将止损点拉近当前价格,从而提供利润保护。

  3. 利润保护:随着价格向开仓盈利方向移动,抛物线转向指标会上移止损水平,这有助于保护部分已累积利润免受可能趋势反转的影响。

  4. 平仓信号:除了追踪止损功能外,抛物线转向指标还可以作为平仓信号,当指标点穿过价格时即表示应关闭仓位。这可以在趋势快速变化时防止进一步损失。

  5. 易于设置:抛物线转向指标的参数(步长和最大值)可以轻松调整,以适应特定的市场波动或交易策略。这使得它成为各种交易条件下的多功能工具。

  6. 适用于所有时间框架:该指标可在各种时间框架中有效使用,因此既适合长期投资者,也适合进行短期交易的交易者。

在趋势市场中,使用抛物线转向指标进行追踪止损尤其有用,因为该工具有助于在趋势持续期间保持持仓的同时最大化利润。然而,请注意,在平稳期或低波动期间使用抛物线转向指标可能会导致仓位因指标点位置频繁变化而过早关闭。


抛物线转向(Parabolic SAR)指标追踪止损

让我们来看看任意一个追踪止损策略的结构图。

通常,追踪止损代码由几个可以从总体结构中分离出来并设计为函数的自给自足的模块组成:

  1. 计算止损水平的模块:计算得到的值传递给追踪止损模块。
  2. 追踪止损模块包括
    1. 过滤模块
    2. 该模块将止损设置为从止损水平计算模块获得的值,
      1. 其中包括用于检查服务器关于符号级别条件的过滤器模块,以及用于调整止损位置的过滤器。
      2. 止损修改模块

用于计算所需止损水平的模块——这里是抛物线转向(Parabolic SAR)指标。其值(通常从第一根K线开始)在每个价格变动时都会被发送到追踪止损(Trailing StopLoss)模块。在这个模块中,通过循环遍历将持仓列表中每个选定持仓的属性转递给过滤模块进行筛选——通常是根据交易品种和magic编号来进行筛选。接下来,如果通过了交易品种或magic编号的筛选,那么所需的止损水平会接受进一步的筛选,以检查是否符合服务器止损级别、跟踪步长、所需止损值相对于其前一个位置的值以及根据持仓盈利点数启动追踪的标准等条件。如果这些筛选条件也都通过了,那么该持仓的止损位就会被修改并设置为一个新的位置。

因此,我们已经有了一个用于计算止损位置的模块——这就是抛物转向(Parabolic SAR)指标。这意味着我们只需要制作一个模块,用于调整由当前交易品种和EA的ID(Magic数字)所选仓位的止损位置。如果magic设置为-1,则图表交易品种上的任何已开仓位都将被追踪。如果指定了magic数字,则只有具有相应magic数字的仓位才会被追踪。追踪止损功能仅在新条形图或新仓位打开时启动。下面我们以一个EA为例进行说明。

在EA中,我们将根据EA设置中指定的参数创建抛物转向(Parabolic SAR)指标。从给定柱形图(默认从第一个)中获取的指标值将被传递给追踪止损函数,该函数将执行所有必要的计算来移动仓位的止损位置。需要考虑交易品种的StopLevel,因为在这个点位距离附近不能设置止损。同时,将检查当前的止损位置,如果它与传递给函数的相同,或者(对于买入仓位)更高,或者(对于卖出仓位)更低,那么则无需移动止损。

所有这些检查将由一个专门用于检查修改止损仓位标准的特殊函数来处理。完成所有必要的检查后,将使用止损修改函数将止损线移动到新的位置。

在\MQL5\Experts\ 文件夹中创建一个名为TrailingBySAR_01.mq5的EA。

在创建新EA文件的第二步中,在新窗口中勾选OnTradeTransaction()事件处理器:


OnTradeTransaction()事件处理器是在新开仓位时触发追踪止损所必需的。

该处理器(handler)在发生交易成交(Trade Transaction)事件 时被调用,该事件也包含新开仓位的情况。

要了解更多关于交易事务和事件的信息,请阅读文章“为莫斯科交易所创建交易机器人从哪里入手?"。

我们向创建的EA文件中添加下面这些输入参数全局变量

//+------------------------------------------------------------------+
//|                                             TrailingBySAR_01.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#define   SAR_DATA_INDEX   1  // bar we get Parabolic SAR data from

//--- input parameters
input ENUM_TIMEFRAMES   InpTimeframeSAR   =  PERIOD_CURRENT;   // Parabolic SAR Timeframe
input double            InpStepSAR        =  0.02;             // Parabolic SAR Step
input double            InpMaximumSAR     =  0.2;              // Parabolic SAR Maximum

//--- global variables
int      ExtHandleSAR =INVALID_HANDLE// Parabolic SAR handle
double   ExtStepSAR   =0;                 // Parabolic SAR step
double   ExtMaximumSAR=0;                 // Parabolic SAR maximum

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---

   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- 

  }
//+------------------------------------------------------------------+
//| TradeTransaction function                                        |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction& trans,
                        const MqlTradeRequest& request,
                        const MqlTradeResult& result)
  {

  }


在EA的OnInit()事件处理器中,为在指标设置中输入的参数设置正确的值创建指标并获取其句柄

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- set the Parabolic SAR parameters within acceptable limits
   ExtStepSAR   =(InpStepSAR<0.0001 ? 0.0001 : InpStepSAR);
   ExtMaximumSAR=(InpMaximumSAR<0.0001 ? 0.0001 : InpMaximumSAR);

//--- if there is an error creating the indicator, display a message in the journal and exit from OnInit with an error
   ExtHandleSAR =iSAR(Symbol(), InpTimeframeSAR, ExtStepSAR, ExtMaximumSAR);
   if(ExtHandleSAR==INVALID_HANDLE)
     {
      PrintFormat("Failed to create iSAR(%s, %s, %.3f, %.2f) handle. Error %d",
                  Symbol(), TimeframeDescription(InpTimeframeSAR), ExtStepSAR, ExtMaximumSAR, GetLastError());
      return(INIT_FAILED);
     }
//--- successful
   return(INIT_SUCCEEDED);
  }

如果指标成功创建,将会为其生成一个句柄,以便后续继续从抛物线转向(Parabolic SAR)指标接收值。如果在创建指标的过程中发生错误,将会在日志中显示包含正在创建的指标数据的错误信息。为了描述指标所基于的时间框架,我们使用一个返回时间框架文本描述的函数:

//+------------------------------------------------------------------+
//| Return timeframe description                                     |
//+------------------------------------------------------------------+
string TimeframeDescription(const ENUM_TIMEFRAMES timeframe)
  {
   return(StringSubstr(EnumToString(timeframe==PERIOD_CURRENT ? Period() : timeframe), 7));
  }

我们从时间框架常量中获取枚举文本的一部分,得到一个仅包含时间框架名称的字符串。
例如,我们从PERIOD_H1常量中获取字符串"PERIOD_H1",但从该字符串中仅返回"H1"部分。

为了检查是否开启了新的K线(时间柱),我们需要将当前K线的开盘时间与之前记录的开盘时间进行比较。如果检查的值不相等,则表示开启了新的K线。
由于这不是一个指标(其中已经有一个预定义的当前交易对象和周期时间序列数组)而是一个EA,因此我们需要创建一个函数来获取K线的开盘时间:

//+------------------------------------------------------------------+
//| Return the bar opening time by timeseries index                  |
//+------------------------------------------------------------------+
datetime TimeOpenBar(const int index)
  {
   datetime array[1];
   ResetLastError();
   if(CopyTime(NULL, PERIOD_CURRENT, index, 1, array)!=1)
     {
      PrintFormat("%s: CopyTime() failed. Error %d", __FUNCTION__, GetLastError());
      return 0;
     }
   return array[0];
  }

将我们想要获取开盘时间的K线的索引传递给函数,将所需数据复制到数组中,并从数组中返回接收到的数据。由于我们只需要一个K线的时间,因此我们定义一个维度为1的数组。

现在,使用这个函数,让我们实现一个返回新K线开启标志的函数:

//+------------------------------------------------------------------+
//| Return new bar opening flag                                      |
//+------------------------------------------------------------------+
bool IsNewBar(void)
  {
   static datetime time_prev=0;
   datetime        bar_open_time=TimeOpenBar(0);
   if(bar_open_time==0)
      return false;
   if(bar_open_time!=time_prev)
     {
      time_prev=bar_open_time;
      return true;
     }
   return false;
  }

通过比较前一个K线(bar)的开盘时间与从TimeOpenBar()函数获得的当前开盘时间:如果这两个值不相等,则记住新的时间以供下次检查,并返回true作为新K线开始的标志。如果发生错误,或者这两个值相等,则返回false——表示没有新K线产生。

为了从抛物线转向(Parabolic SAR)指标接收数据,并将值发送到追踪止损函数,我们将编写一个通过指标句柄接收数据的函数:

//+------------------------------------------------------------------+
//| Return Parabolic SAR data from the specified timeseries index    |
//+------------------------------------------------------------------+
double GetSARData(const int index)
  {
   double array[1];
   ResetLastError();
   if(CopyBuffer(ExtHandleSAR, 0, index, 1, array)!=1)
     {
      PrintFormat("%s: CopyBuffer() failed. Error %d", __FUNCTION__, GetLastError());
      return EMPTY_VALUE;
     }
   return array[0];
  }

这里的情况与获取K线开盘时间的函数完全相同:我们根据传递给函数的索引,在一个维度为1的数组中获取值,如果成功获取,我们就从数组中返回这个值。如果发生错误,返回 EMPTY_VALUE(空值)

为了设置止损(StopLoss)位置,我们需要检查止损价格与当前价格之间的距离是否超过了由StopLevel所设定的限制。如果止损价格比StopLevel所允许的距离更接近当前价格,那么由于“无效止损”错误,将不会设置止损位置。为了避免出现此类错误,我们需要在设置止损位置之前检查这个距离。还有一个FreezeLevel,它表示价格与仓位止损(StopLoss或TakeProfit)之间的距离,在这个距离内不能更改止损位置,因为它们很可能会被触发平仓。但在绝大多数情况下,这些限制不会被用到,因此我们这里不会检查它们。

关于StopLevel有一个细节:如果设置为0,这并不意味着它不存在。这表示我们使用浮动值。通常等于两个点差的值,或者三个点。在这里,我们需要选择这些值,因为它们取决于服务器设置。为此,我们将在获取StopLevel值的函数中设置一个自定义参数。该函数将传递一个乘数 ,在StopLevel设置为零的情况下,通过将该乘数与交易品种的点差相乘,即可得到StopLevel。如果StopLevel不为零,则直接返回该值

//+------------------------------------------------------------------+
//| Return StopLevel value of the current symbol in points           |
//+------------------------------------------------------------------+
int StopLevel(const int spread_multiplier)
  {
   int spread    =(int)SymbolInfoInteger(Symbol(), SYMBOL_SPREAD);
   int stop_level=(int)SymbolInfoInteger(Symbol(), SYMBOL_TRADE_STOPS_LEVEL);
   return(stop_level==0 ? spread * spread_multiplier : stop_level);
  }


实现追踪止损的主函数:

//+------------------------------------------------------------------+
//| Trailing stop function by StopLoss price value                   |
//+------------------------------------------------------------------+
void TrailingStopByValue(const double value_sl, const long magic=-1, const int trailing_step_pt=0, const int trailing_start_pt=0)
  {
//--- price structure
   MqlTick tick={};
//--- in a loop by the total number of open positions
   int total=PositionsTotal();
   for(int i=total-1; i>=0; i--)
     {
      //--- get the ticket of the next position
      ulong  pos_ticket=PositionGetTicket(i);
      if(pos_ticket==0)
         continue;
         
      //--- get the symbol and position magic
      string pos_symbol = PositionGetString(POSITION_SYMBOL);
      long   pos_magic  = PositionGetInteger(POSITION_MAGIC);
      
      //--- skip positions that do not match the filter by symbol and magic number
      if((magic!=-1 && pos_magic!=magic) || pos_symbol!=Symbol())
         continue;
         
      //--- if failed to get the prices, move on
      if(!SymbolInfoTick(Symbol(), tick))
         continue;
      
      //--- get the position type, its opening price and StopLoss level
      ENUM_POSITION_TYPE pos_type=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      double             pos_open=PositionGetDouble(POSITION_PRICE_OPEN);
      double             pos_sl  =PositionGetDouble(POSITION_SL);
      
      //--- if StopLoss modification conditions are suitable, modify the position stop level
      if(CheckCriterion(pos_type, pos_open, pos_sl, value_sl, trailing_step_pt, trailing_start_pt, tick))
         ModifySL(pos_ticket, value_sl);
     }
  }

逻辑很简单:在客户端的已开仓位列表中循环遍历,通过仓单编号(ticket)顺序选择每个仓单,检查仓单的交易品种(symbol)和magic编号是否同仓单筛选条件相匹配,并检查移动止损位的条件是否满足。如果条件满足,则修改止损位置。

用于检查修改止损位置的函数:

//+------------------------------------------------------------------+
//|Check the StopLoss modification criteria and return a flag        |
//+------------------------------------------------------------------+
bool CheckCriterion(ENUM_POSITION_TYPE pos_type, double pos_open, double pos_sl, double value_sl, 
                    int trailing_step_pt, int trailing_start_pt, MqlTick &tick)
  {
//--- if the stop position and the stop level for modification are equal, return 'false'
   if(NormalizeDouble(pos_sl-value_sl, Digits())==0)
      return false;

   double trailing_step = trailing_step_pt * Point(); // convert the trailing step into price
   double stop_level    = StopLevel(2) * Point();     // convert the StopLevel of the symbol into price
   int    pos_profit_pt = 0;                          // position profit in points
   
//--- depending on the type of position, check the conditions for modifying StopLoss
   switch(pos_type)
     {
      //--- long position
      case POSITION_TYPE_BUY :
        pos_profit_pt=int((tick.bid - pos_open) / Point());             // calculate the position profit in points
        if(tick.bid - stop_level > value_sl                             // if the price and the StopLevel level pending from it are higher than the StopLoss level (the distance to StopLevel is observed) 
           && pos_sl + trailing_step < value_sl                         // if the StopLoss level exceeds the trailing step based on the current StopLoss
           && (trailing_start_pt==0 || pos_profit_pt>trailing_start_pt) // if we trail at any profit or position profit in points exceeds the trailing start, return 'true'
          )
           return true;
        break;
        
      //--- short position
      case POSITION_TYPE_SELL :
        pos_profit_pt=int((pos_open - tick.ask) / Point());             // position profit in points
        if(tick.ask + stop_level < value_sl                             // if the price and the StopLevel level pending from it are lower than the StopLoss level (the distance to StopLevel is observed)
           && (pos_sl - trailing_step > value_sl || pos_sl==0)          // if the StopLoss level is below the trailing step based on the current StopLoss or a position has no StopLoss
           && (trailing_start_pt==0 || pos_profit_pt>trailing_start_pt) // if we trail at any profit or position profit in points exceeds the trailing start, return 'true'
          )
           return true;
        break;
        
      //--- return 'false' by default
      default: break;
     }
//--- no matching criteria
   return false;
  }

条件很简单:

  1. 如果止损位和需要移动到的止损位相等,那么就不需要修改——返回false
  2. 如果止损位比StopLevel所允许的距离更接近当前价格,执行将会报错,它不能被修改,返回false
  3. 如果价格在上次修改后还没有移动足够的距离,那么修改还为时过早,因为还没有达到追踪步长,返回false
  4. 如果价格还没有达到指定的点数利润,那么修改还为时过早——返回false

这些简单的规则是任何追踪止损策略的基础。如果满足条件,止损位将被修改。如果不满足,则在下一次调用函数时再次检查条件。

现在,我们来编写一个根据仓单编号来修改止损价格的函数:

//+------------------------------------------------------------------+
//| Modify StopLoss of a position by ticket                          |
//+------------------------------------------------------------------+
bool ModifySL(const ulong ticket, const double stop_loss)
  {
//--- if failed to select a position by ticket, report this in the journal and return 'false'
   ResetLastError();
   if(!PositionSelectByTicket(ticket))
     {
      PrintFormat("%s: Failed to select position by ticket number %I64u. Error %d", __FUNCTION__, ticket, GetLastError());
      return false;
     }
     
//--- declare the structures of the trade request and the request result
   MqlTradeRequest   request={};
   MqlTradeResult    result ={};

//--- fill in the request structure
   request.action    = TRADE_ACTION_SLTP;
   request.symbol    = PositionGetString(POSITION_SYMBOL);
   request.magic     = PositionGetInteger(POSITION_MAGIC);
   request.tp        = PositionGetDouble(POSITION_TP);
   request.position  = ticket;
   request.sl        = NormalizeDouble(stop_loss,(int)SymbolInfoInteger(Symbol(),SYMBOL_DIGITS));
   
//--- if the trade operation could not be sent, report this to the journal and return 'false'
   if(!OrderSend(request, result))
     {
      PrintFormat("%s: OrderSend() failed to modify position #%I64u. Error %d",__FUNCTION__, ticket, GetLastError());
      return false;
     }
     
//--- request to change StopLoss position successfully sent
   return true;
  }

我们根据传递给函数的仓单编号选择一个仓单,填写请求结构中的必要字段,并向服务器发送交易订单。在出现错误的情况下,在日志中显示包含错误代码的消息,并返回false。有关交易操作的更多信息,请参阅MQL5程序语言文档

现在,让我们来实现一个基于抛物线转向(Parabolic SAR)指标值的追踪止损功能:

//+------------------------------------------------------------------+
//| StopLoss trailing function using the Parabolic SAR indicator     |
//+------------------------------------------------------------------+
void TrailingStopBySAR(const long magic=-1, const int trailing_step_pt=0, const int trailing_start_pt=0)
  {
//--- get the Parabolic SAR value from the first bar of the timeseries
   double sar=GetSARData(SAR_DATA_INDEX);
   
//--- if failed to obtain data, leave
   if(sar==EMPTY_VALUE)
      return;
      
//--- call the trailing function with the StopLoss price obtained from Parabolic SAR 
   TrailingStopByValue(sar, magic, trailing_step_pt, trailing_start_pt);
  }

首先获取第一个bar上的指标值。如果没有获取到值,则退出。如果成功接收到来自抛物线转向(Parabolic SAR)指标的值,则按值将其传递给追踪止损函数。

现在,让我们在EA的处理程序中为抛物线转向(Parabolic SAR)指标设置已创建的追踪止损功能。

OnTick()

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- if not a new bar, leave the handler
   if(!IsNewBar())
      return;
      
//--- trail position stops by Parabolic SAR
   TrailingStopBySAR();
  }

每当一个新的柱(bar)形成时,抛物线转向(Parabolic SAR)指标的跟踪止损功能都会被调用(使用默认值)——所有在该交易品种上开立的头寸都将被跟踪,无论它们的magic编码是什么。从头寸开立的那一刻起,它们的止损位置将根据指标值精确地进行跟踪,而不会采用任何跟踪步长,也不会考虑头寸在点数上的盈利。

OnTradeTransaction()

//+------------------------------------------------------------------+
//| TradeTransaction function                                        |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction& trans,
                        const MqlTradeRequest& request,
                        const MqlTradeResult& result)
  {
   if(trans.type==TRADE_TRANSACTION_DEAL_ADD)
      TrailingStopBySAR();
  }

为了确保在头寸开立时追踪止损功能能够正常工作,我们在处理器中调用了追踪函数,但前提是客户端的交易列表中已经添加了一笔新的仓单。如果没有这个处理器,追踪止损仅仅当新的柱形到来时才被激活。

我们已经创建了一个基本的追踪止损函数,并基于抛物线转向(Parabolic SAR)指标开发了一个追踪止损EA。我们可以编译这个EA,并在图表上运行它,以开立头寸并根据抛物线转向指标的值来管理追踪止损。EA文件已附在下方。


使用CTrade标准库

MQL5 标准库旨在让普通用户更容易地开发程序。该库提供了便捷的途径来访问MQL5的内部函数。

借此机会,让我们用CTrade类的PositionModify()方法来替换原来的止损位修改函数。

让我们来看一下仓单修改方法的内容:

//+------------------------------------------------------------------+
//| Modify specified opened position                                 |
//+------------------------------------------------------------------+
bool CTrade::PositionModify(const ulong ticket,const double sl,const double tp)
  {
//--- check stopped
   if(IsStopped(__FUNCTION__))
      return(false);
//--- check position existence
   if(!PositionSelectByTicket(ticket))
      return(false);
//--- clean
   ClearStructures();
//--- setting request
   m_request.action  =TRADE_ACTION_SLTP;
   m_request.position=ticket;
   m_request.symbol  =PositionGetString(POSITION_SYMBOL);
   m_request.magic   =m_magic;
   m_request.sl      =sl;
   m_request.tp      =tp;
//--- action and return the result
   return(OrderSend(m_request,m_result));
  }

并将其与上述EA中编写的止损位修改函数进行比较:

//+------------------------------------------------------------------+
//| Modify StopLoss of a position by ticket                          |
//+------------------------------------------------------------------+
bool ModifySL(const ulong ticket, const double stop_loss)
  {
//--- if failed to select a position by ticket, report this in the journal and return 'false'
   ResetLastError();
   if(!PositionSelectByTicket(ticket))
     {
      PrintFormat("%s: Failed to select position by ticket number %I64u. Error %d", __FUNCTION__, ticket, GetLastError());
      return false;
     }
     
//--- declare the structures of the trade request and the request result
   MqlTradeRequest   request={};
   MqlTradeResult    result ={};
   
//--- fill in the request structure
   request.action    = TRADE_ACTION_SLTP;
   request.symbol    = PositionGetString(POSITION_SYMBOL);
   request.magic     = PositionGetInteger(POSITION_MAGIC);
   request.tp        = PositionGetDouble(POSITION_TP);
   request.position  = ticket;
   request.sl        = NormalizeDouble(stop_loss,(int)SymbolInfoInteger(Symbol(),SYMBOL_DIGITS));
   
//--- if the trade operation could not be sent, report this to the journal and return 'false'
   if(!OrderSend(request, result))
     {
      PrintFormat("%s: OrderSend() failed to modify position #%I64u. Error %d",__FUNCTION__, ticket, GetLastError());
      return false;
     }
     
//--- request to change StopLoss position successfully sent
   return true;
  }

并没有特别的差异。在交易类方法中,最开始有一个检查,用于判断是否要从图表中移除EA。函数中没有这样的检查。
我们没有使用IsStopped()这个标准函数,而是使用了具有形式参数的同名交易类:

//+------------------------------------------------------------------+
//| Checks forced shutdown of MQL5-program                           |
//+------------------------------------------------------------------+
bool CTrade::IsStopped(const string function)
  {
   if(!::IsStopped())
      return(false);
//--- MQL5 program is stopped
   PrintFormat("%s: MQL5 program is stopped. Trading is disabled",function);
   m_result.retcode=TRADE_RETCODE_CLIENT_DISABLES_AT;
   return(true);
  }

在这里,如果IsStopped()返回true,那么首先会在日志中显示一条消息,表明EA已从图表中移除,然后返回EA的停止标志。

交易函数和类方法之间其余的差异并不显著——它们本质上是相同的功能,只是实现方式不同。在类方法中,类头文件中声明的结构体被清空,而在函数中,这些结构体是局部的,并在函数调用时被声明,同时所有字段都被初始化为零。在函数中,如果发送交易请求时出现错误,会立即显示一个包含错误代码的消息。之后,函数会返回结果,而在交易类的方法中,只是简单地返回了OrderSend()函数的调用结果。

让我们对已实现的EA进行修改,并将其保存为新名称TrailingBySAR_02.mq5。我们要能够在策略测试器中测试基于抛物线转向(Parabolic SAR)指标值的开仓操作,并根据该值移动仓单的止损位置。

将交易类包含到EA中在输入参数中声明EA的magic编码并且在全局变量区域声明交易类的实例。此外,在OnInit()处理程序中,将输入参数中的magic编码分配给交易类对象

//+------------------------------------------------------------------+
//|                                             TrailingBySAR_02.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com" 
#property version   "1.00"

#define   SAR_DATA_INDEX   1  // bar we get Parabolic SAR data from

#include <Trade\Trade.mqh>    // replace trading functions with Standard Library methods

//--- input parameters
input ENUM_TIMEFRAMES   InpTimeframeSAR   =  PERIOD_CURRENT;   // Parabolic SAR Timeframe
input double            InpStepSAR        =  0.02;             // Parabolic SAR Step
input double            InpMaximumSAR     =  0.2;              // Parabolic SAR Maximum
input ulong             InpMagic          =  123;              // Magic Number

//--- global variables
int      ExtHandleSAR=INVALID_HANDLE;  // Parabolic SAR handle
double   ExtStepSAR=0;                 // Parabolic SAR step
double   ExtMaximumSAR=0;              // Parabolic SAR maximum
CTrade   ExtTrade;                     // trading operations class instance

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- set the magic number to the trading class object
   ExtTrade.SetExpertMagicNumber(InpMagic);

//--- set the Parabolic SAR parameters within acceptable limits
   ExtStepSAR   =(InpStepSAR<0.0001 ? 0.0001 : InpStepSAR);
   ExtMaximumSAR=(InpMaximumSAR<0.0001 ? 0.0001 : InpMaximumSAR);
   
//--- if there is an error creating the indicator, display a message in the journal and exit from OnInit with an error
   ExtHandleSAR =iSAR(Symbol(), InpTimeframeSAR, ExtStepSAR, ExtMaximumSAR);
   if(ExtHandleSAR==INVALID_HANDLE)
     {
      PrintFormat("Failed to create iSAR(%s, %s, %.3f, %.2f) handle. Error %d",
                  Symbol(), TimeframeDescription(InpTimeframeSAR), ExtStepSAR, ExtMaximumSAR, GetLastError());
      return(INIT_FAILED);
     }   
//--- successful
   return(INIT_SUCCEEDED);
  }


在EA的OnTick()处理程序中,添加根据抛物线转向(Parabolic SAR)指标在策略测试器中开仓的代码。同时,添加代码,将EA的magic编码传递到追踪止损函数中

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- if not a new bar, leave the handler
   if(!IsNewBar())
      return;
      
   if(MQLInfoInteger(MQL_TESTER))
     {
      //--- get Parabolic SAR data from bars 1 and 2
      double sar1=GetSARData(SAR_DATA_INDEX);
      double sar2=GetSARData(SAR_DATA_INDEX+1);
      
      //--- if the price structure is filled out and Parabolic SAR data is obtained
      MqlTick tick={};
      if(SymbolInfoTick(Symbol(), tick) && sar1!=EMPTY_VALUE && sar2!=EMPTY_VALUE)
        {
         //--- if Parabolic SAR on bar 1 is below Bid price, while on bar 2 it is above Bid price, open a long position
         if(sar1<tick.bid && sar2>tick.bid)
            ExtTrade.Buy(0.1);
         //--- if Parabolic SAR on bar 1 is above Ask, while on bar 2 it is below Ask price, open a long position
         if(sar1>tick.ask && sar2<tick.ask)
            ExtTrade.Sell(0.1);
        }
     }
      
//--- trail position stops by Parabolic SAR
   TrailingStopBySAR(InpMagic);
  }


OnTradeTransaction()处理程序中,完成向追踪止损函数传递magic编码的操作:</s1

//+------------------------------------------------------------------+
//| TradeTransaction function                                        |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction& trans,
                        const MqlTradeRequest& request,
                        const MqlTradeResult& result)
  {
   if(trans.type==TRADE_TRANSACTION_DEAL_ADD)
      TrailingStopBySAR(InpMagic);
  }

使用EA中的magic编号在策略测试器中开仓并追踪其止损,将允许您通过magic编号来检查过滤器的操作。

在通用的追踪止损函数中,调用修改函数的操作替换为调用交易类的方法

//+------------------------------------------------------------------+
//| Universal trailing stop function by StopLoss price value         |
//+------------------------------------------------------------------+
void TrailingStopByValue(const double value_sl, const long magic=-1, const int trailing_step_pt=0, const int trailing_start_pt=0)
  {
//--- price structure
   MqlTick tick={};
//--- in a loop by the total number of open positions
   int total=PositionsTotal();
   for(int i=total-1; i>=0; i--)
     {
      //--- get the ticket of the next position
      ulong  pos_ticket=PositionGetTicket(i);
      if(pos_ticket==0)
         continue;
         
      //--- get the symbol and position magic
      string pos_symbol = PositionGetString(POSITION_SYMBOL);
      long   pos_magic  = PositionGetInteger(POSITION_MAGIC);
      
      //--- skip positions that do not match the filter by symbol and magic number
      if((magic!=-1 && pos_magic!=magic) || pos_symbol!=Symbol())
         continue;
         
      //--- if failed to get the prices, move on
      if(!SymbolInfoTick(Symbol(), tick))
         continue;
         
      //--- get the position type, its opening price and StopLoss level
      ENUM_POSITION_TYPE pos_type=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      double             pos_open=PositionGetDouble(POSITION_PRICE_OPEN);
      double             pos_sl  =PositionGetDouble(POSITION_SL);
      
      //--- if StopLoss modification conditions are suitable, modify the position stop level
      if(CheckCriterion(pos_type, pos_open, pos_sl, value_sl, trailing_step_pt, trailing_start_pt, tick))
         ExtTrade.PositionModify(pos_ticket, value_sl, PositionGetDouble(POSITION_TP));
     }
  }

现在为了修改止损,我们调用的不是之前实现的函数,而是CTrade交易类的PositionModify()方法。这些调用之间的区别在于一个参数。如果函数被编写为仅修改仓单的止损价格,那么在指定参数时,需要将正在修改的仓单的ticket值及其新的止损价传递给函数。现在,需要向交易类方法传递三个参数——除了仓单ticket和止损价之外,还需要指定止盈价。由于仓单已经被选中,并且其止盈价不需要更改,我们直接将选中的持仓属性中的止盈价(不变)传递给该方法。

之前实现的ModifySL()函数现在已从EA代码中移除。

让我们编译EA,并在策略测试器中以“Every tick”模式在任一交易品种和任何图表时间框架上运行它:


如我们所见,程序根据第一个柱子(bar)的抛物线转向(Parabolic SAR)指标值进行了正确的追踪止损。

EA文件已附在下方。


EA中的现成追踪止损(Trailing Stop),“几行代码即可实现”

然而,尽管在EA中创建和使用追踪止损功能很容易,但我们并不希望每次在新建的EA中都要写下所有必要的函数,而是希望能够以“即插即用”的方式来完成所有操作。
这是可以实现的。为了做到这一点,我们只需要编写一次包含文件,然后简单地将它连接到所需的EA中,并在需要调用追踪止损函数的地方编写相应的代码。

让我们将所有关于追踪止损的函数移到一个新的文件中。在包含EA的文件夹中创建新的TrailingsFunc.mqh包含文件。建议将所有此类文件保存在所有包含文件的公共文件夹\MQL5Include中,或者保存在该目录下的子文件夹中。然而,对于本次测试,直接在含有EA的文件夹中创建文件,并从那里包含它就已经足够了。

在编辑器中按下Ctrl+N,并选择一个新的包含文件:


在接下来的向导窗口,输入TrailingFunc文件的名称。
默认情况下,输入文件名的字符串已经包含了包含文件的根目录—Include。换句话说,文件将在此文件夹下被创建。
然后我们可以简单地将它移动到包含EA的目标文件夹中,或者在文件名字符串中手动输入目标文件夹的路径(将Include\替换为Experts\,然后输入包含测试EA的文件夹的路径(如果有的话),最后是创建的TrailingFunc文件):


点击结束并创建一个空文件:

//+------------------------------------------------------------------+
//|                                                TrailingsFunc.mqh |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
//+------------------------------------------------------------------+
//| defines                                                          |
//+------------------------------------------------------------------+
// #define MacrosHello   "Hello, world!"
// #define MacrosYear    2010
//+------------------------------------------------------------------+
//| DLL imports                                                      |
//+------------------------------------------------------------------+
// #import "user32.dll"
//   int      SendMessageA(int hWnd,int Msg,int wParam,int lParam);
// #import "my_expert.dll"
//   int      ExpertRecalculate(int wParam,int lParam);
// #import
//+------------------------------------------------------------------+
//| EX5 imports                                                      |
//+------------------------------------------------------------------+
// #import "stdlib.ex5"
//   string ErrorDescription(int error_code);
// #import
//+------------------------------------------------------------------+


现在我们需要将之前在所有测试EA中创建的函数都转移到这里:

//+------------------------------------------------------------------+
//|                                                TrailingsFunc.mqh |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
//+------------------------------------------------------------------+
//| Simple trailing by value                                         |
//+------------------------------------------------------------------+
void SimpleTrailingByValue(const double value_sl, const long magic=-1, 
                           const int trailing_step_pt=0, const int trailing_start_pt=0, const int trailing_offset_pt=0)
  {
//--- price structure
   MqlTick tick={};
   
//--- in a loop by the total number of open positions
   int total=PositionsTotal();
   for(int i=total-1; i>=0; i--)
     {
      //--- get the ticket of the next position
      ulong  pos_ticket=PositionGetTicket(i);
      if(pos_ticket==0)
         continue;
         
      //--- get the symbol and position magic
      string pos_symbol = PositionGetString(POSITION_SYMBOL);
      long   pos_magic  = PositionGetInteger(POSITION_MAGIC);
      
      //--- skip positions that do not match the filter by symbol and magic number
      if((magic!=-1 && pos_magic!=magic) || pos_symbol!=Symbol())
         continue;
         
      //--- if failed to get the prices, move on
      if(!SymbolInfoTick(Symbol(), tick))
         continue;
         
      //--- get the position type, its opening price and StopLoss level
      ENUM_POSITION_TYPE pos_type=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      double             pos_open=PositionGetDouble(POSITION_PRICE_OPEN);
      double             pos_sl  =PositionGetDouble(POSITION_SL);
      
      //--- if StopLoss modification conditions are suitable, modify the position stop level
      if(CheckCriterion(pos_type, pos_open, pos_sl, value_sl, trailing_step_pt, trailing_start_pt, tick))
         ModifySL(pos_ticket, value_sl);
     }
  }
//+------------------------------------------------------------------+
//|Check the StopLoss modification criteria and return a flag        |
//+------------------------------------------------------------------+
bool CheckCriterion(ENUM_POSITION_TYPE pos_type, double pos_open, double pos_sl, double value_sl, 
                    int trailing_step_pt, int trailing_start_pt, MqlTick &tick)
  {
//--- if the stop position and the stop level for modification are equal, return 'false'
   if(NormalizeDouble(pos_sl-value_sl, Digits())==0)
      return false;

   double trailing_step = trailing_step_pt * Point(); // convert the trailing step into price
   double stop_level    = StopLevel(2) * Point();     // convert the StopLevel of the symbol into price
   int    pos_profit_pt = 0;                          // position profit in points
   
//--- depending on the type of position, check the conditions for modifying StopLoss
   switch(pos_type)
     {
      //--- long position
      case POSITION_TYPE_BUY :
        pos_profit_pt=int((tick.bid - pos_open) / Point());             // calculate the position profit in points
        if(tick.bid - stop_level > value_sl                             // if the price and the StopLevel level pending from it are higher than the StopLoss level (the distance to StopLevel is observed) 
           && pos_sl + trailing_step < value_sl                         // if the StopLoss level exceeds the trailing step based on the current StopLoss
           && (trailing_start_pt==0 || pos_profit_pt>trailing_start_pt) // if we trail at any profit or position profit in points exceeds the trailing start, return 'true'
          )
           return true;
        break;
        
      //--- short position
      case POSITION_TYPE_SELL :
        pos_profit_pt=int((pos_open - tick.ask) / Point());             // position profit in points
        if(tick.ask + stop_level < value_sl                             // if the price and the StopLevel level pending from it are lower than the StopLoss level (the distance to StopLevel is observed)
           && (pos_sl - trailing_step > value_sl || pos_sl==0)          // if the StopLoss level is below the trailing step based on the current StopLoss or a position has no StopLoss
           && (trailing_start_pt==0 || pos_profit_pt>trailing_start_pt) // if we trail at any profit or position profit in points exceeds the trailing start, return 'true'
          )
           return true;
        break;
        
      //--- return 'false' by default
      default: break;
     }
//--- no matching criteria
   return false;
  }
//+------------------------------------------------------------------+
//| Modify StopLoss of a position by ticket                          |
//+------------------------------------------------------------------+
bool ModifySL(const ulong ticket, const double stop_loss)
  {
//--- if failed to select a position by ticket, report this in the journal and return 'false'
   ResetLastError();
   if(!PositionSelectByTicket(ticket))
     {
      PrintFormat("%s: Failed to select position by ticket number %I64u. Error %d", __FUNCTION__, ticket, GetLastError());
      return false;
     }
     
//--- declare the structures of the trade request and the request result
   MqlTradeRequest    request={};
   MqlTradeResult     result ={};
   
//--- fill in the request structure
   request.action    = TRADE_ACTION_SLTP;
   request.symbol    = PositionGetString(POSITION_SYMBOL);
   request.magic     = PositionGetInteger(POSITION_MAGIC);
   request.tp        = PositionGetDouble(POSITION_TP);
   request.position  = ticket;
   request.sl        = NormalizeDouble(stop_loss,(int)SymbolInfoInteger(request.symbol,SYMBOL_DIGITS));
   
//--- if the trade operation could not be sent, report this to the journal and return 'false'
   if(!OrderSend(request, result))
     {
      PrintFormat("%s: OrderSend() failed to modify position #%I64u. Error %d",__FUNCTION__, ticket, GetLastError());
      return false;
     }
     
//--- request to change StopLoss position successfully sent
   return true;
  }
//+------------------------------------------------------------------+
//| Return StopLevel in points                                       |
//+------------------------------------------------------------------+
int StopLevel(const int spread_multiplier)
  {
   int spread    =(int)SymbolInfoInteger(Symbol(), SYMBOL_SPREAD);
   int stop_level=(int)SymbolInfoInteger(Symbol(), SYMBOL_TRADE_STOPS_LEVEL);
   return(stop_level==0 ? spread * spread_multiplier : stop_level);
  }
//+------------------------------------------------------------------+
//| Return timeframe description                                     |
//+------------------------------------------------------------------+
string TimeframeDescription(const ENUM_TIMEFRAMES timeframe)
  {
   return(StringSubstr(EnumToString(timeframe==PERIOD_CURRENT ? Period() : timeframe), 7));
  }
//+------------------------------------------------------------------+
//| Return new bar opening flag                                      |
//+------------------------------------------------------------------+
bool IsNewBar(void)
  {
   static datetime time_prev=0;
   datetime        bar_open_time=TimeOpenBar(0);
   if(bar_open_time==0)
      return false;
   if(bar_open_time!=time_prev)
     {
      time_prev=bar_open_time;
      return true;
     }
   return false;
  }
//+------------------------------------------------------------------+
//| Return the bar opening time by timeseries index                  |
//+------------------------------------------------------------------+
datetime TimeOpenBar(const int index)
  {
   datetime array[1];
   ResetLastError();
   if(CopyTime(NULL, PERIOD_CURRENT, index, 1, array)!=1)
     {
      PrintFormat("%s: CopyTime() failed. Error %d", __FUNCTION__, GetLastError());
      return 0;
     }
   return array[0];
  }

在EA中实现了从抛物线转向(Parabolic SAR)指标接收数据的函数。这个函数中的数据是通过指定的指标句柄获得的。这意味着不仅抛物线转向指标可以作为这样的指标,任何适合用作设置止损(StopLoss)价位的其他指标也可以。

因此,在这里,这个函数被重命名为一个通过句柄从指标接收数据的通用函数:

//+------------------------------------------------------------------+
//| Return indicator data by handle                                  |
//| from the specified timeseries index                              |
//+------------------------------------------------------------------+
double GetIndData(const int handle_ind, const int index)
  {
   double array[1];
   ResetLastError();
   if(CopyBuffer(handle_ind, 0, index, 1, array)!=1)
     {
      PrintFormat("%s: CopyBuffer() failed. Error %d", __FUNCTION__, GetLastError());
      return EMPTY_VALUE;
     }
   return array[0];
  }

因此,现在有一个基于从上述函数接收的指标数据的追踪函数:

//+------------------------------------------------------------------+
//| Trailing by indicator data specified by handle                   |
//+------------------------------------------------------------------+
void TrailingByDataInd(const int handle_ind, const int index=1, const long magic=-1, 
                       const int trailing_step_pt=0, const int trailing_start_pt=0, const int trailing_offset_pt=0)
  {
//--- get the Parabolic SAR value from the specified timeseries index
   double data=GetIndData(handle_ind, index);
   
//--- if failed to obtain data, leave
   if(data==EMPTY_VALUE)
      return;
      
//--- call the simple trailing function with the StopLoss price obtained from Parabolic SAR 
   SimpleTrailingByValue(data, magic, trailing_step_pt, trailing_start_pt, trailing_offset_pt);
  }

在函数中,我们首先通过指定的句柄从指定的柱状图索引中获取指标数据,然后按值调用跟踪函数,并把指标接收到的止损值传递给它。

因此,我们可以使用任何适合的指标来跟踪持仓仓单的止损位。

为了在EA代码中不包含创建抛物线转向(Parabolic SAR)指标的代码,请在文件中设置以下函数:

//+------------------------------------------------------------------+
//| Create and return the Parabolic SAR handle                       |
//+------------------------------------------------------------------+
int CreateSAR(const string symbol_name, const ENUM_TIMEFRAMES timeframe, const double step_sar=0.02, const double max_sar=0.2)
  {
//--- set the indicator parameters within acceptable limits
   double step=(step_sar<0.0001 ? 0.0001 : step_sar);
   double max =(max_sar <0.0001 ? 0.0001 : max_sar);

//--- adjust the symbol and timeframe values
   ENUM_TIMEFRAMES period=(timeframe==PERIOD_CURRENT ? Period() : timeframe);
   string          symbol=(symbol_name==NULL || symbol_name=="" ? Symbol() : symbol_name);
 
//--- create indicator handle
   ResetLastError();
   int handle=iSAR(symbol, period, step, max);
   
//--- if there is an error creating the indicator, display an error message in the journal
   if(handle==INVALID_HANDLE)
     {
      PrintFormat("Failed to create iSAR(%s, %s, %.3f, %.2f) handle. Error %d",
                  symbol, TimeframeDescription(period), step, max, GetLastError());
     } 
//--- return the result of creating the indicator handle
   return handle;
  }

本质上,CreateSAR() 函数是从 TrailingBySAR_01.mq5 测试EA的 OnInit() 处理程序中移出的代码。这种方法将允许我们简单地调用这个函数,而无需在EA中为指标编写输入变量修正字符串,并创建其句柄。

在代码的后续部分,有用于创建各种移动平均线的类似函数,例如,用于创建自适应移动平均线的函数:

//+------------------------------------------------------------------+
//| Create and return Adaptive Moving Average handle                 |
//+------------------------------------------------------------------+
int CreateAMA(const string symbol_name, const ENUM_TIMEFRAMES timeframe,
              const int ama_period=9, const int fast_ema_period=2, const int slow_ema_period=30, const int shift=0, const ENUM_APPLIED_PRICE price=PRICE_CLOSE)
  {
//--- set the indicator parameters within acceptable limits
   int ma_period=(ama_period<1 ? 9 : ama_period);
   int fast_ema=(fast_ema_period<1 ? 2 : fast_ema_period);
   int slow_ema=(slow_ema_period<1 ? 30 : slow_ema_period);

//--- adjust the symbol and timeframe values
   ENUM_TIMEFRAMES period=(timeframe==PERIOD_CURRENT ? Period() : timeframe);
   string          symbol=(symbol_name==NULL || symbol_name=="" ? Symbol() : symbol_name);
 
//--- create indicator handle
   ::ResetLastError();
   int handle=::iAMA(symbol, period, ma_period, fast_ema, slow_ema, shift, price);
   
//--- if there is an error creating the indicator, display an error message in the journal
   if(handle==INVALID_HANDLE)
     {
      ::PrintFormat("Failed to create iAMA(%s, %s, %d, %d, %d, %s) handle. Error %d",
                    symbol, TimeframeDescription(period), ma_period, fast_ema, slow_ema,
                    ::StringSubstr(::EnumToString(price),6), ::GetLastError());
     }
//--- return the result of creating the indicator handle
   return handle;
  }

所有其他函数都与上面展示的函数类似。在这里没有必要赘述了。你可以在下面的附件TrailingsFunc.mqh中找到它们。

这些函数旨在快速创建移动平均线,以便在你自己创建各种类型的跟踪策略时,可以使用它们的值来代替抛物线转向(Parabolic SAR)指标的值。


为了测试已实现的函数,请创建一个测试EA,命名为TrailingBySAR_03.mq5,并将新创建的TrailingsFunc.mqh文件包含到其中
在全局区域中,声明一个变量用于存储已创建指标的句柄,并在OnInit() 处理程序中将创建抛物线转向(ParabolicSAR)指标的结果赋值给该变量

//+------------------------------------------------------------------+
//|                                             TrailingBySAR_03.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#define   SAR_DATA_INDEX   1  // bar we get Parabolic SAR data from

#include "TrailingsFunc.mqh"

//--- input parameters
input ENUM_TIMEFRAMES   InpTimeframeSAR   =  PERIOD_CURRENT;   // Parabolic SAR Timeframe
input double            InpStepSAR        =  0.02;             // Parabolic SAR Step
input double            InpMaximumSAR     =  0.2;              // Parabolic SAR Maximum
input long              InpMagic          =  123;              // Expert Magic Number

//--- global variables
int   ExtHandleSAR=INVALID_HANDLE;  // Parabolic SAR handle

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- create Parabolic SAR handle
   ExtHandleSAR=CreateSAR(Symbol(), InpTimeframeSAR, InpStepSAR, InpMaximumSAR);
   
//--- if there is an error creating the indicator, exit OnInit with an error
   if(ExtHandleSAR==INVALID_HANDLE)
      return(INIT_FAILED);

//--- successful
   return(INIT_SUCCEEDED);
  }


现在,我们只需要在OnTick()OnTradeTransaction()处理程序中,根据已创建指标的数据重新启动追踪,并将EA设置中指定的仓单的magic编号传递给跟踪函数:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- if not a new bar, leave the handler
   if(!IsNewBar())
      return;
      
//--- trail position stops by Parabolic SAR
   TrailingByDataInd(ExtHandleSAR, SAR_DATA_INDEX, InpMagic);
  }
//+------------------------------------------------------------------+
//| TradeTransaction function                                        |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction& trans,
                        const MqlTradeRequest& request,
                        const MqlTradeResult& result)
  {
   if(trans.type==TRADE_TRANSACTION_DEAL_ADD)
      TrailingByDataInd(ExtHandleSAR, SAR_DATA_INDEX, InpMagic);
  }

创建的EA是一个基于抛物线转向(Parabolic SAR)指标值的跟踪止损EA。它将在启用了此EA的交易品种上追踪特定仓单的止损位置,这些仓单的magic编号与EA设置中设置的编号一致。


将追踪止损加入EA

最后,让我们将基于抛物线转向(Parabolic SAR)指标的追踪止损功能添加到位于\MQL5\Experts\Advisors\目录下的标准ExpertMACD.mq5 EA中。

将其保存为ExpertMACDPSAR.mq5,并进行修改以包含追踪止损功能。

在全局变量区域中,包含跟踪函数文件添加跟踪输入参数,并声明一个变量来存储由抛物线转向(Parabolic SAR)指标创建的句柄:

//+------------------------------------------------------------------+
//|                                                   ExpertMACD.mq5 |
//|                             Copyright 2000-2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2000-2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Include                                                          |
//+------------------------------------------------------------------+
#include <Expert\Expert.mqh>
#include <Expert\Signal\SignalMACD.mqh>
#include <Expert\Trailing\TrailingNone.mqh>
#include <Expert\Money\MoneyNone.mqh>

#include "TrailingsFunc.mqh"

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
//--- inputs for expert
input group  " - ExpertMACD Parameters -"
input string Inp_Expert_Title            ="ExpertMACD";
int          Expert_MagicNumber          =10981;
bool         Expert_EveryTick            =false;
//--- inputs for signal
input int    Inp_Signal_MACD_PeriodFast  =12;
input int    Inp_Signal_MACD_PeriodSlow  =24;
input int    Inp_Signal_MACD_PeriodSignal=9;
input int    Inp_Signal_MACD_TakeProfit  =50;
input int    Inp_Signal_MACD_StopLoss    =20;

//--- inputs for trail
input group  " - PSAR Trailing Parameters -"
input bool   InpUseTrail       =  true;      // Trailing is Enabled
input double InpSARStep        =  0.02;      // Trailing SAR Step
input double InpSARMaximum     =  0.2;       // Trailing SAR Maximum
input int    InpTrailingStart  =  0;         // Trailing start
input int    InpTrailingStep   =  0;         // Trailing step in points
input int    InpTrailingOffset =  0;         // Trailing offset in points
//+------------------------------------------------------------------+
//| Global expert object                                             |
//+------------------------------------------------------------------+
CExpert ExtExpert;
int     ExtHandleSAR;
//+------------------------------------------------------------------+
//| Initialization function of the expert                            |
//+------------------------------------------------------------------+


OnInit() 处理程序中,创建一个指标并将其句柄写入变量

//+------------------------------------------------------------------+
//| Initialization function of the expert                            |
//+------------------------------------------------------------------+
int OnInit(void)
  {
//--- Initializing trail
   if(InpUseTrail)
     {
      ExtHandleSAR=CreateSAR(NULL,PERIOD_CURRENT,InpSARStep,InpSARMaximum);
      if(ExtHandleSAR==INVALID_HANDLE)
         return(INIT_FAILED);
     }
   
//--- Initializing expert
//...
//...


在OnDeinit() 处理程序中,删除句柄并释放指标占用的空间

//+------------------------------------------------------------------+
//| Deinitialization function of the expert advisor                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ExtExpert.Deinit();
   IndicatorRelease(ExtHandleSAR);
  }


OnTick()OnTrade()处理程序中,根据抛物线转向(Parabolic SAR)指标启动跟踪

//+------------------------------------------------------------------+
//| Function-event handler "tick"                                    |
//+------------------------------------------------------------------+
void OnTick(void)
  {
   ExtExpert.OnTick();
   TrailingByDataInd(ExtHandleSAR, 1, Expert_MagicNumber, InpTrailingStep, InpTrailingStart, InpTrailingOffset);
  }
//+------------------------------------------------------------------+
//| Function-event handler "trade"                                   |
//+------------------------------------------------------------------+
void OnTrade(void)
  {
   ExtExpert.OnTrade();
   TrailingByDataInd(ExtHandleSAR, 1, Expert_MagicNumber, InpTrailingStep, InpTrailingStart, InpTrailingOffset);
  }

这就是所有需要被添加到EA文件中的,用于加入功能完备的基于抛物线转向(Parabolic SAR)指标的跟踪功能。

在这里,在同一个EA中,我们可以为另一个指标(例如移动平均线)创建一个句柄,并将其句柄传递给跟踪函数。在这种情况下,我们得到的跟踪是基于移动平均线的。我们可以将从不同指标获得的值结合起来,并将计算得到的总值发送给跟踪函数,或者,在不同的市场情况下,使用不同的算法来跟踪止损,并将所需计算的仓单止损值传递给跟踪函数。这里尚有很大的实验空间。

重要的是要记住,TrailingFunc.mqh文件中提供的函数允许创建

  1. 基于各种算法或指标值的自定义非交易(non-trading)跟踪EA,
  2. 并向现有的用于交易的EA中添加各种追踪止损功能,这些EA可以根据指标值或它们自己的算法来工作。

但是也存在一些限制:我们无法同时为多头和空头仓单向跟踪函数传递不同的值,也无法跟踪在另一个交易品种上的仓单。在加入追踪止损功能时也会遇到一些不便之处,即需要在EA中创建指标,并将其句柄存储在变量中。我们可以通过创建跟踪类来摆脱上述所有问题以及其他一些问题。我将在下一篇文章中对此进行考虑。

让我们在测试器中使用设定的参数对创建的EA进行一次测试。

选择下面的设置:

  • 交易品种: EURUSD,
  • 时间周期:H15,
  • 对过去一年的数据以每个tick模式进行测试,且不考虑执行延迟。

输入设置


关闭追踪止损功能并在特定时间段进行测试后,我们得到以下统计数据:



现在我们加载EA,在设置中开启追踪止损模式:


显而易见在开启了追踪止损后,图表变得更加的平滑。我邀请读者们自己尝试测试各种不同的追踪止损方式。

所有文件都已随附于文末,供独立学习和测试使用。


结论

我们已经学会了如何快速创建并将追踪止损添加到EA中。要将追踪止损添加到任何EA中,我们只需要做以下几步:

  1. 将TrailingsFunc.mqh包含文件放到EA文件夹中,
  2. 在EA文件中使用#include "TrailingsFunc.mqh"命令包含该文件,
  3. 在EA的OnInit()函数中添加创建抛物线转向(ParabolicSAR)指标:ExtHandleSAR=CreateSAR(NULL,PERIOD_CURRENT);
  4. 在EA的OnTick()以及(如果需要的话)OnTrade()或OnTradeTransaction()事件处理程序中调用追踪止损程序:TrailingByDataInd(ExtHandleSAR);
  5. 在EA的OnDeinit()函数中使用IndicatorRelease(ExtHandleSAR)释放抛物线转向指标的运算部分。


现在,这个EA将内置一个功能完备的追踪止损策略来管理其仓单。我们不仅可以使用抛物线转向指标来进行跟踪,还可以使用任何其他指标。我们还可以创建自定义算法来计算止损位。
我们可以在一个EA中使用具有不同参数的多个追踪止损功能,并根据市场情况在它们之间进行切换。在下一篇文章中,我将介绍追踪类,这些类可以克服简单函数的缺点。



本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/14782

附加的文件 |
ExpertMACDPSAR.mq5 (14.04 KB)
数据处理的分组方法:在MQL5中实现组合算法 数据处理的分组方法:在MQL5中实现组合算法
在本文中,我们将继续探索数据处理家族分组算法,在MQL5中实现组合算法(Combinatorial Algorithm)及其优化版本——组合选择算法(Combinatorial Selective Algorithm)。
如何构建和优化基于波动率的交易系统(Chaikin volatility-CHV) 如何构建和优化基于波动率的交易系统(Chaikin volatility-CHV)
在本文中,我们将介绍另一个基于波动率的指标——蔡金波动率(Chaikin Volatility)。在了解到蔡金波动率的使用方法和构建方式之后,我们将学习如何构建自定义指标。我们将分享一些可用的简单策略,并对其进行测试,以了解哪个策略更优。
神经网络变得简单(第 75 部分):提升轨迹预测模型的性能 神经网络变得简单(第 75 部分):提升轨迹预测模型的性能
我们创建的模型变得越来越大,越来越复杂。这不光提高了它们的训练成本,还有操作成本。不过,做出决定所需的时间往往很关键。有关于此,我们来研究在不损失品质的情况下优化模型性能的方法。
开发回放系统(第 44 部分):Chart Trader 项目(三) 开发回放系统(第 44 部分):Chart Trader 项目(三)
在上一篇文章中,我介绍了如何操作模板数据以便在 OBJ_CHART 中使用。在那篇文章中,我只是概述了这一主题,并没有深入探讨细节,因为在那个版本中,这项工作是以非常简单的方式完成的。这样做是为了更容易解释内容,因为尽管很多事情表面上很简单,但其中有些并不那么明显,如果不了解最简单、最基本的部分,就无法真正理解全局。