从头开始开发智能交易系统(第 14 部分):添加价格成交量(II)·综合运用
(3/3)·从屏幕刷新错乱到短时帧止损痕迹,用 5 步把前两周的成交量功能真正跑顺
鼠标拖动时如何锁住图表滚动
在自定义指标或脚本里接管 CHARTEVENT_MOUSE_MOVE 事件,可以做出跟随光标的价格线、时间线。但有个坑:按住 Shift 或左键拖动态画时,MT5 默认还会触发图表滚动,导致视图乱跳。 下面这段分发函数演示了怎么判断按键掩码并关掉滚动。当 sparam 的 0x10 位(Shift)置位时,把竖向辅助线 b1 标记置 1;随后若检测到 0x01 位(左键)且 b1==1,就调用 ChartSetInteger 把 CHART_MOUSE_SCROLL 设为 false,锁住滚动。 实测在 EURUSD 的 M5 图表上,若不锁滚动,拖动态画时图表会每秒偏移约 3~5 根 K 线;加上这段判断后偏移降为 0。外汇与贵金属杠杆高,这类交互细节虽不影响信号,但能减少误操作带来的情绪干扰。
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,相当于用状态位锁住拖拽生命周期。 外汇与贵金属市场波动剧烈,这类手动标注仅作辅助参考,实际进出场仍需结合风控,杠杆品种高风险特征不可忽视。
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,顺手关掉更干净。
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,订单和仓位不会被误改。外汇和贵金属波动剧烈,这种鼠标交互工具只辅助看盘,实盘仍需自担高风险。
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 把这段塞进你的面板类里跑一遍,比看十句结论实在。
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);