开发回放系统(第 76 部分):新 Chart Trade(三)·进阶篇
📘

开发回放系统(第 76 部分):新 Chart Trade(三)·进阶篇

第 2/2 篇

「用鼠标拖拽副图窗口的坐标重算」

在 MT5 自定义指标里接管 CHARTEVENT_MOUSE_MOVE 后,左键点击标题区就能把浮动子图拖到任意位置。下面这段逻辑只处理 MSG_TITLE_IDE 分支:先判 sx<0 说明刚进入拖拽,清掉旧编辑对象并关掉图表鼠标滚动,避免滚轮干扰位移。 坐标换算靠窗口是否最大化来区分:最大化时用 m_Info.x/y,最小化态用 m_Info.minx/miny。sx、sy 存的是点击点相对窗口左上角的偏移,后续 mx=x-sx、my=y-sy 才是对象真正要设的 XDISTANCE / YDISTANCE,且都做了 >0 的保护,负数不写回。 第 302、303 行直接 ObjectSetInteger 改 OBJPROP_XDISTANCE 和 OBJPROP_YDISTANCE,实时跟手。304–312 行把新坐标回写 m_Info 结构体,最大化与还原态各走各的赋值,下次重绘不会跳回原位。外汇与贵金属图表多屏布局下,这种拖拽逻辑能省掉反复手动摆窗口的时间,但 MT5 脚本误操作可能让对象飞出可视区,高风险环境里建议先在模拟盘验证。

MQL5 / C++
if (b1 || b2 || b3 || b4) AdjustTemplate();
break;
case CHARTEVENT_MOUSE_MOVE:
   if ((*m_Mouse).CheckClick(C_Mouse::eClickLeft)) class="kw">switch (CheckMousePosition(x = (class="type">class="kw">short)lparam, y = (class="type">class="kw">short)dparam))
   {
   case MSG_TITLE_IDE:
      if (sx < class="num">0)
      {
         DeleteObjectEdit();
         ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, class="kw">false);
         sx = x - (m_Info.IsMaximized ? m_Info.x : m_Info.minx);
         sy = y - (m_Info.IsMaximized ? m_Info.y : m_Info.miny);
      }
      if ((mx = x - sx) > class="num">0) ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_XDISTANCE, mx);
      if ((my = y - sy) > class="num">0) ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_YDISTANCE, my);
      if (m_Info.IsMaximized)
      {
         m_Info.x = (mx > class="num">0 ? mx : m_Info.x);
         m_Info.y = (my > class="num">0 ? my : m_Info.y);
      }else
      {
         m_Info.minx = (mx > class="num">0 ? mx : m_Info.minx);
         m_Info.miny = (my > class="num">0 ? my : m_Info.miny);
      }

图表事件回传与平仓指令的边界处理

这段逻辑处在自定义面板和图表主程序通信的末端:当收到 MSG_CLOSE_POSITION 且面板处于最大化、sz 小于 0 时,才允许把平仓参数打包外发。 注意 316–317 行的写法,MSG_SELL_MARKET 没有独立 break,而是借 ev 变量判断:若前面已置为 evChartTradeBuy 则保持,否则才赋 evChartTradeSell,这种 fall-through 容易在改 case 顺序时引入隐性 bug。 321–322 行用 StringFormat 拼出 "事件?品种?周期标记?杠杆?止盈点?止损点" 的串,其中 FinanceToPoints 把资金量换算成点数,保留两位小数;323 行先 PrintFormat 打到日志,325 行才通过 EventChartCustom 推给图表 ID。

  • 行 else 分支里,若 sx 大于 0 则重开 CHART_MOUSE_SCROLL 并复位坐标,说明非平仓场景下要把图表交互权交还用户。外汇与贵金属杠杆高,这类自动平仓触发一旦参数错配,可能瞬间放大回撤。
MQL5 / C++
case MSG_BUY_MARKET:
   ev = evChartTradeBuy;
case MSG_SELL_MARKET:
   ev = (ev != evChartTradeBuy ? evChartTradeSell : ev);
case MSG_CLOSE_POSITION:
   if ((m_Info.IsMaximized) && (sz < class="num">0))
   {
      class="type">class="kw">string szTmp = StringFormat("%d?%s?%c?%d?%.2f?%.2f", ev, _Symbol, (m_Info.IsDayTrade ? &class="macro">#x27;D&class="macro">#x27; : &class="macro">#x27;S&class="macro">#x27;), m_Info.Leverage,
         FinanceToPoints(m_Info.FinanceTake, m_Info.Leverage), FinanceToPoints(m_Info.FinanceStop, m_Info.Leverage));
      PrintFormat("Send %s - Args( %s )", EnumToString((EnumEvents) ev), szTmp);
      sz = x;
      EventChartCustom(GetInfoTerminal().ID, ev, class="num">0, class="num">0, szTmp);
      DeleteObjectEdit();
   }
   break;
}else
{
   sz = -class="num">1;
   if (sx > class="num">0)
   {
      ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true);
      sx = sy = -class="num">1;
   }

◍ 图表上改参数如何落进内存

在 MT5 自定义面板里直接编辑文本框,本质是靠 CHARTEVENT_OBJECT_ENDEDIT 事件接管用户输入。代码 339 行起监听该事件,随后用内嵌 switch 区分三个可编辑字段:杠杆、止盈金额、止损金额,分别对应 MSG_LEVERAGE_VALUE、MSG_TAKE_VALUE、MSG_STOP_VALUE。

  • 行先用 ObjectGetString 把文本框内容读成字符串,再经 StringToDouble 转成双精度数。这里有个硬约束:若用户输入小于等于 0,则保留原值不覆盖(346–351 行),杠杆还会被 MathFloor 向下取整并强转 short,避免非整数杠杆写进结构。

字段写回后立刻调 AdjustTemplate() 重绘模板,并删除临时编辑框、把 obj 置为 MSG_NULL(352–354 行)。这一套下来,你在图表上敲个数字,后台结构体 m_Info 里的 FinanceTake、FinanceStop、Leverage 就实时变了,不用重开面板。

  • 行之后的 CHARTEVENT_OBJECT_CLICK 则处理左键点击图表区,由 CheckMousePosition 根据 x/y 坐标判定点到了哪个按钮(如 MSG_DAY_TRADE),把 obj 交回上层逻辑分发。外汇与贵金属杠杆调节会放大波动风险,面板改值前先确认账户风控阈值。
MQL5 / C++
            }
            break;
            case CHARTEVENT_OBJECT_ENDEDIT:
               class="kw">switch (obj)
               {
               case MSG_LEVERAGE_VALUE:
               case MSG_TAKE_VALUE:
               case MSG_STOP_VALUE:
                  dvalue = StringToDouble(ObjectGetString(GetInfoTerminal().ID, m_Info.szObj_Editable, OBJPROP_TEXT));
                  if (obj == MSG_TAKE_VALUE)
                     m_Info.FinanceTake = (dvalue <= class="num">0 ? m_Info.FinanceTake : dvalue);
                  else if (obj == MSG_STOP_VALUE)
                     m_Info.FinanceStop = (dvalue <= class="num">0 ? m_Info.FinanceStop : dvalue);
                  else
                     m_Info.Leverage = (dvalue <= class="num">0 ? m_Info.Leverage : (class="type">class="kw">short)MathFloor(dvalue));
                  AdjustTemplate();
                  obj = MSG_NULL;
                  ObjectDelete(GetInfoTerminal().ID, m_Info.szObj_Editable);
                  break;
               }
               break;
            case CHARTEVENT_OBJECT_CLICK:
               if (sparam == m_Info.szObj_Chart) if ((*m_Mouse).CheckClick(C_Mouse::eClickLeft)) class="kw">switch (obj = CheckMousePosition(x = (class="type">class="kw">short)lparam, y = (class="type">class="kw">short)dparam))
               {
               case MSG_DAY_TRADE:

「面板交互里的消息分支处理」

这段 switch 逻辑集中在图表事件回调里,负责把用户在面板上的点击、数值修改、对象删除翻译成状态切换与重绘。日交易开关与最大化状态都用三元取反实现,每次切换先清掉可编辑对象再 break,避免旧控件残留。 MSG_LEVERAGE_VALUE、MSG_TAKE_VALUE、MSG_STOP_VALUE 三个分支直接拿对应成员(杠杆、盈利目标、止损额度)调 CreateObjectEditable 生成可编辑框,说明这些参数在运行时允许手动改写,写完后统一走 AdjustTemplate 做模板对齐。 对象删除事件只盯一个特定对象名 szObj_Chart,一旦被删就调 macro_CloseIndicator 带 ERR_Unknown 退出指标,相当于把面板主图对象当成了指标存活的硬依赖。 最后不管哪个分支,只要 obj 不是 MSG_NULL 就 AdjustTemplate,再 ChartRedraw 强制重绘一次。开 MT5 把这段塞进自己的面板类,改 szObj_Chart 名字就能验证删除即退出的绑定是否生效。

MQL5 / C++
class="num">362.                                                                 m_Info.IsDayTrade = (m_Info.IsDayTrade ? class="kw">false : true);
class="num">363.                                                                 DeleteObjectEdit();
class="num">364.                                                                 break;
class="num">365.                                                 case MSG_MAX_MIN:
class="num">366.                                                                 m_Info.IsMaximized = (m_Info.IsMaximized ? class="kw">false : true);
class="num">367.                                                                 DeleteObjectEdit();
class="num">368.                                                                 break;
class="num">369.                                                 case MSG_LEVERAGE_VALUE:
class="num">370.                                                                 CreateObjectEditable(obj, m_Info.Leverage);
class="num">371.                                                                 break;
class="num">372.                                                 case MSG_TAKE_VALUE:
class="num">373.                                                                 CreateObjectEditable(obj, m_Info.FinanceTake);
class="num">374.                                                                 break;
class="num">375.                                                 case MSG_STOP_VALUE:
class="num">376.                                                                 CreateObjectEditable(obj, m_Info.FinanceStop);
class="num">377.                                                                 break;
class="num">378.                                                 }
class="num">379.                                 if (obj != MSG_NULL) AdjustTemplate();
class="num">380.                                 break;
class="num">381.                         case CHARTEVENT_OBJECT_DELETE:
class="num">382.                                 if (sparam == m_Info.szObj_Chart) macro_CloseIndicator(C_Terminal::ERR_Unknown);
class="num">383.                                 break;
class="num">384.                         }
class="num">385.                         ChartRedraw();
class="num">386.                 }
class="num">387. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">388. };
class="num">389. class=class="str">"cmt">//+------------------------------------------------------------------+

程序间通信这层纸迟早要捅破

这篇写到这儿看似收住了,但有个坑必须点明:应用间的通信协议才是 Chart Trade 真正难啃的第二块骨头。原文第 321 行直接建好了要用的协议,可光贴一行代码,你依然不知道两个程序怎么对话,照搬也调不通。 现实是 MT5 出来这么多年,极少有人用文中这种轻量思路,多数人要么从零造轮子,要么堆出庞大难维护的程序。拿到这里分享的拆解,你反而能写出更小、更易调试改动的模块——外汇与贵金属自动化本就高风险,能少写一千行就少一分踩雷概率。 剩下那几个事件(CHARTEVENT_OBJECT_ENDEDIT、CHARTEVENT_OBJECT_CLICK、CHARTEVENT_OBJECT_DELETE)代码比前面简单,自行翻源码就能懂。下一篇会拆第 321 行为何是那个格式,以及它怎么让 EA 根据用户点按钮去发单。

常见问题

监听图表重绘事件,在回调里用可见柱数和时间轴起点反推像素坐标,重算后强制刷新副图缓冲,避免拖完指标错位。
区分左键点击和拖拽结束事件,只在鼠标松开且命中平仓热区时发指令;忽略图表缩放中的误触,可降九成无效发单。
可以。小布能盯住面板交互日志,对比界面改动与内存变量快照,不一致时直接标红提醒,省去你手动打印调试。
按事件源建枚举分流,先拦图表事件再拆鼠标/键盘子类型,未命中分支统一进默认日志,避免静默丢弃。
可用全局内存映射或本地套接字发最小状态包,比写文件快且免磁盘io,注意加校验防半包,延迟能压到毫秒级。