在交易中应用图形资源

当然,美化并不是这些资源的主要目的。我们来看看如何基于这些资源创建一个有用的工具。我们还将消除一个疏漏:到目前为止,我们只在 OBJ_BITMAP_LABEL 对象中使用了资源,这些对象是以屏幕坐标定位的。然而,图形资源也可以嵌入到 OBJ_BITMAP 对象中,并参考报价坐标:价格和时间。

在本书的前面部分,我们看到了 IndDeltaVolume.mq5 指标,它可以计算每个柱线的 delta 成交量(逐笔交易明细或实际交易量)。除了这种 delta 成交量表示法外,还有另一种也很受用户欢迎的表示法:市场概要分析。这是在价位背景下的成交量分布。这种柱状图可以针对整个窗口、给定深度(例如一天内)或单个柱线绘制。

它是我们以新指标 DeltaVolumeProfile.mq5 的形式实现的最后一个选项。我们已经在上述指标的框架内考虑了逐笔交易明细历史请求的主要技术细节,因此现在我们将主要关注图形组件。

输入变量中的 ShowSplittedDelta 标志将控制成交量的显示方式:按买入/卖出方向分解或折叠。

input bool ShowSplittedDelta = true;

指标中没有缓冲区。它将根据用户的要求,特别是通过点击特定柱线,计算并显示该柱线的柱状图。因此,我们将使用 OnChartEvent 处理程序。在该处理程序中,我们获取屏幕坐标,将其重新计算为价格和时间,然后调用辅助函数 RequestData 开始计算。

void OnChartEvent(const int idconst long &lparamconst double &dparamconst string &sparam)
{
   if(id == CHARTEVENT_CLICK)
   {
      datetime time;
      double price;
      int window;
      ChartXYToTimePrice(0, (int)lparam, (int)dparamwindowtimeprice);
      time += PeriodSeconds() / 2;
      const int b = iBarShift(_Symbol_Periodtimetrue);
      if(b != -1 && window == 0)
      {
         RequestData(biTime(_Symbol_Periodb));
      }
   }
   ...
}

为了填充它,我们需要 DeltaVolumeProfile 类,该类类似于 IndDeltaVolume.mq5 中的 CalcDeltaVolume 类。

新类描述的变量考虑了成交量计算方法 (tickType)、构建图表基于的价格类型 (barType)、ShowSplittedDelta 输入变量的模式(将被置于成员变量 delta 中)以及图表上所生成对象的前缀。

class DeltaVolumeProfile
{
   const COPY_TICKS tickType;
   const ENUM_SYMBOL_CHART_MODE barType;
   const bool delta;
   
   static const string prefix;
   ...
public:
   DeltaVolumeProfile(const COPY_TICKS typeconst bool d) :
      tickType(type), delta(d),
      barType((ENUM_SYMBOL_CHART_MODE)SymbolInfoInteger(_SymbolSYMBOL_CHART_MODE))
   {
   }
   
   ~DeltaVolumeProfile()
   {
      ObjectsDeleteAll(0prefix0); // TODO: delete resources
   }
   ...
};
   
static const string DeltaVolumeProfile::prefix = "DVP";
   
DeltaVolumeProfile deltas(TickTypeShowSplittedDelta);

只有在有真实成交量的交易金融工具中,才可将 tick type 更改为 TRADE_TICKS 值。默认启用 INFO_TICKS 模式,该模式适用于所有金融工具。

通过 createProfileBar 方法请求特定柱线的逐笔交易明细。

   int createProfileBar(const int i)
   {
      MqlTick ticks[];
      const datetime time = iTime(_Symbol_Periodi);
      // prev and next - time limits of the bar
      const datetime prev = time;
      const datetime next = prev + PeriodSeconds();
      ResetLastError();
      const int n = CopyTicksRange(_SymbolticksCOPY_TICKS_ALL,
         prev * 1000next * 1000 - 1);
      if(n > -1 && _LastError == 0)
      {
         calcProfile(itimeticks);
      }
      else
      {
         return -_LastError;
      }
      return n;
   }

逐笔交易明细的直接分析和成交量的计算在受保护方法 calcProfile 中进行。在该方法中,我们首先要找出柱线的价格范围及其大小(以像素为单位)。

   void calcProfile(const int bconst datetime timeconst MqlTick &ticks[])
   {
      const string name = prefix + (string)(ulong)time;
      const double high = iHigh(_Symbol_Periodb);
      const double low = iLow(_Symbol_Periodb);
      const double range = high - low;
      
      ObjectCreate(0nameOBJ_BITMAP0timehigh);
      
      int x1y1x2y2;
      ChartTimePriceToXY(00timehighx1y1);
      ChartTimePriceToXY(00timelowx2y2);
      
      const int h = y2 - y1 + 1;
      const int w = (int)(ChartGetInteger(0CHART_WIDTH_IN_PIXELS)
         / ChartGetInteger(0CHART_WIDTH_IN_BARS));
      ...

根据这些信息,我们创建一个 OBJ_BITMAP 对象,为图像分配一个数组,并创建一个资源。整幅图片的背景为空(透明)。每个对象都以其柱线 High 价格的中点为锚点,宽度为一个柱线。

      uint data[];
      ArrayResize(dataw * h);
      ArrayInitialize(data0);
      ResourceCreate(name + (string)ChartID(), datawh00wCOLOR_FORMAT_ARGB_NORMALIZE);
         
      ObjectSetString(0nameOBJPROP_BMPFILE"::" + name + (string)ChartID());
      ObjectSetInteger(0nameOBJPROP_XSIZEw);
      ObjectSetInteger(0nameOBJPROP_YSIZEh);
      ObjectSetInteger(0nameOBJPROP_ANCHORANCHOR_UPPER);
      ...

然后,以传递数组的逐笔交易明细为单位计算交易量。价位数等于以像素为单位的柱线高度 (h)。通常情况下,它小于以点为单位的价格范围,因此像素可以作为计算统计数据的篮子。如果在较小的时间范围内,点的范围小于以像素为单位的大小,直方图在视觉上就会显得稀疏。采购量和销售量分别累加到 plusminus 数组中。

      long plus[], minus[], max = 0;
      ArrayResize(plush);
      ArrayResize(minush);
      ArrayInitialize(plus0);
      ArrayInitialize(minus0);
      
      const int n = ArraySize(ticks);
      for(int j = 0j < n; ++j)
      {
         const double p1 = price(ticks[j]); // returns Bid or Last
         const int index = (int)((high - p1) / range * (h - 1));
         if(tickType == TRADE_TICKS)
         {
            // if real volumes are available, we can take them into account
            if((ticks[j].flags & TICK_FLAG_BUY) != 0)
            {
               plus[index] += (long)ticks[j].volume;
            }
            if((ticks[j].flags & TICK_FLAG_SELL) != 0)
            {
               minus[index] += (long)ticks[j].volume;
            }
         }
         else // tickType == INFO_TICKS or tickType == ALL_TICKS
         if(j > 0)
         {
           // if there are no real volumes,
           // price movement up/down is an estimate of the volume type
            if((ticks[j].flags & (TICK_FLAG_ASK | TICK_FLAG_BID)) != 0)
            {
               const double d = (((ticks[j].ask + ticks[j].bid)
                              - (ticks[j - 1].ask + ticks[j - 1].bid)) / _Point);
               if(d > 0plus[index] += (long)d;
               else minus[index] -= (long)d;
            }
         }
         ...

为使直方图正常化,我们会求出最大值。

         if(delta)
         {
            if(plus[index] > maxmax = plus[index];
            if(minus[index] > maxmax = minus[index];
         }
         else
         {
            if(fabs(plus[index] - minus[index]) > max)
               max = fabs(plus[index] - minus[index]);
         }
      }
      ...

最后,生成的统计数据将输出到图形缓冲区 data 并发送到资源。买入量显示为蓝色,卖出量显示为红色。如果启用了净值模式,则金额显示为绿色。

      for(int i = 0i < hi++)
      {
         if(delta)
         {
            const int dp = (int)(plus[i] * w / 2 / max);
            const int dm = (int)(minus[i] * w / 2 / max);
            for(int j = 0j < dpj++)
            {
               data[i * w + w / 2 + j] = ColorToARGB(clrBlue);
            }
            for(int j = 0j < dmj++)
            {
               data[i * w + w / 2 - j] = ColorToARGB(clrRed);
            }
         }
         else
         {
            const int d = (int)((plus[i] - minus[i]) * w / 2 / max);
            const int sign = d > 0 ? +1 : -1;
            for(int j = 0j < fabs(d); j++)
            {
               data[i * w + w / 2 + j * sign] = ColorToARGB(clrGreen);
            }
         }
      }
      ResourceCreate(name + (string)ChartID(), datawh00wCOLOR_FORMAT_ARGB_NORMALIZE);
   }

现在我们可以返回 RequestData 函数:它的任务是调用 createProfileBar 方法并处理错误(如果有)。

void RequestData(const int bconst datetime timeconst int count = 0)
{
   Comment("Requesting ticks for "time);
   if(deltas.createProfileBar(b) <= 0)
   {
      Print("No data on bar "b", at "TimeToString(time),
         ". Sending event for refresh...");
      ChartSetSymbolPeriod(0_Symbol_Period); // request to update the chart
      EventChartCustom(0TRY_AGAINbcount + 1NULL);
   }
   Comment("");
}

唯一的错误处理策略是再次请求逐笔交易明细,因为它们可能还来不及加载。为此,函数会向图表发送一条自定义 TRY_AGAIN 消息,并自行处理。

void OnChartEvent(const int idconst long &lparamconst double &dparamconst string &sparam)
{
   ...
   else if(id == CHARTEVENT_CUSTOM + TRY_AGAIN)
   {
      Print("Refreshing... ", (int)dparam);
      const int b = (int)lparam;
      if((int)dparam < 5)
      {
         RequestData(biTime(_Symbol_Periodb), (int)dparam);
      }
      else
      {
         Print("Give up. Check tick history manually, please, then click the bar again");
      }
   }
}

我们重复此过程的次数不超过 5 次,因为逐笔交易明细历史记录的深度有限,无缘无故加载计算机毫无意义。

DeltaVolumeProfile 类还具有处理 CHARTEVENT_CHART_CHANGE 消息的机制,以便在改变图表大小或比例时重绘现有对象。详情请参见源代码。

该指标的结果如下图所示。

在图形资源中显示单独成交量的每柱线柱状图

在图形资源中显示单独成交量的每柱线柱状图

请注意,绘制指标后不会立即显示柱状图:必须点击柱线才能计算其柱状图。