外汇EA编写教程:如何分析图表中所选择信号的交易

目录

概述

新信号,免费或付费,会永久性地出现在信号 服务中。 MetaTrader 团队注意到可以在不退出终端的情况下使用该服务。 所有剩余的工作就是选择可接受风险范围能够产生最高利润的信号。 很久以前就讨论过这个问题。 曾提出过通过指定标准 [1] 自动选择信号的方法。 然而,传统观点认为一张图片胜过千言万语。 在本文中,我提议研究和分析品种图表中所选信号的交易历史。 或许,这种方法可以令我们更好地理解交易策略和风险评估。 

1. 为我们未来的工作设定目标

我好像听到您难过了: ‘如果终端已经提供了在图表中显示交易历史的能力,为什么还要重新创建轮子呢? 我的意思是,只需按下终端中的按钮即可选择所需的信号。’

命令 "在图表上显示交易"

在此之后,根据所使用品种信号的数量,在终端中打开新窗口,并在交易上做出标记。 当然,分页图表以及在其中搜索交易是相当费力的活动。 甚或,在不同图表中进行的交易可能会在时间上重合,且在分别分析每个图表时您无法看到。 在此阶段,我们将尝试将我们的部分工作自动化。

为了辨别我们从图表获得的所需分析的品种,我们必须清晰地了解我们需要的最终结果。 以下是我最终想要的基本项目:

  • 查看信号在不同品种上的均匀性如何;
  • 了解如何分配资金的负荷,以及同时可开仓数量;
  • 如果信号同时开多笔仓位,那么它们是否为对冲,亦或增加了资金的负荷;
  • 在什么时刻以及在哪些品种上出现了最大的缩水; 以及
  • 在什么时刻实现最大的盈利。

2. 收集交易统计数据

2.1. 用于保存订单信息的类

因此,我们选择所需的信号并在图表中显示其交易历史。 然后收集我们将要分析的初始数据。 为了记录每笔订单的信息,我们基于 CObject 类创建 COrder 类。 在这个类的变量中,我们保存订单号,交易类型和手数,交易价格,操作类型 (入场/出场),订单开仓时间,当然还有品名。

class COrder : public CObject
  {
private:
   long                 l_Ticket;
   double               d_Lot;
   double               d_Price;
   ENUM_POSITION_TYPE   e_Type;
   ENUM_DEAL_ENTRY      e_Entry;
   datetime             dt_OrderTime;
   string               s_Symbol;
   
public:
                        COrder();
                       ~COrder();
   bool                 Create(string symbol, long ticket, double volume, double price, datetime time, ENUM_POSITION_TYPE type);
//---
   string               Symbol(void)   const {  return s_Symbol;     }
   long                 Ticket(void)   const {  return l_Ticket;     }
   double               Volume(void)   const {  return d_Lot;        }
   double               Price(void)    const {  return d_Price;      }
   datetime             Time(void)     const {  return dt_OrderTime; } 
   ENUM_POSITION_TYPE   Type(void)           {  return e_Type;       }
   ENUM_DEAL_ENTRY      DealEntry(void)const {  return e_Entry;      }
   void                 DealEntry(ENUM_DEAL_ENTRY value) {  e_Entry=value; }
//--- 操纵文件的方法
   virtual bool         Save(const int file_handle);
   virtual bool         Load(const int file_handle);
//---
   //--- 比较对象的方法
   virtual int          Compare(const CObject *node,const int mode=0) const;
  };

伴随数据访问函数,我们向订单类添加了操纵文件函数以便保存和随后读取数据,还有比较类似的函数,因为我们将需要对订单进行排序。

为了与订单进行比较,我们需要重新编写虚函数 Compare。 这是基类的函数,用于比较 CObject 对象。 所以,对象 CObject 的链接和排序方法将作为参数传递给它。 我们仅在一个方向上对订单进行排序,即按照执行日期升序,因此我们不会在函数代码中使用参数 “mode”。 但是,对于通过链接获得的对象 COrder,我们必须首先将其降低到相关类型。 之后,我们比较所获订单的日期和当前订单的日期。 如果所获订单更老,则返回 “-1″。 如果它比较新, 则返回 “1”。 如果执行订单的日期相等,则函数将返回 “0”。

int COrder::Compare(const CObject *node,const int mode=0) const
  {
   const COrder *temp=GetPointer(node);
   if(temp.Time()>dt_OrderTime)
      return -1;
//---
   if(temp.Time()<dt_OrderTime)
      return 1;
//---
   return 0;
  }

2.2. 从图表中收集信息

为了处理订单,我们基于 CArrayObj 类创建 COrdersCollection 类。 将在其中收集和处理信息。 为了存储数据,我们将声明一个对象实例直接处理特定订单,以及一个用于存储所用品种列表的数组。 我们将使用基类函数存储订单数组。

class COrdersCollection : public CArrayObj
  {
private:
   COrder            *Temp;
   string            ar_Symbols[];
   
public:

                     COrdersCollection();
                    ~COrdersCollection();
//--- 初始化
   bool              Create(void);
//--- 加入一笔订单
   bool              Add(COrder *element);
//--- 访问数据
   int               Symbols(string &array[]);
   bool              GetPosition(const string symbol, const datetime time, double &volume, double &price, ENUM_POSITION_TYPE &type);
   datetime          FirstOrder(const string symbol=NULL);
   datetime          LastOrder(const string symbol=NULL);
//--- 获取时间序列
   bool              GetTimeSeries(const string symbol, const datetime start_time, const datetime end_time, const int direct,
                                   double &balance[], double &equity[], double &time[], double &profit, double &loss,int &long_trades, int &short_trades);
//---
   void              SetDealsEntry(void);
  };

函数 ‘Create’ 直接负责收集数据。 在方法实体内,我们将安排一个循环来搜索终端中已打开的所有图表。 我们将在每个图表中搜索 OBJ_ARROW_BUY 和 OBJ_ARROW_SELL 等图形对象。

bool COrdersCollection::Create(void)
  {
   long chart=ChartFirst();
   while(chart>0)
     {
      int total_buy=ObjectsTotal(chart,0,OBJ_ARROW_BUY);
      int total_sell=ObjectsTotal(chart,0,OBJ_ARROW_SELL);
      if((total_buy+total_sell)<=0)
        {
         chart=ChartNext(chart);
         continue;
        }

如果在图表中找到了对象,那么我们将图表品种添加到品种数组中 (不过,我们会预先检查这些品种是否不在已保存的品种中)。

      int symb=ArraySize(ar_Symbols);
      string symbol=ChartSymbol(chart);
      bool found=false;
      for(int i=0;(i<symb && !found);i++)
         if(ar_Symbols[i]==symbol)
           {
            found=true;
            symb=i;
            break;
           }
      if(!found)
        {
         if(ArrayResize(ar_Symbols,symb+1,10)<=0)
            return false;
         ar_Symbols[symb]=symbol;
        }

然后我们安排从图表中收集交易信息,并存储到数据数组。 注意: 图形对象是我们唯一的交易信息来源。 从对象参数里,我们只能得到交易的时间和价格。 我们必须从对象名称的文本字符串中获取所有其它详细信息。

图形对象名称

在图片中,您可以看到对象名称包含交易中的所有数据,以空格分隔。 我们利用这点并将字符串用空格切分成一个字符串元素数组。 然后,我们从相关元素中减少信息量,并保存所需的数据类型。 收集信息后,我们转到下一个图表。

      int total=fmax(total_buy,total_sell);
      for(int i=0;i<total;i++)
        {
         if(i<total_buy)
           {
            string name=ObjectName(chart,i,0,OBJ_ARROW_BUY);
            datetime time=(datetime)ObjectGetInteger(chart,name,OBJPROP_TIME);
            StringTrimLeft(name);
            StringTrimRight(name);
            StringReplace(name,"#","");
            string split[];
            StringSplit(name,' ',split);
            Temp=new COrder;
            if(CheckPointer(Temp)!=POINTER_INVALID)
              {
               if(Temp.Create(ar_Symbols[symb],StringToInteger(split[1]),StringToDouble(split[3]),StringToDouble(split[6]),time,POSITION_TYPE_BUY))
                  Add(Temp);
              }
           }
//---
         if(i<total_sell)
           {
            string name=ObjectName(chart,i,0,OBJ_ARROW_SELL);
            datetime time=(datetime)ObjectGetInteger(chart,name,OBJPROP_TIME);
            StringTrimLeft(name);
            StringTrimRight(name);
            StringReplace(name,"#","");
            string split[];
            StringSplit(name,' ',split);
            Temp=new COrder;
            if(CheckPointer(Temp)!=POINTER_INVALID)
              {
               if(Temp.Create(ar_Symbols[symb],StringToInteger(split[1]),StringToDouble(split[3]),StringToDouble(split[6]),time,POSITION_TYPE_SELL))
                  Add(Temp);
              }
           }
        }
      chart=ChartNext(chart);
     }

图形标记没有每笔交易是开仓还是平仓的信息。 这就是为什么在保存交易信息时此字段仍未填写的原因。 现在,从图表中收集了所有标记后,我们调用函数 SetDealsEntry 来添加缺失的数据。

   SetDealsEntry();
//---
   return true;
  }

为了避免在我们的数据库中重复交易,我们重新编写 Add 函数,按照单号检查订单可用性。

bool COrdersCollection::Add(COrder *element)
  {
   for(int i=0;i<m_data_total;i++)
     {
      Temp=m_data[i];
      if(Temp.Ticket()==element.Ticket())
         return true;
     }
//---
   return CArrayObj::Add(element);
  }

为了安排交易中的操作类型,我们将创建函数 SetDealsEntry。 在开始时,我们调用基类的排序功能。 安排一个循环来搜索所有品种和每个品种的交易。 识别操作类型的算法很简单。 如果在操作中,没有持仓或已有相同的方向的持仓,那么我们将此操作识别为入场仓位。 如果操作与现有持仓相反,则首先用其手数平仓,而剩余手数开新仓 (类似于 MetaTrader 5 的净持结算系统)。

COrdersCollection::SetDealsEntry(void)
  {
   Sort(0);
//---
   int symbols=ArraySize(ar_Symbols);
   for(int symb=0;symb<symbols;symb++)
     {
      double volume=0;
      ENUM_POSITION_TYPE type=-1;
      for(int ord=0;ord<m_data_total;ord++)
        {
         Temp=m_data[ord];
         if(Temp.Symbol()!=ar_Symbols[symb])
            continue;
//---
         if(volume==0 || type==Temp.Type())
           {
            Temp.DealEntry(DEAL_ENTRY_IN);
            volume=NormalizeDouble(volume+Temp.Volume(),2);
            type=Temp.Type();
           }
         else
           {
            if(volume>=Temp.Volume())
              {
               Temp.DealEntry(DEAL_ENTRY_OUT);
               volume=NormalizeDouble(volume-Temp.Volume(),2);
              }
            else
              {
               Temp.DealEntry(DEAL_ENTRY_INOUT);
               volume=NormalizeDouble(volume-Temp.Volume(),2);
               type=Temp.Type();
              }
           }
        }
     }
  }

2.3. 为每个品种创建余额和资金的时间序列

为了随后为每个品种构建余额和资金图表,我们需要为整个分析区间创建计算这些参数的时间序列。 在分析时,我们最好能够改变分析区间。 这将有助于研究信号如何在有限的时间间隔内工作。

我们将在 GetTimeSeries 函数中计算时间序列。 在其参数中,我们指定分析区间的品种和开始与结束时间,以及跟踪多头和空头持仓的交易方向。 该函数将返回三个时间序列: 余额,资金和时间标记。 此外,它将返回分析区间品种的统计数据: 盈利,亏损,多头交易,和空头交易。

展望未来,我期望您把注意力集中到一个事实上,即时间标记的时间序列数组被定义为 double (双高精度)。 这个小技巧是一种有点偏门的解决方案。 之后我们将使用标准类 CGraphic 来构建余额和资金图表,仅接受双精度类型数组。

在函数开始时,我们将收集统计信息的变量清零,检查品种的正确性,并获得一个价格变化点的价格。

bool COrdersCollection::GetTimeSeries(const string symbol,const datetime start_time,const datetime end_time,const int direct,double &balance[],double &equity[], double &time[], double &profit, double &loss,int &long_trades, int &short_trades)
  {
   profit=loss=0;
   long_trades=short_trades=0;
//---
   if(symbol==NULL)
      return false;
//---
   double tick_value=SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_VALUE)/SymbolInfoDouble(symbol,SYMBOL_POINT);
   if(tick_value==0)
      return false;

为了构建时间序列,我们将使用时间帧为 M5 的品种报价,即我们应下载它们。 但请注意: 我们请求的报价可能尚未形成。 在此,我们另有一个技巧: 我们不会循环操作并等待数据加载,因为这会彻底阻塞程序执行,若在指标中使用则也许会减慢终端速度。 若第一次调用不成功的话,我们将退出该函数。 不过,在此之前,我们将创建一个用户定义的事件,稍后将重新调用数据更新函数。

   ENUM_TIMEFRAMES timeframe=PERIOD_M5;
//---
   double volume=0;
   double price=0;
   ENUM_POSITION_TYPE type=-1;
   int order=-1;
//---
   MqlRates rates[];
   int count=0;
   count=CopyRates(symbol,timeframe,start_time,end_time,rates);
   if(count<=0 && !ReloadHistory)
     {
      //--- 发送通知
      ReloadHistory=EventChartCustom(CONTROLS_SELF_MESSAGE,1222,0,0.0,symbol);
      return false;
     }

加载报价后,我们将时间序列数组的大小与已加载报价的数量对齐。

   if(ArrayResize(balance,count)<count || ArrayResize(equity,count)<count || ArrayResize(time,count)<count)
      return false;
   ArrayInitialize(balance,0);

然后我们将安排一个循环来收集时间序列的信息。 我们将辨别每根柱线上的操作。 如果这是一个开仓操作,那么我们将增加当前持仓的手数并重新计算平均开仓价。 如果这是一个平仓操作,我们将重新计算操作的盈利/亏损,将获得的数值添加到当前柱线上的余额变化中,并减少当前持仓的手数。 然后,对于柱线收盘之前尚未平仓的手数,我们计算未平仓的盈利/亏损,并将获得的数值保存在正分析的柱线资金变化中。 搜索整个历史记录之后,我们退出该函数。

   do
     {
      order++;
      if(order<m_data_total)
         Temp=m_data[order];
      else
         Temp=NULL;
     }
   while(CheckPointer(Temp)==POINTER_INVALID && order<m_data_total);
//---
   for(int i=0;i<count;i++)
     {
      while(order<m_data_total && Temp.Time()<(rates[i].time+PeriodSeconds(timeframe)))
        {
         if(Temp.Symbol()!=symbol)
           {
            do
              {
               order++;
               if(order<m_data_total)
                  Temp=m_data[order];
               else
                  Temp=NULL;
              }
            while(CheckPointer(Temp)==POINTER_INVALID && order<m_data_total);
            continue;
           }
//---
         if(Temp!=NULL)
           {
            if(type==Temp.Type())
              {
               price=volume*price+Temp.Volume()*Temp.Price();
               volume+=Temp.Volume();
               price=price/volume;
               switch(type)
                 {
                  case POSITION_TYPE_BUY:
                    long_trades++;
                    break;
                  case POSITION_TYPE_SELL:
                    short_trades++;
                    break;
                 }
              } 
            else
              {
               if(i>0 && (direct<0 || direct==type))
                 {
                  double temp=(Temp.Price()-price)*tick_value*(type==POSITION_TYPE_BUY ? 1 : -1)*MathMin(volume,Temp.Volume());
                  balance[i]+=temp;
                  if(temp>=0)
                     profit+=temp;
                  else
                     loss+=temp;
                 }
               volume-=Temp.Volume();
               if(volume<0)
                 {
                  volume=MathAbs(volume);
                  price=Temp.Price();
                  type=Temp.Type();
                  switch(type)
                    {
                     case POSITION_TYPE_BUY:
                       long_trades++;
                       break;
                     case POSITION_TYPE_SELL:
                       short_trades++;
                       break;
                    }
                 }
              }
           }
         do
           {
            order++;
            if(order<m_data_total)
               Temp=m_data[order];
            else
               Temp=NULL;
           }
         while(CheckPointer(Temp)==POINTER_INVALID && order<m_data_total);
        }
      if(i>0)
        {
         balance[i]+=balance[i-1];
        }
      if(volume>0 && (direct<0 || direct==type))
         equity[i]=(rates[i].close-price)*tick_value*(type==POSITION_TYPE_BUY ? 1 : -1)*MathMin(volume,(Temp!=NULL ? Temp.Volume(): DBL_MAX));
      else
         equity[i]=0;
      equity[i]+=balance[i];
      time[i]=(double)rates[i].time;
     }
//---
   return true;
  }

您可在附件中找到类和方法的完整代码。
 

3. 添加图形外壳

程序的图形界面将包含分析的开始和结束日期,选择图表显示信息的复选框,统计数据块,和相应的图表。

图形界面

我们在类 CStatisticsPanel (继承自 CAppDialog 类) 中构建图形界面。 我们将使用类 CDatePicker 的实例来选择分析的开始/结束日期。 我们加入复选框以便选择在 3 个组中显示的数据:

  • 余额和资金;
  • 多头和空头持仓; 以及
  • 要分析的品种列表。

3.1. 创建图形面板

若要创建复选框板块,我们将利用类 CCheckGroup 的实例。 文本统计信息利用类 CLabel 的实例显示。 图表将利用类 CGraphic 的实例构建。 当然,我们将声明类 COrdersCollection 的实例以便访问我们的订单统计信息。

class CStatisticsPanel : public CAppDialog
  {
private:
   CDatePicker       StartDate;
   CDatePicker       EndDate;
   CLabel            Date;
   CGraphic          Graphic;
   CLabel            ShowLabel;
   CCheckGroup       Symbols;
   CCheckGroup       BalEquit;
   CCheckGroup       Deals;
   string            ar_Symbols[];
   CLabel            TotalProfit;
   CLabel            TotalProfitVal;
   CLabel            GrossProfit;
   CLabel            GrossProfitVal;
   CLabel            GrossLoss;
   CLabel            GrossLossVal;
   CLabel            TotalTrades;
   CLabel            TotalTradesVal;
   CLabel            LongTrades;
   CLabel            LongTradesVal;
   CLabel            ShortTrades;
   CLabel            ShortTradesVal;
   //---
   COrdersCollection Orders;

public:
                     CStatisticsPanel();
                    ~CStatisticsPanel();
   //--- 主应用程序对话框的创建和注销
   virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2);
   virtual void      Destroy(const int reason=REASON_PROGRAM);
   //--- 图表事件处理器
   virtual bool      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);

protected:
   virtual bool      CreateLineSelector(const string name,const int x1,const int y1,const int x2,const int y2);
   virtual bool      CreateDealsSelector(const string name,const int x1,const int y1,const int x2,const int y2);
   virtual bool      CreateCheckGroup(const string name,const int x1,const int y1,const int x2,const int y2);
   virtual bool      CreateGraphic(const string name,const int x1,const int y1,const int x2,const int y2);
   //---
   virtual void      Maximize(void);
   virtual void      Minimize(void);
   //---
   virtual bool      UpdateChart(void);

  };

在 Create 方法中,我们将首先调用父类的相关方法,然后在其位置分配所有对象并初始化订单集合类的实例。 在初始化每个元素时,不要忘记分配初始值并将对象添加到控件元素集合中。 在文章 [2] 和 [3] 中详细介绍了基类,因此我不会赘述该方法,仅仅展示其代码。

bool CStatisticsPanel::Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2)
  {
   if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
      return false;
//---
   if(!TotalProfit.Create(m_chart_id,m_name+"Total Profit",m_subwin,5,80,115,95))
      return false;
   if(!TotalProfit.Text("Total Profit"))
      return false;
   if(!Add(TotalProfit))
      return false;
//---
   if(!TotalProfitVal.Create(m_chart_id,m_name+"Total Profit Value",m_subwin,135,80,250,95))
      return false;
   if(!TotalProfitVal.Text("0"))
      return false;
   if(!Add(TotalProfitVal))
      return false;
//---
   if(!GrossProfit.Create(m_chart_id,m_name+"Gross Profit",m_subwin,5,100,115,115))
      return false;
   if(!GrossProfit.Text("Gross Profit"))
      return false;
   if(!Add(GrossProfit))
      return false;
//---
   if(!GrossProfitVal.Create(m_chart_id,m_name+"Gross Profit Value",m_subwin,135,100,250,115))
      return false;
   if(!GrossProfitVal.Text("0"))
      return false;
   if(!Add(GrossProfitVal))
      return false;
//---
   if(!GrossLoss.Create(m_chart_id,m_name+"Gross Loss",m_subwin,5,120,115,135))
      return false;
   if(!GrossLoss.Text("Gross Loss"))
      return false;
   if(!Add(GrossLoss))
      return false;
//---
   if(!GrossLossVal.Create(m_chart_id,m_name+"Gross Loss Value",m_subwin,135,120,250,135))
      return false;
   if(!GrossLossVal.Text("0"))
      return false;
   if(!Add(GrossLossVal))
      return false;
//---
   if(!TotalTrades.Create(m_chart_id,m_name+"Total Trades",m_subwin,5,150,115,165))
      return false;
   if(!TotalTrades.Text("Total Trades"))
      return false;
   if(!Add(TotalTrades))
      return false;
//---
   if(!TotalTradesVal.Create(m_chart_id,m_name+"Total Trades Value",m_subwin,135,150,250,165))
      return false;
   if(!TotalTradesVal.Text("0"))
      return false;
   if(!Add(TotalTradesVal))
      return false;
//---
   if(!LongTrades.Create(m_chart_id,m_name+"Long Trades",m_subwin,5,170,115,185))
      return false;
   if(!LongTrades.Text("Long Trades"))
      return false;
   if(!Add(LongTrades))
      return false;
//---
   if(!LongTradesVal.Create(m_chart_id,m_name+"Long Trades Value",m_subwin,135,170,250,185))
      return false;
   if(!LongTradesVal.Text("0"))
      return false;
   if(!Add(LongTradesVal))
      return false;
//---
   if(!ShortTrades.Create(m_chart_id,m_name+"Short Trades",m_subwin,5,190,115,215))
      return false;
   if(!ShortTrades.Text("Short Trades"))
      return false;
   if(!Add(ShortTrades))
      return false;
//---
   if(!ShortTradesVal.Create(m_chart_id,m_name+"Short Trades Value",m_subwin,135,190,250,215))
      return false;
   if(!ShortTradesVal.Text("0"))
      return false;
   if(!Add(ShortTradesVal))
      return false;
//---
   if(!Orders.Create())
      return false;
//---
   if(!ShowLabel.Create(m_chart_id,m_name+"Show Selector",m_subwin,285,8,360,28))
      return false;
   if(!ShowLabel.Text("Symbols"))
      return false;
   if(!Add(ShowLabel))
      return false;
   if(!CreateLineSelector("LineSelector",2,30,115,70))
      return false;
   if(!CreateDealsSelector("DealsSelector",135,30,250,70))
      return false;
   if(!CreateCheckGroup("CheckGroup",260,30,360,ClientAreaHeight()-5))
      return false;
//---
   if(!Date.Create(m_chart_id,m_name+"->",m_subwin,118,8,133,28))
      return false;
   if(!Date.Text("->"))
      return false;
   if(!Add(Date))
      return false;
//---
   if(!StartDate.Create(m_chart_id,m_name+"StartDate",m_subwin,5,5,115,28))
      return false;
   if(!Add(StartDate))
      return false;
//---
   if(!EndDate.Create(m_chart_id,m_name+"EndDate",m_subwin,135,5,250,28))
      return false;
   if(!Add(EndDate))
      return false;
//---
   StartDate.Value(Orders.FirstOrder());
   EndDate.Value(Orders.LastOrder());
//---
   if(!CreateGraphic("Chraphic",370,5,ClientAreaWidth()-5,ClientAreaHeight()-5))
      return false;
//---
   UpdateChart();
//---
   return true;
  }

善于观察的读者可能会注意到创建的图表尚未添加到控件元素的集合中。 这是因为对象 CGraphic 不是从 CWnd 类继承的,而您只能将 CWnd 继承对象添加到集合中。 因此,我们将不得不重新编写面板最小化/展开的函数。
初始化所有对象后,我们将调用图表更新函数。

3.2. 图表创建功能

我们来润饰图表创建函数 CreateGraphic。 在其参数中,它获取已创建对象的名称和图表位置的坐标。 在函数的开头,图表已创建 (调用 CGraphic 类的 Create 函数)。 由于类 CGraphic 不是从类 CWnd 继承的,并且我们无法将其添加到面板的控件元素集合中,因此图表坐标将根据客户区位置立即移位。

bool CStatisticsPanel::CreateGraphic(const string name,const int x1,const int y1,const int x2,const int y2)
  {
   if(!Graphic.Create(m_chart_id,m_name+name,m_subwin,ClientAreaLeft()+x1,ClientAreaTop()+y1,ClientAreaLeft()+x2,ClientAreaTop()+y2))
      return false;

然后我们必须为图表中显示的每条曲线创建类 CCurve 的实例。 为此目的,我们将首先获取类 COrdersCollection 实例中使用的品种列表。 然后我们将在循环中创建每个品种的余额和资金曲线,并用空值数组初始化它们。 创建之后,我们会隐藏图表中的曲线,直到获得数据。

   int total=Orders.Symbols(ar_Symbols);
   CColorGenerator ColorGenerator;
   double array[];
   ArrayFree(array);
   for(int i=0;i<total;i++)
     {
      //---
      CCurve *curve=Graphic.CurveAdd(array,array,ColorGenerator.Next(),CURVE_LINES,ar_Symbols[i]+" Balance");
      curve.Visible(false);
      curve=Graphic.CurveAdd(array,array,ColorGenerator.Next(),CURVE_LINES,ar_Symbols[i]+" Equity");
      curve.Visible(false);
     }

在创建曲线时,我们禁用横坐标自动缩放,并指定其显示日期属性。 我们还指定了显示曲线标题的大小,并在屏幕上显示图表。

   CAxis *axis=Graphic.XAxis();
   axis.AutoScale(false);
   axis.Type(AXIS_TYPE_DATETIME);
   axis.ValuesDateTimeMode(TIME_DATE);
   Graphic.HistorySymbolSize(20);
   Graphic.HistoryNameSize(10);
   Graphic.HistoryNameWidth(60);
   Graphic.CurvePlotAll();
   Graphic.Update();
//---
   return true;
  }

3.3. 图表和统计数据更新方法

我们将使用方法 UpdateChart 来更新信号的信息。 在函数开始时,我们准备用于收集数据的变量和数组。

bool CStatisticsPanel::UpdateChart(void)
  {
   double balance[];
   double equity[];
   double time[];
   double total_profit=0, total_loss=0;
   int total_long=0, total_short=0;
   CCurve *Balance, *Equity;

然后我们得到要分析区间的开始/结束日期。

   datetime start=StartDate.Value();
   datetime end=EndDate.Value();

检查标记是否显示多头和空头持仓的统计数据。

   int deals=-2;
   if(Deals.Check(0))
      deals=(Deals.Check(1) ? -1 : POSITION_TYPE_BUY);
   else
      deals=(Deals.Check(1) ? POSITION_TYPE_SELL : -2);

在循环中为每个品种准备初始数据时,我们将调用我们已熟悉的函数 GetTimeSeries 来更新时间序列。 在调用方法之前,我们检查相关品种是否在复选框中勾选。 如果没有,则不调用该方法并隐藏曲线。 成功获得时间序列后,我们将更新余额和资金曲线的日期,并在相关复选框中预先勾选。 如果未勾选,则曲线将在图表中隐藏。

   int total=ArraySize(ar_Symbols);
   for(int i=0;i<total;i++)
     {
      Balance  =  Graphic.CurveGetByIndex(i*2);
      Equity   =  Graphic.CurveGetByIndex(i*2+1);
      double profit,loss;
      int long_trades, short_trades;
      if(deals>-2 && Symbols.Check(i) && Orders.GetTimeSeries(ar_Symbols[i],start,end,deals,balance,equity,time,profit,loss,long_trades,short_trades))
        {
         if(BalEquit.Check(0))
           {
            Balance.Update(time,balance);
            Balance.Visible(true);
           }
         else
            Balance.Visible(false);
         if(BalEquit.Check(1))
           {
            Equity.Update(time,equity);
            Equity.Visible(true);
           }
         else
            Equity.Visible(false);
         total_profit+=profit;
         total_loss+=loss;
         total_long+=long_trades;
         total_short+=short_trades;
        }
      else
        {
         Balance.Visible(false);
         Equity.Visible(false);
        }
     }

下一步,我们为图表指定分析区间的开始/结束日期以及网格大小。 更新图表。 

   CAxis *axis=Graphic.XAxis();
   axis.Min((double)start);
   axis.Max((double)end);
   axis.DefaultStep((end-start)/5);
   if(!Graphic.Redraw(true))
      return false;
   Graphic.Update();

在该方法的结论中,我们更新文本标记中的信息以便显示信号的统计数据。

   if(!TotalProfitVal.Text(DoubleToString(total_profit+total_loss,2)))
      return false;
   if(!GrossProfitVal.Text(DoubleToString(total_profit,2)))
      return false;
   if(!GrossLossVal.Text(DoubleToString(total_loss,2)))
      return false;
   if(!TotalTradesVal.Text(IntegerToString(total_long+total_short)))
      return false;
   if(!LongTradesVal.Text(IntegerToString(total_long)))
      return false;
   if(!ShortTradesVal.Text(IntegerToString(total_short)))
      return false;
//---
   return true;
  }

3.4. “动态” 面板

若要 “动态化” 面板,我们必须为对象的动作构建一个事件处理程序。 哪个事件程序必须响应?

首先是,改变分析区间的开始或结束日期,和改变显示余额与资金曲线的统计数据控件集合的复选框状态。 我们不应该忘记我们的诀窍: 我们必须响应创建的用户事件,就是那个无法加载其中一个品种的历史报价记录。 当任何这些事件发生时,调用数据更新方法 UpdateChart 足矣。 结果就是,事件处理方法将显示为:

EVENT_MAP_BEGIN(CStatisticsPanel)
   ON_EVENT(ON_CHANGE,Symbols,UpdateChart)
   ON_EVENT(ON_CHANGE,BalEquit,UpdateChart)
   ON_EVENT(ON_CHANGE,Deals,UpdateChart)
   ON_EVENT(ON_CHANGE,StartDate,UpdateChart)
   ON_EVENT(ON_CHANGE,EndDate,UpdateChart)
   ON_NO_ID_EVENT(1222,UpdateChart)
EVENT_MAP_END(CAppDialog)

除了上述方法之外,我们还改变了最小化/展开面板的方法,即我们在其中添加了图表隐藏/显示功能。 您可在附件中找到类和方法的完整代码。

4. 创建分析信号的指标

我建议将上述所有内容统一到一个指标当中。 这允许我们在子窗口中创建一个图形面板而不涉及图表本身。

我们程序的所有功能都隐藏在 CStatisticsPanel 类中。 所以,若要创建指标,只需在我们的程序中创建此类的实例即可。 在函数 OnInit 中初始化类。

int OnInit()
  {
//---
   long chart=ChartID();
   int subwin=ChartWindowFind();
   IndicatorSetString(INDICATOR_SHORTNAME,"Signal Statistics");
   ReloadHistory=false;
//---
   Dialog=new CStatisticsPanel;
   if(CheckPointer(Dialog)==POINTER_INVALID)
     {
      ChartIndicatorDelete(chart,subwin,"Signal Statistics");
      return INIT_FAILED;
     }
   if(!Dialog.Create(chart,"Signal Statistics",subwin,0,0,0,250))
     {
      ChartIndicatorDelete(chart,subwin,"Signal Statistics");
      return INIT_FAILED;
     }
   if(!Dialog.Run())
     {
      ChartIndicatorDelete(chart,subwin,"Signal Statistics");
      return INIT_FAILED;
     }
//---
   return(INIT_SUCCEEDED);
  }

 我们将函数 OnCalculate 保留为空,因为程序不会响应逐笔报价。 我们只需在函数 OnDeinit和OnChartEvent 中添加调用相关方法。

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   Dialog.ChartEvent(id,lparam,dparam,sparam);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   Dialog.Destroy(reason);
   delete Dialog;
  }

指标编制完成后,我们只需将所选信号的统计数据加载到终端图表,并将我们的指标附加到其中一个图表上。 现在我们可以研究和分析交易。 还有更好的一点: 我们没有在我们的程序中过滤图表以便进行分析。 所以,指标将收集终端中已打开的所有图表的统计数据。 为了避免指标交易与终端中的任何其它交易混淆,我建议在加载信号交易历史之前关闭所有图表。

指标如何工作的示例

您可在附件中找到该程序的完整代码。

结束语

我们已构建了一个指标,通过图表中的标记来分析交易。 该技术可用于各种目的,例如选择信号或优化您自己的策略。 例如,这将允许您辨别我们的策略中不起作用的品种,以便将来不在这些品种上使用它。

参考

  1. 自动选择有前途的信号
  2. 如何创建任意复杂度的图形面板
  3. 改进面板: 加入透明度,更改背景颜色并从 CAppDialog/CWndClient 继承

本文中使用的程序:

#  名称 类型  描述 
1 Order.mqh  类库  用于存储交易信息的类
2 OrdersCollection.mqh  类库  交易集合类
3 StatisticsPanel.mqh  类库  图形界面类
4 SignalStatistics.mq5  指标  用于分析交易的指标代码

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

附加的文件 |

MQL5.zip
(470.5 KB)

 

 


MyFxtop迈投(www.myfxtop.com)-靠谱的外汇跟单社区,免费跟随高手做交易!

 

免责声明:本文系转载自网络,如有侵犯,请联系我们立即删除,另:本文仅代表作者个人观点,与迈投财经无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。

著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。

風險提示

MyFxtops邁投所列信息僅供參考,不構成投資建議,也不代表任何形式的推薦或者誘導行為。MyFxtops邁投非外匯經紀商,不接觸妳的任何資金。 MYFXTOPS不保證客戶盈利,不承擔任何責任。從事外彙和差價合約等金融產品的槓桿交易具有高風險,損失有可能超過本金,請量力而行,入市前需充分了解潛在的風險。過去的交易成績並不代表以後的交易成績。依據各地區法律法規,MyFxtops邁投不向中國大陸、美國、加拿大、朝鮮居民提供服務。

邁投公眾號

聯繫我們

客服QQ:981617007
Email: service@myfxtop.com

MyFxtops 邁投