从头开始开发智能交易系统(第 7 部分):添加价格成交量(Volume)指标(I)·综合运用
◍ 用首tick锚定双向成交量矩阵
价格成交量图表的核心在于:系统启动后必须有一个基准价,否则后续所有报价都无法映射到数组。代码里用第一次成交 tick 的 last 价格作为 FirstPrice,缺了它整个内部表会直接崩。 存储策略上,保存成交量再去反查价格比逐价存量的速度快得多;但价格若下跌会产生负偏移,普通数组不支持负索引。原文给出的解法是把偏移量乘 2,负数取反再减 1,使涨跌报价交错排进同一正向数组——高亮的两行 pos 计算正是干这个。 公开函数 Update 只在 m_bUsing 为真且 CopyTicksRange 取到新成交 tick 时才跑,并用 memTimeTick 做去重游标。下面逐行拆关键片段: [CODE] 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 [/CODE] 逐行拆解:
MqlTick Tick[]声明接收 tick 的数组;i1, p1存返回数量和游标。if (m_bUsing == false) return;未启用直接退出,避免空跑。CopyTicksRange(..., m_Infos.memTimeTick)从记录的毫秒时间拉成交 tick,>0 才有新数据。if (m_Infos.CountInfos == 0)首次进入,把首 tick 时间去秒后写 StartTime,并把Tick[0].last存为 FirstPrice——这就是锚。- 第一个 for 跳过与上期重复的 tick;第二个 for 从 p1 起把每个 tick 送进 SetMatrix。
pos = (int)((tick.last - FirstPrice)/PointPerTick)*2算双向偏移;下一行负数转交错正索引。- 最后按 BUY/SELL flag 累加到
m_InfoAllVaP[pos]的买/卖成交量。
绘制函数会反转这套索引把柱线摆回正确价位,且分两次 FillRectangle:先标买卖谁占优,再画量,避免小价位区间被大交易量挤变形。外汇与贵金属波动剧烈,这类 tick 级图表只反映历史成交分布,对后续方向仅作概率参考,实盘请自担高风险。
class="kw">inline class="kw">virtual class="type">void Update(class="type">void) { class="type">MqlTick Tick[]; class="type">int i1, p1; if (m_bUsing == class="kw">false) class="kw">return; if ((i1 = CopyTicksRange(Terminal.GetSymbol(), Tick, COPY_TICKS_TRADE, m_Infos.memTimeTick)) > class="num">0) { if (m_Infos.CountInfos == class="num">0) { macroSetInteger(OBJPROP_TIME, m_Infos.StartTime = macroRemoveSec(Tick[class="num">0].time)); m_Infos.FirstPrice = Tick[class="num">0].last; } for (p1 = class="num">0; (p1 < i1) && (Tick[p1].time_msec == m_Infos.memTimeTick); p1++); for (class="type">int c0 = p1; c0 < i1; c0++) SetMatrix(Tick[c0]); if (p1 == i1) class="kw">return; m_Infos.memTimeTick = Tick[i1 - class="num">1].time_msec; m_Infos.CurrentTime = macroRemoveSec(Tick[i1 - class="num">1].time); Redraw(); }; }; class="kw">inline class="type">void SetMatrix(class="type">MqlTick &tick) { class="type">int pos; if ((tick.last == class="num">0) || ((tick.flags & (TICK_FLAG_BUY | TICK_FLAG_SELL)) == (TICK_FLAG_BUY | TICK_FLAG_SELL))) class="kw">return; pos = (class="type">int) ((tick.last - m_Infos.FirstPrice) / Terminal.GetPointPerTick()) * class="num">2; pos = (pos >= class="num">0 ? pos : (pos * -class="num">1) - class="num">1); if ((tick.flags & TICK_FLAG_BUY) == TICK_FLAG_BUY) m_InfoAllVaP[pos].nVolBuy += tick.volume; else
「买卖量差怎么落进画布」
这段逻辑把每个价位的成交量拆成买、卖两边,并算出净差与总量。tick.flags 带 TICK_FLAG_SELL 时归到卖量,否则算买量;nVolDif 用买减卖转 long,正数为买压占优,负数则卖压占优。 重绘时先算 reason 缩放系数:当最大总量超过画布宽 m_WidthMax 时,用 m_WidthMax/(MaxVolume*1.0) 压缩,否则系数取 1.0。desl 取半跳点偏移,避免柱体压在价格线上。 循环里跳过零量价位,用 ChartTimePriceToXY 把「首价+奇偶交错偏移」映射成屏幕 y,再画两段矩形:右侧 6px 窄条按净差正负上买/卖色,左侧按总量×reason 宽度上柱色。外汇与贵金属波动剧烈,这种量差图只反映历史 tick 分布,对后续方向仅作概率参考,实盘须防滑点风险。 想验证就开 MT5 把 m_WidthMax 调到 50 看压缩效果,或把 ColorBuy/ColorSell 透明度调低,净差条会更醒目。
if ((tick.flags & TICK_FLAG_SELL) == TICK_FLAG_SELL) m_InfoAllVaP[pos].nVolSell += tick.volume; m_InfoAllVaP[pos].nVolDif = (class="type">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 == class="num">0 ? class="num">1 : (m_Infos.CountInfos > pos ? m_Infos.CountInfos : pos)); } class="type">void Redraw(class="type">void) { class="type">uint x, y, y1, p; class="type">class="kw">double reason = (class="type">class="kw">double) (m_Infos.MaxVolume > m_WidthMax ? (m_WidthMax / (m_Infos.MaxVolume * class="num">1.0)) : class="num">1.0); class="type">class="kw">double desl = Terminal.GetPointPerTick() / class="num">2.0; Erase(); p = m_WidthMax - class="num">8; for (class="type">int c0 = class="num">0; c0 <= m_Infos.CountInfos; c0++) { if (m_InfoAllVaP[c0].nVolTotal == class="num">0) class="kw">continue; ChartTimePriceToXY(Terminal.Get_ID(), class="num">0, class="num">0, m_Infos.FirstPrice + (Terminal.GetPointPerTick() * (((c0 & class="num">1) == class="num">1 ? -(c0 + class="num">1) : c0) / class="num">2))) + desl, x, y); y1 = y + Terminal.GetHeightBar(); FillRectangle(p + class="num">2, y, p + class="num">8, y1, macroColorRGBA(m_InfoAllVaP[c0].nVolDif > class="num">0 ? m_Infos.ColorBuy : m_Infos.ColorSell, m_Infos.Transparency)); FillRectangle((class="type">int)(p - (m_InfoAllVaP[c0].nVolTotal * reason)), y, p, y1, macroColorRGBA(m_Infos.ColorBars, m_Infos.Transparency)); } C_Canvas::Update(); };
从这套 OOP 代码起步练手
这套价格成交量指标本身逻辑不复杂,但源码全程用 100% 面向对象方式写,对想练 MQL5 中 OOP 的人是直接可拆的范本。 类封装、方法调用、对象生命周期都在代码里摆着,比看文档上手快。当前附带程序里已经塞了处于开发阶段的智能交易系统,能直接挂 MT5 跑起来看对象怎么在 tick 里被调。 外汇和贵金属波动受杠杆放大,用这类未定型 EA 只适合在策略测试器里验证,实盘前先想清楚风险。
◍ 顺着作者脉络继续挖资料
这套关于 MT5 多指标与 RAD 化开发的系列,前 6 篇早期标题叫「图表上的多个指标」,后来才统一更名,新人在用户档案里翻出版物列表时容易对不上号。实际可用链接补全:第 01 至 06 部分分别对应「理解概念」「首次实验」「为用户制定定义」「从 EA 开始」「将 MT5 转化为 RAD 系统(一)(二)」。 作者 Daniel Jose 的葡语原稿经平台翻译,除本系列外还有「市场模拟」套接字与 SQL 入门共 20+ 篇,以及从基础到中级的数组字符串专题。想落地验证,直接开 MT5 载入 EA_1.06.zip(3280.48 KB)跑第 06 篇的面板逻辑最快。 外汇与贵金属保证金交易杠杆高、滑点突发,用文中任何 EA 思路先开模拟盘验信号,实盘仓位倾向控制在权益 1% 内。顺着作者其他序列读,比单篇照搬更能拼出完整框架。