自定义交易历史表述并创建报告图表

24 九月 2018, 11:01
Andrey Azatskiy
0
728

概述

金融交易的一个重要方面是监控成效并分析交易历史的能力。 以往的数据可令您跟踪交易动态并评估所用策略的整体成效。 这对所有交易者都很有用: 对于那些手工执行操作和算法交易者。 在本文中,我建议创建实现这类功能的工具。 

所有交易活动的核心部分是形成盈利/亏损曲线的交易算法。 这种算法应可与合成资产进行比较,形成相对于标的资产 (即所交易金融的工具) 的价值。 例如,在期权交易中,Black-Scholes 模型公式用于根据标的资产价格计算此类合成资产。 但没有可用于交易算法的公式。 相应地,可将算法与合成品种的多头仓位进行比较,合成品种的盈亏曲线由算法的编程逻辑形成。 这种 "资产" 形成的利润在不同时期可能并不稳定。 即使可以通过某种财经模型进行评估,该模型也无法统一。 但是如何跟踪这一资产以及我们的交易阶段? 其中一个适当的解决方案是监控算法交易回逆并检测与预期结果的偏差。

我不会就如何分析算法提出建议,但仅提供一套方法,能够显示您的交易历史的完整图片。 基于所获数据,您能够构建复杂的财经模型,计算概率特征并得出各种结论。

本文将分为两个部分。 在第一部分(技术部分)中,我会介绍基于存储在您的终端中的大数据生成交易报告的方法。 此章节介绍用于分析的源数据。 在第二部分,我们将处理主要数值,即,我们将在所选数据上评估交易回逆。 数据采样可以变化: 所有资产或选定的品种,整个可用历史或一段区间。 分析结果将在单独的文件中表述,并在终端中简略显示。

在分析示例中我所用的数据来自我的真实交易历史。 为代码实现示例准备的数据则使用了测试区间,这些数据是我有意在模拟账户上进行交易从而累积的。



第一章 为交易统计分析准备数据

任何分析都从准备源数据开始。 在此,我们处理给定时间区间的交易历史。 MetaTrader 5 终端存储了详细报告。 但有时泛滥的信息和大量细节可能会成为阻碍而非助力。 在本节中,我们将尝试创建一个可读的、简约的、富含信息量的交易历史样本,其可用于进一步处理和分析。

MetaTrader 5 中的交易历史和相关处理方法

通常,在加载交易历史时会产生大量记录。 每笔仓位都包括进场/离场成交,而这些成交又可以在若干个阶段进行。 此外,还存在阶梯式加仓和重新买入的情况。 此外,历史中还有“宣称成交”的特殊行。 这些成交也许包括:

  • 开仓/平仓期间保证金变动的相关操作 (FORTS);
  • 已有持仓的仓位调整;
  • 入金和出金业务。

报告的不便之处还与历史记录按成交时间排序的事实有关。 成交可能具有不同的生存期,所以相关信息的显示顺序可能会与实际平仓顺序不同。 例如,第一笔成交于 6 月 1 日开仓,并于 6 月 5 日平仓。 第二笔在 6 月 2 日开仓,并在同一天平仓。 此笔成交的盈亏了结早于第一笔成交,但在表格中它的顺序靠后,因为它的开仓时间较晚。

相应地,以此格式记录的交易历史不适于交易分析。 然而,它反映了所分析帐户的完整操作历史。 MQL5 拥有一套有用的工具,用于处理所述的数据数组。 我们将运用此工具将数据表述为可读的形式。 首先,我们需要将所有数据排布为交易历史,按交易资产和成交排序。 为此目的,我们创建以下结构:

//+------------------------------------------------------------------+
//| 选定成交的数据结构                                                  |
//+------------------------------------------------------------------+
struct DealData
  {
   long              ticket;            // 成交单号
   long              order;             // 此笔成交的订单编号
   datetime          DT;                // 开仓日期
   long              DT_msc;            // 开仓日期的毫秒值 
   ENUM_DEAL_TYPE    type;              // 开仓类型
   ENUM_DEAL_ENTRY   entry;             // 开仓入场类型
   long              magic;             // 仓位的独有编号
   ENUM_DEAL_REASON  reason;            // 如何下订单
   long              ID;                // 仓位 ID
   double            volume;            // 仓位交易量 (手数)
   double            price;             // 开仓价格
   double            comission;         // 支付佣金
   double            swap;              // 隔夜利息
   double            profit;            // 盈亏
   string            symbol;            // 品名
   string            comment;           // 开仓时指定的注释
   string            ID_external;       // 外部 ID 
  };
//+------------------------------------------------------------------+
//| 存储特定仓位所有成交的结构,                                           |
//| 由 ID 选择                                                        |
//+------------------------------------------------------------------+
struct DealKeeper
  {
   DealData          deals[];           /* 此仓位的所有成交清单(或仓位逆转时的多笔仓位)*/
   string            symbol;            // 品名
   long              ID;                // 仓位的 ID
   datetime          DT_min;            // 开仓日期
   datetime          DT_max;            // 平仓日期
  };

您可以从代码中看到,DealData 结构包含的交易参数描述过多。

我们的主要结构将是 DealKeeper。 它包含仓位描述及其包含的所有成交。 终端中仓位的主要过滤器是其 ID,此 ID 对于该仓位内的所有成交保持不变。 因此,如果一笔仓位被逆转,ID 仍将保留,但方向会改为相反,这就是为什么 DealKeeper 结构将包含两笔仓位。 在我们的代码中会以 ID 过滤仓位并填充 DealKeeper。

我们来详细研讨如何填充结构。 它由 CDealHistoryGetter 类完成,即它的 getHistory 函数:

//+-----------------------------------------------------------------------+
//| 从终端提取交易历史的类,并                                                 |
//| 将其转换为易于分析的视图                                                  |
//+-----------------------------------------------------------------------+
class CDealHistoryGetter
  {
public:
   bool              getHistory(DealKeeper &deals[],datetime from,datetime till);                // 返回所有历史成交的数据  
   bool              getIDArr(ID_struct &ID_arr[],datetime from,datetime till);                  // 返回每笔成交独有 ID 的数组
   bool              getDealsDetales(DealDetales &ans[],datetime from,datetime till);            // 返回成交数组,其中每一行代表一笔特定成交
private:

   void              addArray(ID_struct &Arr[],const ID_struct &value);                          // 加入到动态数组
   void              addArray(DealKeeper &Arr[],const DealKeeper &value);                        // 加入到动态数组
   void              addArray(DealData &Arr[],const DealData &value);                            // 加入到动态数组
   void              addArr(DealDetales &Arr[],DealDetales &value);                              // 加入到动态数组
   void              addArr(double &Arr[],double value);                                         // 加入到动态数组

/*
    如果存在 InOut 类型,则 inputParam 将含有多笔仓位。 
    如果没有 InPut 类型,则 inputParam 只含有一笔仓位! 
*/
   void              getDeals_forSelectedKeeper(DealKeeper &inputParam,DealDetales &ans[]);      // 从 inputParam 所有仓位中选定仓位形成单笔入场
   double            MA_price(double &prices[],double &lots[]);                                  // 计算加权平均开仓价
   bool              isBorderPoint(DealData &data,BorderPointType &type);                        // 获取是否为边界点及该点类型的信息
   ENUM_DAY_OF_WEEK  getDay(datetime DT);                                                        // 获取自开仓日期的天数
   double            calcContracts(double &Arr[],GetContractType type);                          // 获取最后一笔仓位的信息
  };

我们研究它的实现:

//+------------------------------------------------------------------+
//| 返回历史成交的所有数据                                               |
//+------------------------------------------------------------------+
bool CDealHistoryGetter::getHistory(DealKeeper &deals[],datetime from,datetime till)
  {
   ArrayFree(deals);                                     // 清除结果数组
   ID_struct ID_arr[];
   if(getIDArr(ID_arr,from,till))                        // 获取独有 ID
     {
      int total=ArraySize(ID_arr);
      for(int i=0;i<total;i++)                           // 遍历 ID
        {
         DealKeeper keeper;                              // 存储一笔仓位的成交
         keeper.ID=ID_arr[i].ID;
         keeper.symbol=ID_arr[i].Symb;
         keeper.DT_max = LONG_MIN;
         keeper.DT_min = LONG_MAX;
         if(HistorySelectByPosition(ID_arr[i].ID))       // 选择指定 ID 仓位的所有成交
           {

首先,我们需要获得仓位的独有 ID。 为此目的,我们在循环中遍历成交 ID 并形成含有两个字段的结构: Symbol Position ID。 所获数据将满足 getHistory 函数的操作需要。 

MQL5 语言有一个非常有用的函数 HistorySelectByPosition,它依据所传递的 ID 选取完整的成交历史记录。 它将帮助我们从列表中剔除非必要的操作(帐户处置,入金和提款操作等)。 结果就是,所有开仓时的变化形成了历史记录并存储在内存中。 数据按日期和时间排序。 我们只需要使用 HistoryDealsTotal 函数,该函数返回指定 ID 仓位之前写入的成交总数。

int total_2=HistoryDealsTotal();
            for(int n=0;n<total_2;n++)                        // 循环遍历所选成交
              {
               long ticket=(long)HistoryDealGetTicket(n);
               DealData data;
               data.ID=keeper.ID;
               data.symbol=keeper.symbol;
               data.ticket= ticket;

               data.DT=(datetime)HistoryDealGetInteger(ticket,DEAL_TIME);
               keeper.DT_max=MathMax(keeper.DT_max,data.DT);
               keeper.DT_min=MathMin(keeper.DT_min,data.DT);
               data.order= HistoryDealGetInteger(ticket,DEAL_ORDER);
               data.type = (ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket,DEAL_TYPE);
               data.DT_msc=HistoryDealGetInteger(ticket,DEAL_TIME_MSC);
               data.entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket,DEAL_ENTRY);
               data.magic = HistoryDealGetInteger(ticket,DEAL_MAGIC);
               data.reason= (ENUM_DEAL_REASON)HistoryDealGetInteger(ticket,DEAL_REASON);
               data.volume= HistoryDealGetDouble(ticket,DEAL_VOLUME);
               data.price = HistoryDealGetDouble(ticket,DEAL_PRICE);
               data.comission=HistoryDealGetDouble(ticket,DEAL_COMMISSION);
               data.swap=HistoryDealGetDouble(ticket,DEAL_SWAP);
               data.profit=HistoryDealGetDouble(ticket,DEAL_PROFIT);
               data.comment=HistoryDealGetString(ticket,DEAL_COMMENT);
               data.ID_external=HistoryDealGetString(ticket,DEAL_EXTERNAL_ID);

               addArray(keeper.deals,data);                  // 添加成交
              }

            if(ArraySize(keeper.deals)>0)
               addArray(deals,keeper);                       // 添加仓位
           }
        }
      return ArraySize(deals) > 0;
     }
   else
      return false;                                          // 如果没有独有 ID
  }

因此,通过遍历每个独有 ID,我们形成了一个描述仓位的数据数组。 DealKeeper 结构数组反映了当前账户中所请求时间段内的详细交易历史。

准备分析数据

在上一节中,我们获得了数据。 我们把数据导出到文件中。 以下是其中一份所分析仓位的成交清单:

单号 订单 日期时间 日期时间的毫秒值 类型 入场类型 魔幻数字 原因 ID 成交量 价格 佣金 隔夜非 盈利 品名 注释 ID 外键
10761601 69352663 23.11.2017 17:41 1,51146E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 506789 DEAL_REASON_EXPERT 69352663 1 58736 -0,5 0 0 Si-12.17 Open test position 23818051
10761602 69352663 23.11.2017 17:41 1,51146E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 506789 DEAL_REASON_EXPERT 69352663 1 58737 -0,5 0 0 Si-12.17 Open test position 23818052
10766760 0 24.11.2017 13:00 1,51153E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 58682 0 0 -109 Si-12.17 [variation margin close]
10766761 0 24.11.2017 13:00 1,51153E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 58682 0 0 0 Si-12.17 [variation margin open]
10769881 0 24.11.2017 15:48 1,51154E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 58649 0 0 -66 Si-12.17 [variation margin close]
10769882 0 24.11.2017 15:48 1,51154E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 58649 0 0 0 Si-12.17 [variation margin open]
10777315 0 27.11.2017 13:00 1,51179E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 58420 0 0 -458 Si-12.17 [variation margin close]
10777316 0 27.11.2017 13:00 1,51179E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 58420 0 0 0 Si-12.17 [variation margin open]
10780552 0 27.11.2017 15:48 1,5118E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 58417 0 0 -6 Si-12.17 [variation margin close]
10780553 0 27.11.2017 15:48 1,5118E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 58417 0 0 0 Si-12.17 [variation margin open]
10790453 0 28.11.2017 13:00 1,51187E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 58589 0 0 344 Si-12.17 [variation margin close]
10790454 0 28.11.2017 13:00 1,51187E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 58589 0 0 0 Si-12.17 [variation margin open]
10793477 0 28.11.2017 15:48 1,51188E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 58525 0 0 -128 Si-12.17 [variation margin close]
10793478 0 28.11.2017 15:48 1,51188E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 58525 0 0 0 Si-12.17 [variation margin open]
10801186 0 29.11.2017 13:00 1,51196E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 58515 0 0 -20 Si-12.17 [variation margin close]
10801187 0 29.11.2017 13:00 1,51196E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 58515 0 0 0 Si-12.17 [variation margin open]
10804587 0 29.11.2017 15:48 1,51197E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 58531 0 0 32 Si-12.17 [variation margin close]
10804588 0 29.11.2017 15:48 1,51197E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 58531 0 0 0 Si-12.17 [variation margin open]
10813418 0 30.11.2017 13:00 1,51205E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 58843 0 0 624 Si-12.17 [variation margin close]
10813419 0 30.11.2017 13:00 1,51205E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 58843 0 0 0 Si-12.17 [variation margin open]
10816400 0 30.11.2017 15:48 1,51206E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 58609 0 0 -468 Si-12.17 [variation margin close]
10816401 0 30.11.2017 15:48 1,51206E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 58609 0 0 0 Si-12.17 [variation margin open]
10824628 0 01.12.2017 13:00 1,51213E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 58864 0 0 510 Si-12.17 [variation margin close]
10824629 0 01.12.2017 13:00 1,51213E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 58864 0 0 0 Si-12.17 [variation margin open]
10828227 0 01.12.2017 15:48 1,51214E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 58822 0 0 -84 Si-12.17 [variation margin close]
10828228 0 01.12.2017 15:48 1,51214E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 58822 0 0 0 Si-12.17 [variation margin open]
10838074 0 04.12.2017 13:00 1,51239E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 59093 0 0 542 Si-12.17 [variation margin close]
10838075 0 04.12.2017 13:00 1,51239E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 59093 0 0 0 Si-12.17 [variation margin open]
10840722 0 04.12.2017 15:48 1,5124E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 59036 0 0 -114 Si-12.17 [variation margin close]
10840723 0 04.12.2017 15:48 1,5124E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 59036 0 0 0 Si-12.17 [variation margin open]
10848185 0 05.12.2017 13:00 1,51248E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 58793 0 0 -486 Si-12.17 [variation margin close]
10848186 0 05.12.2017 13:00 1,51248E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 58793 0 0 0 Si-12.17 [variation margin open]
10850473 0 05.12.2017 15:48 1,51249E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 58881 0 0 176 Si-12.17 [variation margin close]
10850474 0 05.12.2017 15:48 1,51249E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 58881 0 0 0 Si-12.17 [variation margin open]
10857862 0 06.12.2017 13:00 1,51257E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 59181 0 0 600 Si-12.17 [variation margin close]
10857863 0 06.12.2017 13:00 1,51257E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 59181 0 0 0 Si-12.17 [variation margin open]
10860776 0 06.12.2017 15:48 1,51258E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 59246 0 0 130 Si-12.17 [variation margin close]
10860777 0 06.12.2017 15:48 1,51258E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 59246 0 0 0 Si-12.17 [variation margin open]
10869047 0 07.12.2017 13:00 1,51265E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 59325 0 0 158 Si-12.17 [variation margin close]
10869048 0 07.12.2017 13:00 1,51265E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 59325 0 0 0 Si-12.17 [variation margin open]
10871856 0 07.12.2017 15:48 1,51266E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 59365 0 0 80 Si-12.17 [variation margin close]
10871857 0 07.12.2017 15:48 1,51266E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 59365 0 0 0 Si-12.17 [variation margin open]
10879894 0 08.12.2017 13:01 1,51274E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 59460 0 0 190 Si-12.17 [variation margin close]
10879895 0 08.12.2017 13:01 1,51274E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 59460 0 0 0 Si-12.17 [variation margin open]
10882283 0 08.12.2017 15:48 1,51275E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 59421 0 0 -78 Si-12.17 [variation margin close]
10882284 0 08.12.2017 15:48 1,51275E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 59421 0 0 0 Si-12.17 [variation margin open]
10888014 0 11.12.2017 13:00 1,513E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 59318 0 0 -206 Si-12.17 [variation margin close]
10888015 0 11.12.2017 13:00 1,513E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 59318 0 0 0 Si-12.17 [variation margin open]
10890195 0 11.12.2017 15:48 1,51301E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 59280 0 0 -76 Si-12.17 [variation margin close]
10890196 0 11.12.2017 15:48 1,51301E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 59280 0 0 0 Si-12.17 [variation margin open]
10895808 0 12.12.2017 13:00 1,51308E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 58920 0 0 -720 Si-12.17 [variation margin close]
10895809 0 12.12.2017 13:00 1,51308E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 58920 0 0 0 Si-12.17 [variation margin open]
10897839 0 12.12.2017 15:48 1,51309E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 58909 0 0 -22 Si-12.17 [variation margin close]
10897840 0 12.12.2017 15:48 1,51309E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 58909 0 0 0 Si-12.17 [variation margin open]
10903172 0 13.12.2017 13:00 1,51317E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 59213 0 0 608 Si-12.17 [variation margin close]
10903173 0 13.12.2017 13:00 1,51317E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 59213 0 0 0 Si-12.17 [variation margin open]
10905906 0 13.12.2017 15:48 1,51318E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 59072 0 0 -282 Si-12.17 [variation margin close]
10905907 0 13.12.2017 15:48 1,51318E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 59072 0 0 0 Si-12.17 [variation margin open]
10911277 0 14.12.2017 13:00 1,51326E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 2 58674 0 0 -796 Si-12.17 [variation margin close]
10911278 0 14.12.2017 13:00 1,51326E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 2 58674 0 0 0 Si-12.17 [variation margin open]
10912285 71645351 14.12.2017 14:48 1,51326E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 506789 DEAL_REASON_EXPERT 69352663 1 58661 -0,5 0 -13 Si-12.17 PartialClose position_2 25588426
10913632 0 14.12.2017 15:48 1,51327E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 1 58783 0 0 109 Si-12.17 [variation margin close]
10913633 0 14.12.2017 15:48 1,51327E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 1 58783 0 0 0 Si-12.17 [variation margin open]
10919412 0 15.12.2017 13:00 1,51334E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 1 58912 0 0 129 Si-12.17 [variation margin close]
10919413 0 15.12.2017 13:00 1,51334E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 1 58912 0 0 0 Si-12.17 [variation margin open]
10921766 0 15.12.2017 15:48 1,51335E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 1 58946 0 0 34 Si-12.17 [variation margin close]
10921767 0 15.12.2017 15:48 1,51335E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 1 58946 0 0 0 Si-12.17 [variation margin open]
10927382 0 18.12.2017 13:00 1,5136E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 1 58630 0 0 -316 Si-12.17 [variation margin close]
10927383 0 18.12.2017 13:00 1,5136E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 1 58630 0 0 0 Si-12.17 [variation margin open]
10929913 0 18.12.2017 15:48 1,51361E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 1 58664 0 0 34 Si-12.17 [variation margin close]
10929914 0 18.12.2017 15:48 1,51361E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 1 58664 0 0 0 Si-12.17 [variation margin open]
10934874 0 19.12.2017 13:00 1,51369E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 1 58635 0 0 -29 Si-12.17 [variation margin close]
10934875 0 19.12.2017 13:00 1,51369E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 1 58635 0 0 0 Si-12.17 [variation margin open]
10936988 0 19.12.2017 15:48 1,5137E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 1 58629 0 0 -6 Si-12.17 [variation margin close]
10936989 0 19.12.2017 15:48 1,5137E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 1 58629 0 0 0 Si-12.17 [variation margin open]
10941561 0 20.12.2017 13:00 1,51377E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 1 58657 0 0 28 Si-12.17 [variation margin close]
10941562 0 20.12.2017 13:00 1,51377E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 1 58657 0 0 0 Si-12.17 [variation margin open]
10943405 0 20.12.2017 15:48 1,51378E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 1 58684 0 0 27 Si-12.17 [variation margin close]
10943406 0 20.12.2017 15:48 1,51378E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 1 58684 0 0 0 Si-12.17 [variation margin open]
10948277 0 21.12.2017 13:00 1,51386E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_VMARGIN 69352663 1 58560 0 0 -124 Si-12.17 [variation margin close]
10948278 0 21.12.2017 13:00 1,51386E+12 DEAL_TYPE_BUY DEAL_ENTRY_IN 0 DEAL_REASON_VMARGIN 69352663 1 58560 0 0 0 Si-12.17 [variation margin open]
10949780 0 21.12.2017 15:45 1,51387E+12 DEAL_TYPE_SELL DEAL_ENTRY_OUT 0 DEAL_REASON_CLIENT 69352663 1 58560 0 0 0 Si-12.17 [instrument expiration] 26163141

所用示例基于 FORTS 市场中模拟账户上进行的 USDRUR 期货交易历史。 从表格中我们可以看到,仓位入场被分成两笔手数相同的订单。 离场也执行了两笔成交。 对应每次入场和离场的成交订单 ID(除了变化保证金)都不为零。 但该仓位最后一笔平仓成交,原本来自一笔零 ID 的订单。 发生这种情况是因为仓位并非手工平仓,而是由于资产到期而被强制平仓。

这是一份直观呈现的历史,反映出在一笔仓位内执行的成交(包括所谓的“宣称”订单)。 不过,这对于最终的仓位分析仍然不方便,需要进一步改进。 我们需要一张表格,每行反映一笔仓位的结算结果并提供相关信息。

我们来创建以下结构:

//+------------------------------------------------------------------+
//| 存储结算结果的结构,以及                                             |
//| 有关所选仓位的主要信息                                               |
//+------------------------------------------------------------------+
struct DealDetales
  {
   string            symbol;                // 品名 
   datetime          DT_open;               // 开仓日期
   ENUM_DAY_OF_WEEK  day_open;              // 开仓日期的周内天数
   datetime          DT_close;              // 平仓日期
   ENUM_DAY_OF_WEEK  day_close;             // 平仓日期的周内天数
   double            volume;                // 交易量 (手数)
   bool              isLong;                // 多头/空头的符号
   double            price_in;              // 仓位入场价格
   double            price_out;             // 仓位立场价格
   double            pl_oneLot;             // 交易一手的盈亏
   double            pl_forDeal;            // 考虑到佣金的实际所获盈亏
   string            open_comment;          // 开仓时的注释
   string            close_comment;         // 平仓时的注释
  };

该结构由 getDealsDetales 方法构成。 它使用了三个关键的私有方法: 

  • isBorderPoint, 判断是否为“边界”仓位(如果它由交易者设置,并且如果是开仓/平仓);
  • MA_price, 它计算实际仓位的开仓价/平仓价;
  • calcContracts, 它计算仓位存在期间市场上的合约数量。

我们来详细查看 MA_price 方法。 例如,如果未平持仓涉及不同价格的多笔合约,那么用第一笔成交的价格作为开仓价是不正确的。 在进一步的计算中这会产生错误。 我们将使用加权平均值。 我们将计算平均开仓/平仓价格,并按成交时的交易量加权。 这就是在代码中实现的逻辑方式:

//+------------------------------------------------------------------+
//| 计算加权平均开仓价                                                  |
//+------------------------------------------------------------------+
double CDealHistoryGetter::MA_price(double &prices[],double &lots[])
  {
   double summ=0;                           // 加权价格的总和
   double delemetr=0;                       // 权重之和
   int total=ArraySize(prices);
   for(int i=0;i<total;i++)                 // 求和循环
     {
      summ+=(prices[i]*lots[i]);
      delemetr+=lots[i];
     }
   return summ/delemetr;                    // 计算平均值
  }
至于交易手数:这是一个更复杂的情况,但不难理解。 我们研究动态仓位管理的一个例子:我们逐渐增加或减少手数:

In Out
Open (t1) 1 0
t2 2 0
t3 5 3
t4 1 0
t5 0 1
t6 1 0
t7 0 1
t8 1 0
t9 0 1
Close (t10) 0 5
总计  11  11 

在仓位生存周期中,我们还多次买卖交易资产(有时轮流,有时几乎同时)。 因此,我们有 11 手买卖。 但这并不意味着持仓的交易量达到最大的 11 手。 我们记下所有的入场和离场:持仓增加和入场的符号为 +,持仓减少的符号为 -。 我们在计算总和时类似于盈亏曲线。


成交交易量 持仓交易量
deal 1 1 1
deal  2 2 3
deal 3 5 8
deal 4 -3 5
deal 5 1 6
deal 6 -1 5
deal 7 1 6
deal 8 -1 5
deal 9 1 6
deal 10 -1 5
deal 11  -5  0

从该表格中可以看出,达到的最大交易量是 8 手。 这是所需的数值。

此为反映手数的函数代码:

//+------------------------------------------------------------------+
//| 计算仓位的实际交易量,以及                                            |
//| 获取有关最后一笔仓位的交易量信息                                       |
//+------------------------------------------------------------------+
double CDealHistoryGetter::calcContracts(double &Arr[],GetContractType type)
  {
   int total;

   if((total=ArraySize(Arr))>1)                        // 如果数组大小大于 1
     {
      double lotArr[];
      addArr(lotArr,Arr[0]);                           // 将第一次的手数添加到数组中
      for(int i=1;i<total;i++)                         // 从第二个数组元素循环
         addArr(lotArr,(lotArr[i-1]+Arr[i]));          // 将前一手数和当前手数的总和添加到数组中 (lotArr[i-1]+Arr[i]))

      if(type==GET_REAL_CONTRACT)
         return lotArr[ArrayMaximum(lotArr)];          // 返回实际最大交易手数
      else
         return lotArr[ArraySize(lotArr)-1];           // 返回最后交易的手数
     }
   else
      return Arr[0];
  }

除了简单的手数计数外,该函数还显示最后一笔活动的手数(在我们的示例中等于 5)。

我们研究 isBorderPoint 函数,创建它是为过滤不必要的成交。 基于数据结构,我们可以使用 4 个变量来判断成交的重要性:

  • 订单 ID;
  • ENUM_DEAL_ENTRY — 入场, 离场, 反转;
  • ENUM_DEAL_TYPE — 买入/卖出;
  • ENUM_DEAL_REASON — 下单方法。

创建枚举:

//+--------------------------------------------------------------------+
//| 显示特定记录类型的辅助枚举                                              |
//| 该记录由 DealData 结构表示                                            |
//+--------------------------------------------------------------------+
enum BorderPointType
  {
   UsualInput,          // 常用的入场类型(DEAL_ENTRY_IN) - 成交的开始
   UsualOutput,         // 常用的离场类型(DEAL_ENTRY_OUT) - 成交的结束
   OtherPoint,          // 余额操作,仓位修正,提款,变动保证金 — 将被忽略
   InOut,               // 持仓反转 (DEAL_ENTRY_INOUT)
   OutBy                // 由相反的订单平仓的仓位 (DEAL_ENTRY_OUT_BY)
  };
我们需要上面提到的五种变体中的四种。 所有要忽略的成交都收集在 OtherPoint 中。 表中列出了每种枚举变体的参数组合。 函数代码可在下面附带的文件中找到。 
枚举变体 订单 ID ENUM_DEAL_ENTRY ENUM_DEAL_TYPE ENUM_DEAL_REASON
UsualInput  >0 DEAL_ENTRY_IN DEAL_TYPE_BUY DEAL_REASON_CLIENT
DEAL_REASON_EXPERT



DEAL_TYPE_SELL DEAL_REASON_WEB


DEAL_REASON_MOBILE



UsualOut >=0(=0 如果是因为到期强制平仓) DEAL_ENTRY_OUT DEAL_TYPE_BUY DEAL_REASON_CLIENT
DEAL_REASON_EXPERT



DEAL_REASON_WEB



DEAL_TYPE_SELL DEAL_REASON_MOBILE


DEAL_REASON_SL



DEAL_REASON_TP



DEAL_REASON_SO



OtherPoint  0 DEAL_ENTRY_IN DEAL_TYPE_BUY DEAL_REASON_ROLLOVER
DEAL_TYPE_SELL



DEAL_TYPE_BALANCE



DEAL_TYPE_CREDIT



DEAL_TYPE_CHARGE



DEAL_TYPE_CORRECTION



DEAL_TYPE_BONUS DEAL_REASON_VMARGIN


DEAL_TYPE_COMMISSION



DEAL_TYPE_COMMISSION_DAILY



DEAL_ENTRY_OUT DEAL_TYPE_COMMISSION_MONTHLY


DEAL_TYPE_COMMISSION_AGENT_DAILY



DEAL_TYPE_COMMISSION_AGENT_MONTHLY



DEAL_TYPE_INTEREST DEAL_REASON_SPLIT


DEAL_TYPE_BUY_CANCELED



DEAL_TYPE_SELL_CANCELED



DEAL_DIVIDEND



DEAL_DIVIDEND_FRANKED



DEAL_TAX



InOut  >0 DEAL_ENTRY_INOUT  DEAL_TYPE_BUY DEAL_REASON_CLIENT
DEAL_REASON_EXPERT



DEAL_TYPE_SELL DEAL_REASON_WEB


DEAL_REASON_MOBILE



OutBy  >0 DEAL_ENTRY_OUT_BY  DEAL_TYPE_BUY DEAL_REASON_CLIENT
DEAL_REASON_EXPERT



DEAL_TYPE_SELL DEAL_REASON_WEB


DEAL_REASON_MOBILE



我们将使用 getDeals_forSelectedKeeper 方法形成所需的历史记录。 我们来查看其一般逻辑,然后详细分析上述每个枚举变量的动作( 从第 303 行 开始)。

//+------------------------------------------------------------------+
//| 在 inputParam  | 中每个选定仓位形成一条记录                           |
//+------------------------------------------------------------------+

void CDealHistoryGetter::getDeals_forSelectedKeeper(DealKeeper &inputParam,DealDetales &ans[])
  {
   ArrayFree(ans);
   int total=ArraySize(inputParam.deals);
   DealDetales detales;                                          // 添加来自以下循环的结果变量
   detales.symbol=inputParam.symbol;
   detales.volume= 0;
   detales.pl_forDeal=0;
   detales.pl_oneLot=0;
   detales.close_comment= "";
   detales.open_comment = "";
   detales.DT_open=0;

// 指示是否应将仓位添加到集合的标志
   bool isAdd=false;
   bool firstPL_setted=false;
// 进场价格,离场价格,进场手数,离场手数,合约的数组
   double price_In[],price_Out[],lot_In[],lot_Out[],contracts[]; // 第 404 行

   for(int i=0;i<total;i++)                                      // 循环遍历成交(所有成交具有相同的 ID,但如果交易类型为 InOut,则可以有多笔成交)
     {
      BorderPointType type;                                      // 成交类型, 第 408 行
      double pl_total=0;

      if(isBorderPoint(inputParam.deals[i],type))                // 找出它是否为边界成交以及成交类型,第 301 行
        {
          // 第 413 行 
        } // 第 414 行
      else
        {
/*
         如果不是边界成交,只需记录清算结果。
         只能是保证金变化和各种修正操作。
         在获得初始数据时过滤掉入金和提款操作 
*/
         detales.pl_forDeal+=(inputParam.deals[i].profit+inputParam.deals[i].comission);
         detales.pl_oneLot+=inputParam.deals[i].profit/calcContracts(contracts,GET_LAST_CONTRACT);
        }
     }

// 过滤仓位活动但不保存它们
   if(isAdd && PositionSelect(inputParam.symbol))                 // 第 541 行
     {
      if(PositionGetInteger(POSITION_IDENTIFIER)==inputParam.ID)
         isAdd=false; 
     }                                                            // 第 546 行

// 保存已了结和已失活的仓位
   if(isAdd)
     {
      detales.price_in=MA_price(price_In,lot_In);
      detales.price_out=MA_price(price_Out,lot_Out);

      detales.volume=calcContracts(contracts,GET_REAL_CONTRACT);
      addArr(ans,detales);
     }
  }

数组在函数的 第 404 行 上声明。 它们将来会在 第 411-414 行 中指定的条件下使用。在此条件下仅考虑边界点,即开仓/平仓,或加仓/部分平仓某个仓位的成交。

如果成交不满足第一个条件,则唯一需要的动作是计算其盈亏。 我们的历史包含实际获得的盈利/亏损,它们分布在成交中。 每笔成交都反映了盈亏变化,从所分析成交执行前的成交开始。 仓位总利润等于所有这些成交的盈亏总和。 如果我们计算开仓价和平仓价之间的差额作为盈亏,则会忽略一些其它因素的数值,例如滑点,仓位的交易量变化,佣金和附加费用。

在代码的 第 541-546 行上,过滤开仓,然后将其保存到结果。 以下是在函数结束时计算的:开仓价和平仓价,以及市场中的最大持仓量。

type 变量用于过滤边界点。 如果此刻正在执行开仓或加仓,我们将切换到下一个条件,该条件从 第 413 行 开始(参见附件中方法的全文)。

if(type==UsualInput)                                                   // 如果是初始入场或加仓
  {
   if(detales.DT_open==0)                                              // 指定开仓日期
     {
      detales.DT_open=inputParam.deals[i].DT;
      detales.day_open=getDay(inputParam.deals[i].DT);
     }
   detales.isLong=inputParam.deals[i].type==DEAL_TYPE_BUY;             // 判断仓位方向
   addArr(price_In,inputParam.deals[i].price);                         // 保存入场价格
   addArr(lot_In,inputParam.deals[i].volume);                          // 保存手数

   pl_total=(inputParam.deals[i].profit+inputParam.deals[i].comission);
   detales.pl_forDeal+=pl_total;                                       // 考虑到佣金的成交盈亏
   if(!firstPL_setted)
     {
      detales.pl_oneLot=pl_total/inputParam.deals[i].volume;           // 如果交易一手,考虑到佣金的成交盈亏
      firstPL_setted=true;
     }
   else
      detales.pl_oneLot=inputParam.deals[i].profit/calcContracts(contracts,GET_LAST_CONTRACT);                 

   if(StringCompare(inputParam.deals[i].comment,"")!=0)                // 成交的注释
      detales.open_comment+=(StringCompare(detales.open_comment,"")==0 ?
                             inputParam.deals[i].comment :
                             (" | "+inputParam.deals[i].comment));
   addArr(contracts,inputParam.deals[i].volume);                       // 增加交易量,且符号为“+”
  }

我们还指定开仓日期并计算利润(总计,以及每手)。 在另一个条件下执行平仓:

if(type==UsualOut || type==OutBy)                         // 平仓
  {
/*
           我们不会立即保存结果,因为可能有几次离场
           因此,保留标志以避免数据丢失 
*/
   if(!isAdd)isAdd=true;                                  // 用于保存仓位的标志

   detales.DT_close=inputParam.deals[i].DT;               // 平仓日期
   detales.day_close=getDay(inputParam.deals[i].DT);      // 平仓日期的周内天数
   addArr(price_Out,inputParam.deals[i].price);           // 保存离场价格
   addArr(lot_Out,inputParam.deals[i].volume);            // 保存离场交易量

   pl_total=(inputParam.deals[i].profit+inputParam.deals[i].comission);          // 考虑到佣金的成交盈亏
   detales.pl_forDeal+=pl_total;

   if(i==total-1)
      detales.pl_oneLot+=pl_total/calcContracts(contracts,GET_LAST_CONTRACT);    // 如果交易一手,考虑到佣金的成交盈亏
   else
      detales.pl_oneLot+=inputParam.deals[i].profit/calcContracts(contracts,GET_LAST_CONTRACT); // 如果交易一手,考虑到佣金的成交盈亏

   if(StringCompare(inputParam.deals[i].comment,"")!=0)                          // 成交注释
      detales.close_comment+=(StringCompare(detales.close_comment,"")==0 ?
                              inputParam.deals[i].comment :
                              (" | "+inputParam.deals[i].comment));
   addArr(contracts,-inputParam.deals[i].volume);                                // 增加交易量,且符号为“-”
  }

此条件指定仓位离场日期并计算成交的利润。 可以有多次入场和离场,因此条件可以触发多次。

InOut 条件是第二和第一条件的组合。 它只发生一次并导致持仓反转。

if(type==InOut)                                                                 // 持仓反转
  {
/*
           第一部分:
           保存以前的仓位
*/
   firstPL_setted=true;
   double closingContract=calcContracts(contracts,GET_LAST_CONTRACT);           // 平仓合约
   double myLot=inputParam.deals[i].volume-closingContract;                     // 开仓合约

   addArr(contracts,-closingContract);                                          // 增加交易量,且符号为“-”
   detales.volume=calcContracts(contracts,GET_REAL_CONTRACT);                   // 获得市场中实际存在的最大实际交易量

   detales.DT_close=inputParam.deals[i].DT;                                     // 平仓日期
   detales.day_close=getDay(inputParam.deals[i].DT);                            // 平仓日期的周内天数
   addArr(price_Out,inputParam.deals[i].price);                                 // 离场价格
   addArr(lot_Out,closingContract);                                             // 离场交易量

   pl_total=(inputParam.deals[i].profit*closingContract)/inputParam.deals[i].volume;        // 计算平仓成交的盈亏
   double commission_total=(inputParam.deals[i].comission*closingContract)/inputParam.deals[i].volume;
   detales.pl_forDeal+=(pl_total+commission_total);
   detales.pl_oneLot+=pl_total/closingContract;                                 // 如果交易一手,考虑到佣金的成交盈亏
   if(StringCompare(inputParam.deals[i].comment,"")!=0)                         // 保存平仓注释
      detales.open_comment+=(StringCompare(detales.open_comment,"")==0 ?
                             inputParam.deals[i].comment :
                             (" | "+inputParam.deals[i].comment));

   detales.price_in=MA_price(price_In,lot_In);                                  // 获取仓位入场价(平均)
   detales.price_out=MA_price(price_Out,lot_Out);                               // 获取离场价格(平均)
   addArr(ans,detales);                                                         // 添加已形成的仓位
   if(isAdd)isAdd=false;                                                        // 如果已启用,请重置标志

                                                                                // 清除数据部分
   ArrayFree(price_In);
   ArrayFree(price_Out);
   ArrayFree(lot_In);
   ArrayFree(lot_Out);
   ArrayFree(contracts);
   detales.close_comment="";
   detales.open_comment="";
   detales.volume=0;

/*
           第二部分:
           从 details 数组中删除数据部分后保存新仓位
*/

   addArr(contracts,myLot);                                                     // 添加开仓手数

   pl_total=((inputParam.deals[i].profit+inputParam.deals[i].comission)*myLot)/inputParam.deals[i].volume; // 考虑到佣金的成交盈亏
   detales.pl_forDeal=pl_total;
   detales.pl_oneLot=pl_total/myLot;                                            // 如果交易一手,考虑到佣金的成交盈亏
   addArr(lot_In,myLot);                                                        // 添加入场手数

   detales.open_comment=inputParam.deals[i].comment;                            // 保存注释

   detales.DT_open=inputParam.deals[i].DT;                                      // 保存开仓日期
   detales.day_open=getDay(inputParam.deals[i].DT);                             // 保存开仓日期的周内天数
   detales.isLong=inputParam.deals[i].type==DEAL_TYPE_BUY;                      // 判断成交方向
   addArr(price_In,inputParam.deals[i].price);                                  // 保存入场价格
  }

上述计算的结果是一个表格,其中每一行反映了一笔仓位的主要参数。 现在,我们可以根据仓位生成所有必需的计算,而非仓位所包含的成交。

金融产品 开仓日期 开仓日期的周内天数 平仓日期 平仓日期的周内天数 合约 方向 开仓价格 平仓价格 每手盈亏 仓位盈亏 开仓注释 平仓注释
RTS-12.17 17.11.2017 19:53 FRIDAY 17.11.2017 19:54 FRIDAY 2.00000000 Long 113200.00000000 113180.00000000 -25.78000000 -55.56000000
RTS-12.17 17.11.2017 19:54 FRIDAY 17.11.2017 19:54 FRIDAY 2.00000000 Short 113175.00000000 113205.00000000 -58.47000000 -79.33000000
RTS-12.17 17.11.2017 19:58 FRIDAY 17.11.2017 19:58 FRIDAY 1.00000000 Short 113240.00000000 113290.00000000 -63.44000000 -63.44000000
RTS-12.17 17.11.2017 19:58 FRIDAY 17.11.2017 19:58 FRIDAY 1.00000000 Long 113290.00000000 113250.00000000 -51.56000000 -51.56000000
Si-12.17 17.11.2017 20:00 FRIDAY 17.11.2017 20:00 FRIDAY 10.00000000 Long 59464.40000000 59452.80000000 -23.86000000 -126.00000000
Si-12.17 17.11.2017 20:00 FRIDAY 17.11.2017 20:00 FRIDAY 5.00000000 Short 59453.20000000 59454.80000000 -5.08666667 -13.00000000
Si-12.17 17.11.2017 20:02 FRIDAY 17.11.2017 20:02 FRIDAY 1.00000000 Short 59460.00000000 59468.00000000 -9.00000000 -9.00000000
Si-12.17 17.11.2017 20:02 FRIDAY 17.11.2017 20:03 FRIDAY 2.00000000 Long 59469.00000000 59460.00000000 -14.50000000 -20.00000000
Si-12.17 21.11.2017 20:50 TUESDAY 21.11.2017 21:06 TUESDAY 2.00000000 Long 59467.00000000 59455.00000000 -13.00000000 -26.00000000
Si-12.17 23.11.2017 17:41 THURSDAY 21.12.2017 15:45 THURSDAY 2.00000000 Long 58736.50000000 58610.50000000 -183.00000000 -253.50000000 Open test position | Open test position PartialClose position_2 | [instrument expiration]
RTS-12.17 23.11.2017 18:07 THURSDAY 14.12.2017 14:45 THURSDAY 1.00000000 Short 115680.00000000 114110.00000000 1822.39000000 1822.39000000 Open test position_2
RTS-3.18 30.01.2018 20:22 TUESDAY 30.01.2018 20:22 TUESDAY 2.00000000 Short 127675.00000000 127710.00000000 -61.01000000 -86.68000000
RTS-3.18 30.01.2018 20:24 TUESDAY 30.01.2018 20:24 TUESDAY 1.00000000 Long 127730.00000000 127710.00000000 -26.49000000 -26.49000000
RTS-3.18 30.01.2018 20:24 TUESDAY 30.01.2018 20:25 TUESDAY 1.00000000 Long 127730.00000000 127680.00000000 -60.21000000 -60.21000000
RTS-3.18 30.01.2018 20:25 TUESDAY 30.01.2018 20:25 TUESDAY 1.00000000 Long 127690.00000000 127660.00000000 -37.72000000 -37.72000000
RTS-3.18 30.01.2018 20:25 TUESDAY 30.01.2018 20:26 TUESDAY 1.00000000 Long 127670.00000000 127640.00000000 -37.73000000 -37.73000000
RTS-3.18 30.01.2018 20:29 TUESDAY 30.01.2018 20:30 TUESDAY 1.00000000 Long 127600.00000000 127540.00000000 -71.45000000 -71.45000000

第二章 创建自定义交易报告

现在,我们来创建一个类,它将生成一个交易报告。 首先,我们定义报告的需求。

  1. 该报告将包含标准盈亏图和扩展图,以便更有效地评估绩效。 图表的构造从零开始(无关初始资本) - 这将增加评估的客观性。
  2. 我们将生成策略的“买入并持有”图表,类似于盈亏图表(两者将由相同的函数计算)。 这两个图表将基于报告中出现的所有资产建立。
  3. 主要系数和交易结果应显示在表格中。
  4. 将构建其它图表,例如按天数的盈亏图。

最后,我们将以变量图的形式呈现所分析的参数,并将所有计算结果下载到 csv 文件中。 这里给出的示例基于一段时间的真实历史。 这段历史附带如下。 可视化和数据下载脚本将包含以下功能:第一个将显示附加交易历史记录的示例,第二个功能将基于您在终端中可用的历史记录。

本文的这一部分将提供最少的代码。 代之,我们将专注于检验获取的数据并解释其含义。 创建报告的类的所有部分都已经过详细评述,因此您可以轻松理解它们。 为了更好地理解所描述的函数,我在这里提供了类的结构。

class CReportGetter
  {
public:
                     CReportGetter(DealDetales &history[]);                      // 只能按已准备格式提供历史记录
                     CReportGetter(DealDetales &history[],double balance);       // 我们可以在此处为即将执行的计算设置相对历史和余额
                    ~CReportGetter();

   bool              get_PLChart(PL_chartData &pl[],
                                 PL_chartData &pl_forOneLot[],
                                 PL_chartData &Indicative_pl[],
                                 string &Symb[]);                                // 盈亏图

   bool              get_BuyAndHold(PL_chartData &pl[],
                                    PL_chartData &pl_forOneLot[],
                                    PL_chartData &Indicative_pl[],
                                    string &Symb[]);                             // 买入并持有图表

   bool              get_PLHistogram(PL_chartData &pl[],
                                     PL_chartData &pl_forOneLot[],
                                     string &Symb[]);                            // 盈亏累积直方图

   bool              get_PL_forDays(PLForDaysData &ans,
                                    DailyPL_calcBy calcBy,
                                    DailyPL_calcType type,
                                    string &Symb[]);                             // 按周内天数的盈亏

   bool              get_extremePoints(PL_And_Lose &total,
                                       PL_And_Lose &forDeal,
                                       string &Symb[]);                          // 极值点(成交以及累积中的最高和最低值)

   bool              get_AbsolutePL(PL_And_Lose &total,
                                    PL_And_Lose &average,
                                    string &Symb[]);                             // 绝对值(累计和平均盈亏)

   bool              get_PL_And_Lose_percents(PL_And_Lose &ans,string &Symb[]);  // 盈利和亏损成交的分布图

   bool              get_totalResults(TotalResult_struct &res,string &Symb[]);   // 主要变量表

   bool              get_PLDetales(PL_detales &ans,string &Symb[]);              // 盈亏图的简要

   void              get_Symbols(string &SymbArr[]);                             // 获取历史记录中可用的品名列表

private:
   DealDetales       history[];                                                  // 存储交易历史
   double            balance;                                                    // 存储余额值

   void              addArray(DealDetales &Arr[],DealDetales &value);            // 将数据添加到动态数组
   void              addArray(PL_chartData &Arr[],PL_chartData &value);          // 将数据添加到动态数组
   void              addArray(string &Arr[],string value);                       // 将数据添加到动态数组
   void              addArray(datetime &Arr[],datetime value);                   // 将数据添加到动态数组

   void              sortHistory(DealDetales &arr[],bool byClose);               // 按开仓或平仓日期排序历史记录

   void              cmpDay(int i,ENUM_DAY_OF_WEEK day,ENUM_DAY_OF_WEEK etaloneDay,PLDrowDown &ans);      // 填充 PLDrowDown 结构

   bool              isSymb(string &Symb[],int i);                               // 检查历史记录中的第 i 个品名是否在 Symb 数组中

   bool              get_PLChart_byHistory(PL_chartData &pl[],
                                           PL_chartData &pl_forOneLot[],
                                           PL_chartData &Indicative_pl[],
                                           DealDetales &historyData[]);          // 基于所传递历史的盈亏图

   ENUM_DAY_OF_WEEK  getDay(datetime DT);                                        // 获取自该日期开始的交易日期

  };

3 种类型的盈亏图

若要创建盈亏图表,我们会根据仓位结算日期对初始数据进行排序。 无需在此解释,因为这是一个标准图表。 它包含两个图表,其中 X 轴是平仓日期。 第一个是实际的盈亏图,而第二个是相对于最大盈亏值的累计回撤,以百分比表示。 我们来研究 3 种类型的图表。

  1. 标准盈利累计图表。
  2. 累计利润图表未考虑交易量,就好像我们在整个历史记录中交易所有品种一手。
  3. 损益图表,按照最大亏损(或盈利)手数常规化。 如果盈亏图表位于红色区域,则将第 i 个亏损除以最大盈利手数。 如果图表位于绿色区域,则将第 i 个利润除以最大亏损手数。

前两个图表可以了解交易量的变化如何影响盈利/亏损。 第三个能够想象为一个极端情况:如果突然间开始一连串亏损,那么将会发生什么,并且这些损失等于每手可能的最大历史损失(这实际上是不可能的)。 它显示盈亏在达到 0 之前可以连续多少次最大损失(所有利润都将丢失) - 反之亦然,如果所有损失都被一连串最大利润所覆盖,会发生什么。 这些图表类型如下所示:

真是盈亏图

1-手的盈亏图

指示性盈亏

1 手交易的盈亏图比真实图更具吸引力。 实际交易中的利润超过了假设的“一手”交易,仅缩水了 30%。 手数介于 1 到 10 之间。 回撤百分比达到 30000%。 情况并不像听起来那么糟糕。 如上所述,盈亏图构造从零开始,而相对于盈亏曲线的最大值计算回撤。 在某些时刻,当损失尚未达到最大值(图中的红色区域)时,利润上升了几卢布,然后下降到 -18000 卢布。 这就是回撤如此巨大的原因。 实际回撤不超过每手 25%,实际交易为 50%。

一手交易的回撤


总回撤


常规化盈利图卡在一个数值上。 这表明需要改变交易方法(例如改变手数管理方法)或重新优化交易算法。 具有可变手数的盈亏图表看起来比同一时期的单手数图表差,但是这种仓位管理仍然具有正面结果:回撤跳跃变得更弱。

生成交易报告的类称为 CReportGetter,图表由 get_PLChart_byHistory 方法构建。 此方法在源代码文件中提供了详细注释。 它的运作如下:

  1. 它遍历所传递的历史。
  2. 每笔仓位都包含在盈亏、最大利润和亏损的计算中。
  3. 回撤计算是相对于累计达到的最大利润值。 如果盈亏图形开始后立即落入红色区域,则每次出现新的最低值时将重新计算回撤。 在此情况下,最大利润为零,并且相对于最大损失重新计算第 i 个和所有先前元素的回撤。 此处的回撤等于 100%。 一旦最大利润上升到零以上(盈亏曲线进入图的正数部分),那么将仅相对于最大利润进一步计算回撤。

我们通过比较三个图表来进行分析。 与使用单一标准盈亏图表相比,此方法可以检测更多的缺陷。 根据相同的算法构建“买入并持有”策略的盈亏图。 我仅会提供一个图表作为示例。 其它的将在可视化脚本启动时自动构建。

“买入并持有”策略的盈亏图

该图显示,如果我们根据“买入并持有”策略交易相同数量的相同资产,盈亏图很难达到零,并且回撤将是大约 80000 卢布(而不是最大获得的利润 70000卢布)。

盈亏累积图表

此图表代表两个直方图,一个在另一个上。 它们的结构与标准盈亏图不同。 利润直方图仅逐一累积盈利交易而创建。 如果第 i 笔仓位亏损,我们忽略其数值并将先前的数值写入第 i 个时间间隔。 亏损交易的直方图是根据镜像逻辑构建的。 以下损益直方图基于我们的数据。

真是盈亏直方图

1 手交易的盈亏直方图

通常的盈利图是利润和亏损直方图之间的简单差值。 吸引眼球的第一件事是实际获得的利润与交易利润之间的巨大差值。 实际获得的利润超过一手交易约 30%,而直方图分析显示差异接近 50%。 缺失的 20% 利润源于第一和第二图表中动态利润增长与损失的差值。 我们来描绘这种分析。 我们根据这些直方图构建了两个盈亏动态图表。 公式很简单:

利润因子公式

动态利润因子


真实动态盈利因子


我们看到,与亏损相比,动态利润从一开始就在增长。 这是事件的理想发展:每次新交易时,利润直方图与亏损交易的直方图的距离都会增加。 如果在整个交易过程中利润增长超过亏损则为正面,我们会得到最好的结果之一。 利润的累积速度更快,相对于盈利,亏损增长相同或更低。

图表的下一段显示,利润和亏损直方图之间的百分比差值停止增加,并继续横向趋势移动。 如果在任意横向趋势区间内利润增长超过亏损,这也被认为是积极的。 如果间隔等于一,则盈亏曲线将接近零(每笔新利润将等于每笔新亏损)。 如果横向间隔低于 1,我们就开始亏钱。 换句话说,这些图表可直观利润因子的逐渐变化。 这是图表:

依赖于利润因子的盈亏直方图


依赖于利润因子的盈亏图


蓝色直方图显示亏损的线性累积(比例范围从 0 到 100)。

  • 绿色: 亏损直方图 * 1.3,
  • 灰色: 亏损直方图 * 1,
  • 红色: 亏损直方图 * 0.7。

图表显示利润因子(显示利润超过亏损的比率)应始终大于 1。 理想的演变是盈利因子逐步增长。 在这种情况下,盈亏图将呈指数增长。 不幸地是,从长远来看这是不现实的,但是如果您在每笔盈利交易之后增加手数,这种情况可能会在短暂的幸运期内发生。 基于这个例子,我们可以 100% 确信交易的主要点不是盈亏图变化的方式,而是盈、亏损成交直方图的行为,以及利润因子是如何动态变化。

现在,我们来研究 20% 的利润损失。 参见一手利润增长动态图:利润增长停止在区间 [1.4 - 1.6]。 这高于 [1.2 - 1.4]。 随着时间的推移,这种差异占潜在利润的 20%。 换句话说,仓位规模管理给出了积极的结果。 如果您运用马丁格尔(逆势翻倍加仓)或反马丁格尔等方法,这些图表,尤其是基于它们计算的指标,可以为分析提供大量有用的信息。 这些图表由 get_PLHistogram 函数构建。

按天数的盈亏

曾经在策略测试器中测试过机器人的人已经知道这个图表了。 直方图的含义和构造方法是相同的。 此外,我实现了使用绝对值(通过简单的按天数利润和损失汇总获得)和平均数据构建它的可能性。 在附加的类中使用简单的平均方法。 其它平均方法(模式/中值,加权平均)可以产生更有趣的结果。 您可以自行实现这些平均类型。

此外,我添加了交易总额(正数和负数):它显示了按天的成交数量。 分析该直方图与按天的盈亏图的组合,可给出一周背景下更准确的交易图。 例如,当我们每天有 50 笔亏损和 3 笔盈利时,它将能够检测巨额利润弥补所有亏损的情况。 以下是基于绝对数据和仓位平仓价建立的柱线图示例。

按天盈亏


每日成交数


周内所有交易日的图表显示正数成交涵盖亏损成交,但亏损成交的数量总是更大(通常,这种情况是算法交易的典型情况)。 此外,负数成交的数量令正数成交的增加数量从未超过 25%,这通常也是正常的。

极端点和绝对值

极值点图是 4 根柱线图的直方图,分为两组。 极值点是盈亏图上的最大和最小偏差(最大实现利润和最大累计亏损)。 另外两根柱线显示了最大的盈利和亏损交易。 两组均基于实际交易数据,而非基于每笔交易的损益数据。 计算盈亏曲线上的最大利润是图表上的最高点。 上面描述了最大回撤计算方法。 这是相关的公式:

最小 最大

极端点图


该图描绘了损益的最大分布。 与最大利润相比,最大亏损的份额为 30%。 与最大盈利交易相比,最大亏损交易的数值为 60%。 在代码中,用循环实现,其中根据指明条件逐步比较盈亏曲线数据。

至于绝对值,它们最好以表格形式表示。 这些也代表两组数据:第一组是所有利润的总和和所有亏损的总和; 第二组是分析历史中的平均损益值。

总计 平均
盈利 323237 2244.701
回撤 261534 1210.806

表格包含简略的盈亏图摘要和主要交易结果

这些表格反映了交易绩效的数字特征。 表格创建代码包含以下结构:

//+------------------------------------------------------------------+
//| 交易结果的结构                                                      |
//+------------------------------------------------------------------+
struct TotalResult_struct
  {
   double            PL;                                // 盈亏总计
   double            PL_to_Balance;                     // 盈亏与当前余额的比率
   double            averagePL;                         // 平均损益
   double            averagePL_to_Balance;              // 平均损益与当前余额的比率
   double            profitFactor;                      // 盈利因子
   double            reciveryFactor;                    // 恢复因子
   double            winCoef                            // 收益因子
   double            dealsInARow_to_reducePLTo_zerro;/* 如果它现在有利润,连续交易的数量与每笔交易的最大损失,
                                                        会令经常账户利润降至零。
                                                        如果它现在亏损,连续交易的数量与每笔交易的最大利润,
                                                        会将亏损降至零*/

   double            maxDrowDown_byPL;                  // 相对于盈亏的最大回撤
   double            maxDrowDown_forDeal;               // 每笔成交的最大回撤
   double            maxDrowDown_inPercents;            // 相对于盈亏的最大回撤占当前余额的百分比
   datetime          maxDrowDown_byPL_DT;               // 相对于盈亏的最大亏损日期
   datetime          maxDrowDown_forDeal_DT             //每笔交易最大回撤的日期

   double            maxProfit_byPL;                    // 盈亏的最大利润
   double            maxProfit_forDeal;                 // 每笔交易的最大利润
   double            maxProfit_inPercents;              // 盈亏的最大利润占当前余额的百分比
   datetime          maxProfit_byPL_DT;                 // 盈亏的最大利润日期
   datetime          maxProfit_forDeal_DT;              // 每笔交易的最大利润日期
  };
//+------------------------------------------------------------------+
//| PL_detales 结构的一部分(在下面声明)                                 |
//+------------------------------------------------------------------+
struct PL_detales_item
  {
   int               orders;                            // 成交数
   double            orders_in_Percent;                 // 订单数量占订单总数的百分比
   int               dealsInARow;                       // 连续成交
   double            totalResult;                       // 存款货币的总结果
   double            averageResult;                     // 存款货币的平均结果
  };
//+-------------------------------------------------------------------+
//| 简要盈亏图的摘要分为 2 个主要模块                                       |
//+-------------------------------------------------------------------+
struct PL_detales
  {
   PL_detales_item   profits;                           // 盈利成交信息
   PL_detales_item   loses;                             // 亏损成交信息
  };

第一个结构 TotalResult_struct 是整个所请求交易历史记录中的关键值摘要。 它包括必要的数值(每笔成交的利润和亏损等),以及计算出的交易业绩系数。

第二和第三结构是相互关联的。 PL_detales 是主要结构,包含损益直方图的简要概述。 在所分析历史上获得以下结果:

数值 含义
盈亏 65039
相对于余额的盈亏 21,8986532
平均盈亏 180,1634349
相对于余额的平均盈亏 0,06066109
盈利因子 1,25097242
恢复因子 3,0838154
收益率 1,87645863
盈利降至零的成交 24,16908213
相对于盈亏的回撤 -23683
相对于每手成交的回撤 -2691
相对于余额的回撤 -0,07974074
相对于盈亏的回撤日期 24.07.2017 13:04
每手成交回撤的日期 31.01.2017 21:53
在盈亏上的盈利 73034
每手成交盈利 11266
型对于余额的盈利 0,24590572
在盈亏上的盈利日期 27.06.2017 23:42
每手成交的盈利日期 14.12.2016 12:51

第二个表格如下:

盈利 亏损
平均结果 2244.701 -1210,81
连续成交 5 10
成交总数 144 216
成交百分比 0,398892 0,598338
总结果 323237 -261534

亏损和盈利仓位的分布可以用圆环图表示:

利润百分比和回撤


从结果可以看出,40% 的成交是正值。

结束语

在交易中使用算法时,仅仅优化并启动算法是不够的。 优化后立即运行算法得到完整的结果,而不应调整它以拟合投资组合结构,那样可能会带来相当意外的结果。 当然,交易员经常玩俄罗斯轮盘赌。 但是,有很多理性的交易者更愿意考虑他们未来的步骤。 本文中使用的历史分析技术为这些交易者提供了一个有趣的机会,令他们能够测试为每个交易算法分配权重的最优性。 

其中一个有趣的应用领域是分析不同的投资组合计算方法,并根据可用的交易历史对其进行测试,然后将新的投资组合表现与实际结果进行比较。 这可以通过一手交易来完成,这是根据本文第一章中的实际交易历史计算的。 但是,请注意,在此类计算中获得的数据只是近似值,因为它们没有考虑资产流动性,佣金和各种强制性操作。


以下文件附在文章中:

  1. CSV 文件 dealHistory.csv 包含交易历史记录(位于文件夹 MQL5\Files\article_4803) - 本文提供的示例基于此历史记录。
  2. 用于 MetaTrader 5 的源文件, 其位于 MQL5\Scripts\Get_TradigHistory 之下
  • 测试脚本项目,分为下面描述的部分
辅助文件:
  • 文件 ListingFilesDirectory.mqh – WinAPI 函数库,允许在整个计算机中使用文件。
  • 文件 Tests.mqh 和 Tests.mq5 在所传递的路径上创建文件夹后将报告保存到文件。
  • 文件 PlotCharts.mqh  – 在终端中绘制可视化报告图表。
  • 测试脚本 Get_TradingHistory.mq5 有两个函数:
    1. test_1 函数接受附加测试历史文件的路径作为一个参数,并将结果选项卡的路径作为第二个参数。 此函数生成测试文件报告并作为参数传递给第一个函数。
    2. 第二个 test_2 函数接受第二个文件夹路径(路径必须不同)作为参数。 您的交易报告将保存在该文件夹中。

主要文件:

  • DealHistoryGetter.mqh 包含本文第一章中描述的数据下载方法的函数实现。
  • ReportGetter.mqh 包含交易报告生成机制的实现。 此文件中包含的类接受生成的历史记录并对其进行处理。


本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/4803

附加的文件 |
MQL5.zip (266.19 KB)
MetaTrader市场提供14,000个EA交易 MetaTrader市场提供14,000个EA交易

目前,最大的自动交易应用程序成品商店可提供13,970个产品。它包含4,800个EA、6,500个指标、2,400个实用工具以及其他解决方案。在这种情况下,差不多有一半的应用程序(6,000)可供租用。此外,产品总数的1/4产品(3,800)可以免费下载。

包含图形用户界面 (GUI) 的 EA 交易: 增加功能 (第二部分) 包含图形用户界面 (GUI) 的 EA 交易: 增加功能 (第二部分)

这是展示开发用于人工交易的多交易品种信号 EA 文章的第二部分,我们已经创建了图形界面,现在是时候把它与程序功能相关联了。

已有950个网站提供来自MetaQuotes的经济日历 已有950个网站提供来自MetaQuotes的经济日历

该小工具为网站提供了一个详细的发布时间表,列出了全球大型经济体的500个指标及指数。因此,除了主要的网站内容之外,交易者还能够迅速收到关于所有重要事件的最新消息及其解释和图表。

同时双向工作的通用 RSI 指标 同时双向工作的通用 RSI 指标

当开发交易算法时,我们经常遇到这样一个难题:如何确定趋势/盘整从哪里开始和结束?在本文中,我们尝试创建一个通用指标,在其中我们会尝试组合几种不同类型策略的信号。在 EA 交易中,我们将尝试尽可能简化取得交易信号的过程,并将给出一个把几个指标组合为一的实例。