从头开始开发智能交易系统(第 7 部分):添加价格成交量(Volume)指标(I)
概述
任何满怀信心尝试交易的人都必须在他们的图表上拥有这个指标。 最常用的指标都是那些喜欢在交易时读磁带的人所采用。 此外,而该指标则是那些交易时仅依据价格动作的人会采用。 这是一个极端有用的水平成交量指标,可用于分析特定价格时段所发生的交易量。 然而,正确读取指标示数可能很棘手。 我在文章末尾添加一个链接,以便您可以了解更多信息。
在此,我们不会详讨如何解释指标读数,因为这超出了本文的范畴。 本文的目的是展示如何设计和创建此指标,令其不会拖累 MetaTrader 5 平台的性能。 此处有一个有趣的事实:虽然许多人认为该指标应该实时更新,但实际上只要延迟长度很短,那么有一点延迟是可以接受的。 根据我自己的经验,延迟约 1 秒的话,我没发现更新信息有啥大问题。 不过,如果实时性对您十分重要,那么您肯定要进行一些小的更改。 不应在指标本身中进行更改,而应在智能交易系统调用该指标的地方进行修改,从而到达实时调用。 不过,我相信对性能的影响会很小,因此延迟可以忽略不计。
界面
价格成交量类的控制界面非常简单,但要实现完全控制,必须确保加载指标的图表所有属性正确。 下图显示了这些属性,主控属性均高亮显示。
如果栅格不可见,则无法调整指标的大小,如以下动画所示。 请注意,该界面非常简单直观:它只有两个控件,一个指示大小,另一个显示成交量分析的起点。
泛泛地说,该指标非常有效,在实现和构建时也非常有趣。 在本文中,我们将处理它的最基础的级别,然后在下一篇文章中再对其进行改进。
关于界面,我没有什么要说的了,所以我们就来继续实现代码。
实现
为了在创建指标时尽可能地减少工作量,我们把源代码分成几个部分,并进行一番修改和增补。 我们首先将代码分解为多个部分,因为我们需要的大部分内容已经在其它地方编写好了。 它的主要部分位于 C_Wallpaper 类之中。 我们要继续做什么? 我们会基于位图创建指标吗? 是的,计算机屏幕上的任何图像都应该被视作位图,但它应该以一种特殊的方式来构建。 如此,新的 C_Wallpaper 对象类看起来像这样:
class C_WallPaper : public C_Canvas { protected: enum eTypeImage {IMAGEM, LOGO, COR}; //+------------------------------------------------------------------+ private : public : //+------------------------------------------------------------------+ ~C_WallPaper() { Destroy(); } //+------------------------------------------------------------------+ bool Init(const string szName, const eTypeImage etype, const char cView = 100) { if (etype == C_WallPaper::COR) return true; if (!Create(szName, 0, 0, Terminal.GetWidth(), Terminal.GetHeight())) return false; if(!LoadBitmap(etype == C_WallPaper::IMAGEM ? "WallPapers\\" + szName : "WallPapers\\Logos\\" + _Symbol, cView)) return false; ObjectSetInteger(Terminal.Get_ID(), szName, OBJPROP_BACK, true); return true; } //+------------------------------------------------------------------+ void Resize(void) { ResizeBitMap(Terminal.GetWidth(), Terminal.GetHeight()); } //+------------------------------------------------------------------+ };
看看,代码已经变得更加紧凑:我们删除了 C_Wallpaper 和 C_VolumeAtPrice 类之间的常见部分,并将所有内容放到另一个类中,即 C_C_Canvas class 类。
但是为什么不用 MetaTrader 5 的 C_Canvas 类呢? 这个问题与实际相比太个人化。 我在编写和开发所有东西时喜欢有更多的控制权,但与真正必要的东西相比,这对 C 程序员来说是一个坏习惯。 这就是为什么我要创建一个可在屏幕上绘制对象的类。 当然,您可以使用 MetaTrader 5 中提供的类。 现在,我们精力集中到 C_VolumeAtPrice 类,这是本文的主要关注点。 该类有七个函数,如下表所示。
函数 | 说明 | 访问类型 |
---|---|---|
Init | 按用户指定的值初始化类。 | 通用 |
Update | 按指定时间间隔更新价格成交量数据。 | 通用 |
Resize | 更改图表上的价格成交量图像的大小,从而能更容易地分析某些细节。 | 通用 |
DispatchMessage | 用于向对象类发送消息。 | 通用 |
FromNowOn | 初始化系统变量 | 私密 |
SetMatrix | 创建并维护包含成交量数据的矩阵 | 私密 |
Redraw | 创建成交量映像 | 私密 |
现在,我们继续实现该系统,从下面代码中的变量声明开始:
#define def_SizeMaxBuff 4096 //+------------------------------------------------------------------+ #define def_MsgLineLimit "Starting point from Volume At Price" //+------------------------------------------------------------------+ class C_VolumeAtPrice : private C_Canvas { #ifdef macroSetInteger ERROR ... #endif #define macroSetInteger(A, B) ObjectSetInteger(Terminal.Get_ID(), m_Infos.szObjEvent, A, B) private : uint m_WidthMax, m_WidthPos; bool m_bChartShift, m_bUsing; double m_dChartShift; struct st00 { ulong nVolBuy, nVolSell, nVolTotal; long nVolDif; }m_InfoAllVaP[def_SizeMaxBuff]; struct st01 { ulong memTimeTick; datetime StartTime, CurrentTime; int CountInfos; ulong MaxVolume; color ColorSell, ColorBuy, ColorBars; int Transparency; string szObjEvent; double FirstPrice; }m_Infos;
这段代码中高亮显示的部分您应该多加注意。 这一部分确保定义不会与我们以其它方式引入的文件中定义冲突。 实际上,当您尝试覆盖现有定义时,MQL5 编译器会显示一条警告,在某些情况下,很难找到解决方法。 因此,为了让我们的生活更轻松,我们用到上面代码中高亮显示的测试。 这段代码中的其余部分并不特别有趣。 唯一需要注意的是 def_SizeMaxBuff 的定义。 它指示成交量数据数组的大小。 如有必要,您可以将此数值改为其它值,但根据测试结果,此值对于绝大多数情况来说都已足够了。 它表示最低价和价格之间即时报价次数的变化,故当前值就能处理大量的情况。
Init 函数:此处是一切的开始
正是由这个函数来正确初始化所有变量。 在智能交易系统里它是如此调用的:
//.... Initial data.... input color user10 = clrForestGreen; //Take Profit line color input color user11 = clrFireBrick; //Stop line color input bool user12 = true; //Day Trade? input group "Volume At Price" input color user15 = clrBlack; //Color of bars input char user16 = 20; //Transparency (from 0 to 100 ) //+------------------------------------------------------------------+ C_SubWindow SubWin; C_WallPaper WallPaper; C_VolumeAtPrice VolumeAtPrice; //+------------------------------------------------------------------+ int OnInit() { Terminal.Init(); WallPaper.Init(user03, user05, user04); if ((user01 == "") && (user02 == "")) SubWin.Close(); else if (SubWin.Init()) { SubWin.ClearTemplateChart(); SubWin.AddThese(C_TemplateChart::SYMBOL, user02); SubWin.AddThese(C_TemplateChart::INDICATOR, user01); } SubWin.InitilizeChartTrade(user06, user07, user08, user09, user10, user11, user12); VolumeAtPrice.Init(user10, user11, user15, user16); // ... Rest of the code
此处没有太多的参数,它们主要表示指标将采用的颜色信息。 接下来,我们来看看该函数的内部代码。 下面的代码展示如何初始化所有内容:
void Init(color CorBuy, color CorSell, color CorBar, char cView) { m_Infos.FirstPrice = Terminal.GetRatesLastDay().open; FromNowOn(macroSetHours(macroGetHour(Terminal.GetRatesLastDay().time), TimeLocal())); m_Infos.Transparency = (int)(255 * macroTransparency(cView)); m_Infos.ColorBars = CorBar; m_Infos.ColorBuy = CorBuy; m_Infos.ColorSell = CorSell; if (m_bUsing) return; m_Infos.szObjEvent = "Event" + (string)ObjectsTotal(Terminal.Get_ID(), -1, OBJ_EVENT); CreateObjEvent(); m_bChartShift = ChartGetInteger(Terminal.Get_ID(), CHART_SHIFT); m_dChartShift = ChartGetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE); ChartSetInteger(Terminal.Get_ID(), CHART_SHIFT, true); ChartSetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE, 0.1); Create("VaP" + (string)MathRand(), 0, 0, 1, 1); Resize(); m_bUsing = true; };
正如您所见,此处的一切都很简单。 尽管如此,此处还是有一些功能让代码变得很有趣。 其中之一是 Terminal.GetRatesLastDay().open。 虽然这也许看起来很奇怪,但当我们遵循面向对象编程(OOP)的原则时,这实际上是一种极其常见的情况。 其中一条原则指出,类之外的任何内容都不应该访问类的内部变量。 但是如何获得类的内部变量的值呢? 正确的途径是使用只出现在 OOP 中的表单,因此我们来看看如何在 C_Terminal class 类中声明 GetRatesLastDay 函数。 这可从下面的代码中看到:
inline MqlRates GetRatesLastDay(void) const { return m_Infos.Rates; }
我们看看它实际上是如何工作的。 我们从保留字 inline 开始。 它指挥编译器将代码放置在它出现的所有位置。 编译器实际上不是生成函数调用,而是把所有代码从函数复制到函数被引用的点。 由于内存消耗较少,这会加快代码执行速度。 但在一个特定的情况下,实际发生的是引用 m_Infos.Rates 变量。 该变量具有 MqlRates 类型,也就是说,我们可以按照 MqlRates 结构访问其值。 在这种情况下,我们不必传递变量的引用地址。 但在某些情况下,为了令代码更快捷,我们传递引用地址,在这种情况下,可以修改类的内部变量值,而这应该被禁止。 为了防止这种情况发生,我们使用了保留字 const,这能保证在没有类本身参与的情况下变量永远不会被更改。 虽然来自 C++ 的许多保留字已经以文档形式存在于 MQL5 中,但其中还有一些尚未文档化,不过因为它与 C++ 非常接近,故它们业已成为 MQL5 的一部分。 在本文的最后,我将为那些想了解更多关于 C++,并在 MQL5 编程中使用相同知识的人增加了相关链接。
现在,在 Init 函数代码内部,有一个有趣的部分,我在下面将其高亮显示,方便解释它的作用:
m_bChartShift = ChartGetInteger(Terminal.Get_ID(), CHART_SHIFT); m_dChartShift = ChartGetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE); ChartSetInteger(Terminal.Get_ID(), CHART_SHIFT, true); ChartSetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE, 0.1);
当 EA 启动之时,它会更改图表,但最好在用户关闭系统时将其重置为初始状态。 因此,我们要保存图表滚动设置,然后创建最小滚动。 这是由高亮显示的部分完成的,故我们需要看到图表上的栅格,从而方便调整尺寸。 这是以交互方式完成的,如本文开头所示。 参阅 CHART_SHIFT 获取更多信息。
保护屏幕上的对象
尽管这个类的内部函数非常简单,但有一些地方仍值得特别注意。 第一种是安全系统,不允许用户删除指示成交量分析的起始点:
这个起始点很小,所以您必须小心才能真正注意到它。
重要提示:如果要更改分析起始点,请注意图表的时间帧。 例如,如果您需要将分析从 9:00 移动到 9:02,则需要使用 1 分钟或 2 分钟的时间帧。 若您所用的图表,例如,5 分钟,则您无法做到这一点。
接下来,我们需要注意确保用户不会意外删除该元素。 这是通过以下代码完成的:
void DispatchMessage(int iMsg, string sparam) { switch (iMsg) { // ... The inside of the code case CHARTEVENT_OBJECT_DELETE: if ((sparam == m_Infos.szObjEvent) && (m_bUsing)) { m_bUsing = false; CreateObjEvent(); Resize(); m_bUsing = true; } break; } };
当该类意识到该对象已被删除时,它将立即重新创建该对象,从而防止用户所需的该类对象缺失,那样的话会被迫重新启动 EA。 采用代码中所示的模型,即可确保每当需要时,用户不会删除敏感对象。 但我们需要添加额外的代码,以确保 EA 注意到该事件:
ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, true);
这简单的一行确保在删除对象时 MetaTrader 5 会发出报告。 参阅 CHART_EVENT_OBJECT_DELETE 获取更多信息。
依价格构建价格成交量图表
这是课程的核心,它有三个函数:一个公开,两个私密。 我们从公开函数开始,如下所示:
inline virtual void Update(void) { MqlTick Tick[]; int i1, p1; if (m_bUsing == false) return; if ((i1 = CopyTicksRange(Terminal.GetSymbol(), Tick, COPY_TICKS_TRADE, m_Infos.memTimeTick)) > 0) { if (m_Infos.CountInfos == 0) { macroSetInteger(OBJPROP_TIME, m_Infos.StartTime = macroRemoveSec(Tick[0].time)); m_Infos.FirstPrice = Tick[0].last; } for (p1 = 0; (p1 < i1) && (Tick[p1].time_msc == m_Infos.memTimeTick); p1++); for (int c0 = p1; c0 < i1; c0++) SetMatrix(Tick[c0]); if (p1 == i1) return; m_Infos.memTimeTick = Tick[i1 - 1].time_msc; m_Infos.CurrentTime = macroRemoveSec(Tick[i1 - 1].time); Redraw(); }; };
高亮显示的行对系统非常重要。 当系统开始工作时,它不知道从哪里开始。 这些行更新这些点,它们通知用户从何处开始分析,以及起始价格是多少,以便系统可以创建一个内部表。 系统将始终等待新的即时报价到达。 一旦发生这种情况,我们就可以分析和收集数据,并将其显示在屏幕上。 好了,下面就是该函数:
inline void SetMatrix(MqlTick &tick) { int pos; if ((tick.last == 0) || ((tick.flags & (TICK_FLAG_BUY | TICK_FLAG_SELL)) == (TICK_FLAG_BUY | TICK_FLAG_SELL))) return; pos = (int) ((tick.last - m_Infos.FirstPrice) / Terminal.GetPointPerTick()) * 2; pos = (pos >= 0 ? pos : (pos * -1) - 1); if ((tick.flags & TICK_FLAG_BUY) == TICK_FLAG_BUY) m_InfoAllVaP[pos].nVolBuy += tick.volume; else if ((tick.flags & TICK_FLAG_SELL) == TICK_FLAG_SELL) m_InfoAllVaP[pos].nVolSell += tick.volume; m_InfoAllVaP[pos].nVolDif = (long)(m_InfoAllVaP[pos].nVolBuy - m_InfoAllVaP[pos].nVolSell); m_InfoAllVaP[pos].nVolTotal = m_InfoAllVaP[pos].nVolBuy + m_InfoAllVaP[pos].nVolSell; m_Infos.MaxVolume = (m_Infos.MaxVolume > m_InfoAllVaP[pos].nVolTotal ? m_Infos.MaxVolume : m_InfoAllVaP[pos].nVolTotal); m_Infos.CountInfos = (m_Infos.CountInfos == 0 ? 1 : (m_Infos.CountInfos > pos ? m_Infos.CountInfos : pos)); }
也许这个函数并不那么重要,因为它只存储和保存价格中的成交量数值,但其中高亮显示的行是系统的核心。 为了真正理解这两行中发生了什么,我们来略微思考一下。 考虑以下几点:哪个更快?存储每个价格,并记下每个价格中的成交量数值;还是只存储成交量数值,查询对应的价格是多少? 第二个选项速度更快,所以我们保存成交量,并找出价格。 但系统中的第一个价格是多少? 是的,因为我们需要一个初始值,缺了它,所有一切都会崩溃。 采用第一次交易的即时报价里的价格怎么样? 对的,这极好。 很完美。 但我们遇到一个问题:如果价格上涨,那当然太好了,所有的数据都可以轻松地存储在一个数组中。 但如果它下降了怎么办? 在这种情况下,我们会得到负数值,并且我们将无法访问含有负数索引的数组。 我们可以使用两个数组来替代一个,但这会导致不必要的负载。 还有一个简单的解决方案。 我们看看下表:
如果索引为正,我们就不必担心;但如果索引为负,我们就会遇到问题,因为我们所用的是双向数组,其中零值表示第一个即时报价的价格,负值表示下跌的值,正值表示上涨的值。 下一步:如果我们有两个方向,那么把索引乘以 2,我们就得到中间列。 这似乎没有什么帮助。 但如果我们将负值转换为正值,然后减去 1,我们就得到了正确的列。 如果仔细观察,可以看到这些值在这一列中交错排列,这为我们提供了访问一个数组的完美索引,我们知道这个数组会增长,但我们不知道它会增长多少。 这正是高亮显示的两行所做的:它们为我们的数组创建一个索引,即在高于起始价格值和低于起始价格值之间交替。 虽然这是一个不错的解决方案,但如果我们不能在屏幕上显示数据,那么也不会带来任何好处,而这正是下一个函数要做的事情。
void Redraw(void) { uint x, y, y1, p; double reason = (double) (m_Infos.MaxVolume > m_WidthMax ? (m_WidthMax / (m_Infos.MaxVolume * 1.0)) : 1.0); double desl = Terminal.GetPointPerTick() / 2.0; Erase(); p = m_WidthMax - 8; for (int c0 = 0; c0 <= m_Infos.CountInfos; c0++) { if (m_InfoAllVaP[c0].nVolTotal == 0) continue; ChartTimePriceToXY(Terminal.Get_ID(), 0, 0, m_Infos.FirstPrice + (Terminal.GetPointPerTick() * (((c0 & 1) == 1 ? -(c0 + 1) : c0) / 2)) + desl, x, y); y1 = y + Terminal.GetHeightBar(); FillRectangle(p + 2, y, p + 8, y1, macroColorRGBA(m_InfoAllVaP[c0].nVolDif > 0 ? m_Infos.ColorBuy : m_Infos.ColorSell, m_Infos.Transparency)); FillRectangle((int)(p - (m_InfoAllVaP[c0].nVolTotal * reason)), y, p, y1, macroColorRGBA(m_Infos.ColorBars, m_Infos.Transparency)); } C_Canvas::Update(); };
此函数用绘制成交量图表,高亮显示的部分负责颠倒成交量捕获期间进行的计算。 为了让显示处于正确的位置,价格会有一点变化,如此柱线的位置就可校准。 函数的其余部分只是绘制例程。 这里需要一些解释。 请注意,有两次调用 FillRectangle。 为什么? 第一次调用指示哪个成交量更大:卖家亦或买家;第二次调用则实际绘制成交量。 但为什么不通过在买家和卖家之间划分交易量区间来将它们组合在一起呢? 具体原因是,随着一个价格范围内交易量的增加,它开始干扰其它较小价格范围的分析。 现在很难判定哪一个交易量更大,是卖出亦或买入。 当以这种方式放置时,这个问题就会消失,从而令数据读取更容易、更容易理解。 结果就是,图表将如下图所示:
所有其它的类函数都是为了支持前面解释的类函数,因此务须详细讨论它们。
结束语
在此,我提出了一个非常简单的价格成交量,但它却是一个非常有效的工具。 如果您开始学习编码,并想专注于面向对象编程(OOP),则需要仔细研究此代码;因为它有几个非常好的概念,所有代码都是基于 100% 面向对象的方法。
该应用程序包含当前开发阶段的智能交易系统。
有用的链接
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10302