开发回放系统(第 77 部分):新 Chart Trade(四)·进阶篇
🧩

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

(2/3)·鼠标指标与控件点击偶发失灵、字符串协议如何切分字段,这篇把进阶坑位一次讲清

案例拆解 第 2/3 篇
先挂鼠标指标再加载 Chart Trade,部分控件点不动只能删了重插——这种诡异失灵很多人当成自己操作问题。根源在事件分发逻辑,不是你手滑。本篇先补这个稳定性缺口,再聊协议怎么搭。

拖拽面板时怎么锁住图表滚动

在自定义悬浮面板里处理 MSG_TITLE_IDE 消息时,若鼠标横坐标 sx 小于 0,说明拖拽起点在标题区左侧,需要先清掉正在编辑的对象并关闭图表鼠标滚动,否则图表会跟着滚轮乱跑。 对应代码第315行调用 ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, false),把当前图表 ID 的鼠标滚动直接禁用;随后用 sx = x - (m_Info.IsMaximized ? m_Info.x : m_Info.minx) 重算拖拽偏移,sy 同理取 y 减最大/最小化状态下的基准坐标。 真正落位置靠第319、320行:只有当 mx = x - sx 和 my = y - sy 都大于 0 时才写 OBJPROP_XDISTANCE / OBJPROP_YDISTANCE,负值时沿用旧坐标,避免面板被拖到图表外丢失锚点。 最大化与最小化两套坐标分开存(m_Info.x/y 与 m_Info.minx/miny),重启后面板位置才可能连续;外汇贵金属图表多品种切换频繁,这套隔离能减少悬浮窗跳变。

MQL5 / C++
if ((m_Info.IsMaximized) && (sz < class="num">0)) CreateObjectEditable(obj = MSG_STOP_VALUE, m_Info.FinanceStop);
class="kw">break;
 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);
 }
 class="kw">break;

「图表事件到自定义信号的映射细节」

这段 switch 分支把面板发来的市场操作指令转成图表自定义事件。MSG_BUY_MARKET 直接映射到 evChartTradeBuy;MSG_SELL_MARKET 则做了防覆盖判断——若 ev 已是买入事件就保留,否则才赋值为 evChartTradeSell,避免同帧重复信号互相冲掉。 平仓分支只在窗口最大化且 sz<0 时拼装参数字符串:用 StringFormat 把事件号、品种、日/波段标记('D'/'S')、杠杆、止盈止损的点数化结果压成一条 "%d?%s?%c?%d?%.2f?%.2f" 格式串,随后 PrintFormat 打印、EventChartCustom 推到指定终端 ID。 分支收尾处若 sz 仍小于 0,会回写 sz=x 并调 AdjustTemplate 重排模板,MSG_NULL 时顺手清掉编辑对象。外汇与贵金属杠杆波动剧烈,这类自定义事件链路任一步错配都可能发不出单,建议在 MT5 策略测试器里把 PrintFormat 的输出抄出来比对。

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);
      }
      class="kw">break;
   }
   if (sz < class="num">0)
   {
      sz = x;
      AdjustTemplate();
      if (obj == MSG_NULL) DeleteObjectEdit();
   }
 }else
 {

◍ 图表事件里的参数回填逻辑

这段分支处理的是 MT5 图表对象编辑结束后的取值动作。当用户在面板里改完杠杆、止盈或止损的可编辑文本,程序通过 CHARTEVENT_OBJECT_ENDEDIT 拿到对象名,再用 StringToDouble 把文本内容转成数值。 关键的防呆写在 368–374 行:dvalue 小于等于 0 时直接沿用旧值,不会把仓位参数洗成无效数。止盈、止损走各自分支写进 m_Info.FinanceTake / FinanceStop,杠杆则额外用 MathFloor 向下取整再强转 short,避免小数杠杆进结构体。 AdjustTemplate() 在 375 行被调用,意味着每次改完参数都会重刷模板;随后立刻 ObjectDelete 删掉临时编辑框,obj 复位成 MSG_NULL。外汇和贵金属杠杆调整属于高风险操作,实际跟单前建议在策略测试器里先跑一遍这套事件链。 [CODE] sz = -1; if (sx > 0) { ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true); sx = sy = -1; } break; case CHARTEVENT_OBJECT_ENDEDIT: 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 <= 0 ? m_Info.FinanceTake : dvalue); else if (obj == MSG_STOP_VALUE) m_Info.FinanceStop = (dvalue <= 0 ? m_Info.FinanceStop : dvalue); else m_Info.Leverage = (dvalue <= 0 ? m_Info.Leverage : (short)MathFloor(dvalue)); AdjustTemplate(); obj = MSG_NULL; ObjectDelete(GetInfoTerminal().ID, m_Info.szObj_Editable); break; }[/CODE]

MQL5 / C++
sz = -class="num">1;
if (sx > class="num">0)
{
 ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true);
 sx = sy = -class="num">1;
}
 class="kw">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">short)MathFloor(dvalue));
 AdjustTemplate();
 obj = MSG_NULL;
 ObjectDelete(GetInfoTerminal().ID, m_Info.szObj_Editable);
 class="kw">break;
 }

图表对象点击后的状态分支处理

这段事件处理代码挂在 CHARTEVENT_OBJECT_CLICK 下,核心逻辑是先判断被点的对象是不是面板主图对象(m_Info.szObj_Chart),再确认左键命中,最后用 CheckMousePosition 反查鼠标落在哪个功能区块。 命中后走 switch 分发:MSG_DAY_TRADE 翻转日内交易开关 IsDayTrade,MSG_MAX_MIN 翻转窗口最大化标记 IsMaximized,两者都会顺手调 DeleteObjectEdit 清掉旧编辑框;MSG_LEVERAGE_VALUE、MSG_TAKE_VALUE、MSG_STOP_VALUE 则分别拉起杠杆、止盈金额、止损金额的可编辑框。

  • 行有个收口判断:只要 obj 不是 MSG_NULL,就跑 AdjustTemplate 重排模板。这意味着任何有效点击都会触发一次界面重绘,外汇和贵金属品种在高杠杆下误触可能瞬间改掉风控参数,属于高风险操作,建议先在模拟盘验证。

另一个容易忽略的坑在 404 行:CHARTEVENT_OBJECT_DELETE 里若删掉的是面板主对象,直接 macro_CloseIndicator 关指标。复制下面代码到 MT5 里断点跟一遍,能看清对象生命周期怎么绑死指标退出。

MQL5 / C++
class="kw">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">short)lparam, y = (class="type">short)dparam))
        {
         case MSG_DAY_TRADE:
            m_Info.IsDayTrade = (m_Info.IsDayTrade ? class="kw">false : true);
            DeleteObjectEdit();
            class="kw">break;
         case MSG_MAX_MIN:
            m_Info.IsMaximized = (m_Info.IsMaximized ? class="kw">false : true);
            DeleteObjectEdit();
            class="kw">break;
         case MSG_LEVERAGE_VALUE:
            CreateObjectEditable(obj, m_Info.Leverage);
            class="kw">break;
         case MSG_TAKE_VALUE:
            CreateObjectEditable(obj, m_Info.FinanceTake);
            class="kw">break;
         case MSG_STOP_VALUE:
            CreateObjectEditable(obj, m_Info.FinanceStop);
            class="kw">break;
        }
        if (obj != MSG_NULL) AdjustTemplate();
        class="kw">break;
     case CHARTEVENT_OBJECT_DELETE:
        if (sparam == m_Info.szObj_Chart) macro_CloseIndicator(C_Terminal::ERR_Unknown);
        class="kw">break;
      }
让小布替你跑这套回放诊断
这些控件响应与协议解析的异常特征,小布盯盘的 AIGC 已内置了初步比对逻辑,打开对应品种页即可看到历史回放中的交互断层提示,你只管判断策略是否成立。

常见问题

旧版在对象点击事件里直接处理交互,加载顺序错乱时回调丢失。移除 CHARTEVENT_OBJECT_CLICK 相关行、把判断收进统一测试后,该问题消失,具体见本文 DispatchMessage 调整。
优先考虑接收方理解的准确性而非效率,例如二进制 0001 0011 转成 "19" 两个字符,虽多占字节但避免解析歧义。
目前小布可在品种页标记历史回放里的交互异常区间,但是否由本文所述的加载顺序引起,仍需你对照事件日志确认。
可用固定长度数组或分隔符策略,各有取舍;本篇后续小节会展开协议规划的具体切分方式,外汇贵金属回测环境请留意高风险。