使用 CGraphic 开发库实现一个剥头皮市场深度

Vasiliy Sokolov | 2 十一月, 2017


目录

简介

本文是两年前发布的描述市场深度相关开发库的逻辑学延伸,从那以后,从仓位访问分时历史已经在 MQL5 中实现了,MetaQuotes 也发布了 CGraphic 开发库,它可以把自定义数据可视化为复杂的统计图形。CGraphic 的目标和R语言中的plot函数类似,这个开发库的用法在一篇单独的文章中已经详细描述过。 

这些功能的引入使得可以把之前提供的市场深度大幅度地现代化。除了订单簿之外,在新版本中还显示了最近交易的分时图表:

图 1. 带有分时图表的市场深度.

之前的开发库版本包含了两个主要模块: 用于操作市场深度的 CMarketBook 类,和一个用于渲染它的图形面板。代码已经有了大量修改和提高,修正了许多错误,而市场深度的图形部分现在有了它自己的简单和轻量级的 CPanel 图形开发库。

让我们回到 CGraphic 以及它的绘制复杂图表和在独立窗口中绘制线形图表的功能。看起来这种特别的功能似乎只对统计问题有用,但是不是这样的!在本文中,我将尝试展示 CGraphic 的除了用于统计方面的功能 — 例如,当创建一个剥头皮市场深度。

从上个版本发布以来的修改

在发表了文章 "MQL5 酷客宝典: 实现您自己的市场深度"之后, 我在实际应用中大量使用了 CMarketBook,而在代码中发现了许多错误。我逐渐修改了接口,这里是所有的修改和更新:

  1. 在市场深度中,最初的图形非常简陋,订单簿表格单元是使用了几个基本元件类来显示的,一段时间之后,在这些类中实现了更多的功能,而它们的简单性和轻量级证明了在设计其他类型的面板时也非常方便,结果,这些类也被加到一个独立的项目中 - CPanel 开发库。它位于 Include 文件夹。
  2. 市场深度的外观也有了提高,例如,用于打开和关闭市场深度窗口的小三角形已经被一个大的方形按钮替代,元件之间的覆盖也修改了 (如果表格重新打开,市场深度元件会在已有的元件上方重新绘制).
  3. 新的设置可以改变打开/关闭市场深度按钮在图表上X轴和Y轴的位置。经常会出现,因为交易品种名称不标准和另外还有交易面板的原因,市场深度的打开/关闭按钮可能会覆盖图表上的其他活动元件,这种可以人工设置按钮位置的功能可以避免这样的覆盖。
  4. CMarketBook 类也有了很大变化。在这个类中已经改正了下面的错误: 超出数组范围错误; 订单簿空白或者部分填充错误; 当改变交易品种时出现的除0错误。CMarketBook 类现在是一个独立的模块了,它位于 MQL5\Include\Trade;
  5. 实现了一些小的修改以提高指标的整体稳定性。

现在我们将从这个提高和修改过的版本开始工作,试着把它逐渐改为一个剥头皮市场深度工具。

CPanel 图形开发库概览

有很多关于使用MQL5创建用户界面的文章,Anatoly Kazharsky 的 "图形化界面"系列在它们之中是比较突出的,在这些文章之后已经很难加上任何新的内容了,所以,我们将不会详细分析图形界面的创建。之前在上面已经说过,市场深度的图形部分现在用的是完整功能的 CPanel 开发库,它的基础架构需要阐述一下,因为我们将会基于这个开发库来创建一个特别的图形元件 "分时图表"。它将与订单簿相结合,来创建一个含有几个元件的完整功能面板。

让我们详细探讨 CPanel 来了解进一步的操作原则。MQL5 中的图形化元件体现为几个图形元素,它们包括:

  • 文本标签
  • 按钮
  • 输入框
  • 长方形标签
  • 位图标签

它们全部都有一些相同的属性,例如,长方形标签、按钮和输入框可以配置,所以很难区分它们。所以,元件几乎可以基于任何图形元素。例如,输入框可以显示为按钮,而按钮可以显示为长方形标签,从视觉上,这样的替代将不会被注意到,而用户点击输入框会想到他实际按下了一个按钮。

也许这样的替代看起来奇怪,而且使用户界面的创建原则更加复杂,但是我们必须明白,除了一般特性之外,每个基本元素还是会有自身独特的属性。例如,输入框不能是透明的,而长方形标签可以。所以,可以使用相同的类来创建独特外观的元件,

这可以通过一个例子来展示,假定我们需要使用一个传统的文本标签来创建一个图形化面板:

图 2. 一个使用无边框文本标签的表单.

但是如果我们想要加上边框,我们将会遇到问题,因为文本标签没有 "frame(边框)" 属性。解决方案很简单: 我们将会使用按钮,而不是文本标签!这里是一个使用按钮的表单:

图 3. 一个使用含边框文本的表单.

当创建图形界面时,会出现很多类似的情况,无法预测用户可能的需求,所以,最好不要基于一个特定的元素来创建图形元件,而是让用户有机会来决定。

这就是 CPanel 开发库中元件的组织方式。CPanel 是一些类,它们中的每个类都表示高级别图形化界面中的某个元件。为了初始化这样的元件,我们需要制定它所基于图形元素的类型。每个这样的类都有一个通用的父类,CNode 类, 它只执行一个功能,也就是保存基础元素的类型,它只有保护的构造函数,需要在元件创建时指定类型。 

还有几个独特的高级图形元件,所谓的独特性是以来于一系列属性的,它们要被赋予到基础元素中才能使它独特。在 CPanel 中, 一个这样的独特元件是 CElChart 类。与所有其他类类似,它派生于 CNode, 而且它包含了用于配置以下属性的方法:

  • 元件的长度和高度,
  • 元件相对图表的 X 和 Y 坐标,
  • 元件的宽度和高度,
  • 元件的背景和边框颜色 (如果支持这样的属性),
  • 元件边框的类型 (如果支持这样的属性),
  • 元件内的文字,它的字体、大小和对齐方式 (如果支持这样的属性).

CElChart 提供了用于设置属性的方法,但是它不保证这些属性会真正设置。只有基本元素才能确定 CElChart 是否将支持某个属性。与 CNode 类似, CElChart 需要制定它所基于的图形化元素的类型。这样,使用 CElChart 可以让您创建通常的表单,以及,例如按钮或者文本栏位。这在实际应用中非常方便。

例子 让我们像图3那样绘制面板。我们需要两个元件:一个含有边框的背景和一个含有边框的文字,它们都是相同的 CElChart 类的实例。但是它们内部使用了两个不同的图形元素: OBJ_RECTANGLE_LABEL 和 BJ_BUTTON. 这里是结果代码:

//+------------------------------------------------------------------+
//|                                       MarketBookArticlePanel.mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Panel\ElChart.mqh>

CElChart Fon(OBJ_RECTANGLE_LABEL);
CElChart Label(OBJ_BUTTON);
//+------------------------------------------------------------------+
//| EA交易初始化函数                                                    |
//+------------------------------------------------------------------+
int OnInit()
{
//---
   Fon.Height(100);
   Fon.Width(250);
   Fon.YCoord(200);
   Fon.XCoord(200);
   Fon.BackgroundColor(clrWhite);
   Fon.BorderType(BORDER_FLAT);
   Fon.BorderColor(clrBlack);
   Fon.Show();
   
   Label.Text("Meta Quotes 语言");
   Label.BorderType(BORDER_FLAT);
   Label.BorderColor(clrBlack);
   Label.Width(150);
   Label.Show();
   Label.YCoord(240);
   Label.XCoord(250);
   ChartRedraw();
//---
   return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| EA交易订单分时函数                                                  |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }
//+------------------------------------------------------------------+


当所有元件创建之后,我们需要在 OnInit 函数中设置它们的属性。现在元件可以通过调用它们的 Show 方法来显示在图表上了。

通过组合基本元素和 CElChart 类, 我们可以创建强大的、灵活的,而且最重要的、简单的图形界面。市场深度的图形化显示是类似的,订单簿包含了多个 CBookCell 元件,它们是基于 CElChart 的。

CPanel 图形化引擎支持嵌套,这意味着一个元件中可以放入更多元件,嵌套可以进行统一控制,例如,一个命令可以从全局表单中发送到它所有的子元件中,让我们把上面的例子这样修改:

//+------------------------------------------------------------------+
//|                                       MarketBookArticlePanel.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Panel\ElChart.mqh>

CElChart Fon(OBJ_RECTANGLE_LABEL);
CElChart *Label;
//+------------------------------------------------------------------+
//| EA交易初始化函数                                                   |
//+------------------------------------------------------------------+
int OnInit()
{
//---
   Fon.Height(100);
   Fon.Width(250);
   Fon.YCoord(200);
   Fon.XCoord(200);
   Fon.BackgroundColor(clrWhite);
   Fon.BorderType(BORDER_FLAT);
   Fon.BorderColor(clrBlack);
   
   Label = new CElChart(OBJ_BUTTON);
   Label.Text("Meta Quotes 语言");
   Label.BorderType(BORDER_FLAT);
   Label.BorderColor(clrBlack);
   Label.Width(150);
   Label.YCoord(240);
   Label.XCoord(250);
   //Label.Show();
   Fon.AddElement(Label);
   
   Fon.Show();
   ChartRedraw();
//---
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| EA交易订单分时函数                                                  |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }


现在,在程序执行时,会动态创建 CLabel, 它是一个指向 CElCahrt 元件的指针,在创建和对应的配置之后,它会被加到表单上。现在不需要使用一个单独的 Show 命令来显示它了,而是只要运行 Fon 元件的 Show 命令,它是我们应用程序的主表单。根据规格需求,这个命令要为所有子元件执行,包含了标签。 

除了设置元件属性,CPanel 还支持高级的事件模型,不仅包括从图表中接收的事件,还有任何可以发生于 CPanel 中的事件。这个功能是由 CEvent 类和 Event 方法提供的。CEvent 是一个抽象类,很多特定的类都是基于它的,例如 CEventChartObjClick。

假定我们的用户表单有几个嵌套的子元件,用户可以创建一个事件,例如它可以点击这些元件中的任何一个,我们怎么知道使用哪个类实例来处理这个事件呢?为此我们将使用 CEventChartObjClick 事件: 让我们创建一个类实例并把它发送到中央表单:

CElChart Fon(OBJ_RECTANGLE_LABEL);
...
...
void OnChartEvent(const int id,         // 事件标识符   
                  const long& lparam,   // 长整数类型的事件参数
                  const double& dparam, // 双精度浮点数类型的事件参数
                  const string& sparam  // 字符串类型的事件参数
  )
{ 
   CEvent* event = NULL;
   switch(id)
   {
      case CHARTEVENT_OBJECT_CLICK:
         event = new CEventChartObjClick(sparam);
         break;
   }
   if(event != NULL)
   {
      Fon.Event(event);
      delete event;
   }
}

使用这个方法,我们已经发送了 CEventChartObjClick 广播事件,所有在 Fon 实例内的元件都回收到它。事件是否将被处理,这依赖于表单的内部逻辑。 

让我们让 MetaQuotes 语言中的标签来处理这次点击,然后把它的文字变成"Enjoy(开心)"。为此,我们创建 CEnjoy 类并为它提供所需的逻辑: 重载 OnClick 方法,它就是对应事件的处理函数:

//+------------------------------------------------------------------+
//|                                       MarketBookArticlePanel.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Panel\ElChart.mqh>
#include <Panel\Node.mqh>

CElChart Fon(OBJ_RECTANGLE_LABEL);

//+------------------------------------------------------------------+
//| EA交易初始化函数                                                    |
//+------------------------------------------------------------------+
class CEnjoy : public CElChart
{
protected:
   virtual void OnClick(void);
public:
                CEnjoy(void);
   
};

CEnjoy::CEnjoy(void) : CElChart(OBJ_BUTTON)
{
}
void CEnjoy::OnClick(void)
{
   if(Text() != "Enjoy!")
      Text("Enjoy!");
   else
      Text("MetaQuotes 语言");
}
CEnjoy Label;
//+------------------------------------------------------------------+
//| EA交易初始化函数                                                    |
//+------------------------------------------------------------------+
int OnInit()
{
//---
   Fon.Height(100);
   Fon.Width(250);
   Fon.YCoord(200);
   Fon.XCoord(200);
   Fon.BackgroundColor(clrWhite);
   Fon.BorderType(BORDER_FLAT);
   Fon.BorderColor(clrBlack);
   
   Label.Text("Meta Quotes 语言");
   Label.BorderType(BORDER_FLAT);
   Label.BorderColor(clrBlack);
   Label.Width(150);
   Label.YCoord(240);
   Label.XCoord(250);
   //Label.Show();
   Fon.AddElement(&Label);
   
   Fon.Show();
   ChartRedraw();
//---
   return(INIT_SUCCEEDED);
}
void OnChartEvent(const int id,         // 事件标识符   
                  const long& lparam,   // 长整数类型的事件参数
                  const double& dparam, // 双精度浮点数类型的事件参数
                  const string& sparam  // 字符串类型的事件参数
  )
{ 
   CEvent* event = NULL;
   switch(id)
   {
      case CHARTEVENT_OBJECT_CLICK:
         event = new CEventChartObjClick(sparam);
         break;
   }
   if(event != NULL)
   {
      Fon.Event(event);
      delete event;
   }
   ChartRedraw();
}
//+------------------------------------------------------------------+
//| EA交易订单分时函数                                                  |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }
//+------------------------------------------------------------------+


我们通过 Event 方法来向表单发送 CEventObjClick 事件,而在 OnClick 中处理它,这看起来可能有些奇怪。很多标准事件 (例如,鼠标的点击) 有它们自己特定的事件方法,如果我们重载了它们,它们将会接收对应的事件。不使用重载,所有的事件将会在高一层处理,也就是在 Event 方法中,它也是一个虚拟方法,并且它也可以像 OnClick 那样进行重载。在这个水平上,所有事件都通过接收到的 CEvent 实例的分析来进行处理, 

我们现在不会详细讨论它。让我们设置 CPanel 的主要属性,

  • 所有实现 GUI 元件的 CPanel 类都是可以基于任何所选的图形元素的,它是在类实例的创建中选择和设置的。
  • 一个绝对的 CPanel 元件可以包含不限数量的其他 CPanel 元件,这就是嵌套和统一控制是如何实现的。所有事件都通过嵌套树发布,所以每个元件都能访问每个事件。
  • CPanel 的事件模型有两层,低层模型是基于 Event 方法和 CEvent 类型的类的,这使得可以处理几乎任何事件,甚至是那些 MQL 中不支持的,通过 CEvent 发送的事件总是广播的。在高层, 标准事件被转换为对应方法的调用,例如,CEventChartObjClick 事件被转换为 OnClick 调用, 而 Show 方法调用在所有子元件中会生成递归的 OnShow 方法调用。在这个层次中可以直接进行事件的调用,例如,如果我们调用 Show(), 它将显示一个面板。调用 Refresh 将会刷新面板的显示。

回顾是简明扼要的,但是总的思路对于了解我们进一步的开发是足够了的,我们将会花一点时间来创建一个剥头皮市场深度工具。

与订单簿同步分时数据流

订单簿是一个动态结构,它的数值在动荡的市场中每秒可能改变很多次,为了访问订单簿的当前状态,您必须在对应的事件处理函数,即 OnBookEvent 函数中处理一个特定的 BookEvent。当订单簿改变时,终端调用 OnBookEvent, 指示对应改变的交易品种。在前面的文章中,我们开发了 CMarketBook 类,它提供了对当前订单簿状态的方便访问,订单簿的当前状态可以在这个类中通过在 OnBookEvent 函数中调用 Refresh() 方法来得到,这就是它看起来的样子:

//+------------------------------------------------------------------+
//|                                                   MarketBook.mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots 0
#include <MarketBook.mqh>

CMarketBook MarketBook.mqh
double fake_buffer[];
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                                 |
//+------------------------------------------------------------------+
int OnInit()
  {
   MarketBook.SetMarketBookSymbol(Symbol());
//--- 指标缓冲区映射
   SetIndexBuffer(0,fake_buffer,INDICATOR_CALCULATIONS);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| MarketBook 改变事件                                                |
//+------------------------------------------------------------------+
void OnBookEvent(const string &symbol)
{
   if(symbol != MarketBook.GetMarketBookSymbol())
      return;
   MarketBook.Refresh();
   ChartRedraw();
}

在新版本中,除了订单簿之外,市场深度还显示了一个实时的分时图表,所以我们需要实现额外的函数来处理新收到的分时信息。在 MQL5 中, 有三种基本的机制来分析分时,它们包括:

  • 使用 SymbolInfoTick 函数来取得最新收到的分时,
  • 对于EA交易,在 OnTick 函数中处理新分时的到来,而对于指标,函数为 OnCalculate,
  • 使用 CopyTicks 和 CopyTicksRange 函数来取得分时历史。

前两种方法可以结合起来,例如,SymbolInfoTick 函数可以在 OnTick 或者 OnCalculate 中调用以取得最新分时的参数,但是,这两种方法对于我们来说没有用,因为分时流的出现。

为了了解分时是如何形成的,让我们回顾文章 以莫斯科交易所衍生产品市场为例的交易定价原则 并考虑用于黄金的市场深度: 

价格, 每盎司黄金的价格$ 盎司数量 (合约数)
1280.8 17
1280.3 3
1280.1 5
1280.0 1
1279.8 2
1279.7 15
1279.3 3
1278.8 13

图 4. 一个市场深度的实例

假定在市场深度更新时,我们请求分时历史并且使用一种特定算法来确定自从上次更新后又有多少分时到达.从理论上讲,每个分时必定对应着订单簿上的至少一次变化,意思是每次订单簿有变化时不能有分时到来.但是现实中是不同的,并且我们需要在分时历史上处理,来进行一次正常的同步。

假定有一个买家想买9个合约,买家将会在买入黄金时至少有三次交易,如果有更多买家想要在 1280.1 买入或者 1280.3 买入, 就会有更多交易。通过进行一个操作 (购买), 买家创建 多个 交易同时进行。这样,会有"一包"分时到达 MetaTrader 5 终端。并且如果我们再 OnCalculate 中使用 SymbolInfoTick 函数,只返回这个系列的最后的分时,而之前的分时会丢失。

这就是为什么我们需要更可靠的机制来使用 CopyTicks 函数取得分时。和 SymbolInfoTick 不同, 这个函数允许接到一系列分时,和 CopyTicksRange 类似. 这就提供了不丢失分时的正确显示分时历史。

但是 CopyTiks 函数不允许请求最新的 N 个分时,它提供的是在制定时刻之后来到的所有分时信息。这使任务更加复杂。我们需要执行一个请求,接受分时数组的数据并把它与前一次更新后的分时数组做比较。这样我们就可以知道那个分时没有包含在前面一次的包裹中。但是分时不能直接相互比较,因为在它们之间外观没有明显区别。例如,参考下面的订单簿例子:

图 5. 一个含有相同交易例子的订单簿

我们可以看到两组绝对相同的分时,它们被使用红色边框做标记: 它们有相同的时间,仓位大小,方向和价格。所以,很明显我们不能比较它们之间的订单分时,

但是我们可以比较一组分时。如果两组分时是相同的,这些分时以及更多的分时都已经在前面价格更新时分析过了。

让我们把 CMarketBook 类和分时流同步: 向其中加上 MqlTiks 数组,它包含了从上次更新后收到的新的分时。新的分时将会通过内部的 CompareTiks 方法进行计算:

//+------------------------------------------------------------------+
//| 比较两个分时集合并寻找新的分时                                         |
//+------------------------------------------------------------------+
void CMarketBook::CompareTiks(void)
{
   MqlTick n_tiks[];
   ulong t_begin = (TimeCurrent()-(1*20))*1000; // 从20秒之前开始
   int total = CopyTicks(m_symbol, n_tiks, COPY_TICKS_ALL, t_begin, 1000);
   if(total<1)
   {
      printf("接收分时失败");
      return;
   }
   if(ArraySize(m_ticks) == 0)
   {
      ArrayCopy(m_ticks, n_tiks, 0, 0, WHOLE_ARRAY);
      return;
   }
   int k = ArraySize(m_ticks)-1;
   int n_t = 0;
   int limit_comp = 20;
   int comp_sucess = 0;
   //遍历从上次开始所有接收到的交易
   for(int i = ArraySize(n_tiks)-1; i >= 0 && k >= 0; i--)
   {
      if(!CompareTiks(n_tiks[i], m_ticks[k]))
      {
         n_t = ArraySize(n_tiks) - i;
         k = ArraySize(m_ticks)-1;
         comp_sucess = 0;
      }
      else
      {
         comp_sucess += 1;
         if(comp_sucess >= limit_comp)
            break;
         k--;
      }
   }
   //记住接收到的分时
   ArrayResize(m_ticks, total);
   ArrayCopy(m_ticks, n_tiks, 0, 0, WHOLE_ARRAY);
   //计算新分时起始的索引并把它们复制到缓冲区来访问
   ArrayResize(LastTicks, n_t);
   if(n_t > 0)
   {
      int index = ArraySize(n_tiks)-n_t;
      ArrayCopy(LastTicks, m_ticks, 0, index, n_t);
   }
}

这个算法不算简单,CompareTicks 会请求最近20秒的所有分时,然后把它们和之前记录的分时数组做比较,如果当前数组有连续20个分时和之前数组的20个分时相同,就假定这20个分时之后的所有分时都是新的。

这里是一个简单框架,可以解释这个算法。假定我们在很短的时间段之间调用两次 CopyTiks,这个函数可能会返回含有0和1的数组,而不是分时,得到了这两个数组之后,我们检查在第二个数组中有多少不与第一个数组中元件匹配的独特的最新元件,这就是自从上次更新后接收到的新的分时的数量。这可能看起来就像下面的概要图:

图 6. 重复序列同步框架.

比较显示,在第一个数组中,从序号6到14与第二个数组中的从1到8的数据是相同的,所以数组 2 有5个新的数值,也就是序号从9到14的元素。这个算法在不同的组合下都是有效的: 数组可以有不同的长度,也许没有相同的元素或者完全相同。在所有这些情况下,新数值的数量都可以正确得到。

当确定了新分时的数量之后,我们把它们复制到 LastTiks 数组中,这个数组是 CMarketBook 类中的一个公有栏位。

这是一个新版本的 CMarketBook 类,加上了包含了分时的数组,其中是之前和当前更新之间的分时信息。例如,下面的代码可以用于找到新的分时:

//+------------------------------------------------------------------+
//| MarketBook 改变事件                                                |
//+------------------------------------------------------------------+
void OnBookEvent(const string &symbol)
{
   
   if(symbol != MarketBook.GetMarketBookSymbol())
      return;
   MarketBook.Refresh();
   string new_tiks = (string)ArraySize(MarketBook.LastTicks);
   printf("已经收到了 " + new_tiks + " 个新的分时信息");
}

在市场深度类中含有分时,这可以使当前的订单与分时流同步,在每次订单簿更新的时刻,我们都有一个在此更新前的分时列表,这样,分时流就能和订单簿完全同步了。晚些时候我们将会使用这个重要的质量保证,现在我们继续转到 CGraphic 图形开发库。

CGraphic 基础

CGraphic 开发库包含了线形,柱形图,点,以及复杂的几何图形。根据我们的目标,我们将只使用它功能的一小部分,我们将需要两条线来显示卖家报价和买家报价的水平,以及特定的点来显示最新的交易。CGraphic 是一个 CCurve 对象的容器,就像名称中暗示的,这些对象中的每一个都是一条曲线,包含有X和Y坐标的点。根据显示类型,它们可以使用线来连接,可以显示为柱形图,或者显示为点。 

因为最新交易显示的特性,我们将使用二维图表,让我们试着创建一个简单的二维线性图表:

//+------------------------------------------------------------------+
//|                                                   TestCanvas.mq5 |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Graphics\Graphic.mqh>

CGraphic Graph;
//+------------------------------------------------------------------+
//| EA交易初始化函数                                                    |
//+------------------------------------------------------------------+
int OnInit()
{
   double x[] = {1,2,3,4,5,6,7,8,9,10};
   double y[] = {1,2,3,2,4,3,5,6,4,3};
   CCurve* cur = Graph.CurveAdd(x, y, CURVE_LINES, "Line");   
   Graph.CurvePlotAll();
   Graph.Create(ChartID(), "Ticks", 0, (int)50, (int)60, 510, 300); 
   Graph.Redraw(true);
   Graph.Update();
   return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| EA交易订单分时函数                                                  |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }
//+------------------------------------------------------------------+


如果您以 EA 交易的形式运行它,将在图表上出现下面的画面:

图 7. 使用 CGraphic 创建的二维线性图标实例

现在我们将试着改变表现形式,我们把显示类型从二维曲线改成点:

//+------------------------------------------------------------------+
//| EA交易初始化函数                                                    |
//+------------------------------------------------------------------+
int OnInit()
{
   double x[] = {1,2,3,4,5,6,7,8,9,10};
   double y[] = {1,2,3,2,4,3,5,6,4,3};
   CCurve* cur = Graph.CurveAdd(x, y, CURVE_POINTS, "Points");   
   cur.PointsType(POINT_CIRCLE);
   Graph.CurvePlotAll();
   Graph.Create(ChartID(), "Ticks", 0, (int)50, (int)60, 510, 300); 
   Graph.Redraw(true);
   Graph.Update();
   return(INIT_SUCCEEDED);
}

以点的形式表现的相同图表看起来如下:

图 8. 一个使用 CGraphic 创建的二维点图表实例

您可以看到,这里我们创建一个曲线对象,它的数值必须包含在 x 和 y 数组中: 

double x[] = {1,2,3,4,5,6,7,8,9,10};
double y[] = {1,2,3,2,4,3,5,6,4,3};
CCurve* cur = Graph.CurveAdd(x, y, CURVE_POINTS, "Points"); 

创建的对象位于 CGraphic 内部, 而 CurveAdd 方法可以返回它的一个引用。这就可以设置曲线所需的对象,我们已经在第二个例子中这样做了,我们把曲线类型设为 CURVE_POINTS 并且把点的类型设为圆形:

cur.PointsType(POINT_CIRCLE);

在加上所有的线之后,我们需要在图表上绘制并显示它们,使用的是 Create 和 Redraw 命令。

我们在我们的市场深度项目中重复相同的操作序列,但是用于曲线的数据将以特别方式准备,所有的命令都放在从 CElChart 派生的特定的 CElTickGraph 类中。

使用 CPanel 开发库做 CGraphic 的整合

我们已经讨论了使用 CGraphic 进行工作的要点,现在是时候把这个类加到 CPanel 开发库中了。我们已经谈到,Panel 提供了对事件的访问,正确组织图形元件并管理它们的属性,所有这些都是在市场深度面板中制作分时图表所需要的。所以,我们首先要些一个特别的 CElTickGraph 元件,它是 CPanel 的一部分,把 CGraphic 整合到面板中。另外,CElTickGraph 将会接收到更新的分时价格流并且重绘我们的分时图表。最后的任务是最困难的一个,这里是 CElTickGraph 应该可以完成任务的简要枚举。

  • CElTickGraph 标识了市场深度面板内部的一个长方形区域,该区域使用黑色边框标识。 
  • CElTickGraph 区域之内,有一个 CGraphic 图表显示了分时价格流。
  • 分时流显示了最新的N个分时,N 数字可以在设置中修改。
  • CElTickGraph 更新 CGraphic 中包含的 CCurve 曲线的值,这样旧的分时就被从图表上删除,而新的分时会加到图表上。这使得 CElTickGraph 可以创建一种平滑改变的分时图表的效果。

为了使 CElTickGraph 的任务简单化, 我们将使用一个辅助方案: 一个环形缓冲区,它的运行原则已经在一篇独立文章中详细介绍了。

让我们创建四个环形缓冲区来显示下面的数值:

  • 卖家报价水平 (以红色线显示);
  • 买家报价水平 (以蓝色线显示);
  • 来自买入方的最后的交易 (以指向下方的蓝色三角形显示);
  • 来自卖出方的最后的交易 (以指向上方的红色三角形显示).

所以我们将把最后的买入和卖出交易分开,这就是为什么我们需要两个环形缓冲区。

第二个难点是关于这样的情况,在卖家报价/买家报价之间的点数和最新价格不相等。当我们画出一条连续的线时,对于X轴上的每个点,都有相应的Y值。如果不使用线上的点,在X时刻,一个点可能出现或者不出现在图表上,我们应当把这个属性考虑在内,并使用一个二维图表。假定我们在以下X-Y坐标上有一个点: 1000-57034. 在新的分时到来时,相同的点的坐标将是 999-57034. 在再过去5个分时之后,它会移动到 994-57034. 最后的位置将是 0-57034. 然后,它就会从图表上消失。下一个点可能距离它有一定的步数,当点 1 坐标为 994-57034, 点 2 将位于 995:57035 或者 998:57035. 通过在二维图表上组合线形,我们可以正确映射这些距离,而不需要把分时流转变成连续的队列,

让我们考虑一个假定的分时图表,在订单簿上显示,并考虑到索引:

索引 卖家报价 卖家报价 买入 卖出
999 57034 57032 57034
998 57034 57032
57032
997 57034 57031 57034
996 57035 57033 57035
995 57036 57035

994 57036 57035

993 57036 57035
57035
992 57036 57034
57034
991 57036 57035 57035
...



表格 1. 分时流与二维表格(图表)同步的框架

卖家报价和卖家报价的数值都填充到表格中,而买入和卖出交易有的时候是缺失的。数值的位置依赖于正确同步了不同长度序列的索引,不管我们最近有多少交易,它们总是与所需的卖家报价和买家报价水平相关联的。

我们已经描述了 CElTickGraph 一般原则,下面是它的完整代码,在那之后我们将分析最难的部分。

//+------------------------------------------------------------------+
//|                                                         Graf.mqh |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#include <Panel\ElChart.mqh>
#include <RingBuffer\RiBuffDbl.mqh>
#include <RingBuffer\RiBuffInt.mqh>
#include <RingBuffer\RiMaxMin.mqh>
#include "GlobalMarketBook.mqh"
#include "GraphicMain.mqh"
#include "EventNewTick.mqh"

input int TicksHistoryTotal = 200;
//+------------------------------------------------------------------+
//| 确定 CGraphic 对象中曲线的数量                                       |
//+------------------------------------------------------------------+
enum ENUM_TICK_LINES
{
   ASK_LINE,
   BID_LINE,
   LAST_BUY,
   LAST_SELL,
   LAST_LINE,
   VOL_LINE
};
//+------------------------------------------------------------------+
//| 显示分时图表的图形元件                                               |
//+------------------------------------------------------------------+
class CElTickGraph : public CElChart
{
private:
   
   CGraphicMain m_graf;
   /* 用于快速操作分时流得关键缓冲区 */
   CRiMaxMin    m_ask;
   CRiMaxMin    m_bid;
   CRiMaxMin    m_last;
   CRiBuffDbl   m_last_buy;
   CRiMaxMin    m_last_sell;
   CRiBuffInt   m_vol;
   CRiBuffInt   m_flags;
   
   double       m_xpoints[];  // 索引的数组
   void         RefreshCurves();
   void         SetMaxMin(void);
public:
                CElTickGraph(void);
   virtual void Event(CEvent* event);
   void         SetTiksTotal(int tiks);
   int          GetTiksTotal(void);
   void         Redraw(void);
   virtual void Show(void);
   virtual void OnHide(void);
   virtual void OnRefresh(CEventRefresh* refresh);
   void         AddLastTick();
};
//+------------------------------------------------------------------+
//| 图表初始化                                                         |
//+------------------------------------------------------------------+
CElTickGraph::CElTickGraph(void) : CElChart(OBJ_RECTANGLE_LABEL)
{
   double y[] = {0};
   y[0] = MarketBook.InfoGetDouble(MBOOK_BEST_ASK_PRICE);
   double x[] = {0};
   
   CCurve* cur = m_graf.CurveAdd(x, y, CURVE_LINES, "Ask");   
   cur.Color(ColorToARGB(clrLightCoral, 255));
   cur.LinesEndStyle(LINE_END_ROUND);
   
   cur = m_graf.CurveAdd(x, y, CURVE_LINES, "Bid");
   cur.Color(ColorToARGB(clrCornflowerBlue, 255));
   
   cur = m_graf.CurveAdd(x, y, CURVE_POINTS, "Buy");
   cur.PointsType(POINT_TRIANGLE_DOWN);
   cur.PointsColor(ColorToARGB(clrCornflowerBlue, 255));
   cur.Color(ColorToARGB(clrCornflowerBlue, 255));
   cur.PointsFill(true);
   cur.PointsSize(5);
   
   
   cur = m_graf.CurveAdd(x, y, CURVE_POINTS, "Sell");
   cur.PointsType(POINT_TRIANGLE);
   cur.PointsColor(ColorToARGB(clrLightCoral, 255));
   cur.Color(ColorToARGB(clrLightCoral, 255));
   cur.PointsFill(true);
   cur.PointsSize(5);
   
   m_graf.CurvePlotAll();
   m_graf.IndentRight(1);
   m_graf.GapSize(1);
   SetTiksTotal(TicksHistoryTotal);
}
//+------------------------------------------------------------------+
//| 设置图表窗口中分时的数量                                              |
//+------------------------------------------------------------------+
void CElTickGraph::SetTiksTotal(int tiks)
{
   m_last.SetMaxTotal(tiks);
   m_last_buy.SetMaxTotal(tiks);
   m_last_sell.SetMaxTotal(tiks);
   m_ask.SetMaxTotal(tiks);
   m_bid.SetMaxTotal(tiks);
   m_vol.SetMaxTotal(tiks);
   ArrayResize(m_xpoints, tiks);
   for(int i = 0; i < ArraySize(m_xpoints); i++)
      m_xpoints[i] = i;
}

//+------------------------------------------------------------------+
//| 更新分时线                                                         |
//+------------------------------------------------------------------+
void CElTickGraph::RefreshCurves(void) 
{
   int total_last = m_last.GetTotal();
   int total_ask = m_ask.GetTotal();
   int total_bid = m_bid.GetTotal();
   int total = 10;
   for(int i = 0; i < m_graf.CurvesTotal(); i++)
   {
      CCurve* curve = m_graf.CurveGetByIndex(i);
      double y_points[];
      double x_points[];
      switch(i)
      {
         case LAST_LINE:
         {
            m_last.ToArray(y_points);
            if(ArraySize(x_points) < ArraySize(y_points))
               ArrayCopy(x_points, m_xpoints, 0, 0, ArraySize(y_points));
            curve.Update(x_points, y_points);
            break;
         }
         case ASK_LINE:
            m_ask.ToArray(y_points);
            if(ArraySize(x_points) < ArraySize(y_points))
               ArrayCopy(x_points, m_xpoints, 0, 0, ArraySize(y_points));
            curve.Update(x_points, y_points);
            break;
         case BID_LINE:
            m_bid.ToArray(y_points);
            if(ArraySize(x_points) < ArraySize(y_points))
               ArrayCopy(x_points, m_xpoints, 0, 0, ArraySize(y_points));
            curve.Update(x_points, y_points);
            break;
         case LAST_BUY:
         {
            m_last_buy.ToArray(y_points);
            CPoint2D points[];
            ArrayResize(points, ArraySize(y_points));
            int k = 0;
            for(int p = 0; p < ArraySize(y_points);p++)
            {
               if(y_points[p] == -1)
                  continue;
               points[k].x = p;
               points[k].y = y_points[p];
               k++;
            }
            if(k > 0)
            {
               ArrayResize(points, k);
               curve.Update(points);
            }
            break;
         }
         case LAST_SELL:
         {
            m_last_sell.ToArray(y_points);
            CPoint2D points[];
            ArrayResize(points, ArraySize(y_points));
            int k = 0;
            for(int p = 0; p < ArraySize(y_points);p++)
            {
               if(y_points[p] == -1)
                  continue;
               points[k].x = p;
               points[k].y = y_points[p];
               k++;
            }
            if(k > 0)
            {
               ArrayResize(points, k);
               curve.Update(points);
            }
            break;
         }
      }
   }
   
}
//+------------------------------------------------------------------+
//| 返回图表窗口中分时的数量                                             |
//+------------------------------------------------------------------+
int CElTickGraph::GetTiksTotal(void)
{
   return m_ask.GetMaxTotal();
}
//+------------------------------------------------------------------+
//| 在订单簿更新时更新图表                                               |
//+------------------------------------------------------------------+
void CElTickGraph::OnRefresh(CEventRefresh* refresh)
{
   //在图表上画最近接收到的分时
   int dbg = 5;
   int total = ArraySize(MarketBook.LastTicks);
   for(int i = 0; i < ArraySize(MarketBook.LastTicks); i++)
   {
      MqlTick tick = MarketBook.LastTicks[i];
      if((tick.flags & TICK_FLAG_BUY)==TICK_FLAG_BUY)
      {
         m_last_buy.AddValue(tick.last);
         m_last_sell.AddValue(-1);
         m_ask.AddValue(tick.last);
         m_bid.AddValue(tick.bid);
      }
      if((tick.flags & TICK_FLAG_SELL)==TICK_FLAG_SELL)
      {
         m_last_sell.AddValue(tick.last);
         m_last_buy.AddValue(-1);
         m_bid.AddValue(tick.last);
         m_ask.AddValue(tick.ask);
      }
      if((tick.flags & TICK_FLAG_ASK)==TICK_FLAG_ASK ||
         (tick.flags & TICK_FLAG_BID)==TICK_FLAG_BID)
      {
         m_last_sell.AddValue(-1);
         m_last_buy.AddValue(-1);
         m_bid.AddValue(tick.bid);
         m_ask.AddValue(tick.ask);
      }
   }
   MqlTick tick;
   if(!SymbolInfoTick(Symbol(), tick))
       return;
   if(ArraySize(MarketBook.LastTicks)>0)
   {
      RefreshCurves();
      m_graf.Redraw(true);
      m_graf.Update();
   }
}
void CElTickGraph::Event(CEvent *event)
{
   CElChart::Event(event);
   if(event.EventType() != EVENT_CHART_CUSTOM)
      return;
   CEventNewTick* ent = dynamic_cast<CEventNewTick*>(event);
   if(ent == NULL)
      return;
   MqlTick tick;
   ent.GetNewTick(tick);
   if((tick.flags & TICK_FLAG_BUY) == TICK_FLAG_BUY)
   {
      int last = m_last_buy.GetTotal()-1;
      if(last >= 0)
         m_last_buy.ChangeValue(last, tick.last);
   }
}
//+------------------------------------------------------------------+
//| 计算在轴上的尺度,保证当前价格总在                                      |
//| 价格图表的中间                                                      |
//+------------------------------------------------------------------+
void CElTickGraph::SetMaxMin(void)
{
   double max = m_last.MaxValue();
   double min = m_last.MinValue();
   double curr = m_last.GetValue(m_last.GetTotal()-1);
   double max_delta = max - curr;
   double min_delta = curr - min;
   if(max_delta > min_delta)
      m_graf.SetMaxMinValues(0, m_last.GetTotal(), (max-max_delta*2.0), max);
   else
      m_graf.SetMaxMinValues(0, m_last.GetTotal(), min, (min+min_delta*2.0));
}
//+------------------------------------------------------------------+
//| 刷新图表                                                           |
//+------------------------------------------------------------------+
void CElTickGraph::Redraw(void)
{
   m_graf.Redraw(true);
   m_graf.Update();
}
//+------------------------------------------------------------------+
//| 拦截图表的显示并改变显示的优先级                                       |
//+------------------------------------------------------------------+
void CElTickGraph::Show(void)
{
   BackgroundColor(clrNONE);
   BorderColor(clrBlack);
   Text("Ticks:");
   //m_graf.BackgroundColor(clrWhiteSmoke);
   m_graf.Create(ChartID(), "Ticks", 0, (int)XCoord()+20, (int)YCoord()+30, 610, 600); 
   m_graf.Redraw(true);
   m_graf.Update();
   CElChart::Show();
}

//+------------------------------------------------------------------+
//| 在显示时我们显示图表                                                 |
//+------------------------------------------------------------------+
void CElTickGraph::OnHide(void)
{
   m_graf.Destroy();
   CNode::OnHide();
}

让我们详细分析代码,我们从 CElTickGraph::CElTickGraph 类构造函数开始,从构造函数中很明显可以看到,类是基于 OBJ_RECTANGLE_LABEL 图形元素的,也就是在长方形标签上。在构造函数中创建了几个 CCurve 类型的曲线,它们中的每个都用于某个数据类型。每条曲线都要设置下面的属性: 线的名称,类型,和颜色。在曲线创建的时候,还不知道它应当显示的数值,所以我们使用假的 double x 和 y 的数组,来作为第一个点的坐标。当曲线创建并放到 CGraphic 对象之后,就在 SetTiksTotal 方法中配置环形缓冲区,这里的配置意思是设置记录分时的最大数量,它是在 TicksHistoryTotal 外部参数中设置的。

在所需曲线加到 CGraphic ,而环形缓冲区被正确配置之后,市场深度就可以使用了。在市场深度运行时主要调用两个方法: CElTickGraph::OnRefresh 和 CElTickGraph::RefreshCurves. 让我们看一下它们,

OnRefresh 方法是在订单簿有变化时调用的,这样的变化是使用 OnBookEvent 函数跟踪的:

//+------------------------------------------------------------------+
//| MarketBook 改变事件                                                |
//+------------------------------------------------------------------+
void OnBookEvent(const string &symbol)
{
   
   if(symbol != MarketBook.GetMarketBookSymbol())
      return;
   MarketBook.Refresh();
   MButton.Refresh();
   ChartRedraw();
}

首先更新订单簿 (MarketBook.Refresh()), 然后在面板上显示它: MButton.Refresh(). 因为面板是以按钮显示的,并且可以最小化/扩大,则按钮是整个面板的父控件。所有的事件,包括刷新请求都是通过这个按钮接收到的。刷新请求会传送到按钮中的所有元件,并且最终达到 CElTickGraph, 它包含了刷新图表的算法。算法是在 OnRefresh 方法中实现的,

该算法接收上次刷新后又出现的一定数量的分时,然后每个分时的值都加到对应的环形缓冲区中。分时的卖家报价加到 m_ask 环形缓冲区, 买家报价加到 m_bid, 等等。 如果最后的分时类型为最后的交易,则卖家报价和买家报价要强制同步到最后的价格。这个过程是必须的,因为终端不会同步数值和提供之前分时的卖家报价和买家报价。这样,最后交易总是在卖家报价或者卖家报价水平上的。请注意,标准的订单簿没有进行这种同步,而最后价格可以出现在两条线之间。

一旦最后的分时序列放到环形缓冲区之后,就会调用负责在图表上绘制分时的 OnRefreshCurves 方法。这个方法包含了一个循环,检查所有可用的 CCurve 曲线。完全在每条曲线上刷新所有点,要使用 curve.Update 方法。Y 轴上的坐标是通过把环形缓冲区上的所有元素复制到一个双精度缓冲区上做到的,而 X 轴上的点坐标的获得是通过一种更加复杂的方式。对于每个 y 点,x 坐标变为 x-1. 也就是说,如果 x 元素等于 1000, 它的数值会改为 999. 这会在图表上画新值时,产生移动的效果,而旧的数值会从中消失。

在所有这些数值都放到所需的索引处时,CCurve 曲线就要更新,我们需要刷新市场深度。为此,图表的刷新方法在 OnRefresh 方法中调用method: m_graf.Redraw 和 m_graf.Update.

分时图表的显示算法允许从两种模式中选择一种,

  • 分时图表可以不绑定订单簿中的最新价格来显示,图表的高低自动在 CGraphic 内部自动计算。
  • 分时图表也可以绑定订单簿中的最近价格来显示,不论高低在哪里,当前(最新)价格总是在图表的中心。

在第一种情况下,会调用 CGraphic 的自动缩放。在第二种情况下,缩放是通过 SetMaxMin 方法进行的。

安装. 动力学性能市场深度特性的特点比较

这个应用程序所需的所有文件可以分成四组:

  • CPanel 开发库的文件,位于 MQL5\Include\Panel;
  • MarketBook 类的文件,位于 MQL5\Include\Trade;
  • 缓冲区环的类文件,位于 MQL5\Include\RingBuffer;
  • 剥头皮市场深度的文件,位于 MQL5\Indicators\MarketBookArticle.

附件中的 zip 文件包含了上面的对应文件夹中的文件,如需安装程序,只要把档案解压缩到 MQL5 文件夹,您不需要创建任何额外的子文件夹。在解压缩之后,编译文件 MQL5\Indicators\MarketBookArticle\MarketBook.mq5. 编译之后,相应的指标就会被加到 MetaTrader 5 导航窗口中, 

评估算法结果的最好方式就是动态显示分时图表的变化,下面的视频展示了分时图表是如何随着时间变化的,以及图表窗口是如何平滑移动到右方的:


我们市场深度的分时图表的结果与 MetaTrader 5 中标准市场深度图表有很大不同,这些不同点在下面的比较表格中显示:

标准的 MetaTrader 5 市场深度 本文中开发的市场深度
最新报价,卖方报价和买方报价没有相互关联,最新报价可以是与卖方报价和买方报价不同的价格水平。 最新报价,卖方报价和买方报价是同步的,最新价格永远和卖方报价或者买方报价是相同的。
最近价格以半径不同的圆圈来显示,直接显示了交易量。半径最大的圆形对应着在最后N个分时中交易量最大的那个交易,而N是移动分时图表窗口的周期数。 买入交易以蓝色向下的三角形表示,卖出交易以红色向上三角形显示。交易没有基于交易量进行突出显示,
分时图表的缩放尺度是与挂单表格高度同步的,这样,订单簿中任何水平都对应着分时图表上的同样水平。这种方案的缺点是不可能以大的缩放显示分时图表,优点是价格都能看见,订单簿水平与分时图表水平完全对应。 分时图表的缩放和订单簿的缩放不必匹配,分时图表的当前价格只能大约在订单簿的中间,这种方案的缺点是订单簿水平和分时图表缺乏视觉上的对应。优点是可以给分时图表设置任何想要的缩放尺度。
分时图表中还含有额外的订单交易量的柱形图。  分时图表没有加入更多内容。

表 2. 标准的和新开发的市场深度的特征比较。

结论

我们已经讨论了剥头皮市场深度开发的过程;

  • 我们已经提高了订单簿的外观;
  • 我们已经把基于 CGraphic 和更新的图形引擎的价格图表加到面板上了;
  • 我们已经提高了 Market Depth 类并实现了同步分时和当前订单簿的算法;

但是,尽管当前版本的市场深度里完整功能的剥头皮版本差距很大,当然,许多用户可能会失望,这篇文章读到这里还是没有看到标准市场深度或者特定的程序例如 Bondar 驱动或者 QScalp. 但是,任何复杂的软件产品必须在它的发展过程中通过多次革命性的步骤。这里是在将来版本中要加到市场深度中的:

  • 可以从市场深度直接设置挂单
  • 可以在分时图表上跟踪大的订单
  • 根据交易量区分最后的交易,并在图表上用不同方法来显示
  • 在分时图表上显示额外的指标,例如,在分时图表下方,我们可以显示所有限价买入和所有限价卖出的挂单的比例柱形图。
  • 还有,最终最重要的部分是下载和保存市场深度历史,然后可以在离线测试模式下创建交易策略。

所有这些想法都可以实现,也许这些选项以后将会出现,s如果读者觉得这个主题有趣,系列文章将会继续。