交易系统的评估 - 有关进入、退出与交易效率的概述

Mykola Demko | 12 九月, 2013

简介

有很多指标可用于确定一个交易系统的效率;交易者会选择他 们喜欢的交易系统。本文讲述S.V. Bulashev 所著《Statistika dlya traderov(面向交易者的统计)》一书所描述的方法。很不幸,该书的数量太少,并且很久以来没有再版;但是,仍然可以在很多网站上获得其电子版本。


序言

需要指出的是该书是2003年出版的。这也是采用MQL- II编程语言开发的MetaTrader 3的推出时间。从那时起,平台就在不断进步。因此,我们可以通过将交易条件与现代的MetaTrader 5客户 端进行比较来跟踪交易条件本身的变化。应该指出,该书的作者已经成为一代又一代交易者的领袖(考虑到此领域的快速时代变化)。然而时间不会静止不前;尽管 该书描述的原则仍然适用,但方法应做出改变。

首先,S.V. Bulashev 是依据当时的实际交易条件写下该书的。这是为什么我们不能在不做出改变的情况下使用作者描述的统计的原因之所在。为了更加清楚地说明这一点,让我们回想一 下那时的交易可能性:现货市场中的预付款交易意味着买入一种货币以获取投机盈利,然后过一段时间后将其卖出。

这些是基础,并且值得指出的是在写《面向交易者的统计》一 书时使用了精确的解释。每1手成交都应以具有相同数量的反向成交来平仓。但是在两年后(即在2005年),此类统计的使用需要重新整理。原因是成交的部分 平仓在 MetaTrader 4中 成为可能。因此,要使用 Bulashev 描述的统计,我们需要强化解释系统,尤其是解释应依据平仓事实而不是依据建仓事实来进行。

在经过另外五年后,情形有了显著变化。所谓的惯常术语 Order(订单)在哪里?它不见了。考虑到本论坛的问题流动,最好准确地描述MetaTrader 5中的解释系统。

因此,现在再也没有经典的术语 Order(订单)了。现在,任何订单都是对经纪人服务器的交易请求,是交易者或MTS为了建仓或改仓而进行的。现在它是一个仓位;为了理解其意义,我已 经提到过预付款交易。事实上,预付款交易是用借进的钱进行的;仓位是在借进的钱存在之后才存在的。

只要您通过平仓来与借款人进行结算,从而锁定盈利或损失, 则您的仓位就不再存在。通过这种方式解释了仓位反向不会平仓的原因。事实上,借款仍然存在;如果您借钱进行买入或卖出,中间并没有差异。成交仅是一个已执 行订单的历史记录。

现在,让我们谈谈交易的特点。目前,在 MetaTrader 5中,我们既可以部分平仓交易,也可以增加现有交易。因此,经典的解释系统,其中某个数量的建仓都跟着相同数量的平仓,已经成为过去。但是真的不能从存储 在 MetaTrader 5 中的信息恢复吗?那么,我们首先重新整理解释。


进入效率

很多人希望让他们的交易更加有效并不是一个秘密,但是如何 描述(规范化)此术语呢?如果您假定成交是按价格传递的路径,显然在该路径上有两个极端点:所观察区间内的最低价和最高价。每个人都竭力尽可能地靠近最低 价进入市场(买入时)。这可以被视为任何交易的主要原则:低买高卖。

进入效率决定了您买入时有多接近最低价。换言之,进入的效 率是最高价和进入价之间的距离对整个路径的比值。为什么我们通过最高价的差异来衡量到最低价的距离?当以最低价进入时,我们需要效率等于1(当 以最高价进入时等于 0)。

这是为什么我们用余下的距离,而不是最低价与进入价之间的 距离来计算比值。在这里,我们需要指出卖出情形与买入情形相比是相反的。

 

进入仓位的效率显示 MTS 在某次交易期间实现相对于进入价的潜在盈利有多好。它是按以下公式计算的:

对于买入仓位
enter_efficiency=(max_price_trade-enter_price)/(max_price_trade-min_price_trade);
对于卖出仓位
enter_efficiency=(enter_price-min_price_trade)/(max_price_trade-min_price_trade);
进入效率可以是一个0到1之间的数值.


退出效率

退出情形是类似的:

从某个仓位退出的效率显示 MTS 在某次交易期间实现相对于退出价的潜在盈利有多好。它是按以下公式计算的:


对于买入仓位
exit_efficiency=(exit_price - min_price_trade)/(max_price_trade - min_price_trade);

对于卖出仓位
exit_efficiency=(max_price_trade - exit_price)/(max_price_trade - min_price_trade);
退出效率可以是一个0到1之间的数值.


交易效率

整体而言,交易的效率是由进入价和退出价决定的。它也可以 作为交易期间进入价和退出价之到最高价的距离之间的比值(即最低价和最高价之差)来计算。因此,可以用两种方法来计算交易效率:直接使用有关交易的主要信 息,或使用已经计算出来的进入和离开结果(有间隔)。

交易的效率显示 MTS 在某次交易期间实现的总体潜在盈利有多好。它是按以下公式计算的:

对于买入仓位
trade_efficiency=(exit_price-enter_price)/(max_price_trade-min_price_trade);

对于卖出仓位
trade_efficiency=(enter_price-exit_price)/(max_price_trade-min_price_trade);

通用公式
trade_efficiency=enter_efficiency+exit_efficiency-1;

交易的效率值可以是一个从-1到1之间的数值.
交易的效率值必须大于0,2. 可视化地分析效率显示了提升系统的方向,因为它允许把进入和退出仓位的信号质量相互隔离区分。

解释的转变

首先,为了避免任何类型的混淆,我们需要澄清解释对象的名 称。因为 MetaTrader 5 和 Bulachev 使用相同的术语 - order(订单)、deal(成交)、 position(仓位) ,我们需要区分它们。在我的文章中,我将使用 "trade"(交易)来表示 Bulachev 的解释对象,即用trade(交易)表示 deal(成交);他还用术语 "Order"(订单)表示交易,在这种上下文环境中,它们是相同的。Bulachev 将未完成的成交称为仓位,我们称之为未平仓的交易。

在这里,您可以看到所有 3 个术语都可以用一个单词 "trade"(交易)来轻松表达。我们在MetaTrader 5 中将不会重新命名解释,这三个术语将保持与客户端开发者的设计相同。因此,我们有4个要使用的单词:Position(仓位)、 Deal(成交)、Order(订单)Trade(交易)

因为Order(订单)是发给服务器的用于建仓/改仓的命 令,并且它不直接涉及统计,但是它通过成交间接地涉及统计(原因是发送订单并不始终会导致指定数量和价格的对应成交的执行),因此按成交而不是订单来进行 统计是正确的。

让我们以一个例子来说明相同仓位的解释(从而使以上描述更 加清楚):

МТ 中的解释-5
deal[ 0 ]  in      0.1   sell   1.22218   2010.06.14 13:33
deal[ 1 ]  in/out  0.2   buy    1.22261   2010.06.14 13:36
deal[ 2 ]  in      0.1   buy    1.22337   2010.06.14 13:39
deal[ 3 ]  out     0.2   sell   1.22310   2010.06.14 13:41
Bulachev 的解释
trade[ 0 ]  in 0.1  sell 1.22218 2010.06.14 13:33   out 1.22261 2010.06.14 13:36
trade[ 1 ]  in 0.1  buy  1.22261 2010.06.14 13:36   out 1.22310 2010.06.14 13:41
trade[ 2 ]  in 0.1  buy  1.22337 2010.06.14 13:39   out 1.22310 2010.06.14 13:41


现在我将描述这些进行操作执行的方法。Deal[ 0 ] 建仓,我们将其写为新交易的开始:

trade[0]  in 0.1  sell 1.22218 2010.06.14 13:33

接着是仓位反向;它意味着所有以前的交易都应平仓。对应 地,在平仓交易和建仓交易中都要考虑有关反向deal[ 1 ] 的信息。一旦在具有买入/卖出方向的成交之前的所有未平仓的交易被平仓,我 们需要建立新的交易。即我们仅使用有关选定成交的price(价 格)和time(时 间)信息,与建立一个交易相对,在还使用type(类 型)和volume(数 量)的情况下。在这里,我们需要澄清一个在新的解释中出现以前还未使用过的术语:成交方向。以前,我们通过说 "direction"(方向)来指买入或卖出,术语 "type"(类型)具有相同的含义。从现在起,类型和方向是不同的术语。

类型指买入或卖出,而方向指进入或退出仓位。这是为什么 始终以买入建仓, 并以卖出平仓的原 因。但是方向并不仅限于建仓和平仓。此术语还包含增加仓位的数量(如果“买入”不在列表的最前)以及部分平仓 (“卖出”不在列表的最后)。因为可以部分平仓,引入仓位反向也是符合逻辑的;当执行数量大于当前仓位的相反成 交,即它是一个买入/卖出时, 即发生反向。

通过这种方式,我们已经平了以前建立的交易(反向仓位):

trade[0]  in 0.1  sell 1.22218 2010.06.14 13:33 out 1.22261 2010.06.14 13:36

剩余数量为 0.1 手,用于建立新的交易:

trade[1]  in 0.1  buy  1.22261 2010.06.14 13:36

接着是 deal[2], 方向为 in(买入),建立另一交易:

trade[2]  in 0.1  buy  1.22337 2010.06.14 13:39

最后是平仓成交 - deal[ 3 ] 平了尚未平仓的仓位中的所有交易:

trade[1]  in 0.1  buy  1.22261 2010.06.14 13:36   out 1.22310 2010.06.14 13:41
trade[2]  in 0.1  buy  1.22337 2010.06.14 13:39   out 1.22310 2010.06.14 13:41

上述解释显示了 Bulachev 使用的解释的要点 - 每个建仓交易都有一个进入点和 一个退出点,并且 有自己的数量类型。但是此解释系统并没有考虑一 个细微差别:部分平仓。如果您进一步观察,您将看到交易次数等于买 入的次数(考虑买入/卖出)。在此示例中,值得按买入解释,但是在部分平仓有更多卖出(有可能出现买入与卖出的次数 相同,但数量并不相互对应的情形)。

为了处理所有卖出,我们应按卖出进行解释。如果我们进行分开的 成交处理,首先处理全部买入, 然后处理全部卖出(或 相反顺序),则此冲突似乎是不可解决的。但是如果我们按顺序处理成交,并且向每个成交应用特殊的处理规则,则不会有冲突。

这里有一个示例,其中卖出的次数大于买入的次数(含描述):

МТ 中的解释-5
deal[0]  in      0.3   sell      1.22133   2010.06.15 08:00
deal[1]  out     0.2   buy       1.22145   2010.06.15 08:01
deal[2]  in/out  0.4   buy       1.22145   2010.06.15 08:02
deal[3]  in/out  0.4   sell      1.22122   2010.06.15 08:03
deal[4]  out     0.1   buy       1.2206    2010.06.15 08:06
Bulachev 的解释                                       trade[0]  in 0.2  sell 1.22133 2010.06.15 08:00   out 1.22145 2010.06.15 08:01   trade[1]  in 0.1  sell  1.22133 2010.06.15 08:00   out 1.22145 2010.06.15 08:02   trade[2]  in 0.3  buy     1.22145 2010.06.15 08:02   out 1.22122 2010.06.15 08:03   trade[3]  in 0.1  sell  1.22122 2010.06.15 08:03   out 1.2206  2010.06.15 08:06    

我们有一种情形,其中平仓在建仓之后,但不是全部数量,仅 是其中的一部分(建仓数量为0.3手, 而平仓数量为0.2手)。 如何处理此类情形?如果每笔交易都以相同数量平仓,则这种情形可被视为通过一笔成交建立几笔交易。因此,它们具有相同的建仓点和不同的平仓点(显然每笔交 易的数量由平仓数量决定)。例如,我们选择deal[ 0 ] 来处理、建立交易:

trade[0]  in 0.3  sell 1.22133 2010.06.15 08:00

然后我们选择 deal[ 1 ] 来平已建仓的交易,并且在平仓期间我们发现平仓数量不够。复制先前建仓的交易,并在其 "volume'(交易量)参数中指定缺乏数量。之后用成交数量平初始交易(即我们用平仓数量更改建仓时指定的初始交易数量)。

trade[0]  in 0.2  sell 1.22133 2010.06.15 08:00   out 1.22145 2010.06.15 08:01   trade[1]  in 0.1  sell 1.22133 2010.06.15 08:00

此类转变可能不适合某交易者,因为该交易者可能想平仓另一 交易,而不是此交易。但是无论如何,纠正转变并不会危害系统的评估。唯一能危害的是交易者对在 MetaTrader 4 中进行交易而没有损失交易的信心;这个重新计算系统将展现所有错觉。

在Bulachev的书中所描述的统计性解释系统并没有情 感,并且能够从进入、退出仓位以及整体评价忠实地评估决策。解释转变的可能性(从一个系统转变到另一系统而不损失数据)证明声称不能针对 MetaTrader 5 的解释系统改造为 MetaTrader 4 开发的 MTS 是错误的。转变解释时唯一的损失可能是数量对不同订单的归属关系 (MetaTrader 4)。但是,如果没有更多要统计的订单(使用此术语的旧含义),则这只是交易者的主观估计。

用于转变解释的代码

让我们看一看代码本身。为了准备转换程序,我们需要 OOP 的继 承功能。这是为什么我建议那些已经精通的人仍然要打开MQL5 用户指南并学习理论的原因。首先,让我们描述一下成交解释的结构(我 们可以通过直接使用 MQL5 的标准函数获取这些值来加速代码编写,但是这样会造成可读性降低,从而可能导致您产生混淆)。

//+------------------------------------------------------------------+
//| 成交的结构                                                        |
//+------------------------------------------------------------------+
struct S_Stat_Deals
  {
public:
   ulong             DTicket;         // 成交的订单号   
   ENUM_DEAL_TYPE     deals_type;      // 成交的类型
   ENUM_DEAL_ENTRY   deals_entry;     // 成交的方向 
   double            deals_volume;    // 成交量    
   double            deals_price;     // 成交建仓价   
   datetime          deals_date;      // 成交建仓时间  
                     S_Stat_Deals(){};
                    ~S_Stat_Deals(){};
  };

此结构包含有关成交的所有主要细节,不包含派生细节,因为 我们可以在必要时计算派生细节。因为开发人员已经在策略测试程序中实施了Bulachev统计的很多方法,仅需要我们用自定义方法进行补充即可。接着,让 我们实施整体交易效率、建仓效率和平仓效率等方法。

为了获取这些值,我们需要实施主要信息的解释,例如一笔交 易期间的建仓价/平仓价、建仓时间/平仓时间、最低价/最高价。如果我们拥有此类主要信息,则我们可以获得很多派生信息。我还希望您注意下述交易的结构, 它是主结构,解释的所有转换都以其为基础。

//+------------------------------------------------------------------+
//| 交易结构                                                          |
//+------------------------------------------------------------------+
struct S_Stat_Trades
  {
public:
   ulong             OTicket;         // 建仓交易订单号
   ulong             CTicket;         // 平仓交易订单号     
   ENUM_DEAL_TYPE     trade_type;     // 交易类型
   double            trade_volume;    // 交易量
   double            max_price_trade; // 交易最高价位
   double            min_price_trade; // 交易最低价位
   double            enter_price;     // 交易建仓价位
   datetime          enter_date;      // 交易建仓时间
   double            exit_price;      // 交易平仓价位
   datetime          exit_date;       // 交易平仓时间
   double            enter_efficiency;// 进入效率
   double            exit_efficiency; // 退出效率
   double            trade_efficiency;// 交易效率
                     S_Stat_Trades(){};
                    ~S_Stat_Trades(){};
  };

现在,我们已经创建了两个主结构,我们可以定义新的类C_Pos, 该类转换解释。首先,让我们声明成交和交易解释的结构指针。因为信息可能在继承的函数中是必不可少的,将其声明为public(公 共);并且因为可能有很多的成交和交易,使用一个数组而不是变量来作为结构指针。因此,可以从任何地方存取构造的信息。

之后,我们需要将历史记录分为不同的仓位,并在每个仓位内 作为一个完整的交易循环执行所有转换。为此,声明仓位属性的解释变量(仓位 id、仓位交易品种、成 交次数、交易次数)。

//+------------------------------------------------------------------+
//| 把成交转换为交易的类                                                |
//+------------------------------------------------------------------+
class C_Pos
  {
public:
   S_Stat_Deals      m_deals_stats[];  // 成交结构
   S_Stat_Trades     m_trades_stats[]; // 交易结构
   long              pos_id;          // 仓位编号
   string            symbol;          // 仓位交易品种
   int               count_deals;     // 成交数量
   int               count_trades;    // 交易数量
   int               trades_ends;     // 完成的交易数量
   int               DIGITS;          // 仓位交易品种最小交易量的精确度  
                     C_Pos()
     {
      count_deals=0;
      count_trades=0;
      trades_ends=0;
     };
                    ~C_Pos(){};
   void              OnHistory();         // 建立仓位历史
   void              OnHistoryTransform();// 把仓位历史转换为新的系统解释
   void              efficiency();        // 根据 Bulachev's 方法计算的效率值
private:
   void              open_pos(int c);
   void              copy_pos(int x);
   void              close_pos(int i,int c);
   double            nd(double v){return(NormalizeDouble(v,DIGITS));};// 规范化最小交易量
   void              DigitMinLots(); // 最小交易量精确度
   double            iHighest(string          symbol_name,// 交易品种名称
                              ENUM_TIMEFRAMES  timeframe,  // 周期
                              datetime         start_time, // 开始时间
                              datetime         stop_time   // 结束时间
                              );
   double            iLowest(string          symbol_name,// 交易品种名称
                             ENUM_TIMEFRAMES  timeframe,  // 周期
                             datetime         start_time, // 开始时间
                             datetime         stop_time   // 结束时间
                             );
  };

类具有三个用于处理仓位的公共方法。

OnHistory()创建仓位历史记录:
//+------------------------------------------------------------------+
//| 填充历史成交的结构                                                  |
//+------------------------------------------------------------------+
void C_Pos::OnHistory()
  {
   ArrayResize(m_deals_stats,count_deals);
   for(int i=0;i<count_deals;i++)
     {
      m_deals_stats[i].DTicket=HistoryDealGetTicket(i);
      m_deals_stats[i].deals_type=(ENUM_DEAL_TYPE)HistoryDealGetInteger(m_deals_stats[i].DTicket,DEAL_TYPE);   // 成交类型
      m_deals_stats[i].deals_entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(m_deals_stats[i].DTicket,DEAL_ENTRY);// 成交方向
      m_deals_stats[i].deals_volume=HistoryDealGetDouble(m_deals_stats[i].DTicket,DEAL_VOLUME);              // 成交量
      m_deals_stats[i].deals_price=HistoryDealGetDouble(m_deals_stats[i].DTicket,DEAL_PRICE);                // 建仓价格
      m_deals_stats[i].deals_date=(datetime)HistoryDealGetInteger(m_deals_stats[i].DTicket,DEAL_TIME);        // 建仓时间
     }
  };

对于每笔成交,该方法创建结构副本,并向结构副本填以成交 信息。这是我在前文中说我们可以在不使用它的情况下也能做到,但它会更加方便的确切含义(追求微秒级时间减少的人可以用与等号右边相同的行来代替这些结构 的调用)。

OnHistoryTransform() 将仓位历史记录转换为新的解释系统:

我建议必要时在任何地方都使用这种方法;每次出现新的解释 对象时就分配一次内存。如果没有所需内存量的精确信息,则我们需要将其分配到一个近似值。在任何情况下,它都比在每一步重新分配整个数组要更加经济。

接着是在其中使用三个筛选对仓位中的所有成交进行筛选的循 环:如果成交是 in(买 入)、in/out(买入/卖出)、out(卖出)。对每种类型实施具体的操 作。筛选是按顺序、嵌套进行的。换言之,如果一个筛选返回 false (错),则只有在这种情况下我们才检查下一筛选。此类构造在资源方面很经济,因为省掉了不必要的操作。要使代码的可读性更好, 很多操作都在类中作为private(私人)函数声明。通过这种方式,这些函数在开发期间为public函数,但是我进一 步认识到在代码的其他部分不需要它们,因此它们将被重新声明为 private 函数。在 OOP 中处理数据的作用范围就是如此容易。

因此,在 in 筛选中执行新交易的创建(函数 open_pos()),这是为什么我们将指针数组的大小加 1 并且将成交的结构复制到交易结构的对应字段的原因。此外,因为交易结构有两倍的价格和时间字段,则在建立交易时仅填写建仓字段,因此将其作为未完成的交易 来统计;您可以通过 count_trades trades_ends 的差异来理解这一点。问题是计数器在开始时为零值。只要交易出现,count_trades 计数器的值增大,当交易平仓时,trades_ends 计数器的值增大。因此,count_trades trades_ends 之差可告诉您任何时候有多少交易未平仓。

函数 open_pos() 非 常简单,它仅建立交易并触发对应的计数器;其他此类函数就并非如此简单了。因此,如果一个成交不是 in 类型,则它要么是 in/out 类型,要么是 out 类型。对于这两种类型,首先检查执行更容易的一个(这不是一个基础性问题,但是我已经按照执行由易到难的顺序建立检查)。

处理 in/out 筛选的函数按所有未平仓的交易汇总未平仓位(我已经提过如何使用 count_trades trades_ends 之差来确定哪些交易未平仓)。因此,我们通过给定的成交计算已平仓的总数量(剩下的数量将被重新建仓,但是类型为当前成交的类型)。在这里,我们需要指出 成交的方向为 in/out, 这意味着数量不会超过先前未平仓位的总数量。这是为什么计算仓位和 in/out 成交之差,从而知道要重新建仓的新交易的数量是符合逻辑的原因。

如果成交的方向为 out,则所有事情都变得更加复 杂。首先,仓位中的最后一笔成交的方向始终为out, 因此我们在这里可以做出一个例外 - 如果是最后一笔成交,则平我们拥有的一切仓位。否则(如果成交不是最后一笔)有两种情 形。因为成交方向不是 in/out 而是 out, 则情形如下:第一种情形是数量与正在建仓的成交完全相同,即正在建仓的成交的数量等于正在平仓的成交的数量;第二种情形是这两个数量不相等。

第一种情形通过平仓来解决。第二种情形更加复杂,又有两种 情形:数量大于正在建仓的成交以及数量小于正在建仓的成交。数量较大时,平仓下一交易,直到正在平仓的数量等于或小于正在建仓的数量。如果数量不足以平仓 下一整个交易(数量不足),则意味着部分平仓。在这里,我们需要用新的数量平仓交易(在先前的操作之后留下的数量),但是在此之前,创建一个含有缺少数量 的交易的副本。当然,不要忘记了计数器。

在交易中,可能出现这样的情形,即在重新建仓一个交易后在 部分平仓时已经有足够的后续交易。为了避免混淆,所有这些交易都应偏移一个位置,以保持平仓的时间顺序。

//+------------------------------------------------------------------+
//| 把成交转换为交易的类 (引擎类)                                        |
//+------------------------------------------------------------------+
void C_Pos::OnHistoryTransform()
  {
   DigitMinLots();// 填充DIGITS值
   count_trades=0;trades_ends=0;
   ArrayResize(m_trades_stats,count_trades,count_deals);
   for(int c=0;c<count_deals;c++)
     {
      if(m_deals_stats[c].deals_entry==DEAL_ENTRY_IN)
        {
         open_pos(c);
        }
      else// else in
        {
         double POS=0;
         for(int i=trades_ends;i<count_trades;i++)POS+=m_trades_stats[i].trade_volume;
         if(m_deals_stats[c].deals_entry==DEAL_ENTRY_INOUT)
           {
            for(int i=trades_ends;i<count_trades;i++)close_pos(i,c);
            trades_ends=count_trades;
            open_pos(c);
            m_trades_stats[count_trades-1].trade_volume=m_deals_stats[c].deals_volume-POS;
           }
         else// else in/out
           {
            if(m_deals_stats[c].deals_entry==DEAL_ENTRY_OUT)
              {
               if(c==count_deals-1)// 如果是最后一个成交
                 {
                  for(int i=trades_ends;i<count_trades;i++)close_pos(i,c);
                  trades_ends=count_trades-1;
                 }
               else// 如果不是最后一个成交
                 {
                  double out_vol=nd(m_deals_stats[c].deals_volume);
                  while(nd(out_vol)>0)
                    {
                     if(nd(out_vol)>=nd(m_trades_stats[trades_ends].trade_volume))
                       {
                        close_pos(trades_ends,c);
                        out_vol-=nd(m_trades_stats[trades_ends].trade_volume);
                        trades_ends++;
                       }
                     else// 如果平仓后剩余量小于下一次交易
                       {
                        // 把所有交易向前移动一位
                        count_trades++;
                        ArrayResize(m_trades_stats,count_trades);
                        for(int x=count_trades-1;x>trades_ends;x--)copy_pos(x);
                        // 建仓大小等于当前剩余量和下次交易量之间的差
                        m_trades_stats[trades_ends+1].trade_volume=nd(m_trades_stats[trades_ends].trade_volume-out_vol);
                        // 平仓量等于剩余值
                        close_pos(trades_ends,c);
                        m_trades_stats[trades_ends].trade_volume=nd(out_vol);
                        out_vol=0;
                        trades_ends++;
                       }
                    }// while(out_vol>0)
                 }// 如果不是最后一个订单
              }// if out
           }// else in/out
        }// else in
     }
  };


效率的计算

一旦转换了解释系统,我们就可以按 Bulachev 的方法计算交易的效率。对此类评估必不可少的函数位于 efficiency() 方法中,也在此处用计算出来的数据填写交易结构。进入和退出的效率在 0 1 的范围内,而整个交易的效率在 -1 1 的范围内。

//+------------------------------------------------------------------+
//| 计算效率值                                                         |
//+------------------------------------------------------------------+
void C_Pos::efficiency()
  {
   for(int i=0;i<count_trades;i++)
     {
      m_trades_stats[i].max_price_trade=iHighest(symbol,PERIOD_M1,m_trades_stats[i].enter_date,m_trades_stats[i].exit_date); // 交易最高价
      m_trades_stats[i].min_price_trade=iLowest(symbol,PERIOD_M1,m_trades_stats[i].enter_date,m_trades_stats[i].exit_date);  // 交易最低价
      double minimax=0;
      minimax=m_trades_stats[i].max_price_trade-m_trades_stats[i].min_price_trade;// 最高最低价的差
      if(minimax!=0)minimax=1.0/minimax;
      if(m_trades_stats[i].trade_type==DEAL_TYPE_BUY)
        {
         //进入效率
         m_trades_stats[i].enter_efficiency=(m_trades_stats[i].max_price_trade-m_trades_stats[i].enter_price)*minimax;
         //退出效率
         m_trades_stats[i].exit_efficiency=(m_trades_stats[i].exit_price-m_trades_stats[i].min_price_trade)*minimax;
         //交易效率
         m_trades_stats[i].trade_efficiency=(m_trades_stats[i].exit_price-m_trades_stats[i].enter_price)*minimax;
        }
      else
        {
         if(m_trades_stats[i].trade_type==DEAL_TYPE_SELL)
           {
            //进入效率
            m_trades_stats[i].enter_efficiency=(m_trades_stats[i].enter_price-m_trades_stats[i].min_price_trade)*minimax;
            //退出效率
            m_trades_stats[i].exit_efficiency=(m_trades_stats[i].max_price_trade-m_trades_stats[i].exit_price)*minimax;
            //交易效率
            m_trades_stats[i].trade_efficiency=(m_trades_stats[i].enter_price-m_trades_stats[i].exit_price)*minimax;
           }
        }
     }
  }

该方法使用两个私有方法 iHighest() 和 iLowest(),它们是类似的,唯一的差别在于请求的数据和搜索函数 fminfmax

//+------------------------------------------------------------------+
//| 在 start_time --> stop_time  时间段中寻找最大值                     |
//+------------------------------------------------------------------+
double C_Pos::iHighest(string           symbol_name,// 交易品种名称
                       ENUM_TIMEFRAMES  timeframe,  // 周期
                       datetime         start_time, // 开始时间
                       datetime         stop_time   // 结束时间
                       )
  {
   double  buf[];
   datetime  start_t=(start_time/60)*60;// 规范化开始时间
   datetime  stop_t=(stop_time/60+1)*60;// 规范化结束时间  
   int period=CopyHigh(symbol_name,timeframe,start_t,stop_t,buf);
   double res=buf[0];
   for(int i=1;i<period;i++)
      res=fmax(res,buf[i]);
   return(res);
  }

方法搜索两个指定日期之间的最大值。日期作为start_timestop_time 参数传递给函数。因为交易的日期被传递给函数,并且交易请求甚至可能在1分钟时间条的中间出现,将日期标准化到最接近的值是在函数中执行的。在 iLowest() 函数中也是如此。使用已开发的方法 efficiency(),我们拥有处理仓位的整个功能;但是仍然不能处理仓位本身。让我们通过确定一个新的类来解决此问题,先前的所有方法都可以用于该 类;换言之,将其声明为 C_Pos派 生

派生类(工程类)

class C_PosStat:public C_Pos

为了考虑统计信息,创建一个要指定给新类的结构。

//+------------------------------------------------------------------+
//| 效率值的结构                                                       |
//+------------------------------------------------------------------+
struct S_efficiency
  {
   double            enter_efficiency; // 进入效率
   double            exit_efficiency;  // 退出效率
   double            trade_efficiency; // 交易效率
                     S_efficiency()
     {
      enter_efficiency=0;
      exit_efficiency=0;
      trade_efficiency=0;
     };
                    ~S_efficiency(){};
  };


类本身的主体如下:

//+------------------------------------------------------------------+
//| 整体上的交易统计类                                                  |
//+------------------------------------------------------------------+
class C_PosStat:public C_Pos
  {
public:
   int               PosTotal;         // 历史中的仓位数量
   C_Pos             pos[];            // 仓位的指针数组
   int               All_count_trades; // 历史中交易的总数
   S_efficiency      trade[];          // 效率结构的指针数组
   S_efficiency      avg;              // 效率平均值的结构
   S_efficiency      stdev;            // 效率标准偏差值
                                       // 根据效率值和其平均值计算
                     C_PosStat(){PosTotal=0;};
                    ~C_PosStat(){};
   void              OnPosStat();                         // 引擎类
   void              OnTradesStat();                      // 收集交易信息并放入数组中
      // 把信息写进文件的函数
   void              WriteFileDeals(string folder="deals");
   void              WriteFileTrades(string folder="trades");
   void              WriteFileTrades_all(string folder="trades_all");
   void              WriteFileDealsHTML(string folder="deals");
   void              WriteFileDealsHTML2(string folder="deals");
   void              WriteFileTradesHTML(string folder="trades");
   void              WriteFileTradesHTML2(string folder="trades");
   string            enum_translit(ENUM_DEAL_ENTRY x,bool latin=true);// 把枚举转换为字符串
   string            enum_translit(ENUM_DEAL_TYPE x,bool latin=true);                       // 把枚举转换为字符串(重载)
private:      S_efficiency      AVG(int count);                                        // 算数平均
   S_efficiency      STDEV(const S_efficiency &mo,int count);               // 标准偏差
   S_efficiency      add(const S_efficiency &a,const S_efficiency &b);      //加
   S_efficiency      take(const S_efficiency &a,const S_efficiency &b);     //减
   S_efficiency      multiply(const S_efficiency &a,const S_efficiency &b); //乘
   S_efficiency      divided(const S_efficiency &a,double b);               //除
   S_efficiency      square_root(const S_efficiency &a);                    //平方根
   string            Head_style(string title);
  };  

我建议按相反顺序,即从末尾到开头分析此类。所有一切都以 将一个成交和交易表写入文件来结束。为此缩写了一组函数(您可以从名称理解各个函数的用途)。这些函数创建有关成交和交易的csv报告以及两种类型的 html报告(仅视觉效果不同,而内容相同)。

      void              WriteFileDeals();      // 写成交的csv报告
      void              WriteFileTrades();     // 写交易的csv报告
      void              WriteFileTrades_all(); // 写健康函数的综合csv报告
      void              WriteFileDealsHTML2(); // 写成交的html报告, 格式1
      void              WriteFileTradesHTML2();// 写交易的html报告, 格式2

enum_translit() 函数用于将枚 举值转换为字符串类型以将它们写入到日志文件。private部分包含 S_efficiency 结构的几个函数。所有函数弥补了语言的不足之处,特别是结构的算术运算。因为有关实施这些方法的意见各有不同,因此也有不同的方法来实现它们。我已经用结 构的字段通过算术运算方法来实现它们。有些人可能会说使用单个的方法来处理结构的每个字段可能更好。整体而言,我会说,有多少程序员就会有多少种意见。我 希望在将来,我们能够使用内置方法执行此类运算。

AVG() 方法计算所传递数组的算术平均值,但是它不显示整体分布,这是为什么它与计算标准偏差的另一方法 STDEV() 一起提供的原因。OnTradesStat() 函数获取效率值(以前在 OnPosStat() 中计算),并且用统计方法处理它们。最后是类的主函数 - OnPosStat()。

应详细考虑此函数。它包含两个部分,因此可以轻易地被分 开。第一部分搜索所有仓位并处理它们的id,将其保存到临时数组 id_pos 中。逐步:选择整个可用历史记录,计算成交次数,运行处理成交的循环。循环:如果成交类型是平衡,则跳过(无需解释开始成交),否则,将仓位的 id 保存到变量并执行搜索。如果相同的 id 已经存在于资料库(id_pos 数组)中,则前往下一成交,否则将 id 写入资料库。通过这种方式,在处理完所有成交之后,我们拥有了包含所有现有仓位的 id 和仓位数量的数组。

   long  id_pos[];// 创建仓位历史的辅助数组
   if(HistorySelect(0,TimeCurrent()))
     {
      int HTD=HistoryDealsTotal();
      ArrayResize(id_pos,PosTotal,HTD);
      for(int i=0;i<HTD;i++)
        {
         ulong DTicket=(ulong)HistoryDealGetTicket(i);
         if((ENUM_DEAL_TYPE)HistoryDealGetInteger(DTicket,DEAL_TYPE)==DEAL_TYPE_BALANCE)
            continue;// 如果是平衡成交,跳过它
         long id=HistoryDealGetInteger(DTicket,DEAL_POSITION_ID);
         bool present=false; // 初始状态,没有这样的仓位           
         for(int j=0;j<PosTotal;j++)
           { if(id==id_pos[j]){ present=true; break; } }// 如果已经存在这样的仓位 break
         if(!present)// 新仓位出现时写下编号
           {
            PosTotal++;
            ArrayResize(id_pos,PosTotal);
            id_pos[PosTotal-1]=id;
           }
        }
     }
   ArrayResize(pos,PosTotal);

在第二部分,我们实现先前在基类 C_Pos 中描述的所有方法。它包含一个循环,遍历仓位并运行相应的仓位处理方法。在下面的代码中提供了有关方法的描述。

   for(int p=0;p<PosTotal;p++)
     {
      if(HistorySelectByPosition(id_pos[p]))// 选择仓位
        {
         pos[p].pos_id=id_pos[p]; // 把仓位编号赋值给C_Pos类的对应栏位
         pos[p].count_deals=HistoryDealsTotal();// 把仓位的成交数量赋值给C_Pos类的对应栏位
         pos[p].symbol=HistoryDealGetString(HistoryDealGetTicket(0),DEAL_SYMBOL);// 和交易品种行为相同
         pos[p].OnHistory();          // 在结构中填写仓位历史
         pos[p].OnHistoryTransform(); // 转换解释,填写结构.
         pos[p].efficiency();         // 计算获得数据的效率
         All_count_trades+=pos[p].count_trades;// 为显示总交易数保存交易数量
        }
     }

调用类的方法

现在,我们已经考虑了整个类。让我们提供一个有关调用的例 子。为了保持构造的可能性,我没有在一个函数中声明显式调用。此外,您可以依据您的需要增加类,实施新的数据统计处理方法。以下是从脚本调用类的方法的一 个例子:

//+------------------------------------------------------------------+
//| 脚本程序起始函数                                                   |
//+------------------------------------------------------------------+
#include <Bulaschev_Statistic.mqh> void OnStart()
  {
   C_PosStat  start;
   start.OnPosStat();
   start.OnTradesStat();
   start.WriteFileDeals();
   start.WriteFileTrades();
   start.WriteFileTrades_all();
   start.WriteFileDealsHTML2();
   start.WriteFileTradesHTML2();
   Print("cko tr ef=" ,start.stdev.trade_efficiency);
   Print("mo  tr ef=" ,start.avg.trade_efficiency);
   Print("cko out ef=",start.stdev.exit_efficiency);
   Print("mo  out ef=",start.avg.exit_efficiency);
   Print("cko in ef=" ,start.stdev.enter_efficiency);
   Print("mo  in ef=" ,start.avg.enter_efficiency);
  }

脚本依据函数的数量创建 5 个报告文件,这些函数将数据写入 Files\OnHistory 目录中的文件。在这里提供了以下主函数 - OnPosStat() OnTradesStat (),它们用于调用所有必需的方法。脚本以打印获得的整体交易效率值结束。这些值中的每一个值都可用于基因优化。

因为在优化期间无需将每个报告都写到一个文件,在EA(专 家顾问)交易程序中类的调用看起来有所不同。首先,与脚本相比,EA交易程序可以在测试程序中运行(这是我们准备的目的)。在策略测试程序中工作有其先决 条件。在进行优化时,我们能够使用OnTester() 函数,并且该函数的执行先于OnDeinit() 函数。因此,可以分开调用转换的主方法。为了便于从 EA 交易程序的参数修改适当性函数,我声明了一个全局枚举,而不是作为类的一部分。并且,该枚举与类C_PosStat 的方法在同一层级。

//+------------------------------------------------------------------+
//| 适当性函数的枚举                                                    |
//+------------------------------------------------------------------+
enum Enum_Efficiency
  {
   avg_enter_eff,
   stdev_enter_eff,
   avg_exit_eff,
   stdev_exit_eff,
   avg_trade_eff,
   stdev_trade_eff
  };

这是应添加到EA交易程序的头部的内容。

#include <Bulaschev_Statistic.mqh>
input Enum_Efficiency result=0;// 适当性函数


现在,我们可以只描述如何使用switch 运算符传递必需的参数。

//+------------------------------------------------------------------+
//| EA优化函数                                                        |
//+------------------------------------------------------------------+
double OnTester()
  {
   start.OnPosStat();
   start.OnTradesStat();
   double res;
   switch(result)
     {
      case 0: res=start.avg.enter_efficiency;   break;
      case 1: res=-start.stdev.enter_efficiency; break;
      case 2: res=start.avg.exit_efficiency;     break;
      case 3: res=-start.stdev.exit_efficiency;  break;
      case 4: res=start.avg.trade_efficiency;    break;
      case 5: res=-start.stdev.trade_efficiency; break;
      default : res=0; break;
     }  
   return(res);
  }

我想提醒您注意,函数 OnTester() 用于自定义函数的最大值。如果您需要查找自定义函数的最小值,则最好将函数本身乘以-1。与标准偏差示例类似,每 个人都理解标准偏差越小,交易效率之间的差异就越小,因此交易的稳定性就越高。这是标准偏差应该最小化的原因之所在。现在,我们已经处理了类方法的调用, 让我们考虑将报告写入一个文件。

我在先前已经提过用于创建报告的类方法。现在,我们将看一 看在哪里以及何时应该调用它们。报告只应该在为了单次运行而启动 EA 交易程序时才创建。否则,EA交易程序将会以优化模式创建文件;即不是创建一个文件,而是创建很多文件(如果每次传递不同的文件名),或者只创建一个文 件,但是所有运行都创建相同名称的文件,这样绝对没有意义,因为它会浪费用于处理以后要擦除的信息的资源。

无论如何,您不应在优化期间创建报告文件。如果您获得很多 具有不同名称的文件,或许您并不会打开它们中的大多数。第二种情形会浪费用于获取要马上删除的信息的资源。

这是为什么最佳情形是进行筛选(仅在 Optimization[disabled](禁 用优化) 模式中开始报告)因此,不会将绝对不会被查看的报告写入 HDD。而且,优化速度也会加快(文件操作是最慢的操作并不是一个秘密);此外,仍然保留了快速获取含有必要参数的报告的可能性。实际上,在哪里放置筛选 并不重要,在OnTester或在OnDeinit 函数中皆可。重点是创建报告的类方法应在执行转换的主方法之后调用。我已经将筛选放在 OnDeinit() 中,从而不会使代码重复:

//+------------------------------------------------------------------+
//| EA 去初始化函数                                                    |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(!(bool)MQL5InfoInteger(MQL5_OPTIMIZATION))
     {
      start.WriteFileDeals();      // 写成交的csv报告
      start.WriteFileTrades();     // 写交易的csv报告
      start.WriteFileTrades_all(); // 用适当性函数写综合csv报告
      start.WriteFileDealsHTML2(); // 写成交的html报告
      start.WriteFileTradesHTML2();// 写交易的html报告
     }
  }
//+------------------------------------------------------------------+

调用方法的顺序并不重要。创建报告所需的一切都在 OnPosStat 和OnTradesStat 方法中准备。同样地,您调用全部报告写入方法还是调用部分报告写入方法也不重要;每个报告写入方法都是单独操作的;它是对已经存储于类中的信息的解释。

在策略测试程序中检查

下面给出在策略测试程序中单次运行的结果:

交易报告移动平均线统计
# 单证 类型  交易量 建仓 平仓 价格 效率
建仓 平仓 价格 时间 价格 时间 最高 最低 进入 退出 交易
pos[0] id 2 EURUSD
0 2 3 0.1 1.37203 2010.03.15 13:00:00 1.37169 2010.03.15 14:00:00 1.37236 1.37063 0.19075 0.61272 -0.19653
pos[1] id 4 EURUSD
1 4 5 0.1 1.35188 2010.03.23 08:00:00 1.35243 2010.03.23 10:00:00 1.35292 1.35025 0.61049 0.18352 -0.20599
pos[2] id 6 EURUSD
2 6 7 0.1 1.35050 2010.03.23 12:00:00 1.35343 2010.03.23 16:00:00 1.35600 1.34755 0.34911 0.30414 -0.34675
pos[3] id 8 EURUSD
3 8 9 0.1 1.35167 2010.03.23 18:00:00 1.33343 2010.03.26 05:00:00 1.35240 1.32671 0.97158 0.73842 0.71000
pos[4] id 10 EURUSD
4 10 11 0.1 1.34436 2010.03.30 16:00:00 1.33616 2010.04.08 23:00:00 1.35904 1.32821 0.52384 0.74213 0.26597
pos[5] id 12 EURUSD
5 12 13 0.1 1.35881 2010.04.13 08:00:00 1.35936 2010.04.15 10:00:00 1.36780 1.35463 0.68261 0.35915 0.04176
pos[6] id 14 EURUSD
6 14 15 0.1 1.34735 2010.04.20 04:00:00 1.34807 2010.04.20 10:00:00 1.34890 1.34492 0.61055 0.20854 -0.18090
pos[7] id 16 EURUSD
7 16 17 0.1 1.34432 2010.04.20 18:00:00 1.33619 2010.04.23 17:00:00 1.34491 1.32016 0.97616 0.35232 0.32848
pos[8] id 18 EURUSD
8 18 19 0.1 1.33472 2010.04.27 10:00:00 1.32174 2010.04.29 05:00:00 1.33677 1.31141 0.91916 0.59267 0.51183
pos[9] id 20 EURUSD
9 20 21 0.1 1.32237 2010.05.03 04:00:00 1.27336 2010.05.07 20:00:00 1.32525 1.25270 0.96030 0.71523 0.67553

效率报告
适当性函数 平均值 标准偏差
进 入 0.68 0.26
退 出 0.48 0.21
交 易 0.16 0.37

平衡图为:

在图表中,您可以清楚地看到,自定义优化函数并没有试图选 择具有更大成交数量的参数,而是选择具有较长持续时间的成交,而且成交具有几乎相同的利润,即离差并不大。

因为移动平均线的代码并不包含仓位增加 数量或部分平仓的功能,因此转换结果看起来并不接近以上描述的。在下面,您可以在专为测试代码而开立的帐户中找到启动脚本的另一结果:

pos[286] id 1019514 EURUSD
944 1092288 1092289 0.1 1.26733 2010.07.08 21:14:49 1.26719 2010.07.08 21:14:57 1.26752 1.26703 0.38776 0.32653 -0.28571
pos[287] id 1019544 EURUSD
945 1092317 1092322 0.2 1.26761 2010.07.08 21:21:14 1.26767 2010.07.08 21:22:29 1.26781 1.26749 0.37500 0.43750 -0.18750
946 1092317 1092330 0.2 1.26761 2010.07.08 21:21:14 1.26792 2010.07.08 21:24:05 1.26782 1.26749 0.36364 -0.30303 -0.93939
947 1092319 1092330 0.3 1.26761 2010.07.08 21:21:37 1.26792 2010.07.08 21:24:05 1.26782 1.26749 0.36364 -0.30303 -0.93939
pos[288] id 1019623 EURUSD
948 1092394 1092406 0.1 1.26832 2010.07.08 21:36:43 1.26843 2010.07.08 21:37:38 1.26882 1.26813 0.72464 0.43478 0.15942
pos[289] id 1019641 EURUSD
949 1092413 1092417 0.1 1.26847 2010.07.08 21:38:19 1.26852 2010.07.08 21:38:51 1.26910 1.26829 0.77778 0.28395 0.06173
950 1092417 1092433 0.1 1.26852 2010.07.08 21:38:51 1.26922 2010.07.08 21:39:58 1.26916 1.26829 0.26437 -0.06897 -0.80460
pos[290] id 1150923 EURUSD
951 1226007 1226046 0.2 1.31653 2010.08.05 16:06:20 1.31682 2010.08.05 16:10:53 1.31706 1.31611 0.55789 0.74737 0.30526
952 1226024 1226046 0.3 1.31632 2010.08.05 16:08:31 1.31682 2010.08.05 16:10:53 1.31706 1.31611 0.77895 0.74737 0.52632
953 1226046 1226066 0.1 1.31682 2010.08.05 16:10:53 1.31756 2010.08.05 16:12:49 1.31750 1.31647 0.33981 -0.05825 -0.71845
954 1226046 1226078 0.2 1.31682 2010.08.05 16:10:53 1.31744 2010.08.05 16:15:16 1.31750 1.31647 0.33981 0.05825 -0.60194
pos[291] id 1155527 EURUSD
955 1230640 1232744 0.1 1.31671 2010.08.06 13:52:11 1.32923 2010.08.06 17:39:50 1.33327 1.31648 0.01370 0.24062 -0.74568
956 1231369 1232744 0.1 1.32584 2010.08.06 14:54:53 1.32923 2010.08.06 17:39:50 1.33327 1.32518 0.08158 0.49938 -0.41904
957 1231455 1232744 0.1 1.32732 2010.08.06 14:58:13 1.32923 2010.08.06 17:39:50 1.33327 1.32539 0.24492 0.51269 -0.24239
958 1231476 1232744 0.1 1.32685 2010.08.06 14:59:47 1.32923 2010.08.06 17:39:50 1.33327 1.32539 0.18528 0.51269 -0.30203
959 1231484 1232744 0.2 1.32686 2010.08.06 15:00:20 1.32923 2010.08.06 17:39:50 1.33327 1.32539 0.18655 0.51269 -0.30076
960 1231926 1232744 0.4 1.33009 2010.08.06 15:57:32 1.32923 2010.08.06 17:39:50 1.33327 1.32806 0.38964 0.77543 0.16507
961 1232591 1232748 0.4 1.33123 2010.08.06 17:11:29 1.32850 2010.08.06 17:40:40 1.33129 1.32806 0.98142 0.86378 0.84520
962 1232591 1232754 0.4 1.33123 2010.08.06 17:11:29 1.32829 2010.08.06 17:42:14 1.33129 1.32796 0.98198 0.90090 0.88288
963 1232591 1232757 0.2 1.33123 2010.08.06 17:11:29 1.32839 2010.08.06 17:43:15 1.33129 1.32796 0.98198 0.87087 0.85285
pos[292] id 1167490 EURUSD
964 1242941 1243332 0.1 1.31001 2010.08.10 15:54:51 1.30867 2010.08.10 17:17:51 1.31037 1.30742 0.87797 0.57627 0.45424
965 1242944 1243333 0.1 1.30988 2010.08.10 15:55:03 1.30867 2010.08.10 17:17:55 1.31037 1.30742 0.83390 0.57627 0.41017
pos[293] id 1291817 EURUSD
966 1367532 1367788 0.4 1.28904 2010.09.06 00:24:01 1.28768 2010.09.06 02:53:21 1.28965 1.28710 0.76078 0.77255 0.53333

这就是转换后的信息看起来的样子;为了使读者能够对一切事 情深思熟虑(比较出真知),我将成交的原始历史记录保存到一个单独的文件;这是现在很多习惯于在 MetaTrader 4 的 [Results](结果)部分查 看的交易者缺少的历史记录。

总结

在结论中,我想建议开发人员不仅仅是通过自定义参数,还要 将其与标准参数结合在一起,从而增加优化 EA 交易程序的可能性,因为它是与其他优化函数一起完成的。对本文进行总结,我可以说它仅仅包含基本内容,最初的可能性;我希望读者能够依据他们自己的需要对 类进行改善。祝您好运!