从头开始开发智能交易系统(第 7 部分):添加价格成交量(Volume)指标(I)·进阶篇
(2/3)· 多数人以为 Volume 必须零延迟实时刷新,却不知 1 秒延迟在性能与可用间更平衡
接上篇,我们继续深挖智能交易系统里价格成交量指标的底层实现。不少开发者一上来就把指标绑死实时刷新,结果图表卡顿、回测掉帧,却没想过短暂延迟在绝大多数盯盘场景里根本无感。本篇聚焦如何用分层位图类把指标开销压到最低,为下一篇的增强留好接口。
成交量分布图的类骨架与缓冲区上限
在 MT5 里做价格成交量(Volume At Price)可视化,第一步是把数据存储和画布操作封进一个私有继承 C_Canvas 的类。下面这段类声明直接决定了后续能回溯多少根 tick 级别的成交量。 缓冲区大小写死为 4096:#define def_SizeMaxBuff 4096。这意味着 m_InfoAllVaP[def_SizeMaxBuff] 最多存 4096 个价位的买/卖/总成交量与净差,超出会静默丢弃。实盘黄金 1 分钟图活跃时段,4096 个价位大约覆盖数小时到一天,回看深度有限。 宏 macroSetInteger(A,B) 被重定义为 ObjectSetInteger(Terminal.Get_ID(), m_Infos.szObjEvent, A, B),所有图形属性改动都走这个事件对象通道,避免直接调全局函数把图表对象名写死。 类里两个结构体分工明确:st00 按价位存 nVolBuy / nVolSell / nVolTotal / nVolDif;st01 存全局状态,包括 StartTime、MaxVolume、三套配色与透明度。注意 m_bChartShift 与 m_dChartShift 是用来兼容图表右移(chart shift)的,不处理会导致重叠画布错位。 别把 4096 当万能上限。若你要做日线级 VAP,改这个宏必须同步检查数组越界,否则 MT5 会在第 4097 次写入时报 array out of range。
class="macro">#define def_SizeMaxBuff class="num">4096 class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_MsgLineLimit "Starting point from Volume At Price" class=class="str">"cmt">//+------------------------------------------------------------------+ class C_VolumeAtPrice : class="kw">private C_Canvas { class="macro">#ifdef macroSetInteger ERROR ... class="macro">#endif class="macro">#define macroSetInteger(A, B) ObjectSetInteger(Terminal.Get_ID(), m_Infos.szObjEvent, A, B) class="kw">private : class="type">uint m_WidthMax, m_WidthPos; class="type">bool m_bChartShift, m_bUsing; class="type">class="kw">double m_dChartShift; class="kw">struct st00 { class="type">class="kw">ulong nVolBuy, nVolSell, nVolTotal; class="type">long nVolDif; }m_InfoAllVaP[def_SizeMaxBuff]; class="kw">struct st01 { class="type">class="kw">ulong memTimeTick; class="type">class="kw">datetime StartTime, CurrentTime; class="type">int CountInfos; class="type">class="kw">ulong MaxVolume; class="type">class="kw">color ColorSell, ColorBuy, ColorBars; class="type">int Transparency; class="type">class="kw">string szObjEvent; class="type">class="kw">double FirstPrice; }m_Infos; }
「Init 里的变量初始化与图表接管」
EA 启动第一道关口是 OnInit,它负责把外部 input 参数灌进各个功能类。上面这段初始化里,颜色与透明度这类 input(如 user16 设 20 代表 0~100 透明度档位)直接传给 VolumeAtPrice,决定成交量分布柱的视觉样式。 有意思的是 Terminal.GetRatesLastDay().open 的写法。按 OOP 封装原则,类外部不该直接碰内部变量,于是 C_Terminal 类用 inline 方法 GetRatesLastDay 返回 m_Infos.Rates——编译器会把函数体原地展开,省掉调用开销,同时用 const 锁死,避免外部误改类内状态。 被高亮的那行 VolumeAtPrice.Init(...) 只是入口,真正的图表接管在更后面:EA 会先读 CHART_SHIFT 存下用户滚动设置,再强制最小偏移,方便肉眼对齐栅格做交互调整。想验证的话,开 MT5 加载这类 EA,关掉前看图表是否自动复位即可。 外汇与贵金属波动剧烈,这类自动改图表设置的 EA 在高波动时段可能干扰手动操作,实盘前务必在模拟盘跑一轮。
class=class="str">"cmt">//.... Initial data.... class="kw">input class="type">class="kw">color user10 = clrForestGreen; class=class="str">"cmt">//Take Profit line class="type">class="kw">color class="kw">input class="type">class="kw">color user11 = clrFireBrick; class=class="str">"cmt">//Stop line class="type">class="kw">color class="kw">input class="type">bool user12 = true; class=class="str">"cmt">//Day Trade? class="kw">input group "Volume At Price" class="kw">input class="type">class="kw">color user15 = clrBlack; class=class="str">"cmt">//Color of bars class="kw">input class="type">char user16 = class="num">20; class=class="str">"cmt">//Transparency(from class="num">0 to class="num">100 ) class=class="str">"cmt">//+------------------------------------------------------------------+ C_SubWindow SubWin; C_WallPaper WallPaper; C_VolumeAtPrice VolumeAtPrice; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">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); class=class="str">"cmt">// ... Rest of the code class="type">void Init(class="type">class="kw">color CorBuy, class="type">class="kw">color CorSell, class="type">class="kw">color CorBar, class="type">char cView) { m_Infos.FirstPrice = Terminal.GetRatesLastDay().open; FromNowOn(macroSetHours(macroGetHour(Terminal.GetRatesLastDay().time), TimeLocal())); m_Infos.Transparency = (class="type">int)(class="num">255 * macroTransparency(cView)); m_Infos.ColorBars = CorBar; m_Infos.ColorBuy = CorBuy; m_Infos.ColorSell = CorSell; if (m_bUsing) class="kw">return; m_Infos.szObjEvent = "Event" + (class="type">class="kw">string)ObjectsTotal(Terminal.Get_ID(), -class="num">1, OBJ_EVENT); CreateObjEvent(); m_bChartShift = ChartGetInteger(Terminal.Get_ID(), CHART_SHIFT);
◍ 把图表右移量锁进对象初始化里
在自定义指标或面板的构造收尾阶段,先把当前图表的右移状态读出来存进成员变量,是避免后续被覆盖的常见做法。下面这段逻辑先取 CHART_SHIFT_SIZE 原值到 m_dChartShift,再强制开启 CHART_SHIFT 并把偏移量设为 0.1,等于把最新 K 线右侧留出 10% 宽度的空白区。 顺手生成一个名为 VaP+随机数的子图对象并调用 Resize() 重算尺寸,最后把 m_bUsing 置 true 表示控件已挂载。注意 0.1 这个值是相对窗口宽度的比例,不是像素,换到超宽屏上肉眼留白会明显变大。 另一处平行写法用 ChartGetInteger 先备份 CHART_SHIFT 开关,再同样写回 true 与 0.1。如果你在 EA 里复用这段,建议把 0.1 提为输入参数,实盘前在 MT5 里拖一下不同品种验证右移后标签是否还贴住影线。外汇与贵金属波动大,图表重绘逻辑改错可能引发误读,务必先在模拟盘跑通。
m_dChartShift = ChartGetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE); ChartSetInteger(Terminal.Get_ID(), CHART_SHIFT, true); ChartSetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE, class="num">0.1); Create("VaP" + (class="type">class="kw">string)MathRand(), class="num">0, class="num">0, class="num">1, class="num">1); Resize(); m_bUsing = true; }; class="kw">inline class="type">MqlRates GetRatesLastDay(class="type">void) class="kw">const { class="kw">return m_Infos.Rates; } 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, class="num">0.1);
别让用户误删成交量锚点
成交量分析类里有个不起眼的起始点对象,体积小到容易看漏。它一旦丢失,EA 就得重启才能恢复,所以类内部做了防删:侦测到对象被删就立刻重建,保证敏感元素始终在场。 调分析起点时先看图表周期。想把锚点从 9:00 挪到 9:02,必须用 1 分钟或 2 分钟周期;若图表停在 5 分钟,这种 2 分钟级别的移动根本落不了地。 光靠重建还不够,得让 EA 主动感知删除事件。下面这行开启 MT5 的对象删除通知,缺了它类里那段恢复逻辑不会被触发:ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, true); 具体消息常量见终端文档 CHART_EVENT_OBJECT_DELETE。 删除事件的派发写在 DispatchMessage 里。命中对象名且正处于使用态时,先卸标志、重建并重算尺寸,再恢复使用态,整套动作在消息回调中同步完成,用户侧无感知。
class="type">void DispatchMessage(class="type">int iMsg, class="type">class="kw">string sparam) { class="kw">switch (iMsg) { class=class="str">"cmt">// ... The inside of the code case CHARTEVENT_OBJECT_DELETE: if ((sparam == m_Infos.szObjEvent) && (m_bUsing)) { m_bUsing = class="kw">false; CreateObjEvent(); Resize(); m_bUsing = true; } break; } }; ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, class="num">0, true);