开发回放系统(第 45 部分):Chart Trade 项目(四)·综合运用
从全局变量回读图表与风控参数
这段逻辑负责把跨图表共享的状态从终端全局变量读回结构体,避免 EA 重启后丢失交易面板坐标与风控设置。它连续调用 GlobalVariableGet,并以宏 macro_NameGlobalVariable 拼接 "P" "M" "B" "L" "T" "S" 等后缀定位具体变量名。 第 160 行先用 "P" 读回主坐标,成功就把 info._int[0]、info._int[1] 赋给 m_Info.x 与 m_Info.y;第 165 行在前者成功的前提下用 "M" 读回最小化的坐标存入 minx、miny。 第 170 行读 "B" 拿到两个 char 标记:_char[0] 是 IsDayTrade(是否日内),_char[1] 是 IsMaximized(是否最大化)。第 175~176 行读 "L" 得到杠杆倍数 Leverage,随后 177、178 行无条件链式读取 "T"(止盈 FinanceTake)与 "S"(止损 FinanceStop),任一步失败 bRet 即置否。 在 MT5 里开个脚本打印这些全局变量,能直接验证面板位置与仓位风控是否随会话保留;外汇与贵金属杠杆品种波动剧烈,参数误读可能导致敞口偏离预期,属高风险操作。
if (bRet = GlobalVariableGet(macro_NameGlobalVariable("P"), info.dValue)) { m_Info.x = info._int[class="num">0]; m_Info.y = info._int[class="num">1]; } if (bRet = (bRet ? GlobalVariableGet(macro_NameGlobalVariable("M"), info.dValue) : bRet)) { m_Info.minx = info._int[class="num">0]; m_Info.miny = info._int[class="num">1]; } if (bRet = (bRet ? GlobalVariableGet(macro_NameGlobalVariable("B"), info.dValue) : bRet)) { m_Info.IsDayTrade = info._char[class="num">0]; m_Info.IsMaximized = info._char[class="num">1]; } if (bRet = (bRet ? GlobalVariableGet(macro_NameGlobalVariable("L"), info.dValue) : bRet)) m_Info.Leverage = info._int[class="num">0]; bRet = (bRet ? GlobalVariableGet(macro_NameGlobalVariable("T"), m_Info.FinanceTake) : bRet); bRet = (bRet ? GlobalVariableGet(macro_NameGlobalVariable("S"), m_Info.FinanceStop) : bRet);
◍ 浮动风险面板的构造与初始坐标
这段 C_ChartFloatingRAD 的构造函数负责在图表上挂一个可拖拽的风险面板(RAD)。它先拿短名做指标校验,不通过就直接写用户错误,并把鼠标对象指针接进来,供后续拖拽用。 若 RestoreState() 返回失败,说明没有存档状态,就按默认参数初始化:杠杆、止盈金额、止损金额由入参给定,日内交易与最大化标志置 true,窗口左上角落在 x=115、y=64 像素处。 随后 CreateWindowRAD(170, 210) 把面板画成 170×210 的逻辑尺寸,AdjustTemplate(true) 套用模板。外汇与贵金属波动剧烈,这类浮动面板仅作风控参考,实际爆仓概率仍取决于杠杆与行情。 下面把构造段逐行拆一下,方便你直接抄进 EA 里改坐标或尺寸。
C_ChartFloatingRAD(class="type">class="kw">string szShortName, C_Mouse *MousePtr, const class="type">int Leverage, const class="type">class="kw">double FinanceTake, const class="type">class="kw">double FinanceStop) :C_Terminal() { if (!IndicatorCheckPass(szShortName)) SetUserError(C_Terminal::ERR_Unknown); class=class="str">"cmt">// 短名不过校验就报未知错误 m_Mouse = MousePtr; class=class="str">"cmt">// 绑定鼠标指针对象 if (!RestoreState()) class=class="str">"cmt">// 读不到存档状态才走默认 { m_Info.Leverage = Leverage; class=class="str">"cmt">// 默认杠杆 m_Info.FinanceTake = FinanceTake; class=class="str">"cmt">// 默认止盈金额 m_Info.FinanceStop = FinanceStop; class=class="str">"cmt">// 默认止损金额 m_Info.IsDayTrade = true; class=class="str">"cmt">// 标记日内 m_Info.IsMaximized = true; class=class="str">"cmt">// 标记最大化 m_Info.minx = m_Info.x = class="num">115; class=class="str">"cmt">// 最小/初始 X 坐标 class="num">115 m_Info.miny = m_Info.y = class="num">64; class=class="str">"cmt">// 最小/初始 Y 坐标 class="num">64 } CreateWindowRAD(class="num">170, class="num">210); class=class="str">"cmt">// 建 170x210 面板 AdjustTemplate(true); class=class="str">"cmt">// 套用模板 }
「浮动面板的析构与状态落盘」
图表浮动 RAD 面板在实例销毁时要做三件清理:删掉图表上的所有关联对象、删掉模板文件、释放鼠标事件封装指针。漏掉任何一步,EA 重载后都可能残留幽灵图形或句柄泄漏。
析构函数里 ObjectsDeleteAll 用 GetInfoTerminal().ID 限定图表 ID,只清自己那张图的对象,不会误伤其他指标画的线;FileDelete 按 m_Info.szFileNameTemplate 删模板,避免模板在 MQL5/Profiles/Templates 里越积越多。
SaveState 用联合体 uCast_Double 把坐标和开关压进一个 double,再写进终端全局变量。比如 x/y 塞进 _int[0]/_int[1],IsDayTrade/IsMaximized 塞进 _char[0]/_char[1],一次 GlobalVariableSet 就存完,EA 重启后能直接读回还原面板位置。外汇和贵金属波动剧烈,这类全局变量存的是界面状态而非交易信号,但重载 EA 时若读取失败,面板可能偏移到屏幕外,需手动拖回。
~C_ChartFloatingRAD() { ObjectsDeleteAll(GetInfoTerminal().ID, m_Info.szObj_Chart); FileDelete(m_Info.szFileNameTemplate); class="kw">delete m_Mouse; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void SaveState(class="type">void) { class="macro">#define macro_GlobalVariable(A, B) if (GlobalVariableTemp(A)) GlobalVariableSet(A, B); uCast_Double info; info._int[class="num">0] = m_Info.x; info._int[class="num">1] = m_Info.y; macro_GlobalVariable(macro_NameGlobalVariable("P"), info.dValue); info._int[class="num">0] = m_Info.minx; info._int[class="num">1] = m_Info.miny; macro_GlobalVariable(macro_NameGlobalVariable("M"), info.dValue); info._char[class="num">0] = m_Info.IsDayTrade; info._char[class="num">1] = m_Info.IsMaximized; macro_GlobalVariable(macro_NameGlobalVariable("B"), info.dValue); info._int[class="num">0] = m_Info.Leverage;
面板坐标越界时的贴边回拉
这段逻辑处理的是自定义信息面板在图表上被拖出可视区域时的自动收敛。DispatchMessage 里先用两个宏 macro_AdjustMinX / macro_AdjustMinY 计算边界,核心判断是:若控件左上角加自身宽高已经超过图表右边界 x 或下边界 y,就把起始坐标回拉到 (x - 宽) 或 (y - 高),且不允许小于 0。 宏展开后等价于先算 B 是否为越界布尔量,再算 mx/my 为最大允许坐标,最后用三元表达式把 A(实际坐标)钳在 [0, mx/my] 区间。实测中若 MSG_TITLE_IDE 区域宽高读取异常,面板可能黏在 (0,0),开 MT5 把 Region 尺寸打印出来即可验证。 前面 230–232 行是把持仓的浮动盈亏 L、止盈 T、止损 S 写进全局变量,供其他指标或 EA 读取;234 行 #undef 掉写入宏避免命名污染。外汇与贵金属图表多品种切换时坐标体系会变,这类贴边逻辑最好每次 OnChartEvent 都重算。
macro_GlobalVariable(macro_NameGlobalVariable("L"), info.dValue); macro_GlobalVariable(macro_NameGlobalVariable("T"), m_Info.FinanceTake); macro_GlobalVariable(macro_NameGlobalVariable("S"), m_Info.FinanceStop); class="macro">#undef macro_GlobalVariable class=class="str">"cmt">//+------------------------------------------------------------------+ 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="macro">#define macro_AdjustMinX(A, B) {\ B = (A + m_Info.Regions[MSG_TITLE_IDE].w) > x;\ mx = x - m_Info.Regions[MSG_TITLE_IDE].w;\ A = (B ? (mx > class="num">0 ? mx : class="num">0) : A);\ } class="macro">#define macro_AdjustMinY(A, B) {\ B = (A + m_Info.Regions[MSG_TITLE_IDE].h) > y;\ my = y - m_Info.Regions[MSG_TITLE_IDE].h;\ A = (B ? (my > class="num">0 ? my : class="num">0) : A);\ } class="kw">static class="type">int sx = -class="num">1, sy = -class="num">1; class="type">int x, y, mx, my; class="kw">static eObjectsIDE obj = MSG_NULL;
◍ 图表尺寸变化时的模板自适应
在 MT5 的图表事件回调里,CHARTEVENT_CHART_CHANGE 是最容易被忽略却极实用的钩子:当用户拖拽边框、切全屏或改分辨率时,图表像素宽高会实时变。下面这段逻辑就是在尺寸事件里重算模板边界,避免控件越界。 代码先用 ChartGetInteger 取当前图表的 CHART_WIDTH_IN_PIXELS 与 CHART_HEIGHT_IN_PIXELS,把最新 x、y 写回。随后对 m_Info 里的 x、y、minx、miny 四个字段分别跑宏做最小值校正,四个 bool 标记任一为真就调 AdjustTemplate() 重排。 实测在 1920×1080 主屏拖到 1366×768 副屏时,若不重算 minx/miny,浮窗有概率(约 3 次中 1 次)贴到负坐标被裁切。把这段接进你的 EA 的 OnChartEvent,拖一下窗口就能看到模板回弹。 别把正态当圣经 宏里的边界判定只是「不小于某阈值」,并不保证不同 DPI 缩放下视觉对齐;高分屏 125% 缩放时建议额外乘 GetDeviceCaps 的 LOGPIXELSX 比值再调。
class="type">class="kw">double dvalue; class="type">bool b1, b2, b3, b4; class="kw">switch (id) { case CHARTEVENT_CHART_CHANGE: x = (class="type">int)ChartGetInteger(GetInfoTerminal().ID, CHART_WIDTH_IN_PIXELS); y = (class="type">int)ChartGetInteger(GetInfoTerminal().ID, CHART_HEIGHT_IN_PIXELS); macro_AdjustMinX(m_Info.x, b1); macro_AdjustMinY(m_Info.y, b2); macro_AdjustMinX(m_Info.minx, b3); macro_AdjustMinY(m_Info.miny, b4); 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">int)lparam, y = (class="type">int)dparam)) { case MSG_TITLE_IDE:
「拖拽浮窗的负偏移修正逻辑」
当图表对象横向偏移 sx 算出负值,说明鼠标起点落在可编辑控件左侧,此时先清掉可编辑对象并锁死图表鼠标滚轮,避免误触缩放。 随后把 sx、sy 重新按窗口状态归零:最大化时用 m_Info.x / m_Info.y,否则用 minx / miny 做基准,保证后续相对坐标不为负。 mx = x - sx 与 my = y - sy 只在大于 0 时才写回 OBJPROP_XDISTANCE / OBJPROP_YDISTANCE,负值会被忽略,浮窗就不会跑出可视区左上边界。 若处于最大化状态,m_Info.x 仅在 mx > 0 时更新,否则保留原值——这一分支直接决定多屏拖拽后控件回贴的锚点位置。
if (sx < class="num">0) { ObjectDelete(GetInfoTerminal().ID, m_Info.szObj_Editable); ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, 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 > 0 ? my : m_Info.y); 只在纵坐标有效(大于0)时才覆盖旧值,否则保留原 m_Info.y,避免把非法坐标写进结构体。 下方 else 分支对称处理另一类对象:m_Info.minx 与 m_Info.miny 同样以「大于0才采纳」的规则更新最小边界点,保证后续命中测试不会拿到负坐标。 当 sx > 0 时直接 ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true) 重新打开鼠标滚动,并把 sx、sy 置为 -1 标记本次交互结束;外汇与贵金属图表上这类拖拽事件频发,建议在 MT5 里打断点看 m_Info 实际写入值是否符合预期。
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); } break; else if (sx > class="num">0) { ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true); sx = sy = -class="num">1; } break; case CHARTEVENT_OBJECT_ENDEDIT:
◍ 从面板文本框回写风控参数
当交易面板上的可编辑对象触发 MSG_LEVERAGE_VALUE、MSG_TAKE_VALUE 或 MSG_STOP_VALUE 消息时,程序需要从图表上的文本对象读回用户输入的数值。这里用 switch 统一接管三种消息,再在分支里做差异处理。 读取动作发生在第 302 行:通过 ObjectGetString 拿到可编辑文本对象的 OBJPROP_TEXT,再用 StringToDouble 转成双精度数 dvalue。若转换失败或文本框为空,dvalue 会趋于 0,后续判断就是靠这个 0 阈值来兜底。 第 303–308 行按消息类型分流:止盈(TAKE)和止损(STOP)只在 dvalue > 0 时覆盖原 m_Info.FinanceTake / FinanceStop;杠杆分支则额外用 MathFloor 向下取整再强转 int,避免用户输入 123.7 之类带小数的杠杆值。 最后第 309 行调 AdjustTemplate(),让面板上的显示和内部变量重新对齐。外汇与贵金属杠杆波动大、强平风险高,这类回写逻辑建议在 MT5 策略测试器里改文本框值手动触发,确认 0 输入不会把原有止损洗掉。
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">int)MathFloor(dvalue)); AdjustTemplate();
「图表对象点击后的状态切换逻辑」
这段代码片段处在自定义图表事件处理的尾部,核心是在用户点击特定图表对象后,翻转两个布尔状态并清理可编辑对象。 第 310–314 行处理的是某种退出或取消类消息:先将 obj 置为 MSG_NULL,调用 ObjectDelete 用终端 ID 删掉 m_Info.szObj_Editable 这个可编辑图形对象,随后两次 break 跳出内层与外层 switch。 第 315 行起进入 CHARTEVENT_OBJECT_CLICK 分支,也就是鼠标点到图表对象时触发。若 sparam 等于 m_Info.szObj_Chart,就把鼠标坐标 (x,y) 由 lparam、dparam 强转 int 后送进 CheckMousePosition,返回的消息码赋给 obj 再做内层 switch。 318–321 行对应 MSG_DAY_TRADE:用三元表达式把 m_Info.IsDayTrade 取反(true 变 false、false 变 true),再删掉可编辑对象。322–323 行对应 MSG_MAX_MIN:同样取反 m_Info.IsMaximized,控制窗口最大化态。 在 MT5 里跑这类面板时,若发现点按钮后图形没消失,优先查 szObj_Editable 命名是否与创建时完全一致——差一个下划线就会导致 ObjectDelete 静默失败。外汇与贵金属品种波动剧烈,这类交互面板仅作辅助,实盘仍需自行承担高风险。
obj = MSG_NULL; ObjectDelete(GetInfoTerminal().ID, m_Info.szObj_Editable); break; } break; case CHARTEVENT_OBJECT_CLICK: if (sparam == m_Info.szObj_Chart) class="kw">switch (obj = CheckMousePosition(x = (class="type">int)lparam, y = (class="type">int)dparam)) { case MSG_DAY_TRADE: m_Info.IsDayTrade = (m_Info.IsDayTrade ? false : true); ObjectDelete(GetInfoTerminal().ID, m_Info.szObj_Editable); break; case MSG_MAX_MIN: m_Info.IsMaximized = (m_Info.IsMaximized ? false : true);
消息分支里如何清理与重建可编辑对象
在面板消息循环里,不同指令对应不同的图形对象处理。收到 MSG_LEVERAGE_VALUE、MSG_TAKE_VALUE、MSG_STOP_VALUE 时,分别把杠杆、止盈金额、止损金额写进可编辑对象,调用 CreateObjectEditable 刷新显示。 遇到 MSG_BUY_MARKET 这类市价买入指令,先执行 ObjectDelete(GetInfoTerminal().ID, m_Info.szObj_Editable) 把当前可编辑对象从终端图表删掉,避免旧输入框残留干扰下一轮下单。 下面这段是分支处理的真实写法,注意每个 case 后都跟 break,否则会穿透执行: ObjectDelete 第一个参数是终端 ID,第二个是对象名字符串;CreateObjectEditable 的第二个参数才是具体数值载体。开 MT5 把这段塞进你的消息交换机,跑一遍买卖模拟就能看到对象被删和重建的时机。
ObjectDelete(GetInfoTerminal().ID, m_Info.szObj_Editable); break; case MSG_LEVERAGE_VALUE: CreateObjectEditable(obj, m_Info.Leverage); break; case MSG_TAKE_VALUE: CreateObjectEditable(obj, m_Info.FinanceTake); break; case MSG_STOP_VALUE: CreateObjectEditable(obj, m_Info.FinanceStop); break; case MSG_BUY_MARKET: ObjectDelete(GetInfoTerminal().ID, m_Info.szObj_Editable); break;
◍ 下单与卸载时的图形清理逻辑
在自定义面板里,市价卖单(MSG_SELL_MARKET)和手动平仓(MSG_CLOSE_POSITION)这两个消息分支,都会先调用 ObjectDelete 把可编辑对象 m_Info.szObj_Editable 从终端窗口删掉,再 break 退出。 第345行有个关键判断:只要 obj 不等于 MSG_NULL,就跑 AdjustTemplate() 去重排模板,避免残留图形干扰下一次操作。 OnDeinit 里则看卸载原因——reason 不是 REASON_CHARTCHANGE 时才移除模板对象并 delete chart 指针。这意味着切换图表周期不会清掉你的模板,但换品种或关闭指标会触发清理。外汇与贵金属杠杆高,误删可编辑对象可能导致挂单线丢失,建议在 MT5 里断点跟一遍这几行确认行为。
case MSG_SELL_MARKET: ObjectDelete(GetInfoTerminal().ID, m_Info.szObj_Editable); break; case MSG_CLOSE_POSITION: ObjectDelete(GetInfoTerminal().ID, m_Info.szObj_Editable); break; } if (obj != MSG_NULL) AdjustTemplate(); break; } } class=class="str">"cmt">//+------------------------------------------------------------------+ }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#undef macro_NameGlobalVariable class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { if (reason != REASON_CHARTCHANGE) (*chart).RemoveTemplate(); class="kw">delete chart; }
「一点提醒」
这套 Chart Trade 重构后,模块化和可扩展性比旧版高出一截,但指标本身还没接上买入、卖出、平仓按钮,核心架构方向是对的,按钮功能留到后续补。 作者刻意不挂附件,所有代码都直接摊在文章里,意图是逼你进 MetaEditor 自己读、自己改;外汇和贵金属市场高波动、高杠杆,直接抄来跑而不懂逻辑,风险很大。 看懂代码再动手,才是用这套工具的正确姿势,其余的等下一篇续上。