请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  量化平台 帖子:3364760 新帖:24

如何缩短 EA 代码以让程序更加简单同时减少错误

外汇工厂发表于:4 月 17 日 20:06回复(1)

简介

有很多交易系统都是基于技术分析的,无论是指标还是图解图,它们都有一个重要的特性。我是指此类系统在交易方向的对称性。由于此特性,此类系统中的交易信号和交易下单机制通常可相对于交易方向进行表示。

下面介绍的一个简单方法允许使用此特性基于对称系统大幅缩短 Expert Advisor 的代码长度。使用该方法的 Expert Advisor 针对多头和空头使用同一个代码来检测交易信号和生成交易订单。

基于对称系统开发 Expert Advisor 的常见做法是,首先为一个方向上的交易信号生成和处理编写代码,然后将代码复制用于另一个方向并对其进行优化。在这种情况下,很容易出错,而且难以检测到此类错误。因此,减少 Expert Advisor 的逻辑中可能出现的错误数量,是该方法的另一个优势。

1.Expert Advisor 的内置项在交易方向上保持不变性

所讨论的概念是基于交易方向的。此方向可以是多头(买入信号和订单),也可以是空头(卖出信号和订单)。我们的目标是以这样的方式编写 Expert Advisor,以使它们的代码在当前交易方向上保持不变性。为了避免本文冗杂,我们将这种代码称为不变代码,它仅在交易方向上保持不变性。

因此,我们将输入一个函数或变量,它的值将始终显示当前交易方向(两个可能值之一)。

该变量在代码中的表示方法很重要。尽管bool类型看起来很符合此类用途,但用一个略微不同的表示法会更加有效,即整数。交易方向本身的编码如下:

  • 多头交易方向:+1
  • 空头交易方向:-1

与逻辑表示法相比,上述表示法的优势是,可有效地用于在 Expert Advisor 的代码中进行各种计算和检查,而无需执行传统方法中使用的条件分支。

2.如何从传统代码转换为不变代码的示例

让我们在一些例子上明确此声明。但是,让我们先讨论一下稍后将反复使用的一组辅助函数:

int sign( double v )
{
    if( v < 0 ) return( -1 );
    return( 1 );
}
 
double iif( bool condition, double ifTrue, double ifFalse )
{
    if( condition ) return( ifTrue );
    
    return( ifFalse );
}
 
string iifStr( bool condition, string ifTrue, string ifFalse )
{
    if( condition ) return( ifTrue );
    
    return( ifFalse );
}
 
int orderDirection()
{
    return( 1 - 2 * ( OrderType() % 2 ) );
}

sign() 函数的用途很明显;当参数为非负值时返回 1,当参数为负值时返回 -1。

函数 iif() 相当于名称为“condition ? ifTrue : ifFalse”的 C 语言运算符,可大大简化不变的 Expert Advisor,使其更紧凑、更具代表性。它使用double它使用 double 类型的参数,因此它可与此类型以及类型 Int datetime 的值一起使用。要对字符串进行同样的操作,我们需要完全类似的函数 iifStr(),它使用 String 类型的值。

根据我们有关如何表示交易方向的协定,函数 orderDirection() 将返回当前交易订单的方向(即函数 OrderSelect() 选择的方向)。

现在,让我们来看看具有此类交易方向编码的不变方式可如何简化 Expert Advisor 的代码:

2.1 示例 1.转换追踪止损位实现

典型代码:

if( OrderType() == OP_BUY )
{
    bool modified = OrderModify( OrderTicket(), OrderOpenPrice(), Bid - Point *
        TrailingStop, OrderTakeProfit(), OrderExpiration() );
 
    int error = GetLastError();
    if( !modified && error != ERR_NO_RESULT )
    {
        Print( "Failed to modify order " + OrderTicket() + ", error code: " +
            error );
    }
}
else
{
    modified = OrderModify( OrderTicket(), OrderOpenPrice(), Ask + Point *
        TrailingStop, OrderTakeProfit(), OrderExpiration() );
 
    error = GetLastError();
    if( !modified && error != ERR_NO_RESULT )
    {
        Print( "Failed to modify order " + OrderTicket() + ", error code: " +
            error );
    }
}

不变代码:

double closePrice = iif( orderDirection() > 0, Bid, Ask );
 
bool modified = OrderModify( OrderTicket(), OrderOpenPrice(), closePrice -
    orderDirection() * Point * TrailingStop, OrderTakeProfit(),
    OrderExpiration() );
 
int error = GetLastError();
if( !modified && error != ERR_NO_RESULT )
{
    Print( "Failed to modify order " + OrderTicket() + ", error code: " +
        error );
}

总结:

  1. 我们设法避免了繁重的条件分支;
  2. 我们仅使用一个字符串调用函数 OrderModify() 而不是两个初始函数;并且,
  3. 关于 (2) 的含义,我们缩短了错误处理的代码。

请注意,由于我们直接利用了计算止损位的算术表达式中的交易订单方向,因此我们设法仅调用了一次 OrderModify()。如果我们使用了交易方向的逻辑表示法,这样做是不可能的。

基本上,经验丰富的 Expert Advisor 编写者能够使用传统的方式来仅调用一次 OrderModify()。然而,在我们的例子中,这种操作方式非常自然,而且不需要任何其他步骤。

2.2 示例 2. 转换交易信号检测

例如,让我们来看看有两个移动平均数的系统中的交易信号检测情况:

double slowMA = iMA( Symbol(), Period(), SlowMovingPeriod, 0, MODE_SMA,
    PRICE_CLOSE, 0 );
double fastMA = iMA( Symbol(), Period(), FastMovingPeriod, 0, MODE_SMA,
    PRICE_CLOSE, 0 );
 
if( fastMA > slowMA + Threshold * Point )
{
    // open a long position
    int ticket = OrderSend( Symbol(), OP_BUY, Lots, Ask, Slippage, 0, 0 );
    
    if( ticket == -1 )
    {
        Print( "Failed to open BUY order, error code: " + GetLastError() );
    }
}
else if( fastMA < slowMA - Threshold * Point )
{
    // open a short position
    ticket = OrderSend( Symbol(), OP_SELL, Lots, Bid, Slippage, 0, 0 );
    
    if( ticket == -1 )
    {
        Print( "Failed to open SELL order, error code: " + GetLastError() );
    }
}

现在,让我们来使代码在交易方向上保持不变:

double slowMA = iMA( Symbol(), Period(), SlowMovingPeriod, 0, MODE_SMA,
    PRICE_CLOSE, 0 );
double fastMA = iMA( Symbol(), Period(), FastMovingPeriod, 0, MODE_SMA,
    PRICE_CLOSE, 0 );
 
if( MathAbs( fastMA - slowMA ) > Threshold * Point )
{
    // open a position
    int tradeDirection = sign( fastMA - slowMA );
    int ticket = OrderSend( Symbol(), iif( tradeDirection > 0, OP_BUY, OP_SELL ),
        Lots, iif( tradeDirection > 0, Ask, Bid ), Slippage, 0, 0 );
 
    if( ticket == -1 )
    {
        Print( "Failed to open " + iifStr( tradeDirection > 0, "BUY", "SELL" ) +
            " order, error code: " + GetLastError() );
    }
}

我认为,显而易见,该代码变得更加紧凑。两个错误检查很自然地变成了一个错误检查。

尽管上述示例都非常简单,但正在讨论的方法的主要优势一定非常明显。在一些更为复杂的示例中,传统方法和正在讨论的方法之间的差别甚至会更加明显。我们保证在标准 Expert Advisor MACD 样本的示例中一定会是如此。

3.如何简化 MACD 样本

为了避免本文冗杂,我们在这里不讨论此 Expert Advisor 的完整代码。让我们转到代码区域,根据正在讨论的概念,它们不会发生更改。

此 EA 的完整代码包括在 MetaTrader 4 交付包中。本文也附上了交付包(文件 MACD Sample.mq4)及其简化版本 (MACD Sample-2.mq4),以方便你使用。

让我们启动为检测交易信号而编写的块。其初始代码如下:

// check for long position (BUY) possibility
if(MacdCurrent<0 && MacdCurrent>SignalCurrent && MacdPrevious<SignalPrevious &&
   MathAbs(MacdCurrent)>(MACDOpenLevel*Point) && MaCurrent>MaPrevious)
  {
   ticket=OrderSend(Symbol(),OP_BUY,Lots,Ask,3,0,Ask+TakeProfit*Point,
     "macd sample",16384,0,Green);
   if(ticket>0)
     {
      if(OrderSelect(ticket,SELECT_BY_TICKET,MODE_TRADES))
        Print("BUY order opened : ",OrderOpenPrice());
     }
   else Print("Error opening BUY order : ",GetLastError()); 
   return(0); 
  }
// check for short position (SELL) possibility
if(MacdCurrent>0 && MacdCurrent<SignalCurrent && MacdPrevious>SignalPrevious && 
   MacdCurrent>(MACDOpenLevel*Point) && MaCurrent<MaPrevious)
  {
   ticket=OrderSend(Symbol(),OP_SELL,Lots,Bid,3,0,Bid-TakeProfit*Point,
     "macd sample",16384,0,Red);
   if(ticket>0)
     {
      if(OrderSelect(ticket,SELECT_BY_TICKET,MODE_TRADES))
        Print("SELL order opened : ",OrderOpenPrice());
     }
   else Print("Error opening SELL order : ",GetLastError()); 
   return(0); 
  }

现在,让我们来使用上述方法重新编写此代码,以便它对于买入和卖出信号都是一样的:

int tradeDirection = -sign( MacdCurrent );
 
// check if we can enter the market
if( MacdCurrent * tradeDirection < 0 && ( MacdCurrent - SignalCurrent ) *
    tradeDirection > 0 && ( MacdPrevious - SignalPrevious ) * tradeDirection < 0
    && MathAbs(MacdCurrent)>(MACDOpenLevel*Point) && ( MaCurrent - MaPrevious ) *
    tradeDirection > 0 )
  {
   int orderType = iif( tradeDirection > 0, OP_BUY, OP_SELL );
   string orderTypeName = iifStr( tradeDirection > 0, "BUY", "SELL" );
   double openPrice = iif( tradeDirection > 0, Ask, Bid );
   color c = iif( tradeDirection > 0, Green, Red );
   ticket = OrderSend( Symbol(), orderType, Lots, openPrice, 3 , 0, openPrice +
     tradeDirection * TakeProfit * Point, "macd sample", 16384, 0, c );
   if(ticket>0)
     {
      if(OrderSelect(ticket,SELECT_BY_TICKET,MODE_TRADES))
        Print( orderTypeName + " order opened : ", OrderOpenPrice() );
     }
   else Print("Error opening " + orderTypeName + " order : ",GetLastError()); 
   return(0); 
  }

现在,让我们转到负责关闭持仓和处理追踪止损位的块:首先,让我们像前面一样分析其初始版本:

if(OrderType()==OP_BUY)   // long position is opened
  {
   // should it be closed?
   if(MacdCurrent>0 && MacdCurrent<SignalCurrent && MacdPrevious>SignalPrevious &&
      MacdCurrent>(MACDCloseLevel*Point))
       {
        OrderClose(OrderTicket(),OrderLots(),Bid,3,Violet); // close position
        return(0); // exit
       }
   // check for trailing stop
   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 // go to short position
  {
   // should it be closed?
   if(MacdCurrent<0 && MacdCurrent>SignalCurrent &&
      MacdPrevious<SignalPrevious && MathAbs(MacdCurrent)>(MACDCloseLevel*Point))
     {
      OrderClose(OrderTicket(),OrderLots(),Ask,3,Violet); // close position
      return(0); // exit
     }
   // check for trailing stop
   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);
           }
        }
     }
  }

让我们来将此代码转换为在交易方向上保持不变的代码:

tradeDirection = orderDirection();
double closePrice = iif( tradeDirection > 0, Bid, Ask );
c = iif( tradeDirection > 0, Green, Red );
 
// should it be closed?
if( MacdCurrent * tradeDirection > 0 && ( MacdCurrent - SignalCurrent ) *
    tradeDirection < 0 && ( MacdPrevious - SignalPrevious ) * tradeDirection > 0 
    && MathAbs( MacdCurrent ) > ( MACDCloseLevel * Point ) )
    {
     OrderClose(OrderTicket(),OrderLots(), closePrice, 3,Violet); // close position
     return(0); // exit
    }
// check for trailing stop
if(TrailingStop>0)  
  {                 
   if( ( closePrice - OrderOpenPrice() ) * tradeDirection > Point * TrailingStop )
     {
      if( OrderStopLoss() == 0 || ( OrderStopLoss() - ( closePrice - tradeDirection *
        Point * TrailingStop ) ) * tradeDirection < 0 )
        {
         OrderModify( OrderTicket(), OrderOpenPrice(), closePrice - tradeDirection *
            Point * TrailingStop, OrderTakeProfit(), 0, c );
         return(0);
        }
     }
  }

请注意,初始版本的 EA 仅针对处理追踪止损位过程中的空仓检查条件 OrderStopLoss() == 0。如果未能设置初始止损位(例如由于过于接近市价)而导致出现一些状况,必须进行处理。

如果未针对多头检查此条件,将被视为错误,此类错误在使用复制并粘贴的方法编写此类对称的 Expert Advisor 时很典型。

请注意,在已改进的代码中,已自动修复两个交易方向上的此类错误。还必须注意,如果在编写不变代码时出现了此错误,在处理多头头寸和空头头寸时也会出现此错误。不言而喻,这会提高在测试期间进行检测的可能性。

就是这样。如果你测试的 Expert Advisor 对于相同的数据其设置也相同,你将看到它们是完全等同的。然而,简化版更紧凑、更易于维护。

4.有关“从头开始”编写对称的 Expert Advisor 的建议

直到最近,我们讨论了如何将 Expert Advisor 的传统代码转换成不变代码的可能性。然而,使用上述原则“从头开始”开发交易机器人甚至会更加有效。

第一眼看上去,似乎并不简单,因为它需要在制定条件和表达式方面具有一定程度的技能和经验,同时这些条件和表达式在交易方向上应保持不变。但是,在经过一些实践之后,这种方式的代码编写很简单,就像代码本身一样。

现在,我尝试提供一些可能有助于开始使用更有效的方式的建议:

  1. 在开发这样或那样的代码区之前,首先,处理多头交易方向——在大多数情况下,会更容易合成不变代码,因为此交易方向由值 +1 表示,在编写和分析不变关系时不用执行很多操作。
  2. 如果你开始处理多头方向,首先尝试编写一个不包括任何能够反映交易方向的变量或函数的条件。确保表达式正确并且把交易方向添加进去。在获得了一些经验之后,你可以继续操作,而无需逐步分开。
  3. 不要“停留”在多头交易方向上——有时显示空头方向的条件更加有效。
  4. 如果有可能进行算术计算,应尝试避免条件分支和使用函数 iif()。

对于最后一个从句,我想补充一点,可能会有不能缺少条件分支的情况。然而,你应尝试收集这些情况并利用这些情况区分不依赖于具体 Expert Advisor 的交易方向辅助函数。这些函数以及上述函数 sign()、iif() 和 orderDirection() 可适用于所有 Expert Advisor 稍后将用到的一个专用库。

为了再次明确所有事项,让我们来讨论以下问题:

在交易订单中,对于多头头寸,止损位必须位于前一柱体的最低位,对于空头头寸,止损位必须位于前一柱体的最高位。

它在代码中可表示为:

double stopLevel = iif( tradeDirection > 0, Low[ 1 ], High[ 1 ] );

看似简单明了,但是,这些简单的结构可以而且必须集中在要反复使用的小而简单的函数中。

让我们将条件运算符放在辅助函数中从而避开它,以实现更普遍的用途:

double barPeakPrice( int barIndex, int peakDirection )
{
    return( iif( peakDirection > 0, High[ barIndex ], Low[ barIndex ] ) );
}

现在,我们可以按照如下方式表示止损位的计算:

double stopLevel = barPeakPrice( 1, -tradeDirection );

如果区别看似微不足道,请不要被你的第一印象所误导。此变体有重大优势:

  • 它以不变形式明确表示其用途;
  • 它用合适方式模拟编写 EA 的代码;
  • 它更易于查看,并且便于进一步开发。

这只是一个示例。实际上,可用类似形式表示 Expert Advisor 代码的许多标准元素。你自己也能够做到这一点。因此,我强烈建议你用这样的方式分析并修订代码。

5.为什么要这样?

我认为,上面介绍的概念具有以下明显优点:

  • 在无损功能的情况下缩短源代码,从而缩短开发和调整交易系统过程中所耗费的时间;
  • 减少潜在的错误数量;
  • 增加检测到现有错误的可能性;
  • 简化 Expert Advisor 的进一步修改(更改自动应用于多头和空头的信号和头寸)。

我只注意到了一个缺点:该概念会导致在初期阶段有些难以理解和学习。但是,此缺点足以被上述列出的优点所抵消。而且,这仅仅是时间和积累一些经验的问题——不变代码的开发将变得自然而简单。

总结

上面介绍用于在 MQL4 中编写 Expert Advisor 代码的方法是基于使用和有效表示交易方向概念的。它可以避免重复编写几乎完全相同的代码区,而在使用传统方法编写的 Expert Advisor 中通常会看到这种重复编写情况。借助所有这些优点,利用上述方法能够大幅缩短源代码量。

本文提供了一些示例,旨在帮助交易系统的初学者和一些更有经验的开发人员理解现有代码。这些示例以及作者的一些建议将帮助他们编写在交易方向上保持不变的紧凑代码。

所讨论的示例相当简单,以便于更易于查看和理解问题。然而,上述方法曾成功地用于实现非常复杂的系统,这些系统应用了以下技术分析工具:趋势线、通道、安德鲁干草叉、艾略特波浪以及其他一些中级和高级的市场分析方法。

全部回复

0/140

达人推荐

量化课程

    移动端课程