从头开始开发智能交易系统(第 23 部分):新订单系统 (VI)·进阶篇
(2/3)· 每次拖动点位都卡在服务器响应?这篇讲清如何让 MT5 本地先算、再少发指令
挂单与结果线的配色分支
这段逻辑按指示类型(it)给三条线对象分配颜色:当类型为 IT_PENDING 时,cor1 取 clrCornflowerBlue、cor2 取 clrDarkGoldenrod、cor3 用 def_ColorVolumeEdit;其余(含 IT_RESULT 与 default)统一走深蓝 cor1/cor2 加 def_ColorVolumeResult。 注意 IT_PENDING 分支里 cor3 没写 clrNONE,而前面某个分支显式把 cor3 置为 clrNONE 并 break,说明非挂单状态下第三色可能被有意隐藏。开 MT5 把这段塞进你的对象创建函数,改 cor2 为 clrRed 能立刻看出挂单线在图形上的区分度变化。 紧接着三行 Create 调用里,第一行用了带 true 参数的 MountName 生成“幽灵线”并给 def_IndicatorGhostColor;第二行用普通 MountName 建实际线 cor2;最后若 ticket 等于 def_IndicatorTicket0,还对挂单线做 SpotLight 高亮。外汇与贵金属图表对象密集时,这种双线叠加重色容易误读,建议先在回测品种上验证。
cor2 = clrMaroon; cor3 = clrNONE; class="kw">break; case IT_PENDING: cor1 = clrCornflowerBlue; cor2 = clrDarkGoldenrod; cor3 = def_ColorVolumeEdit; class="kw">break; case IT_RESULT : class="kw">default: cor1 = clrDarkBlue; cor2 = clrDarkBlue; cor3 = def_ColorVolumeResult; class="kw">break; } m_TradeLine.Create(ticket, MountName(ticket, it, EV_LINE, true), def_IndicatorGhostColor); m_TradeLine.Create(ticket, MountName(ticket, it, EV_LINE), cor2); if (ticket == def_IndicatorTicket0) m_TradeLine.SpotLight(MountName(ticket, IT_PENDING, EV_LINE));
◍ 挂单与成交标签的尺寸分配
这段逻辑负责给交易面板里的背景框和文字编辑框按类型定尺寸,核心在 switch(it) 分支里落具体像素。 IT_TAKE、IT_STOP、IT_PENDING 三类共用一套底图规格:背景宽 92、高 22,m_BackGround.Size 对 sz0 和 sz1 都写死 92×22。IT_RESULT 单独走 84×34,框更宽更矮,容纳平仓结果信息。 文字层 m_EditInfo1 不区分类型,sz0 与 sz1 都强制 60×14。如果你在 MT5 里改面板布局,优先动 IT_RESULT 的 84 和 34 两个值,其余三类的 92×22 最好保持同步,否则背景和文字会错位。外汇与贵金属品种波动大,这类 GUI 尺寸错乱不会直接亏钱,但容易误点平仓。
if (it != IT_RESULT) m_BackGround.Create(ticket, sz0 = MountName(ticket, it, EV_GROUND, true), def_IndicatorGhostColor); m_BackGround.Create(ticket, sz1 = MountName(ticket, it, EV_GROUND), cor1); class="kw">switch (it) { case IT_TAKE: case IT_STOP: case IT_PENDING: m_BackGround.Size(sz0, class="num">92, class="num">22); m_BackGround.Size(sz1, class="num">92, class="num">22); class="kw">break; case IT_RESULT: m_BackGround.Size(sz1, class="num">84, class="num">34); class="kw">break; } m_BtnClose.Create(ticket, MountName(ticket, it, EV_CLOSE), def_BtnClose); m_EditInfo1.Create(ticket, sz0 = MountName(ticket, it, EV_EDIT, true), def_IndicatorGhostColor, class="num">0.0); m_EditInfo1.Create(ticket, sz1 = MountName(ticket, it, EV_EDIT), cor3, class="num">0.0); m_EditInfo1.Size(sz0, class="num">60, class="num">14); m_EditInfo1.Size(sz1, class="num">60, class="num">14);
「挂单面板的移动按钮与盈亏字段定位」
这段逻辑处理的是交易面板里两类控件的分支创建:当订单不是 IT_RESULT(即普通挂单或持仓)时,生成一对 Wingdings 字体的『u』形移动按钮,字号 17、尺寸 21×21,其中带背景色的一层用 def_IndicatorGhostColor 做幽灵色,另一层用 cor2 实色叠在上面。 若 it 等于 IT_RESULT(平仓结果行),则改走 m_EditInfo2,建一个透明背景、宽度 60 高度 14 的只读框,专门挂这笔交易的浮动盈亏数值,尺寸比移动按钮小一圈。 macroSetAxleY 宏把同 ticket 下背景、交易线、平仓键、编辑框、移动键、盈亏框的 Y 轴一次性对齐:普通行 y 直接落位,IT_RESULT 行的移动键被甩到 9999(屏幕外隐藏),而盈亏框只在结果行显示、编辑框 1 则上移 1 像素错开。 在 MT5 里把 B 参数置 true 跑一遍,能看到挂单行有可拖拽箭头、结果行只剩盈亏文本,这种分工能避免面板控件互相遮挡。外汇与贵金属杠杆高,面板只是辅助,实盘前务必在策略测试器用历史 tick 验证渲染不卡顿。
if (it != IT_RESULT) { m_BtnMove.Create(ticket, sz0 = MountName(ticket, it, EV_MOVE, true), "Wingdings", "u", class="num">17, def_IndicatorGhostColor); m_BtnMove.Create(ticket, sz1 = MountName(ticket, it, EV_MOVE), "Wingdings", "u", class="num">17, cor2); m_BtnMove.Size(sz1, class="num">21, class="num">21); }else { m_EditInfo2.Create(ticket, sz1 = MountName(ticket, it, EV_PROFIT), clrNONE, class="num">0.0); m_EditInfo2.Size(sz1, class="num">60, class="num">14); } class="macro">#define macroSetAxleY(A, B) { \ m_BackGround.PositionAxleY(MountName(ticket, A, EV_GROUND, B), y); \ m_TradeLine.PositionAxleY(MountName(ticket, A, EV_LINE, B), y); \ m_BtnClose.PositionAxleY(MountName(ticket, A, EV_CLOSE, B), y); \ m_EditInfo1.PositionAxleY(MountName(ticket, A, EV_EDIT, B), y, (A == IT_RESULT ? -class="num">1 : class="num">0)); \ m_BtnMove.PositionAxleY(MountName(ticket, A, EV_MOVE, B), (A == IT_RESULT ? class="num">9999 : y)); \ m_EditInfo2.PositionAxleY(MountName(ticket, A, EV_PROFIT, B), (A == IT_RESULT ? y : class="num">9999), class="num">1);
用宏批量摆正挂单面板的 X 轴
在 MT5 自定义面板里,每个挂单组件(背景、交易线、平仓键、信息框、拖动键、盈亏框)都要对齐到同一条纵向基准线。手写七个 PositionAxleX 调用既啰嗦又容易错位,用宏一口气铺完更稳。 下面这段宏 macroSetAxleX(A, B, C) 接收三个参数:A 是组件类别标识,B 是 X 坐标基准值,C 是挂单索引。宏体里把背景和交易线直接放到 B,平仓键偏移 +3 像素,两个信息编辑框偏移 +21,拖动键偏移 +80,盈亏框同样 +21。 配套的 ReDrawAllsIndicator 负责遍历当前终端所有对象:用 ObjectsTotal 拿到总数(Terminal.Get_ID() 下 -1,-1 表示不过滤类型和窗口),逐个取对象名丢给 GetIndicatorInfos 解析出 ticket、price、类型与事件,命中才重绘。外汇与贵金属挂单面板高频刷新,这种遍历在对象数破百时可能有肉眼可察的延迟,建议只在必要时调用。 把宏粘进你的 EA 头文件,改 B 的基准值就能整体平移面板,不用挨个调坐标。
class="macro">#define macroSetAxleX(A, B, C) \ m_BackGround.PositionAxleX(MountName(ticket, A, EV_GROUND, C), B); \ m_TradeLine.PositionAxleX(MountName(ticket, A, EV_LINE, C), B); \ m_BtnClose.PositionAxleX(MountName(ticket, A, EV_CLOSE, C), B + class="num">3); \ m_EditInfo1.PositionAxleX(MountName(ticket, A, EV_EDIT, C), B + class="num">21); \ m_BtnMove.PositionAxleX(MountName(ticket, A, EV_MOVE, C), B + class="num">80); \ m_EditInfo2.PositionAxleX(MountName(ticket, A, EV_PROFIT, C), B + class="num">21); \ } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">void ReDrawAllsIndicator(class="type">void) { class="type">int max = ObjectsTotal(Terminal.Get_ID(), -class="num">1, -class="num">1); class="type">class="kw">ulong ticket; class="type">class="kw">double price; eIndicatorTrade it; eEventType ev; for (class="type">int c0 = class="num">0; c0 <= max; c0++) if (GetIndicatorInfos(ObjectName(Terminal.Get_ID(), c0, -class="num">1, -class="num">1), ticket, price, it, ev))
◍ 把持仓轴价映射到图表坐标
持仓相关的轴价线要画得准,核心是把价格转成屏幕像素坐标,再回填对象属性。下面这段内联函数就是干这个的:先拿 ChartTimePriceToXY 把 0 时刻、指定 price 换到 x/y,再写 tooltip 和轴偏移。 IT_TAKE 的横向偏移给 110 像素,IT_STOP 给 220,其余默认 0;纵向轴通过 macroSetAxleY 正反各设一次。横向还叠了图表宽度的 0.2 倍做基准,意味着在 1920 宽图表上约额外推 384 像素,止损失控线会明显比止盈线更靠右。 外汇与贵金属杠杆高,这类可视化只是辅助定位,实际滑点可能让线位和实际成交有偏差,别当成触发保证。 开 MT5 把这段塞进 EA,改 desl 的 110/220 看看标签拥挤度变化,比凭空调参直观。
class="kw">inline class="type">void PositionAxlePrice(class="type">class="kw">ulong ticket, eIndicatorTrade it, class="type">class="kw">double price) { class="type">int x, y, desl; ChartTimePriceToXY(Terminal.Get_ID(), class="num">0, class="num">0, price, x, y); ObjectSetString(Terminal.Get_ID(), MountName(ticket, it, EV_LINE), OBJPROP_TOOLTIP, DoubleToString(price)); macroSetAxleY(it, true); macroSetAxleY(it, class="kw">false); class="kw">switch (it) { case IT_TAKE: desl = class="num">110; class="kw">break; case IT_STOP: desl = class="num">220; class="kw">break; class="kw">default: desl = class="num">0; } macroSetAxleX(it, desl + (class="type">int)(ChartGetInteger(Terminal.Get_ID(), CHART_WIDTH_IN_PIXELS) * class="num">0.2), true); macroSetAxleX(it, desl + (class="type">int)(ChartGetInteger(Terminal.Get_ID(), CHART_WIDTH_IN_PIXELS) * class="num">0.2), class="kw">false); } class="macro">#undef macroSetAxleX class="macro">#undef macroSetAxleY
「挂单与止盈止损的图形对象清理」
这段 switch 逻辑负责把持仓轴的不同要素映射到界面控件:成交价、挂单量、止盈止损的预估盈亏都走同一套 SetTextValue 写入。注意 IT_PENDING 分支里 value0 除以 Terminal.GetVolumeMinimal(),意味着挂单手数被换算成最小交易单位的整数倍再显示,MT5 里常见最小手数是 0.01,除完数值会放大 100 倍。 IT_TAKE 和 IT_STOP 合并处理,finance 用 value1 除以 Terminal.GetAdjustToTrade() 再乘 value0,得到的是该止损或止盈位触发时的资金变动估算,并非真实已实现盈亏。外汇与贵金属杠杆高,这类估算和实际滑点后的结果可能有明显偏差。 RemoveIndicator 用宏 macroDestroy 批量删对象,一口气干掉 EV_GROUND、EV_LINE、EV_CLOSE 三组图形。如果你在 EA 里频繁增删持仓标注,直接抄这个宏能少写十几行 ObjectDelete,但记得 MountName 的 B 参数要传对图表子标识,否则可能漏删残留对象。
class="kw">switch (it) { case IT_RESULT : PositionAxlePrice(ticket, it, priceOpen); PositionAxlePrice(ticket, IT_PENDING, class="num">0); m_EditInfo2.SetTextValue(MountName(ticket, it, EV_PROFIT), value1); case IT_PENDING: value0 = value0 / Terminal.GetVolumeMinimal(); m_EditInfo1.SetTextValue(MountName(ticket, it, EV_EDIT), value0, def_ColorVolumeEdit); m_EditInfo1.SetTextValue(MountName(ticket, it, EV_EDIT, true), value0, def_IndicatorGhostColor); class="kw">break; case IT_TAKE : case IT_STOP : finance = (value1 / Terminal.GetAdjustToTrade()) * value0; m_EditInfo1.SetTextValue(MountName(ticket, it, EV_EDIT), finance); m_EditInfo1.SetTextValue(MountName(ticket, it, EV_EDIT, true), finance, def_IndicatorGhostColor); class="kw">break; } class="kw">inline class="type">void RemoveIndicator(class="type">class="kw">ulong ticket, eIndicatorTrade it = IT_NULL) { class="macro">#define macroDestroy(A, B) { \ ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_GROUND, B)); \ ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_LINE, B)); \ ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_CLOSE, B)); \