繁簡切換您正在訪問的是FX168財經網,本網站所提供的內容及信息均遵守中華人民共和國香港特別行政區當地法律法規。

FX168财经网>人物频道>帖子

利用 CCanvas 的自定义指标和信息图

作者/萨达撒撒 2019-04-17 15:22 0 来源: FX168财经网人物频道

内容

  • 概述
  • CLineRounded 类
  • CHexagon 类
  • CPetal 类
  • CHistogram 类
  • CPyramid 类
  • 结论


概述

在我们 之前的 文章中, 我们已研究了利用 CCanvas 类开发简单图元的方法来构建图形指标的原理。然而, 自定义图形库具有更广泛的功能, 因此我建议您查阅拥有更复杂结构实现的新型指标。此外, 我们将绘制伪 3D 指标类型和动态图。


CLineRounded 类

我不打算在本文中重新创建基类的一般结构来实现图形对象。代之, 我们将使用现成的在 之前文章 中描述过的 CustomGUI 函数库。特别地, CCanvasBase 类将会作为基类。新开发的类将在 CustomGUI.mqh 文件中列出。

为了开发一个简单的线性指标, 我们应定义其结构和基本元素。图例. 1 示意使用此类型指标时您可以管理的元素。请注意, 它们不是简单的元素。 


图例. 1. 简单线性指标的基本结构

CCanvas 类方法实现的元素可认为是简单的 (基本的)。例如, 在线性指标中只有 Value 可认为是简单的, 因为它使用 CCanvas::TextOut() 方法。其余的三个元素与此类似, 主要由三个基本图形组成 — 两个实心圆 (CCanvas::FillCircle() 实现) 和一个矩形 (CCanvas::FillRectangle())。

CustomGUI/Indicators 文件夹里创建 LineRounded.mqh 文件, 在新生成的文件里创建 СLineRounded 类, 并为其指派以前创建的 CCanvasBase 类作为基类。

//+------------------------------------------------------------------+
//|                                                  LineRounded.mqh |
//|                                  版权所有 2017, Alexander Fedosov |
//|                           https://www.mql5.com/zh/users/alex2356 |
//+------------------------------------------------------------------+
#include "..\CanvasBase.mqh"
//+------------------------------------------------------------------+
//| 圆形线性指标                                                       |
//+------------------------------------------------------------------+
class CLineRounded : public CCanvasBase

还有, 将其 添加CustomGUI.mqh 文件清单中以便所有函数库中的类能够快速访问。以前研究并开发的类, 其代码如下:

//+------------------------------------------------------------------+
//|                                                    CustomGUI.mqh |
//|                                  版权所有 2017, Alexander Fedosov |
//|                           https://www.mql5.com/zh/users/alex2356 |
//+------------------------------------------------------------------+
#include "Indicators\CircleSimple.mqh"
#include "Indicators\CircleArc.mqh"
#include "Indicators\CircleSection.mqh"
#include "Indicators\LineGraph.mqh"
#include "Indicators\LineRounded.mqh"
//+------------------------------------------------------------------+

СLineRounded 类的所有方法和属性清单, 可在 CustomGUI/Indicators/LineRounded.mqh 文件里得到。我们把重点放在开发和设定指标的方法, 并更新其数值。如上所述, 指标由复合元素组成。这些元素的实现已在下表中的单独模块中进行了讨论。

//+------------------------------------------------------------------+
//| 创建指标                                                           |
//+------------------------------------------------------------------+
void CLineRounded::Create(string name,int x,int y)
  {
//--- 相对于坐标轴修改指标位置
   x=(x<m_x_size/2)?m_x_size/2:x;
   y=(y<m_y_size/2)?m_y_size/2:y;
   Name(name);
   X(x);
   Y(y);
   XSize(m_x_size);
   YSize(m_y_size);
   if(!CreateCanvas())
      Print("错误。不能创建画布。");
//--- 边框
   m_canvas.FillRectangle(YSize()/2,0,XSize()-YSize()/2,YSize(),ColorToARGB(m_border_color,m_transparency));
   m_canvas.FillCircle(YSize()/2,YSize()/2-1,YSize()/2,ColorToARGB(m_border_color,m_transparency));
   m_canvas.FillCircle(XSize()-YSize()/2-1,YSize()/2,YSize()/2,ColorToARGB(m_border_color,m_transparency));
//--- 底图
   m_canvas.FillRectangle(YSize()/2,m_border,XSize()-YSize()/2,YSize()-m_border,ColorToARGB(m_bg_color,m_transparency));
   m_canvas.FillCircle(YSize()/2,YSize()/2,YSize()/2-m_border,ColorToARGB(m_bg_color,m_transparency));
   m_canvas.FillCircle(XSize()-YSize()/2,YSize()/2,YSize()/2-m_border,ColorToARGB(m_bg_color,m_transparency));
   m_canvas.Update();
  }

正如您所看到的, 边框和底图是使用两种方法实现的。不要忘记这些对象是分层的, 并且彼此叠加绘制。因此, 我们首先绘制边框, 随后是较小的底图, 指标刻度和数值。

参考 CLineRounded::NewValue() 方法。 

//+------------------------------------------------------------------+
//| 设置并更新指标数值                                                   |
//+------------------------------------------------------------------+
void CLineRounded::NewValue(double value,double maxvalue,int digits=0)
  {
   int v;
   v=int((XSize()-YSize()/2)/maxvalue*value);

//--- 底图
   m_canvas.FillRectangle(YSize()/2,m_border,XSize()-YSize()/2,YSize()-m_border,ColorToARGB(m_bg_color,m_transparency));
   m_canvas.FillCircle(YSize()/2,YSize()/2,YSize()/2-m_border,ColorToARGB(m_bg_color,m_transparency));
   m_canvas.FillCircle(XSize()-YSize()/2,YSize()/2,YSize()/2-m_border,ColorToARGB(m_bg_color,m_transparency));
//--- 刻度
   if(v>=YSize()/2 && v<=(XSize()-YSize()/2))
     {
      m_canvas.FillRectangle(YSize()/2,m_border,v,YSize()-m_border,ColorToARGB(m_scale_color,m_transparency));
      m_canvas.FillCircle(YSize()/2,YSize()/2,YSize()/2-m_border,ColorToARGB(m_scale_color,m_transparency));
      m_canvas.FillCircle(v,YSize()/2,YSize()/2-m_border,ColorToARGB(m_scale_color,m_transparency));
     }
   else if(v>(XSize()-YSize()/2))
     {
      m_canvas.FillRectangle(YSize()/2,m_border,XSize()-YSize()/2,YSize()-m_border,ColorToARGB(m_scale_color,m_transparency));
      m_canvas.FillCircle(YSize()/2,YSize()/2,YSize()/2-m_border,ColorToARGB(m_scale_color,m_transparency));
      m_canvas.FillCircle(XSize()-YSize()/2,YSize()/2,YSize()/2-m_border,ColorToARGB(m_scale_color,m_transparency));
     }
   else if(v>0 && v<YSize()/2)
      m_canvas.FillCircle(YSize()/2,YSize()/2,YSize()/2-m_border,ColorToARGB(m_scale_color,m_transparency));

//--- 数值
   m_canvas.FontSizeSet(m_font_size);
   m_canvas.TextOut(XSize()/2,YSize()/2,DoubleToString(value,digits),ColorToARGB(m_value_color,m_transparency),TA_CENTER|TA_VCENTER);
   m_canvas.Update();
  }
//+------------------------------------------------------------------+

正如我们看到的那样, 底图实现模块在方法的开头再次出现。为什么?要记住, 元素是通过与应用相应的方法逐层绘制。换言之, 首先, 我们显示刻度背景, 随后指标, 其长度由最大参数值定义。当新的指标值到达时, 首先重新绘制底图层, 然后重新绘制新的长度。

以非常简单的方式实现创建和传递值到指标。在下表中, 您可以看到两个较少设置的指标, 它们的区别仅在于其中一个的最大值是另一个的两倍。 

//+------------------------------------------------------------------+
//|                                                          001.mq5 |
//|                                  版权所有 2017, Alexander Fedosov |
//|                           https://www.mql5.com/zh/users/alex2356 |
//+------------------------------------------------------------------+
#property version   "1.00"
#property indicator_plots 0
#property indicator_chart_window

#include <CustomGUI\CustomGUI.mqh>
//---
CLineRounded ind1,ind2;
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                                 |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   ind1.XSizeInd(350);
   ind1.YSizeInd(30);
   ind1.Create("line1",300,100);
//---
   ind2.XSizeInd(350);
   ind2.YSizeInd(30);
   ind2.ScaleColor(clrFireBrick);
   ind2.Create("line2",300,150);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| 自定义指标迭代函数                                                   |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   ind1.NewValue(50,100,2);
   ind2.NewValue(50,200,2);
//--- 返回 prev_calculated 的数值以便下次调用
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| 自定义指标逆初函数                                                   |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ind1.Delete();
   ind2.Delete();
  }
//+------------------------------------------------------------------+

图例. 2 清晰地表明, 在具有相似值和不同尺度的情况下, 指标刻度长度具有适当的值。选择数值 50 和最大值 (第一个为 100, 第二个为 200), 以便直观地评估显示结果。

图例. 2. 圆形线性指标示例


CHexagon 类

我们可以运用许多方法利用 CCanvas 类设置的基元来绘制正六边形指标。它们包括构筑多边形, 然后填充该区域, 并 "装配" 六个等边三角形, 有如切片的匹萨饼。然而, 我们的目标是最大简化。所以, 我们将使用三个基元构造一个正六边形 — 一个矩形和两个等腰三角形。


图例. 3. 正六边形结构

正六角形插入到方形画布中。记住图形的属性:

  • 正六边形的一边等于围绕它的圆形半径 (在我们的例子中, 这是画布边长的一半)。绘制矩形时, 此功能是必需的。
  • 六边形角度为 120 度, 因此等腰三角形的底角为 30 度。当利用 CCanvas::FillTriangle() 方法时, 此数据对于定义三角形高度和查找三角形基点的坐标是必需的。

指标本身的基本结构非常简单, 除了六边形外, 还包括两个文本对象 - 数值和描述 (图例. 4)。


图例. 4. 六角形指标的基本结构

CustomGUI/Indicators 文件夹里创建 Hexagon.mqh 文件, 在新生成的文件里创建 СHexagon 类, 并为其指派以前创建的 CCanvasBase 类作为基类。将其包含在清单里:

//+------------------------------------------------------------------+
//|                                                    CustomGUI.mqh |
//|                                  版权所有 2017, Alexander Fedosov |
//|                           https://www.mql5.com/zh/users/alex2356 |
//+------------------------------------------------------------------+
#include "Indicators\CircleSimple.mqh"
#include "Indicators\CircleArc.mqh"
#include "Indicators\CircleSection.mqh"
#include "Indicators\LineGraph.mqh"
#include "Indicators\LineRounded.mqh"
#include "Indicators\Hexagon.mqh"
//+------------------------------------------------------------------+

属性和方法的完整清单可在上面创建的文件里找到。我们高亮指标可视化的方法。 

//+------------------------------------------------------------------+
//| 创建指标                                                          |
//+------------------------------------------------------------------+
void CHexagon::Create(string name,int x,int y)
  {
   int a,r;
   r=m_size;
//--- 相对于坐标轴修改指标位置 
   x=(x<r/2)?r/2:x;
   y=(y<r/2)?r/2:y;
   Name(name);
   X(x);
   Y(y);
   XSize(r);
   YSize(r);
   if(!CreateCanvas())
      Print("错误。不能创建画布。");
//--- 计算三角形高度
   a=int(YSize()/2*MathSin(30*M_PI/180));
//--- 六角形
   m_canvas.FillTriangle(XSize()/2,0,0,a,XSize(),a,ColorToARGB(m_bg_color,m_transparency));
   m_canvas.FillRectangle(0,a,XSize(),a+YSize()/2,ColorToARGB(m_bg_color,m_transparency));
   m_canvas.FillTriangle(0,a+YSize()/2,XSize(),a+YSize()/2,XSize()/2,YSize(),ColorToARGB(m_bg_color,m_transparency));
//--- 描述和数值
   m_canvas.FontNameSet("Trebuchet MS");
   m_canvas.FontSizeSet(m_value_font_size);
   m_canvas.TextOut(r/2,r/2,"-",ColorToARGB(m_value_color,m_transparency),TA_CENTER|TA_VCENTER);
   m_canvas.FontSizeSet(m_label_font_size);
   m_canvas.TextOut(r/2,r/2+m_value_font_size,m_label_value,ColorToARGB(m_label_color,m_transparency),TA_CENTER|TA_VCENTER);
//---
   m_canvas.Update();
  }

Create() 方法当中, 等腰三角形的值分配到 a 变量。实际上, 我们以这种方式来查找沿纵坐标 (Y) 轴的三角形顶点坐标。事实上, 传递和更新数值的方法其不同之处在于 负责绘制数值的文本对象接收 value 参数:

//+------------------------------------------------------------------+
//| 设置并更新指标数值                                                  |
//+------------------------------------------------------------------+
void CHexagon::NewValue(double value,int digits=2)
  {
   int a,r;
   r=m_size;
//--- 计算三角形高度
   a=int(YSize()/2*MathSin(30*M_PI/180));
//--- 六角形
   m_canvas.FillTriangle(XSize()/2,0,0,a,XSize(),a,ColorToARGB(m_bg_color,m_transparency));
   m_canvas.FillRectangle(0,a,XSize(),a+YSize()/2,ColorToARGB(m_bg_color,m_transparency));
   m_canvas.FillTriangle(0,a+YSize()/2,XSize(),a+YSize()/2,XSize()/2,YSize(),ColorToARGB(m_bg_color,m_transparency));
//--- 文本
   m_canvas.FontNameSet("Trebuchet MS");
   m_canvas.FontSizeSet(m_value_font_size);
   m_canvas.TextOut(r/2,r/2,DoubleToString(value,digits),ColorToARGB(m_value_color,m_transparency),TA_CENTER|TA_VCENTER);
   m_canvas.FontSizeSet(m_label_font_size);
   m_canvas.TextOut(r/2,r/2+m_value_font_size,m_label_value,ColorToARGB(m_label_color,m_transparency),TA_CENTER|TA_VCENTER);
//---
   m_canvas.Update();
  }
//+------------------------------------------------------------------+

我们来实现一套六角形指标作为示例:

//+------------------------------------------------------------------+
//|                                                          002.mq5 |
//|                                  版权所有 2017, Alexander Fedosov |
//|                           https://www.mql5.com/zh/users/alex2356 |
//+------------------------------------------------------------------+
#property version   "1.00"
#property indicator_plots 0
#property indicator_chart_window

#include <CustomGUI\CustomGUI.mqh>
//---
CHexagon ind1,ind2,ind3,ind4,ind5,ind6,ind7,ind8,ind9,ind10,ind11,ind12,ind13,ind14;
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                                 |
//+------------------------------------------------------------------+
int OnInit()
  {
//---   
   ind1.BgColor(clrWhite);
   ind1.Size(110);
   ind1.Create("hex1",300,200);
   ind2.BgColor(C'60,170,220');
   ind2.Create("hex2",300,200);
//---
   ind3.BgColor(clrWhite);
   ind3.Size(110);
   ind3.Create("hex3",300,80);
   ind4.BgColor(C'230,80,25');
   ind4.Create("hex4",300,80);
//---
   ind5.BgColor(clrWhite);
   ind5.Size(110);
   ind5.Create("hex5",300,320);
   ind6.BgColor(C'150,190,15');
   ind6.Create("hex6",300,320);
//---
   ind7.BgColor(clrWhite);
   ind7.Size(110);
   ind7.Create("hex7",180,140);
   ind8.BgColor(C'10,115,185');
   ind8.Create("hex8",180,140);
//---
   ind9.BgColor(clrWhite);
   ind9.Size(110);
   ind9.Create("hex9",420,140);
   ind10.BgColor(C'20,150,150');
   ind10.Create("hex10",420,140);
//---
   ind11.BgColor(clrWhite);
   ind11.Size(110);
   ind11.Create("hex11",420,280);
   ind12.BgColor(C'225,0,80');
   ind12.Create("hex12",420,280);
//---
   ind13.BgColor(clrWhite);
   ind13.Size(110);
   ind13.Create("hex13",180,280);
   ind14.BgColor(C'240,145,5');
   ind14.Create("hex14",180,280);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| 自定义指标逆初函数                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ind1.Delete();
   ind2.Delete();
   ind3.Delete();
   ind4.Delete();
   ind5.Delete();
   ind6.Delete();
   ind7.Delete();
   ind8.Delete();
   ind9.Delete();
   ind10.Delete();
   ind11.Delete();
   ind12.Delete();
   ind13.Delete();
   ind14.Delete();
  }
//+------------------------------------------------------------------+

结果显示在图例. 5 当中。这个基本模板很容易配置和修改。 

图例. 5. 利用 CHexagon 类实现指标的示例


CPetal 类

利用 CCanvas 类方法中的 2-3 个基元实现花瓣状指标。依据形状, 这些是填充圆 (FillCircle() 方法) 以及 1-2 个填充三角形 (FillTriangle())。元素的结构和集合如图例. 6 所示。


图例. 6. 花瓣状指标的基本结构

正如我们所见, 这里使用了两个三角形, 但我们将会用 "枚举" 把多个表单类型添加到类中。我们来更详细地研究这些类型:

enum ENUM_POSITION
  {
   TOPRIGHT=1,
   TOPLEFT=2,
   BOTTOMRIGHT=3,
   BOTTOMLEFT=4,
   BOTHRIGHT=5,
   BOTHLEFT=6
  };

  • TOPRIGHT — 由一个圆形和三角形组成, 其可见顶点指向 右上 角。
  • TOPLEFT — 由一个圆形和三角形组成, 其可见顶点指向 左上 角。
  • BOTTOMRIGHT — 由一个圆形和三角形组成, 其可见顶点指向 右下 角。
  • BOTTOMLEFT — 由一个圆形和三角形组成, 其可见顶点指向 左下 角。
  • BOTHRIGHT — 由一个圆形和两个三角形组成。上三角形位于 右上 角。
  • BOTHLEFT— 由一个圆形和两个三角形组成。上三角形位于 左上 角。


图例. 7. 花瓣状指标类型

我们开始实现。在 CustomGUI/Indicators 文件夹里创建 Petal.mqh 文件, 在新生成的文件里创建 СPetal 类, 并为其指派以前创建的 CCanvasBase 类作为基类。此外, 将其包含在通用列表中, 现在看起来如下:

//+------------------------------------------------------------------+
//|                                                    CustomGUI.mqh |
//|                                  版权所有 2017, Alexander Fedosov |
//|                           https://www.mql5.com/zh/users/alex2356 |
//+------------------------------------------------------------------+
#include "Indicators\CircleSimple.mqh"
#include "Indicators\CircleArc.mqh"
#include "Indicators\CircleSection.mqh"
#include "Indicators\LineGraph.mqh"
#include "Indicators\LineRounded.mqh"
#include "Indicators\Hexagon.mqh"
#include "Indicators\Petal.mqh"
//+------------------------------------------------------------------+

我们不会介绍标准属性。我们来研究创建指标和更新其数值的方法。在 Create() 方法当中, 在构造填充圆之后, 根据先前研究的 "form type" 属性, 参考 ENUM_POSITION 枚举在指定位置上绘制三角形:

//+------------------------------------------------------------------+
//| 创建指标                                                           |
//+------------------------------------------------------------------+
void CPetal::Create(string name,int x,int y)
  {
   int r=m_radius;
//--- 相对于半径修改指标位置
   x=(x<r)?r:x;
   y=(y<r)?r:y;
   Name(name);
   X(x);
   Y(y);
   XSize(2*r+1);
   YSize(2*r+1);
   if(!CreateCanvas())
      Print("错误。不能创建画布。");
//---
   m_canvas.FillCircle(r,r,r,ColorToARGB(m_bg_color,m_transparency));
   if(m_orientation==TOPRIGHT)
      m_canvas.FillTriangle(XSize()/2,0,XSize(),0,XSize(),YSize()/2,ColorToARGB(m_bg_color,m_transparency));
   else if(m_orientation==TOPLEFT)
      m_canvas.FillTriangle(0,0,XSize()/2,0,0,YSize()/2,ColorToARGB(m_bg_color,m_transparency));
   else if(m_orientation==BOTTOMRIGHT)
      m_canvas.FillTriangle(XSize(),YSize(),XSize(),YSize()/2,XSize()/2,YSize(),ColorToARGB(m_bg_color,m_transparency));
   else if(m_orientation==BOTTOMLEFT)
      m_canvas.FillTriangle(0,YSize(),0,YSize()/2,XSize()/2,YSize(),ColorToARGB(m_bg_color,m_transparency));
   else if(m_orientation==BOTHRIGHT)
     {
      m_canvas.FillTriangle(XSize()/2,0,XSize(),0,XSize(),YSize()/2,ColorToARGB(m_bg_color,m_transparency));
      m_canvas.FillTriangle(0,YSize(),0,YSize()/2,XSize()/2,YSize(),ColorToARGB(m_bg_color,m_transparency));
     }
   else if(m_orientation==BOTHLEFT)
     {
      m_canvas.FillTriangle(0,0,XSize()/2,0,0,YSize()/2,ColorToARGB(m_bg_color,m_transparency));
      m_canvas.FillTriangle(XSize(),YSize(),XSize(),YSize()/2,XSize()/2,YSize(),ColorToARGB(m_bg_color,m_transparency));
     }
//--- 描述和数值
   m_canvas.FontNameSet("Trebuchet MS");
   m_canvas.FontSizeSet(m_value_font_size);
   m_canvas.TextOut(r,r,"-",ColorToARGB(m_value_color,m_transparency),TA_CENTER|TA_VCENTER);
   m_canvas.FontSizeSet(m_label_font_size);
   m_canvas.TextOut(r,r+m_value_font_size,m_label_value,ColorToARGB(m_label_color,m_transparency),TA_CENTER|TA_VCENTER);
   m_canvas.Update();
  }

我们来开发创建蝴蝶形指标的模板作为示例。

//+------------------------------------------------------------------+
//|                                                          003.mq5 |
//|                                  版权所有 2017, Alexander Fedosov |
//|                           https://www.mql5.com/zh/users/alex2356 |
//+------------------------------------------------------------------+
#property version   "1.00"
#property indicator_plots 0
#property indicator_chart_window

#include <CustomGUI\CustomGUI.mqh>
//---
CPetal ind1,ind2,ind3,ind4;
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                                 |
//+------------------------------------------------------------------+
int OnInit()
  {
   int b=2;
//---   
   ind1.BgColor(C'230,80,80');
   ind1.Orientation(BOTHLEFT);
   ind1.Create("petal1",200-b,100-b);

   ind2.BgColor(C'35,170,190');
   ind2.Orientation(BOTHRIGHT);
   ind2.Create("petal2",300+b,100-b);

   ind3.BgColor(C'245,155,70');
   ind3.Orientation(BOTHRIGHT);
   ind3.Create("petal3",200-b,200+b);

   ind4.BgColor(C'90,200,130');
   ind4.Orientation(BOTHLEFT);
   ind4.Create("petal4",300+b,200+b);
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| 自定义指标迭代函数                                                  |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---

//--- 返回 prev_calculated 的数值以便下次调用
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| 自定义指标逆初函数                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ind1.Delete();
   ind2.Delete();
   ind3.Delete();
   ind4.Delete();
  }
//+------------------------------------------------------------------+


图例. 8. 使用花瓣状指标

CHistogram 类

直方柱类基于 CLIneGraph 类, 或是更精确地 — 构造坐标轴, 轴刻度及其值的一般结构。因此, 没有必要描述这一阶段。我们来详细了解直方柱的类型以及利用 CCanvas 类基元的实现。在继续实现直方柱指标之前, 让我们来定义表单类型。

enum ENUM_TYPE_HISTOGRAM
  {
   SIMPLE=1,
   TRIANGLE=2,
   RECTANGLE=3
  };

  • SIMPLE — 一种简单形式, 基于三角形直方图 (图例. 10), 其高度在图表上为固定数值。
  • TRIANGLE — 伪高度类型的三角形直方图 (图例.11)。
  • RECTANGLE — 列形式的标准直方图类型 (图例. 12)。


图例. 10. 简单类型的直方图


图例. 11. 三角类型直方图

图例. 12. 矩形类型直方图

CustomGUI/Indicators 文件夹里创建 Histogram.mqh 文件, 在新生成的文件里创建 СHistogram 类, 并为其指派以前创建的 CCanvasBase 类作为基类。此外, 将其包含在 CustomGUI.mqh 文件里的通用列表中:大多数类方法和属性与 CLineGraph 类相似, 包括 Create() 方法。所以, 我们只需查看基本的区别, 名为 SetArrayValue() 的方法。

//+------------------------------------------------------------------+
//| 设置输入数组                                                       |
//+------------------------------------------------------------------+
void CHistogram::SetArrayValue(double &data[])
  {
   int x,y,y1,y2;
//--- 创建图形边框和底图
   m_canvas.FillRectangle(0,0,XSize(),YSize(),ColorToARGB(m_border_color,m_transparency));
   m_canvas.FillRectangle(1,1,XSize()-2,YSize()-2,ColorToARGB(m_bg_color,m_transparency));
//--- 创建坐标轴和图形背景
   m_canvas.FillRectangle(m_gap-1,m_gap-1,XSize()-m_gap+1,YSize()-m_gap+1,ColorToARGB(m_border_color,m_transparency));
   m_canvas.FillRectangle(m_gap,m_gap,XSize()-m_gap,YSize()-m_gap,ColorToARGB(m_bg_graph_color,m_transparency));
//--- 如果最大数据数组值超过 Y 轴上边界, 则坐标轴将被缩放。
   if(data[ArrayMaximum(data)]>m_y_max)
      m_y_max=data[ArrayMaximum(data)];
//--- 创建坐标轴刻度及其值
   VerticalScale(m_y_min,m_y_max,m_num_grid);
   HorizontalScale(ArraySize(data));
   ArrayResize(m_label_value,ArraySize(data));
//--- 根据数据数组创建直方图
   for(int i=0;i<ArraySize(data);i++)
     {
      if(data[i]>0)
        {
         x=int(m_x[i]+(m_x[i+1]-m_x[i])/2);
         y=int((YSize()-m_gap)-(YSize()-2*m_gap)/m_y_max*data[i]);
         y1=int((YSize()-m_gap)-(YSize()-2*m_gap)/m_y_max*data[i]*0.3);
         y2=int((YSize()-m_gap)-(YSize()-2*m_gap)/m_y_max*data[i]*0.1);
         y=(y<m_gap)?m_gap:y;
         if(m_type==SIMPLE)
            m_canvas.FillTriangle(m_x[i],YSize()-m_gap,m_x[i+1],YSize()-m_gap,x,y,ColorToARGB(m_graph_color1,m_transparency));
         else if(m_type==TRIANGLE)
           {
            m_canvas.FillTriangle(m_x[i],YSize()-m_gap,x,y,x,y1,ColorToARGB(m_graph_color1,m_transparency));
            m_canvas.FillTriangle(m_x[i],YSize()-m_gap,m_x[i+1],YSize()-m_gap,x,y1,ColorToARGB(m_graph_color2,m_transparency));
            m_canvas.FillTriangle(x,y,x,y1,m_x[i+1],YSize()-m_gap,ColorToARGB(m_graph_color3,m_transparency));
           }
         else if(m_type==RECTANGLE)
           {
            int x1,x2;
            x1=int(m_x[i]+(m_x[i+1]-m_x[i])/3);
            x2=int(m_x[i]+2*(m_x[i+1]-m_x[i])/3);
            m_canvas.FillRectangle(x1,y,x2,YSize()-m_gap,ColorToARGB(m_graph_color1,m_transparency));
           }
         //--- 描述和数值
         m_canvas.FontNameSet("Trebuchet MS");
         m_canvas.FontSizeSet(m_value_font_size);
         m_canvas.TextOut(x,y2,DoubleToString(data[i],2),ColorToARGB(m_value_color,m_transparency),TA_CENTER|TA_VCENTER);
         m_canvas.TextOut(x,y-5,m_label_value[i],ColorToARGB(m_label_color,m_transparency),TA_CENTER|TA_VCENTER);
        }
     }
   m_canvas.Update();
  }

根本区别是, 在直方图绘制模块中, 通过指定的数据数组参考上述的 ENUM_TYPE_HISTOGRAM 直方图类型。作为用例, 已经实现了三个不同周期的 RSI 指标的可视显示方法。

//+------------------------------------------------------------------+
//|                                                          004.mq5 |
//|                                  版权所有 2017, Alexander Fedosov |
//|                           https://www.mql5.com/zh/users/alex2356 |
//+------------------------------------------------------------------+
#property copyright "版权所有 2017, Alexander Fedosov"
#property link      "https://www.mql5.com/zh/users/alex2356"
#property version   "1.00"
#property indicator_plots 0
#property indicator_chart_window

#include <CustomGUI\CustomGUI.mqh>
//+------------------------------------------------------------------+
//|  指标输入                                                         |
//+------------------------------------------------------------------+

input int                  RSIPeriod1=10;        //指标1 的周期
input int                  RSIPeriod2=14;        //指标2 的周期
input int                  RSIPeriod3=18;        //指标3 的周期
input ENUM_TYPE_HISTOGRAM  Type=RECTANGLE;       //直方图类型
//---
CHistogram ind;
int InpInd_Handle1,InpInd_Handle2,InpInd_Handle3;
double rsi1[],rsi2[],rsi3[],ex[3];
string descr[3];
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                                 |
//+------------------------------------------------------------------+
int OnInit()
  {
//---- 获取指标句柄
   InpInd_Handle1=iRSI(Symbol(),PERIOD_CURRENT,RSIPeriod1,PRICE_CLOSE);
   InpInd_Handle2=iRSI(Symbol(),PERIOD_CURRENT,RSIPeriod2,PRICE_CLOSE);
   InpInd_Handle3=iRSI(Symbol(),PERIOD_CURRENT,RSIPeriod3,PRICE_CLOSE);
   if(InpInd_Handle1==INVALID_HANDLE ||
      InpInd_Handle2==INVALID_HANDLE ||
      InpInd_Handle3==INVALID_HANDLE
      )
     {
      Print("获取指标句柄失败");
      return(INIT_FAILED);
     }
//---
   descr[0]="RSI("+IntegerToString(RSIPeriod1)+")";
   descr[1]="RSI("+IntegerToString(RSIPeriod2)+")";
   descr[2]="RSI("+IntegerToString(RSIPeriod3)+")";
   ind.NumGrid(10);
   ind.YMax(100);
   ind.Type(Type);
   ind.Create("rsi",350,250);
   ind.SetArrayLabel(descr);
//---
   ArraySetAsSeries(rsi1,true);
   ArraySetAsSeries(rsi2,true);
   ArraySetAsSeries(rsi3,true);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| 自定义指标迭代函数                                                  |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   if(CopyBuffer(InpInd_Handle1,0,0,1,rsi1)<=0   ||
      CopyBuffer(InpInd_Handle2,0,0,1,rsi2)<=0   ||
      CopyBuffer(InpInd_Handle3,0,0,1,rsi3)<=0
      )
      return(0);
   ex[0]=rsi1[0];
   ex[1]=rsi2[0];
   ex[2]=rsi3[0];
   ind.SetArrayValue(ex);
//--- 返回 prev_calculated 的数值以便下次调用
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| 自定义指标逆初函数                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ind.Delete();
   ChartRedraw();
  }
//+------------------------------------------------------------------+


图例. 13. 以三个周期的 RSI 直方图形式显示的指标结果


CPyramid 类

我们已研究了使用基元构造复杂形状的类和原理。在描述构造直方图类型指标的类时, 我曾提到通过颜色选择绘制伪 3D 对象 (图例. 13)。然而, 金字塔不是一个平直的数字。因此, 我们必须在给定的二维坐标系中使用其等轴投影。基本结构不包含太多元素 (图例. 14), 但是金字塔投影方法和可视化是这个类需要实现的主要部分。


图例. 14. CPyramid 类的基本结构

CustomGUI/Indicators 文件夹里创建 Pyramid.mqh 文件, 在新生成的文件里创建 СPyramid 类, 并为其指派以前创建的 CCanvasBase 类作为基类。此外, 将其包含在 CustomGUI.mqh 文件里的通用列表中:现在, 包含类的通用列表如下所示:

//+------------------------------------------------------------------+
//|                                                    CustomGUI.mqh |
//+------------------------------------------------------------------+
#include "Indicators\CircleSimple.mqh"
#include "Indicators\CircleArc.mqh"
#include "Indicators\CircleSection.mqh"
#include "Indicators\LineGraph.mqh"
#include "Indicators\LineRounded.mqh"
#include "Indicators\Hexagon.mqh"
#include "Indicators\Petal.mqh"
#include "Indicators\Histogram.mqh"
#include "Indicators\Pyramid.mqh"
//+------------------------------------------------------------------+

您可以在类的本身当中找到属性和方法的完整列表。让我们关注重点。在研究基本的 Create()NewValue() 方法之前, 我们将介绍其中使用的私有方法。 

//+------------------------------------------------------------------+
//| 将两点直线方程比值写入数组                                            |
//+------------------------------------------------------------------+
void CPyramid::Equation(double x1,double y1,double x2,double y2,double &arr[])
  {
   ArrayResize(arr,2);
   arr[0]=(y1-y2)/(x1-x2);
   arr[1]=y2-arr[0]*x2;
  }

Equation() 方法用于查找两点直线方程。特别地, 它可从常规的两点直线方程来定义通用方程式 y = kx + b kb 比率:

该方法通过指定点来进一步定义直线方程, 以便找出金字塔基线的平行直线与金字塔边线 ABAC 相交的交叉点。图例. 15 中所示点的坐标是构成金字塔侧面相似三角形所必要的。反过来, 它们是我们指标的一部分。

图例. 15. 金字塔边线和金字塔基线平行直线的交叉点

知道两条直线的比率, 我们用一个方程组来计算交叉点坐标。Cross() 方法即用于此:

//+---------------------------------------------------------------------------------+
//| 将两条直线交点的坐标写成其比率                                                        |
//+---------------------------------------------------------------------------------+
void CPyramid::Cross(double k1,double b1,double k2,double b2,double &arr[])
  {
   double x,y;
   ArrayResize(arr,2);
   y=(b1*k2-b2*k1)/(k2-k1);
   x=(y-b2)/k2;
   arr[0]=x;
   arr[1]=y;
  }
//+------------------------------------------------------------------+

现在, 我们掌握了绘制金字塔使用的函数, 我们可以完全专注于 Create() 方法。

//+------------------------------------------------------------------+
//| 创建指标                                                           |
//+------------------------------------------------------------------+
void CPyramid::Create(string name,int x,int y)
  {
   int size=m_size;
//--- 相对于其大小修改指标位置
   x=(x<size/2)?size/2:x;
   y=(y<size/2)?size/2:y;
   Name(name);
   X(x);
   Y(y);
   XSize(size);
   YSize(size);
   if(!CreateCanvas())
      Print("错误。不能创建画布。");
//--- 描述和数值
   m_canvas.FontNameSet("Trebuchet MS");
   m_canvas.FontSizeSet(m_font_size);
   m_canvas.FontAngleSet(200);
//--- 查找 AB 直线方程
   double x1,x2,y1,y2;
   x1=XSize()/2;y1=0;
   x2=0;y2=4*YSize()/5;
   Equation(x1,y1,x2,y2,m_line1);
//--- 构建指标的左侧部分
   for(int i=5;i>0;i--)
     {
      //--- 定义两点的坐标 
      y1=i*YSize()/5;
      y2=(i-1)*YSize()/5;
      Equation(x1,y1,x2,y2,m_line2);
      //--- 定义 AB 边线与平行于金字塔底边直线之间的交叉点的坐标
      Cross(m_line1[0],m_line1[1],m_line2[0],m_line2[1],m_cross);
      m_canvas.FillTriangle(int(x1),0,int(x1),int(y1),int(m_cross[0]),int(m_cross[1]),ColorToARGB(m_section_color[i-1],m_transparency));
      //--- 绘制刻度标记
      m_canvas.LineAA(int(x1),int(y1),int(m_cross[0]),int(m_cross[1]),ColorToARGB(m_scale_color,m_transparency));
     }
//--- 查找 AC 直线方程
   x1=XSize()/2;y1=0;
   x2=XSize();y2=4*YSize()/5;
   Equation(x1,y1,x2,y2,m_line1);
//--- 构建指标的右侧部分
   for(int i=5;i>0;i--)
     {
      //--- 定义两点的坐标 
      y1=i*YSize()/5;
      y2=(i-1)*YSize()/5;
      Equation(x1,y1,x2,y2,m_line2);
      //--- 定义 AC 边线与平行于金字塔底边直线之间的交叉点的坐标
      Cross(m_line1[0],m_line1[1],m_line2[0],m_line2[1],m_cross);
      m_canvas.FillTriangle(int(x1),0,int(x1),int(y1),int(m_cross[0]),int(m_cross[1]),ColorToARGB(m_section_color[i-1]));
      //--- 绘制刻度标记
      m_canvas.LineAA(int(x1),int(y1),int(m_cross[0]),int(m_cross[1]),ColorToARGB(m_scale_color,m_transparency));
      //--- 绘制刻度数值
      m_canvas.TextOut(int(x1+2),int(y2+YSize()/6)," - ",ColorToARGB(m_label_color,m_transparency),TA_LEFT|TA_VCENTER);
     }
   m_canvas.LineVertical(XSize()/2,0,YSize(),ColorToARGB(m_scale_color,m_transparency));
   m_canvas.Update();
  }

金字塔构造算法如下:

  • 建立金字塔的左侧。定义 A 和 B 点坐标并使用它们来查找直线方程。
  • 之后, 在循环中查找平行于金字塔底边的直线方程, 以及 与 AB 边线交叉的点
  • 使用得到的点构建区段和刻度标记 (分段)。
  • 以类似的方式构建金字塔的右侧。
  • 除了区段和刻度标记外, 在右侧 添加刻度值
  • 两区段的垂直分离。

设置和更新数据的方法带有两个参数: 当前传递值和最大值。方法的本质: 基于最大设定值定义阈值。当传递它们时, 每个区段会改变其颜色。当超过阈值时, 此区段接收指定的颜色。当数值交叉向下时, 此区段接收的颜色设置为不活跃。 

//+------------------------------------------------------------------+
//| 设置并更新指标数值                                                  |
//+------------------------------------------------------------------+
void CPyramid::NewValue(double value,double maxvalue)
  {
//---
   double s;
   color col;
//--- 查找 AB 直线方程
   double x1,x2,y1,y2;
   x1=XSize()/2;y1=0;
   x2=0;y2=4*YSize()/5;
   Equation(x1,y1,x2,y2,m_line1);
//--- 构建指标的左侧部分
   for(int i=5;i>0;i--)
     {
      //--- 查找刻度标记数值
      s=maxvalue-(i-1)*maxvalue/5;
      //--- 定义区段颜色
      col=(value>=s)?m_section_color[i-1]:m_bg_color;
      //--- 定义两点的坐标 
      y1=i*YSize()/5;
      y2=(i-1)*YSize()/5;
      Equation(x1,y1,x2,y2,m_line2);
      //--- 定义 AB 边线与平行于金字塔底边直线之间的交叉点的坐标
      Cross(m_line1[0],m_line1[1],m_line2[0],m_line2[1],m_cross);
      m_canvas.FillTriangle(int(x1),0,int(x1),int(y1),int(m_cross[0]),int(m_cross[1]),ColorToARGB(m_section_color[i-1],m_transparency));
      m_canvas.FillTriangle(int(x1),0,int(x1),int(y1),int(m_cross[0]),int(m_cross[1]),ColorToARGB(col,m_transparency));
      //--- 绘制刻度标记
      m_canvas.LineAA(int(x1),int(y1),int(m_cross[0]),int(m_cross[1]),ColorToARGB(clrWhite));
     }
//--- 查找 AC 直线方程
   x1=XSize()/2;y1=0;
   x2=XSize();y2=4*YSize()/5;
   Equation(x1,y1,x2,y2,m_line1);
//--- 构建指标的右侧部分
   for(int i=5;i>0;i--)
     {
      //--- 查找刻度标记数值
      s=maxvalue-(i-1)*maxvalue/5;
      //--- 定义区段颜色
      col=(value>=s)?m_section_color[i-1]:m_bg_color;
      //--- 定义两点的坐标 
      y1=i*YSize()/5;
      y2=(i-1)*YSize()/5;
      Equation(x1,y1,x2,y2,m_line2);
      //--- 定义 AC 边线与平行于金字塔底边直线之间的交叉点的坐标
      Cross(m_line1[0],m_line1[1],m_line2[0],m_line2[1],m_cross);
      m_canvas.FillTriangle(int(x1),0,int(x1),int(y1),int(m_cross[0]),int(m_cross[1]),ColorToARGB(m_section_color[i-1],m_transparency));
      m_canvas.FillTriangle(int(x1),0,int(x1),int(y1),int(m_cross[0]),int(m_cross[1]),ColorToARGB(col,m_transparency));
      //--- 定义刻度标记
      m_canvas.LineAA(int(x1),int(y1),int(m_cross[0]),int(m_cross[1]),ColorToARGB(m_scale_color,m_transparency));
      //--- 绘制刻度数值
      m_canvas.TextOut(int(x1+2),int(y2+YSize()/6),DoubleToString(s,0),ColorToARGB(m_label_color,m_transparency),TA_LEFT|TA_VCENTER);
     }
   m_canvas.LineVertical(XSize()/2,0,YSize(),ColorToARGB(m_scale_color,m_transparency));
   m_canvas.Update();
  }

尽管指标渲染的实现更复杂, 但与之前相比, 其创建、设置和应用并不困难。我们将其应用于显示标准 ADX 的数值作为一个小例子。取决于指标位置, 在左上角添加圆形指标进行直观比较。

//+------------------------------------------------------------------+
//|                                                          005.mq5 |
//|                                  版权所有 2017, Alexander Fedosov |
//|                           https://www.mql5.com/zh/users/alex2356 |
//+------------------------------------------------------------------+
#property copyright "版权所有 2017, Alexander Fedosov"
#property link      "https://www.mql5.com/zh/users/alex2356"
#property version   "1.00"
#property indicator_plots 0
#property indicator_chart_window

#include <CustomGUI\CustomGUI.mqh>
//+------------------------------------------------------------------+
//|  指标输入                                                          |
//+------------------------------------------------------------------+
input int               period=10;                                //ADX 周期
input double            maxval=50;                                //最大值
//---
CPyramid ind1;
CCircleSimple ind2;
int InpInd_Handle;
double adx[];
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                                 |
//+------------------------------------------------------------------+
int OnInit()
  {
//---- 获取指标句柄
   InpInd_Handle=iADX(Symbol(),PERIOD_CURRENT,period);
   if(InpInd_Handle==INVALID_HANDLE)
     {
      Print("获取指标句柄失败");
      return(INIT_FAILED);
     }
   ArraySetAsSeries(adx,true);
//---
   ind1.Size(250);
   ind1.Create("pyramid",200,0);
//---
   ind2.FontSize(ind1.FontSize());
   ind2.LabelSize(ind1.FontSize());
   ind2.Label("ADX");
   ind2.Radius(30);
   ind2.Create("label",ind1.X()-ind1.Size()/3,ind1.Y()-ind1.Size()/4);
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| 自定义指标迭代函数                                                  |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   if(CopyBuffer(InpInd_Handle,0,0,2,adx)<=0)
      return(0);
   ind1.NewValue(adx[0],maxval);
   ind2.NewValue(adx[0]);
//--- 返回 prev_calculated 的数值以便下次调用
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| 自定义指标逆初函数                                                   |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ind1.Delete();
   ind2.Delete();
   ChartRedraw();
  }
//+------------------------------------------------------------------+

正如我们从图例. 16 所见, 圆形指标显示高于第三阈值 30 的数值, 因此, 三个区段以当前最大值着色, 在这种情况下设置为50。

图例. 16. 使用金字塔形指标

结论

实现以 CCanvas 为基准的类证明了函数库的能力在图形方面相当广泛。显示这种类型指标的类型和方式仅受想象力的限制。以函数库的形式创建并安排这些指标, 不需要特殊的知识或工作量。

附带存档包含文章中应用的所有文件, 它们位于相应的文件夹中。若要正确操作, 将来自存档的 MQL5 文件夹放置到终端的根目录。 

本文中用到的 存档程序:

#
 名称
类型
描述
1
CanvasBase.mqh 函数库  自定义图形基类
2
CustomGUI.mqh 函数库  包含列表中的所有库类文件 
3 CircleArc.mqh 函数库  包括 CCircleArc 指标类
4 CircleSection.mqh 函数库  包括 CCircleSection 指标类
5 CircleSimple.mqh 函数库  包括 CCircleSimle 指标类
LineGraph.mqh 函数库  包括 CLineGraph 线性图形类
7 LineRounded.mqh  函数库   包括 CLineRounded 圆形线性指标类
 8 Hexagon.mqh  函数库   包括 CHexagon 六角形指标类
 9 Petal.mqh  函数库   包括 CPetal 花瓣状指标类
 10 Histogram.mqh  函数库   包括 CHistogram 直方图指标类
 11 Pyramid.mqh  函数库   包括 CPyramid 金字塔指标类
12 CLineRounded.mq5 指标  CLineRounded 类的实现示例
13 CHexagon.mq5 指标  CHexagon 类的实现示例
14 CPetal.mq5 指标  CPetal 类的实现示例
 15 CHistogram.mq5 指标  CHistogram 类的实现示例
 16 CPyramid.mq5  指标   CPyramid 类的实现示例

分享到:
举报财经168客户端下载

全部回复

0/140

投稿 您想发表你的观点和看法?

更多人气分析师

  • 张亦巧

    人气2184文章4145粉丝45

    暂无个人简介信息

  • 梁孟梵

    人气2176文章3177粉丝39

    qq:2294906466 了解群指导添加微信mfmacd

  • 指导老师

    人气1864文章4423粉丝52

    暂无个人简介信息

  • 李冉晴

    人气2320文章3821粉丝34

    李冉晴,专业现贷实盘分析师。

  • 王启蒙现货黄金

    人气296文章3138粉丝8

    本人做分析师以来,并专注于贵金属投资市场,尤其是在现货黄金...

  • 张迎妤

    人气1896文章3305粉丝34

    个人专注于行情技术分析,消息面解读剖析,给予您第一时间方向...

  • 金泰铬J

    人气2328文章3925粉丝51

    投资问答解咨询金泰铬V/信tgtg67即可获取每日的实时资讯、行情...

  • 金算盘

    人气2696文章7761粉丝125

    高级分析师,混过名校,厮杀于股市和期货、证券市场多年,专注...

  • 金帝财神

    人气4760文章8329粉丝119

    本文由资深分析师金帝财神微信:934295330,指导黄金,白银,...

FX168财经

FX168财经学院

FX168财经

FX168北美