获取测试财务统计数据:TesterStatistics

我们通常根据交易报告来评估 EA 交易的质量,交易报告类似于用测试程序进行交易的测试报告。其包含了大量表征交易风格、稳定性以及盈利能力的变量。除了少数例外情况,所有这些指标均可通过特殊函数 TesterStatistics 用于 MQL 程序。因此,EA 交易开发人员能够分析代码中的单个变量,并根据这些变量构建他们自己的组合优化质量标准。

double TesterStatistics(ENUM_STATISTICS statistic)

TesterStatistics 函数可返回指定统计变量的值,该值是根据测试程序中 EA 交易的单独运行结果计算得出。可以在 OnDeinitOnTester 处理程序中调用函数,这将在后面讨论。

ENUM_STATISTICS 枚举中总结了所有可用的统计变量。其中一些变量具有定性特征,即实数型(通常包括利润总额、提款、比率等),另一些变量具有定量特征,即整数型(例如交易次数)。但是,两组变量都由具有 double 结果的相同函数控制。

下表显示了实数型指标(货币金额和系数)。所有货币金额都以存款货币表示。

标识符

说明

STAT_INITIAL_DEPOSIT

初始存款

STAT_WITHDRAWAL

从账户中提取的金额

STAT_PROFIT

测试结束时的净利润或亏损,即 STAT_GROSS_PROFIT 和 STAT_GROSS_LOSS 的总和

STAT_GROSS_PROFIT

总利润,即所有盈利交易的总和(大于等于零)

STAT_GROSS_LOSS

总亏损,即所有亏损交易的总和(小于或等于零)

STAT_MAX_PROFITTRADE

最大利润:所有盈利交易中的最大值(大于等于零)

STAT_MAX_LOSSTRADE

最大亏损:所有亏损交易中的最小值(小于或等于零)

STAT_CONPROFITMAX

一系列止盈交易的最大总利润(大于等于零)

STAT_MAX_CONWINS

最长止盈交易系列的总利润

STAT_CONLOSSMAX

一系列亏损交易的最大总亏损(小于或等于零)

STAT_MAX_CONLOSSES

最长亏损交易系列的总亏损

STAT_BALANCEMIN

最小余额值

STAT_BALANCE_DD

最大余额提款

STAT_BALANCEDD_PERCENT

在最大余额提款 (STAT_BALANCE_DD) 时记录的以百分比表示的余额提款

STAT_BALANCE_DDREL_PERCENT

最大余额提取百分比

STAT_BALANCE_DD_RELATIVE

在最大余额提款百分比 (STAT_BALANCE_DDREL_PERCENT) 时记录的以等值货币表示的余额提款

STAT_EQUITYMIN

最小净值

STAT_EQUITY_DD

最大提款金额

STAT_EQUITYDD_PERCENT

在货币中资金的最大提款时 (STAT_EQUITY_DD) 记录的以百分比表示的提款

STAT_EQUITY_DDREL_PERCENT

最大提款百分比

STAT_EQUITY_DD_RELATIVE

最大提款时记录的以百分比表示的提款 (STAT_EQUITY_DDREL_PERCENT)

STAT_EXPECTED_PAYOFF

盈利的数学期望(总利润和交易次数的算术平均值)

STAT_PROFIT_FACTOR

盈利能力,即 STAT_GROSS_PROFIT/STAT_GROSS_LOSS 的比率(如果 STAT_GROSS_LOSS = 0;盈利能力取值 DBL_MAX)

STAT_RECOVERY_FACTOR

恢复系数:STAT_PROFIT/STAT_BALANCE_DD 的比率

STAT_SHARPE_RATIO

夏普比率

STAT_MIN_MARGINLEVEL

达到的最小保证金水平

STAT_CUSTOM_ONTESTER

OnTester 函数返回的自定义优化准则的值

下表显示了整数型指标(金额)。

标识符

说明

STAT_DEALS

已完成的交易次数

STAT_TRADES

交易次数(退出市场的交易)

STAT_PROFIT_TRADES

盈利交易

STAT_LOSS_TRADES

亏损交易

STAT_SHORT_TRADES

空头交易

STAT_LONG_TRADES

多头交易

STAT_PROFIT_SHORTTRADES

空头盈利交易

STAT_PROFIT_LONGTRADES

多头盈利交易

STAT_PROFITTRADES_AVGCON

一系列盈利交易的平均长度

STAT_LOSSTRADES_AVGCON

一系列亏损交易的平均长度

STAT_CONPROFITMAX_TRADES

构成 STAT_CONPROFITMAX 的交易次数(盈利交易序列中的最大利润)

STAT_MAX_CONPROFIT_TRADES

最长盈利交易系列的交易次数

STAT_CONLOSSMAX_TRADES

构成 STAT_CONLOSSMAX 的交易次数(亏损交易序列中的最大亏损)

STAT_MAX_CONLOSS_TRADES

最长亏损交易系列的交易次数 STAT_MAX_CONLOSSES

我们尝试使用给出的指标来创建我们自己的复杂 EA 交易质量标准。要做到这一点,我们需要某种 MQL 程序的“实验”示例。我们以 EA 交易 MultiMartingale.mq5 为起点,但是我们将对其进行简化:删除多币种、内置错误处理和时间安排。此外,我们将选择一个信号交易策略以及一个单一的计算柱线,即开盘价。这将加快优化速度,并扩大实验领域。

该策略将基于 OsMA 指标确定的超买和超卖条件。叠加在 OsMA 上的 Bollinger 带指标可帮你动态找到波动幅度过大的边界,即交易信号。

当 OsMA 从下向上穿越通道下边界并回到通道内时,我们将开启买入交易。当 OsMA 从上到下以同样的方式越过上边界时,我们则开启卖出交易。为了退出仓位,我们使用了移动平均线,也适用于 OsMA。如果 OsMA 显示反向移动(多头向下或空头向上)并触及 MA,该仓位将被平仓。下面的截图展示了这种策略。

基于 OsMA、BBands 和 MA 指标的交易策略

基于 OsMA、BBands 和 MA 指标的交易策略

蓝色竖线对应的是开仓买入时的柱线,因为在前两根柱线上,下方的 Bollinger 带被 OsMA 柱状图从下向上穿过(这个仓位在子窗口内用蓝色空心箭头标出)。红色竖线是反转符号的位置,所以买入交易关闭,卖出交易打开。在子窗口中,在这个位置(或者更确切地说,在前面两根柱线上,空心红色箭头所在的位置),OsMA 柱状图自上而下穿过上 Bollinger 带。最后,绿线表示成交,因为柱状图开始高于红色均线。

我们将 EA 交易命名为 BandOsMA.mq5。一般设置将包括一个魔术编号、一个固定手数和一个以点为单位的止损距离。对于止损,我们将使用前面示例中的 TrailingStop。此处不用止盈。

input group "C O M M O N   S E T T I N G S"
sinput ulong Magic = 1234567890;
input double Lots = 0.01;
input int StopLoss = 1000;

三组设置可用于这些指标。

input group "O S M A   S E T T I N G S"
input int FastOsMA = 12;
input int SlowOsMA = 26;
input int SignalOsMA = 9;
input ENUM_APPLIED_PRICE PriceOsMA = PRICE_TYPICAL;
   
input group "B B A N D S   S E T T I N G S"
input int BandsMA = 26;
input int BandsShift = 0;
input double BandsDeviation = 2.0;
   
input group "M A   S E T T I N G S"
input int PeriodMA = 10;
input int ShiftMA = 0;
input ENUM_MA_METHOD MethodMA = MODE_SMA;

MultiMartingale.mq5 EA 交易中,我们没有交易信号,而开仓方向是由用户设定的。此处我们有交易信号,合理做法是将其单独列为一类。首先,我们说明一下抽象接口 TradingSignal

interface TradingSignal
{
   virtual int signal(void);
};

该接口和我们的其他接口 TradingStrategy 一样简单。这样很好。接口和对象越简单,就越有可能执行同样的操作,这是一种很好的编程风格,因为可最大限度地减少错误,并使大型软件项目更容易理解。由于在任何使用 TradingSignal 的程序中都有抽象概念,便可以用一个信号替换另一个信号。我们也可以替换策略。我们的策略现在负责准备和发送订单,并根据市场分析发出启动订单符号。

就我们的情况下而言,我们将 TradingSignal 的具体实现打包到 BandOsMaSignal 类中。当然,我们需要变量来存储 3 个指标的说明符。指标实例分别在构造函数和析构函数中创建和删除。所有参数都将通过输入变量传递。注意,iBandsiMA 是基于 hOsMA 处理程序构建的。

class BandOsMaSignalpublic TradingSignal
{
   int hOsMAhBandshMA;
   int direction;
public:
   BandOsMaSignal(const int fastconst int slowconst int signal,
      const ENUM_APPLIED_PRICE price,
      const int bandsconst int shiftconst double deviation,
      const int periodconst int xENUM_MA_METHOD method)
   {
      hOsMA = iOsMA(_Symbol_Periodfastslowsignalprice);
      hBands = iBands(_Symbol_PeriodbandsshiftdeviationhOsMA);
      hMA = iMA(_Symbol_PeriodperiodxmethodhOsMA);
      direction = 0;
   }
   
   ~BandOsMaSignal()
   {
      IndicatorRelease(hMA);
      IndicatorRelease(hBands);
      IndicatorRelease(hOsMA);
   }
   ...

当前交易信号的方向放在 direction 变量内:0 - 无信号(未定义情况),+1 - 买入,-1 - 卖出。我们将在 signal 方法中填充这个变量。其代码重复了上面 MQL5 中信号的口头说明。

   virtual int signal(voidoverride
   {
      double osma[2], upper[2], lower[2], ma[2];
      // get two values of each indicator on bars 1 and 2
      if(CopyBuffer(hOsMA012osma) != 2return 0;
      if(CopyBuffer(hBandsUPPER_BAND12upper) != 2return 0;
      if(CopyBuffer(hBandsLOWER_BAND12lower) != 2return 0;
      if(CopyBuffer(hMA012ma) != 2return 0;
      
      // if there was a signal already, check if it has ended
      if(direction != 0)
      {
         if(direction > 0)
         {
            if(osma[0] >= ma[0] && osma[1] < ma[1])
            {
               direction = 0;
            }
         }
         else
         {
            if(osma[0] <= ma[0] && osma[1] > ma[1])
            {
               direction = 0;
            }
         }
      }
      
      // in any case, check if there is a new signal
      if(osma[0] <= lower[0] && osma[1] > lower[1])
      {
         direction = +1;
      }
      else if(osma[0] >= upper[0] && osma[1] < upper[1])
      {
         direction = -1;
      }
      
      return direction;
   }
};

可以看到,指标值是为第 1 和第 2 根柱线读取的,因为我们将打开一根柱线,而第 0 根柱线在我们调用 signal 方法时刚刚打开。

实现 TradingStrategy 接口的新类将被命名为 SimpleStrategy

该类提供了一些新特性,同时还使用了一些以前存在的部分。特别是,其为 PositionStateTrailingStop 保留了自动指针,并为 TradingSignal 信号提供了一个新的自动指针。此外,因为我们只在柱线打开时交易,所以我们需要 lastBar 变量,其用于存储最后一个处理柱线的时间。

class SimpleStrategypublic TradingStrategy
{
protected:
   AutoPtr<PositionStateposition;
   AutoPtr<TrailingStoptrailing;
   AutoPtr<TradingSignalcommand;
   
   const int stopLoss;
   const ulong magic;
   const double lots;
   
   datetime lastBar;
   ...

全局参数被传递给 SimpleStrategy 构造函数。我们还传递一个指向 TradingSignal 对象的指针:在这种情况下,为 BandOsMaSignal,该指针必须通过调用代码来创建。接下来,构造函数试图在现有仓位中找到具有所需 magic 号和符号的仓位,如果成功,则添加一个跟踪止损。如果 EA 交易因为这样或那样的原因而中止运行,并且仓位已经被打开,这将是有用的。

public:
   SimpleStrategy(TradingSignal *signalconst ulong mconst int slconst double v):
      command(signal), magic(m), stopLoss(sl), lots(v), lastBar(0)
   {
 // select "our" position among the existing ones (if there is a suitable one)
      PositionFilter positions;
      ulong tickets[];
      positions.let(POSITION_MAGICmagic).let(POSITION_SYMBOL_Symbol).select(tickets);
      const int n = ArraySize(tickets);
      if(n > 1)
      {
         Alert(StringFormat("Too many positions: %d"n));
 // TODO: close extra positions - this is not allowed by the strategy
      }
      else if(n > 0)
      {
         position = new PositionState(tickets[0]);
         if(stopLoss)
         {
           trailing = new TrailingStop(tickets[0], stopLossstopLoss / 50);
         }
      }
   }

trade 方法的实现类似于鞅的示例。但是,我们已经删除了许多乘法,并增加了 signal 方法调用。

   virtual bool trade() override
   {
      // we work only once when a new bar appears
      if(lastBar == iTime(_Symbol_Period0)) return false;
      
      int s = command[].signal(); // getting a signal
      
      ulong ticket = 0;
      
      if(position[] != NULL)
      {
         if(position[].refresh()) // position exists
         {
            // the signal has changed to the opposite or disappeared
            if((position[].get(POSITION_TYPE) == POSITION_TYPE_BUY && s != +1)
            || (position[].get(POSITION_TYPE) == POSITION_TYPE_SELL && s != -1))
            {
               PrintFormat("Signal lost: %d for position %d %lld",
                  sposition[].get(POSITION_TYPE), position[].get(POSITION_TICKET));
               if(close(position[].get(POSITION_TICKET)))
               {
                  position = NULL;
               }
               else
               {
                 // update internal flag 'ready'
                 // according to whether or not there was a closure
                  position[].refresh();
               }
            }
            else
            {
               position[].update();
               if(trailing[]) trailing[].trail();
            }
         }
         else // position is closed
         {
            position = NULL;
         }
      }
      
      if(position[] == NULL && s != 0)
      {
         ticket = (s == +1) ? openBuy() : openSell();
      }
      
      if(ticket > 0// new position just opened
      {
         position = new PositionState(ticket);
         if(stopLoss)
         {
            trailing = new TrailingStop(ticketstopLossstopLoss / 50);
         }
      }
      // store the current bar
      lastBar = iTime(_Symbol_Period0);
      
      return true;
   }

辅助方法 openBuyopenSell 等都做了极小的改动,就不一一列举了(完整源代码附后)。

因为在这个 EA 交易中我们总是只有一个策略,与多币种鞅相反,在多币种鞅中每个符号都需要自己的设置,所以我们可以排除策略池,直接管理策略对象。

AutoPtr<TradingStrategystrategy;
   
int OnInit()
{
   if(FastOsMA >= SlowOsMAreturn INIT_PARAMETERS_INCORRECT;
   strategy = new SimpleStrategy(
      new BandOsMaSignal(FastOsMASlowOsMASignalOsMAPriceOsMA,
         BandsMABandsShiftBandsDeviation,
         PeriodMAShiftMAMethodMA),
         MagicStopLossLots);
   return INIT_SUCCEEDED;
}
   
void OnTick()
{
   if(strategy[] != NULL)
   {
      strategy[].trade();
   }
}

我们现在有了一个现成的 EA 交易,可以用来作为研究测试程序的工具。首先,我们创建一个辅助结构体 TesterRecord,用于查询和存储所有统计数据。

struct TesterRecord
{
   string feature;
   double value;
   
   static void fill(TesterRecord &stats[])
   {
      ResetLastError();
      for(int i = 0; ; ++i)
      {
         const double v = TesterStatistics((ENUM_STATISTICS)i);
         if(_LastErrorreturn;
         TesterRecord t = {EnumToString((ENUM_STATISTICS)i), v};
         PUSH(statst);
      }
   }
};

在这种情况下,只有信息日志输出才需要 feature 字符串型字段。要保存所有指标(例如,为了能够在以后生成你自己的报告),只需要一个适当长度的 double 类型的简单数组。

使用 OnDeinit 处理程序中的结构体,我们可确保 MQL5 API 返回与测试程序报告相同的值。

void OnDeinit(const int)
{
   TesterRecord stats[];
   TesterRecord::fill(stats);
   ArrayPrint(stats2);
}

例如,在存款为 10000 且未进行任何优化(使用默认设置)的情况下,在 EURUSD,H1 上运行 EA 交易时,对于 2021 年,我们将获得大约以下值(片段):

                        [feature]  [value]
[ 0] "STAT_INITIAL_DEPOSIT"       10000.00
[ 1] "STAT_WITHDRAWAL"                0.00
[ 2] "STAT_PROFIT"                    6.01
[ 3] "STAT_GROSS_PROFIT"            303.63
[ 4] "STAT_GROSS_LOSS"             -297.62
[ 5] "STAT_MAX_PROFITTRADE"          15.15
[ 6] "STAT_MAX_LOSSTRADE"           -10.00
...
[27] "STAT_DEALS"                   476.00
[28] "STAT_TRADES"                  238.00
...
[37] "STAT_CONLOSSMAX_TRADES"         8.00
[38] "STAT_MAX_CONLOSS_TRADES"        8.00
[39] "STAT_PROFITTRADES_AVGCON"       2.00
[40] "STAT_LOSSTRADES_AVGCON"         2.00

知道了所有这些值,我们可以编写我们自己的公式,用于 EA 交易质量的组合度量指标,同时用于目标优化函数。但是在任何情况下,这个指标的值都需要向测试程序报告。这就是 OnTester 函数的作用。