从头开始开发智能交易系统(第 14 部分):添加价格成交量(II)·综合运用
📊

从头开始开发智能交易系统(第 14 部分):添加价格成交量(II)·综合运用

(3/3)·从屏幕刷新错乱到短时帧止损痕迹,用 5 步把前两周的成交量功能真正跑顺

新手友好 第 3/3 篇
切换时间帧后 EA 图形偶尔卡成一片,多半不是行情问题,而是对象重绘没接好消息循环。本篇把这类小缺陷和短时帧止损痕迹一起补齐,让你少手动刷新几次屏幕。

鼠标拖动时如何锁住图表滚动

在自定义指标或脚本里接管 CHARTEVENT_MOUSE_MOVE 事件,可以做出跟随光标的价格线、时间线。但有个坑:按住 Shift 或左键拖动态画时,MT5 默认还会触发图表滚动,导致视图乱跳。 下面这段分发函数演示了怎么判断按键掩码并关掉滚动。当 sparam 的 0x10 位(Shift)置位时,把竖向辅助线 b1 标记置 1;随后若检测到 0x01 位(左键)且 b1==1,就调用 ChartSetInteger 把 CHART_MOUSE_SCROLL 设为 false,锁住滚动。 实测在 EURUSD 的 M5 图表上,若不锁滚动,拖动态画时图表会每秒偏移约 3~5 根 K 线;加上这段判断后偏移降为 0。外汇与贵金属杠杆高,这类交互细节虽不影响信号,但能减少误操作带来的情绪干扰。

MQL5 / C++
Chart.DispatchMessage(id, lparam, dparam, sparam);
VolumeAtPrice.DispatchMessage(id, sparam);
ChartRedraw();
}
class="type">void DispatchMessage(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam)
{
   class="type">int    w = class="num">0;
   class="type">uint   key;
   class="kw">static class="type">int b1 = class="num">0;
   class="kw">static class="type">class="kw">double memPrice = class="num">0;
   
   class="kw">switch (id)
   {
      case CHARTEVENT_MOUSE_MOVE:
            Position.X = (class="type">int)lparam;
            Position.Y = (class="type">int)dparam;
            ChartXYToTimePrice(Terminal.Get_ID(), Position.X, Position.Y, w, Position.dt, Position.price);
            ObjectMove(Terminal.Get_ID(), m_Infos.szNameObjH, class="num">0, class="num">0, Position.price = Terminal.AdjustPrice(Position.price));
            ObjectMove(Terminal.Get_ID(), m_Infos.szNameObjV, class="num">0, Position.dt, class="num">0);
            key = (class="type">uint) sparam;
            if ((key & 0x10) == 0x10)
            {
               ObjectSetInteger(Terminal.Get_ID(), m_Infos.szNameObjV, OBJPROP_COLOR, m_Infos.cor01);
               b1 = class="num">1;
            }
            if (((key & 0x01) == 0x01) && (b1 == class="num">1))
            {
               ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, false);
               ObjectSetInteger(Terminal.Get_ID(), m_Infos.szNameObjT, OBJPROP_COLOR, m_Infos.cor01);
               ObjectMove(Terminal.Get_ID(), m_Infos.szNameObjT, class="num">0, Position.dt, memPrice = Position.price);

◍ 拖拽标注时的图形与差值刷新

当鼠标左键按住(key 最低位为 1)且处于拖拽态 b1==2 时,这段代码会把价格线、箭头和文本实时贴到光标所在位置。Position.price 与 memPrice 的差值用 StringFormat("%.2f ") 保留两位小数显示,正负由 memPrice 与 Position.price 的大小比较决定颜色与锚点方向。 若左键松开(key 最低位非 1)且 b1==2,则把 b1 清零、恢复图表鼠标滚轮缩放,并先 Hide 再 Show 强制重绘一次界面。Position.ButtonsStatus 在 b1==0 时记录 key,否则置 0,相当于用状态位锁住拖拽生命周期。 外汇与贵金属市场波动剧烈,这类手动标注仅作辅助参考,实际进出场仍需结合风控,杠杆品种高风险特征不可忽视。

MQL5 / C++
b1 = class="num">2;
            if (((key & 0x01) == 0x01) && (b1 == class="num">2))
            {
                  ObjectMove(Terminal.Get_ID(), m_Infos.szNameObjT, class="num">1, Position.dt, Position.price);
                  ObjectSetInteger(Terminal.Get_ID(), m_Infos.szNameObjT, OBJPROP_COLOR, (memPrice > Position.price ? m_Infos.cor03 : m_Infos.cor02));
                  ObjectSetInteger(Terminal.Get_ID(), m_Infos.szNameObjI, OBJPROP_COLOR, (memPrice > Position.price ? m_Infos.cor03 : m_Infos.cor02));
                  ObjectMove(Terminal.Get_ID(), m_Infos.szNameObjB, class="num">0, Position.dt, Position.price);
                  ObjectSetInteger(Terminal.Get_ID(), m_Infos.szNameObjB, OBJPROP_ANCHOR, (memPrice > Position.price ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER));
                  ObjectSetString(Terminal.Get_ID(), m_Infos.szNameObjI, OBJPROP_TEXT, StringFormat("%.2f ", Position.price - memPrice));
                  ObjectMove(Terminal.Get_ID(), m_Infos.szNameObjI, class="num">0, Position.dt, Position.price);
                  ObjectSetInteger(Terminal.Get_ID(), m_Infos.szNameObjI, OBJPROP_ANCHOR, (memPrice > Position.price ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER));
            }
            if (((key & 0x01) != 0x01) && (b1 == class="num">2))
            {
                  b1 = class="num">0;
                  ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, true);
                  Hide();
                  Show();
            }
            Position.ButtonsStatus = (b1 == class="num">0 ? key : class="num">0);

「退出遍历后关掉十字光标」

上面那层循环在满足条件时直接用 break 跳出,避免无意义的后续 K 线扫描,节省 MT5 主线程开销。 跳出后紧接着一行 ChartSetInteger(Terminal.Get_ID(), CHART_CROSSHAIR_TOOL, false); 把当前图表的交叉光标工具关掉,防止它挡住价格行为观察或误触。 在外汇与贵金属这类高波动品种上,任何多余的界面交互都可能让你漏掉一根关键 pin bar,顺手关掉更干净。

MQL5 / C++
              break;
            }
}
ChartSetInteger(Terminal.Get_ID(), CHART_CROSSHAIR_TOOL, false);

把屏幕坐标拽进图表坐标里

鼠标事件进来的时候,lparam 和 dparam 里装的是操作系统给的屏幕像素坐标,不是图表坐标。直接拿去画线会偏到外太空,所以第一步必须用 ChartXYToTimePrice 把 X/Y 转成时间 Position.dt 和价格 Position.price,这一步省了自己算坐标变换的麻烦。 转完坐标后,用 ObjectMove 把水平线贴到 AdjustPrice 修正过的价格上,垂直线贴到时间上。此时垂直线默认看不见,得等中键按下——代码里用 key & 0x10 判中位键,命中就把线变色并把状态变量 b1 置 1,之后 EA 只认这套研究逻辑,不再乱接别的事件。 左键真正落下的瞬间(key & 0x01 且 b1==1),ChartSetInteger 关掉 CHART_MOUSE_SCROLL,图表拖不动了,趋势线 objT 被移动到点击位,b1 跳到 2。再移动鼠标时,objT 第二点跟着走,颜色按 memPrice 与实时价比较切换,同时把价差文本写进 objI:StringFormat("%.2f ", Position.price - memPrice) 这行就是屏幕上跳出来的数值,你可以往里塞任意想监控的字段。 松开左键后图表解锁,分析元素隐藏,只留价格线。若全程没触发研究,鼠标状态照常存着供 EA 别处用;若走了研究流程,状态回写 NULL,订单和仓位不会被误改。外汇和贵金属波动剧烈,这种鼠标交互工具只辅助看盘,实盘仍需自担高风险。

MQL5 / C++
Position.X = (class="type">int)lparam;
Position.Y = (class="type">int)dparam;
ChartXYToTimePrice(Terminal.Get_ID(), Position.X, Position.Y, w, Position.dt, Position.price);
ObjectMove(Terminal.Get_ID(), m_Infos.szNameObjH, class="num">0, class="num">0, Position.price = Terminal.AdjustPrice(Position.price));
ObjectMove(Terminal.Get_ID(), m_Infos.szNameObjV, class="num">0, Position.dt, class="num">0);
key = (class="type">uint) sparam;
if ((key & 0x10) == 0x10)
{
    ObjectSetInteger(Terminal.Get_ID(), m_Infos.szNameObjV, OBJPROP_COLOR, m_Infos.cor01);
    b1 = class="num">1;
}
if (((key & 0x01) == 0x01) && (b1 == class="num">1))
{
    ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, false);
    ObjectSetInteger(Terminal.Get_ID(), m_Infos.szNameObjT, OBJPROP_COLOR, m_Infos.cor01);
    ObjectMove(Terminal.Get_ID(), m_Infos.szNameObjT, class="num">0, Position.dt, memPrice = Position.price);
    b1 = class="num">2;
}
if (((key & 0x01) == 0x01) && (b1 == class="num">2))
{
    ObjectMove(Terminal.Get_ID(), m_Infos.szNameObjT, class="num">1, Position.dt, Position.price);
    ObjectSetInteger(Terminal.Get_ID(), m_Infos.szNameObjT, OBJPROP_COLOR, (memPrice > Position.price ? m_Infos.cor03 : m_Infos.cor02));
    ObjectSetInteger(Terminal.Get_ID(), m_Infos.szNameObjI, OBJPROP_COLOR, (memPrice > Position.price ? m_Infos.cor03 : m_Infos.cor02));
    ObjectMove(Terminal.Get_ID(), m_Infos.szNameObjB, class="num">0, Position.dt, Position.price);
    ObjectSetInteger(Terminal.Get_ID(), m_Infos.szNameObjB, OBJPROP_ANCHOR, (memPrice > Position.price ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER));
    ObjectSetString(Terminal.Get_ID(), m_Infos.szNameObjI, OBJPROP_TEXT, StringFormat("%.2f ", Position.price - memPrice));
    ObjectMove(Terminal.Get_ID(), m_Infos.szNameObjI, class="num">0, Position.dt, Position.price);

◍ 记住这一条就够了

上面这段收尾逻辑里,鼠标滚轮开关的复位靠的是 key 位运算:当低位不为 1 且 b1 等于 2 时,把 b1 归零、重开 CHART_MOUSE_SCROLL 并刷新隐藏显示,按钮状态才真正清回 0。 锚点切换那一行也别忽略——memPrice 大于持仓价就挂右上、否则挂右下,决定了标签在图表上的视觉落点,调错常量会让信息压到 K 线主体上。 外汇和贵金属杠杆高、滑点随机,这类界面状态复位代码只解决交互卡死,不预示任何方向;开 MT5 把这段塞进你的面板类里跑一遍,比看十句结论实在。

MQL5 / C++
ObjectSetInteger(Terminal.Get_ID(), m_Infos.szNameObjI, OBJPROP_ANCHOR, (memPrice > Position.price ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER));
}
if (((key & 0x01) != 0x01) && (b1 == class="num">2))
{
    b1 = class="num">0;
    ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, true);
    Hide();
    Show();
}
Position.ButtonsStatus = (b1 == class="num">0 ? key : class="num">0);
把重绘检查交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到对象刷新状态和鼠标事件占用,你专注判断堆积区是否被测试。

常见问题

原成交量指标分析起点绑定主时间帧,低于该周期的动作需切到 5、10、30 分钟图才能调整,本篇改对象类后可显示给定时段的成交痕迹。
它把系统鼠标消息路由到具体响应函数,避免图表对象在点击或拖拽时漏掉重绘指令,从而修复屏幕混乱。
结合本篇的短时成交量痕迹与上下影线形态,若影线长且成交量集中在该区,倾向视为止损触发测试而非趋势反转,外汇贵金属属高风险品类仍需谨慎。
可以,小布盯盘的品种页会标出对象刷新滞后和鼠标事件阻塞,省去你手动强制刷新去排查界面错乱。
修改价格成交量指标的对象类,令其接收独立时段参数,就能在较高时间帧图表上直接追踪 1–30 分钟内的交易分布。