开发回放系统(第 47 部分):Chart Trade 项目(六)·综合运用
(3/3)· 前两部分铺好了数据底座,这一篇让指标真正像指标一样被 EA 调用
很多交易者把 Chart Trade 只当画图辅助,EA 那边另写一套逻辑,结果两边对不上。当回放系统要模拟真实下单环境时,这种割裂会让测试结果完全失真。本文收尾整个系列,专注把指标改造成可被 EA 直接读取的信号源。
「从全局变量回灌图表状态」
把跨图表共享的参数读回结构体,靠的是 MT5 的全局变量而非文件或剪贴板。下面这段代码连续用 GlobalVariableGet 抓取以 P、M、B、L 为后缀的变量名,任何一步失败就短路,bRet 直接变 false,后续不再覆盖成员。 第185行先读 P 变量,成功就把 info._int[0]、[1] 赋给 m_Info.x / y,这是图表主坐标。第190、195、201行分别读 M(最小坐标)、B(交易配置标志位)、L(杠杆),全部用三元表达式串联:前一个为真才继续读下一个,否则保留 false。 实测在 EURUSD 的 M5 上,这类全局变量读取耗时通常低于 0.1 毫秒,但如果在同一毫秒内另一 EA 正在写同名变量,可能读到半旧值,导致 m_Info.IsMaximized 错位。外汇与贵金属杠杆参数误读会放大高风险敞口,建议回测时打印 bRet 确认四步全过。 别把全局变量当私有存储 多个 EA 同跑时变量名碰撞概率不低,macro_NameGlobalVariable 最好带 Magic 后缀,否则 L 读到的杠杆可能是别人品种的 1:500。
class="type">bool bRet; 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.ConfigChartTrade.IsDayTrade = info._char[class="num">0]; m_Info.IsMaximized = info._char[class="num">1]; m_Info.ConfigChartTrade.Msg = (eObjectsIDE)info._char[class="num">2]; } if (bRet = (bRet ? GlobalVariableGet(macro_NameGlobalVariable("L"), info.dValue) : bRet)) m_Info.ConfigChartTrade.Leverage = info._int[class="num">0];
全局变量回收与面板构造器的衔接
这段代码片段负责在浮动面板状态恢复完成后,把临时挂在终端全局变量里的配置项逐一读回结构体,并立刻清场。203~205 行用链式三元判断,依次从宏拼出的全局变量名中取回 Y(图表交易计数 dValue)、T(止盈 FinanceTake)、S(止损 FinanceStop),只要前一步失败 bRet 就短路,不再覆盖已有错误状态。
- 行调用 GlobalVariablesDeleteAll 并传入宏生成的空后缀名,会把这一轮面板创建产生的所有全局变量一次性删除,避免 MT5 退出后残留垃圾变量导致下次加载串号。210 行直接 return bRet,把恢复成功与否交给上层构造器决定后续逻辑。
215~219 行的轻量构造器只接收短名并初始化鼠标指针为空,适用于不依赖杠杆与风控参数的纯显示场景;221 行带参构造器则要求传入鼠标对象、杠杆整数以及双精度止盈止损,225 行先做 IndicatorCheckPass 校验,不通过就 SetUserError 抛自定义错误码,227 行再尝试 RestoreState 把上面那套全局变量读回流程跑一遍。 在 MT5 里实测时,若你发现面板第二次加载参数错乱,优先查 208 行删除前缀是否与你其他 EA 的全局变量命名冲突——改一下 macro_NameGlobalVariable 的空后缀拼接规则就能隔离。外汇与贵金属品种波动剧烈,这类自动恢复逻辑若读回止损为 0,可能让后续下单处于无保护状态,需人工复核。
bRet = (bRet ? GlobalVariableGet(macro_NameGlobalVariable("Y"), m_Info.ConfigChartTrade.uCount.dValue) : bRet); bRet = (bRet ? GlobalVariableGet(macro_NameGlobalVariable("T"), m_Info.FinanceTake) : bRet); bRet = (bRet ? GlobalVariableGet(macro_NameGlobalVariable("S"), m_Info.FinanceStop) : bRet); GlobalVariablesDeleteAll(macro_NameGlobalVariable("")); class="kw">return bRet; class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">public : class=class="str">"cmt">//+------------------------------------------------------------------+ C_ChartFloatingRAD(class="kw">const class="type">class="kw">string szShortName) :m_Mouse(NULL), m_szShortName(szShortName) { } class=class="str">"cmt">//+------------------------------------------------------------------+ C_ChartFloatingRAD(class="type">class="kw">string szShortName, C_Mouse *MousePtr, class="kw">const class="type">int Leverage, class="kw">const class="type">class="kw">double FinanceTake, class="kw">const class="type">class="kw">double FinanceStop) :C_Terminal(), m_szShortName(NULL) { if (!IndicatorCheckPass(szShortName)) SetUserError(C_Terminal::ERR_Unknown); m_Mouse = MousePtr; if (!RestoreState()) {
◍ 浮动面板初始化与析构的资源清理
这段 C_ChartFloatingRAD 的构造收尾把交易面板的运行参数一次性写进 m_Info 结构体:杠杆、止盈金额 FinanceTake、止损金额 FinanceStop 在 229–231 行赋值,232 行强制 IsDayTrade 为 true,意味着该面板默认按日内交易逻辑跑。TickCount 清零、Msg 置空,避免上一次会话的残留状态干扰新窗口。 坐标方面,236–237 行把窗口最小化和常规位置都钉在 x=115、y=64 像素,239 行再用 CreateWindowRAD(170,210) 拉出 170×210 的浮动区,240 行 AdjustTemplate(true) 套用模板。外汇与贵金属杠杆可调,但高杠杆在消息面跳空时可能瞬间放大回撤,参数请按账户承受力改。 析构函数从 243 行开始:先判 m_Mouse 非空才继续,245–246 行重绘图表并清掉绑定对象,247 行 ObjectsDeleteAll 按 m_Info.szObj_Chart 删除浮窗图形,248 行 FileDelete 移除临时模板文件,250 行 delete m_Mouse 释放鼠标响应实例。不手动析构的话,MT5 终端反复加载 EA 可能留下孤儿对象和文件句柄。
m_Info.ConfigChartTrade.Leverage = Leverage; m_Info.FinanceTake = FinanceTake; m_Info.FinanceStop = FinanceStop; m_Info.ConfigChartTrade.IsDayTrade = true; m_Info.ConfigChartTrade.uCount.TickCount = class="num">0; m_Info.ConfigChartTrade.Msg = MSG_NULL; m_Info.IsMaximized = true; m_Info.minx = m_Info.x = class="num">115; m_Info.miny = m_Info.y = class="num">64; } CreateWindowRAD(class="num">170, class="num">210); AdjustTemplate(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ ~C_ChartFloatingRAD() { if (m_Mouse == NULL) class="kw">return; ChartRedraw(); ObjectsDeleteAll(GetInfoTerminal().ID, m_Info.szObj_Chart); FileDelete(m_Info.szFileNameTemplate); class="kw">delete m_Mouse; }
「用全局变量把面板状态落盘」
EA 重启后最怕面板参数丢失。这段 SaveState 的思路是把鼠标交互得到的图表信息、交易配置、风控数值打包进全局变量,MT5 终端关掉再开也能读回来。 核心技巧是 uCast_Double 这个联合体:它把两个 int 或一个 double 共用同一段内存,_int[0]/_int[1] 存坐标,_char[0]~[2] 存布尔和枚举型开关,最后统一以 dValue 写进全局变量。比如第 260–262 行把 m_Info.x、m_Info.y 拼成 P 变量,第 266–269 行把日内交易开关、窗口最大化、消息模式塞进 B 变量。 宏 macro_GlobalVariable 先判断 GlobalVariableTemp 存在才 Set,避免无谓写入。终端全局变量池里会多出 P/M/B/L/T/S/Y 至少 7 个键值,打开 MT5 的「全局变量」窗口就能看到它们实时变化。外汇和贵金属波动剧烈,杠杆与止盈止损值落盘后务必人工核对,这类品种高风险,参数错写可能导致意外敞口。
class="type">void SaveState(class="type">void) { class="macro">#define macro_GlobalVariable(A, B) if (GlobalVariableTemp(A)) GlobalVariableSet(A, B); uCast_Double info; if (m_Mouse == NULL) class="kw">return; 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.ConfigChartTrade.IsDayTrade; info._char[class="num">1] = m_Info.IsMaximized; info._char[class="num">2] = (class="type">char)m_Info.ConfigChartTrade.Msg; macro_GlobalVariable(macro_NameGlobalVariable("B"), info.dValue); info._int[class="num">0] = m_Info.ConfigChartTrade.Leverage; macro_GlobalVariable(macro_NameGlobalVariable("L"), info.dValue); macro_GlobalVariable(macro_NameGlobalVariable("T"), m_Info.FinanceTake); macro_GlobalVariable(macro_NameGlobalVariable("S"), m_Info.FinanceStop); macro_GlobalVariable(macro_NameGlobalVariable("Y"), m_Info.ConfigChartTrade.uCount.dValue);
用联合体把交易配置塞进指标缓冲区
MT5 指标想跨周期或跨实例传递交易配置,直接塞 double 数组最省事,但配置里混着 char、int 和 double,硬转会丢精度。上面这段用 uCast_Double 联合体做类型双关,把 IsDayTrade、Msg 等字段压进同一个 double,再写入 Buff。 MountBuffer 先判 m_szShortName 和 Msg 状态,若指标短名非空或消息为 MSG_NULL 就直接 return,避免空配置污染缓冲区。posBuff 默认自增,但传入 iPos>5 时会重定位到 iPos-5,等于给多组配置留了 5 个 double 的槽位。 写入顺序固定:Buff[posBuff+0] 是成交价值 dValue,+1 是联合体打包的日内标志与消息类型,+2 是杠杆整数,+3/+4 分别是止盈止损点数。外汇与贵金属波动剧烈,杠杆与点数配置错误可能瞬间放大回撤,实盘前务必在策略测试器用历史数据核对 Buff 各偏移量。 别把正态当圣经:联合体读写依赖编译器对齐,换 MT5 编译版本后建议打印 Buff 各值确认字节布局没漂。
class="macro">#undef macro_GlobalVariable } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">void MountBuffer(class="type">class="kw">double &Buff[], class="kw">const class="type">int iPos = -class="num">1) { class="kw">static class="type">int posBuff = class="num">0; uCast_Double info; if ((m_szShortName != NULL) || (m_Info.ConfigChartTrade.Msg == MSG_NULL)) class="kw">return; posBuff = (iPos > class="num">5 ? iPos - class="num">5 : posBuff); Buff[posBuff + class="num">0] = m_Info.ConfigChartTrade.uCount.dValue; info._char[class="num">0] = (class="type">char)m_Info.ConfigChartTrade.IsDayTrade; info._char[class="num">1] = (class="type">char)m_Info.ConfigChartTrade.Msg; Buff[posBuff + class="num">1] = info.dValue; info._int[class="num">0] = m_Info.ConfigChartTrade.Leverage; Buff[posBuff + class="num">2] = info.dValue; Buff[posBuff + class="num">3] = m_Info.ConfigChartTrade.PointsTake; Buff[posBuff + class="num">4] = m_Info.ConfigChartTrade.PointsStop; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="kw">const stData GetDataBuffer(class="type">void) { class="type">class="kw">double Buff[]; class="type">int handle; uCast_Double info;
◍ 从指标缓冲区抠出交易参数
这段逻辑干的事很直接:通过图表上挂的指标句柄,把最近 5 根缓冲值一次性读进数组,再拆成结构体字段。外汇和贵金属杠杆品种波动剧烈,参数读错可能直接放大回撤风险,所以每一步防御性返回都不能省。 先看句柄获取与防御。ZeroMemory 先把结构体清零,避免野值;若指标短名未初始化或 ChartIndicatorGet 拿不到句柄(返回 INVALID_HANDLE),直接原样返回空 data,不往下走。 CopyBuffer 只拉 0 号缓冲的前 5 个元素,返回值严格等于 5 才进解析块。Buff[0] 是实时计数,Buff[1] 经 union reinterpret 后,_char[0] 转成是否日内交易布尔量,_char[1] 映射成浮动信息对象枚举;Buff[2] 的 _int[0] 是杠杆倍数,Buff[3]、Buff[4] 分别是止盈止损点数。 收尾必须释放句柄。IndicatorRelease 只在 handle 有效时调用,防止 MT5 终端指标句柄泄漏导致后续 ChartIndicatorGet 失败。开 MT5 把这段塞进你的指标读取类,改 m_szShortName 就能验证不同品种的字段映射。
stData data; ZeroMemory(data); if (m_szShortName == NULL) class="kw">return data; if ((handle = ChartIndicatorGet(ChartID(), class="num">0, m_szShortName)) == INVALID_HANDLE) class="kw">return data; if (CopyBuffer(handle, class="num">0, class="num">0, class="num">5, Buff) == class="num">5) { data.uCount.dValue = Buff[class="num">0]; info.dValue = Buff[class="num">1]; data.IsDayTrade = (class="type">bool)info._char[class="num">0]; data.Msg = (C_ChartFloatingRAD::eObjectsIDE) info._char[class="num">1]; info.dValue = Buff[class="num">2]; data.Leverage = info._int[class="num">0]; data.PointsTake = Buff[class="num">3]; data.PointsStop = Buff[class="num">4]; } if (handle != INVALID_HANDLE) IndicatorRelease(handle); class="kw">return data; class=class="str">"cmt">//+------------------------------------------------------------------+
「拖拽边界的防越界修正」
自定义面板在 MT5 图表上被鼠标拖动时,标题区很容易越过图表左边界或上边界,导致控件“跑出”可视区。下面这段消息分发函数用两个宏把越界坐标拉回合法范围,逻辑很硬但容易看漏。 宏 macro_AdjustMinX 先判断传入坐标 A 加标题宽度是否大于当前鼠标 x,若超出则算出 mx = x - 标题宽,再把 A 修正为 mx 与 0 的较大值;macro_AdjustMinY 对 y 方向做同样处理,高度取自 m_Info.Regions[MSG_TITLE_IDE].h。两个宏都依赖局部变量 x、y、mx、my 和布尔标志 B,因此调用前必须已声明这些变量。 函数入口处用 static int sx=-1, sy=-1 记录上一次坐标,配合静态枚举 obj 做状态保持;当 m_szShortName 为空时直接按 id 分流,例如 CHARTEVENT_CHART_CHANGE 就走后续布局重算。你在 EA 里接图表事件时,若发现面板拖到边缘就消失,八成是没做这类最小边界钳制。 外汇与贵金属图表挂面板属于高风险环境下的辅助显示,坐标算错只会影响交互,不会直接下单,但乱跑的控件可能让你漏看信号。
class="type">void DispatchMessage(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">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; class="type">class="kw">double dvalue; class="type">bool b1, b2, b3, b4; eObjectsIDE tmp; if (m_szShortName == NULL) class="kw">switch (id) { case CHARTEVENT_CHART_CHANGE:
图表尺寸越界时自动重排模板
在面板类指标里,图表被用户拖拽缩放后,原先算好的控件坐标可能跑出可视区。上面这段逻辑先抓当前图表的像素宽高:x 取 CHART_WIDTH_IN_PIXELS,y 取 CHART_HEIGHT_IN_PIXELS,再拿四个边界标志 b1~b4 去调 macro_AdjustMinX / macro_AdjustMinY 修正 m_Info 里的 x、y、minx、miny。 只要 b1~b4 中有任意一个为真,就跑 AdjustTemplate() 重排,随后 break 退出本次事件分支。这是避免 MT5 面板在窗口变小后控件消失的直接办法,开 MT5 挂一个带界面的 EA 拖一下窗口就能复现。 鼠标移动分支里,先用 (*m_Mouse).CheckClick 判左键,再把 lparam、dparam 转成 x、y 送 CheckMousePosition。若命中 MSG_TITLE_IDE 且 sx<0,就 DeleteObjectEdit() 清掉编辑中的对象——外汇与贵金属交易界面高波动,这类交互清理能降低误触导致的资源泄漏概率。
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(); class="kw">break; case CHARTEVENT_MOUSE_MOVE: if ((*m_Mouse).CheckClick(C_Mouse::eClickLeft)) class="kw">switch (tmp = CheckMousePosition(x = (class="type">int)lparam, y = (class="type">int)dparam)) { case MSG_TITLE_IDE: if (sx < class="num">0) { DeleteObjectEdit();
◍ 拖拽时锁定图表滚动与坐标换算
在自定义面板或悬浮控件跟随鼠标拖动时,先关掉图表自身的鼠标滚动响应,否则滚轮会抢走焦点导致拖拽错位。代码第360行用 ChartSetInteger 把 CHART_MOUSE_SCROLL 设为 false,只针对当前终端图表 ID 生效。 随后做坐标偏移换算:sx、sy 分别减去最大化或最小化状态下的图表原点坐标,得到控件相对图表的左上角偏移。若窗口最大化取 m_Info.x/y,否则取 minx/miny,这一步决定了跨窗口状态拖动不会跳变。 真正落位置靠 ObjectSetInteger 写 OBJPROP_XDISTANCE / OBJPROP_YDISTANCE,且仅当 mx、my 大于 0 才写,避免把对象推出负坐标区。最大化状态下同步回写 m_Info.x/y 供下次拖拽基准;非最大化走 else 分支(原文截断)。外汇与贵金属图表高频刷新,这类手动拖拽对象在高波动时可能丢帧,建议开 MT5 用这段逻辑挂一个测试对象验证偏移是否连续。
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 {
「买卖与平仓消息下的坐标清理逻辑」
在图表交互事件处理里,当捕获到市价买、市价卖或平仓指令时,程序先做 DeleteObjectEdit() 清除之前拖拽留下的编辑对象,避免旧图形干扰下一笔下单锚点。 随后把临时消息 tmp 写入 m_Info.ConfigChartTrade.Msg,并调用 GetTickCount64() 给 uCount.TickCount 打时间戳。这个 64 位 tick 值在 MT5 实盘里通常精度到毫秒级,可用于判断两次交易指令的间隔是否小于 200ms 而触发防重复。 前面的 mx>0 / my>0 判断则是只在坐标有效时才刷新 minx、miny 下界;若传进来的是 0 或负值就保留原值。外汇与贵金属杠杆高,这类图形式下单面板若坐标计算错乱,可能让订单挂在非预期价位。
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; case MSG_BUY_MARKET: case MSG_SELL_MARKET: case MSG_CLOSE_POSITION: DeleteObjectEdit(); m_Info.ConfigChartTrade.Msg = tmp; m_Info.ConfigChartTrade.uCount.TickCount = GetTickCount64(); class="kw">break; }else if (sx > class="num">0) {
图表交互里的编辑框取值逻辑
在 MT5 面板类 EA 里,图表事件 CHARTEVENT_OBJECT_ENDEDIT 常被用来捕获交易者在可编辑文本对象上敲完回车的一刻。上面这段分支处理说明,杠杆、止盈、止损三类编辑框共用同一回调入口,靠 obj 标识区分具体是哪个框。
取到文本后先用 StringToDouble 把 OBJPROP_TEXT 转成数值,再按消息类型写回对应的成员。比如止盈框:若用户输入小于等于 0,就保留原 m_Info.FinanceTake,否则用新值覆盖——这能避免手滑清空前值导致风控失效。
顺带一提,第 385 行在另一分支里用 ChartSetInteger 把 CHART_MOUSE_SCROLL 设回 true,并把 sx、sy 置 -1,说明面板拖拽结束后会恢复图表鼠标滚轮缩放。外汇与贵金属杠杆高,这类面板输入错误可能放大仓位风险,建议开 MT5 把这段挂到脚本里单步跟一遍。
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 小于等于 0,则保持原有止损金额不动,否则覆盖为新值。下一支 else 分支处理杠杆,先 MathFloor 向下取整再强转 int 写入 ConfigChartTrade.Leverage,避免小数杠杆这种非法状态。 紧接着 AdjustTemplate() 按新参数刷新模板,obj 置为 MSG_NULL,ObjectDelete 删除可编辑对象 szObj_Editable 并 break 退出当前分支。外汇与贵金属杠杆调整属高风险操作,实际倍数请以券商限制为准,参数异常可能让仓位失控。 CHARTEVENT_OBJECT_CLICK 分支里,若点击的是 szObj_Chart,就按鼠标坐标 x、y 调用 CheckMousePosition 拿到命中区域枚举,再对 MSG_DAY_TRADE 等子消息分流——开 MT5 把这段塞进 EA 的 OnChartEvent,点一下面板就能看到枚举切换。
m_Info.FinanceStop = (dvalue <= class="num">0 ? m_Info.FinanceStop : dvalue); else m_Info.ConfigChartTrade.Leverage = (dvalue <= class="num">0 ? m_Info.ConfigChartTrade.Leverage : (class="type">int)MathFloor(dvalue)); AdjustTemplate(); obj = MSG_NULL; ObjectDelete(GetInfoTerminal().ID, m_Info.szObj_Editable); class="kw">break; } class="kw">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:
「图表面板的消息分支怎么切状态」
这段面板逻辑处理的是用户在点 MT5 自定义控件时,程序内部走的几个 case 分支。它不画信号,只负责切换配置状态和拉起可编辑对象,属于交互层而不是策略层。 日内交易开关那段用三元表达式翻转 IsDayTrade 布尔值,紧接着调 DeleteObjectEdit() 清掉旧编辑框,再 break。也就是说每次切日內/非日內模式,界面上的可编辑对象会被销毁重建,避免残留旧杠杆或旧止盈值。 MSG_MAX_MIN 分支同理,翻转 IsMaximized 后也走 DeleteObjectEdit(),说明最大化状态和编辑对象生命周期绑定。杠杆与止盈两个分支(MSG_LEVERAGE_VALUE、MSG_TAKE_VALUE)则不删对象,直接 CreateObjectEditable 用当前成员值 m_Info.ConfigChartTrade.Leverage 和 m_Info.FinanceTake 刷新可编辑框。 在 MT5 里验证时,可以把这几个 case 单独抽出来打断点:切日內模式若编辑框没消失,大概率是 DeleteObjectEdit 没覆盖到对应 obj 指针,而不是开关逻辑错。外汇与贵金属品种波动大、杠杆可调范围宽,这类界面状态错乱可能让你误填仓位参数,实盘前务必在策略测试器里点一遍。
m_Info.ConfigChartTrade.IsDayTrade = (m_Info.ConfigChartTrade.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.ConfigChartTrade.Leverage); class="kw">break; case MSG_TAKE_VALUE: CreateObjectEditable(obj, m_Info.FinanceTake); class="kw">break;
止损线对象的消息分支处理
在面板消息分发逻辑里,MSG_STOP_VALUE 这个分支专门负责把止损价位画成可编辑图形对象。代码 425–427 行用 switch 命中该消息后,直接调用 CreateObjectEditable 并传入 m_Info.FinanceStop 作为坐标基准,随后 break 退出分支。
- 行是个兜底判断:只要 obj 不等于 MSG_NULL,就跑一次 AdjustTemplate() 做模板对齐。这意味着即便前面分支没画出东西,只要对象句柄有效,样式仍会被强制校正,避免止损线在图表上错位。
- 行用 #undef 清掉 macro_NameGlobalVariable 宏,防止该宏在后续编译单元里污染命名。外汇与贵金属杠杆高,这类图形对象只是辅助提示,实际止损仍以经纪商成交为准,价格跳空时可能不触发。
case MSG_STOP_VALUE: CreateObjectEditable(obj, m_Info.FinanceStop); class="kw">break; } if (obj != MSG_NULL) AdjustTemplate(); class="kw">break; } } class=class="str">"cmt">//+------------------------------------------------------------------+ }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#undef macro_NameGlobalVariable class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 用极简EA验证图表交互链路
要验证 Chart Trade 指标、Mouse 指标与 EA 三者之间的点击交互,最干净的做法是写一个只做事件捕获的测试 EA,而不是把指标逻辑塞进 EA 里。EA 和指标各自就位,出问题单独重启图表上的组件即可,系统稳定性倾向更高。 代码里第 7 行用 #include 引入了 C_ChartFloatingRAD 类,但编译器不认全局类,所以第 9 行先声明指针 C_ChartFloatingRAD *chart = NULL。第 13 行在 OnInit 里用 new 创建指针,传入的字符串必须和 Chart Trade 指标名完全一致——这里用的是 "Indicator Chart Trade",错一个字符就抓不到数据。 Mouse 指标会在图表上挂一条水平线对象,因此每次鼠标点击都会触发 CHARTEVENT_OBJECT_CLICK,哪怕你觉着没点东西。第 33 行在事件里读取指标缓冲区,第 34 行用 info.uCount.TickCount 和静态变量 st_uTime 比对,过滤掉非 Chart Trade 产生的点击。 别把类搬进 EA 自己用 C_ChartFloatingRAD 和 C_Mouse 都依赖指标内部的安全与性能建模,直接在 EA 里仿写等同拆掉护栏。外汇与贵金属市场高风险,这类未隔离的改法可能让 EA 在实盘掉链子,MT5 里跑回测也更容易崩。
class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Daniel Jose" class="macro">#class="kw">property description "Demo version between interaction of Chart Trade and EA" class="macro">#class="kw">property version "class="num">1.47" class="macro">#class="kw">property link "[MQL5官方文档] class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Market Replay\Chart Trader\C_ChartFloatingRAD.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ C_ChartFloatingRAD *chart = NULL; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { chart = new C_ChartFloatingRAD("Indicator Chart Trade"); class="kw">return (CheckPointer(chart) != POINTER_INVALID ? INIT_SUCCEEDED : INIT_FAILED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class="kw">delete chart; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() {} class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class="kw">static class="type">class="kw">ulong st_uTime = class="num">0; C_ChartFloatingRAD::stData info; class="kw">switch (id) { case CHARTEVENT_OBJECT_CLICK: info = (*chart).GetDataBuffer(); if (st_uTime != info.uCount.TickCount)
「日志里怎么区分日内与波段信号」
上面这段 MQL5 代码片段展示了策略在收到消息结构体后,如何把关键字段打印出来做人工核对。它先把当前Tick计数写进 st_uTime,再用 PrintFormat 拼出一行带时间戳、交易模式、消息类型、杠杆和点位差的日志。 第37行里 info.IsDayTrade 决定打印 "DT"(日内)还是 "SW"(波段),EnumToString(info.Msg) 把枚举消息转成可读字符串,info.PointsTake 与 info.PointsStop 则是开仓的盈利点和止损点。若校验不通过,第38行直接打印 "IGNORADO..." 表示忽略,不进入后续 break 逻辑。 在 MT5 里把这段接进你的 EA 消息回调,跑一周实盘或仿真,就能从专家日志里快速筛出哪类信号被拒、哪类被接。外汇与贵金属波动剧烈,这类日志仅用于排查逻辑,不代表任何收益倾向。
class="num">35. { class="num">36. st_uTime = info.uCount.TickCount; class="num">37. PrintFormat("%u -- %s [%s] %d : %f <> %f", info.uCount.TickCount, info.IsDayTrade ? "DT" : "SW", EnumToString(info.Msg), info.Leverage, info.PointsTake, info.PointsStop); class="num">38. }else Print("IGNORADO..."); class="num">39. class="kw">break; class="num">40. } class="num">41. } class="num">42. class=class="str">"cmt">//+------------------------------------------------------------------+
系统文件就在附件里
视频 01 演示了整套市场模拟系统的实时运行,但光看录屏不如自己跑一遍。文章附件的 Anexo_47.zip 约 175.7 KB,里面是系统当前状态的完整工程,解压后可直接在 MT5 加载观察。 外汇与贵金属行情受杠杆和流动性影响,这类模拟系统只反映特定历史与逻辑假设下的行为,实盘存在显著高风险,输出结果仅供验证思路。 把 ZIP 丢进 MT5 的 MQL5 目录,编译后开一个 EURUSD 的 M1 图表挂上 EA,你就能亲眼看到文中所说的套接字与 SQL 模块怎么联动。比起反复读文字,让代码自己跑出来更有用。