开发回放系统(第 44 部分):Chart Trader 项目(三)·进阶篇
「替换表的闭环写出逻辑」
这段片段处在配置文件重写流程的末尾,负责把内存里的查找/替换数组落盘。第82行先判断 m_szReplace[c0] 是否有内容:有就拼成「查找串=替换串」的形式写回,没有则把上一步正则拆出的 res[1] 补进替换数组,避免空值漏写。 第86行用 FileWriteString 把拼好的 sz0 加 \r\n 写入输出文件,这是 Windows 文本换行标准,少写会导致 MT5 读回时行尾粘连。 整段被两个大括号收口(84、85行与87、88行),说明它嵌在更外层循环里,每扫完一个配置项就写出一行。开 MT5 把这段塞进你自己的 CConfig 类,跑一遍含空替换项的 ini,能直接验证第83行兜底是否生效。
class="num">81. { class="num">82. if (StringLen(m_szReplace[c0])) sz0 = m_szFind[c0] + "=" + m_szReplace[c0]; class="num">83. else m_szReplace[c0] = res[class="num">1]; class="num">84. } class="num">85. } class="num">86. FileWriteString(m_FileOut, sz0 + "\r\n"); class="num">87. }; class="num">88. } class="num">89. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">90. }; class="num">91. class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 浮动面板类如何借模板伪装出多个可点击对象
C_ChartFloatingRAD 类当前版本的复杂度明显高于前一篇,但新增逻辑大多下沉到了已讲过的 C_AdjustTemplate 类里。显示的指标主体代码没动,变的是这个类——它让 Chart Trade 指标在 MT5 上多出本来不存在的交互能力。 第 10 行的枚举 eObjectsIDE 给模板里的对象编了号,末尾的 MSG_NULL 不是对象,而是用来标记“点到了空白/无效区”。第 25 行附近的结构体数组元素个数直接引用这个枚举末值,所以数组大小随枚举自动伸缩,不用手填数字。 第 54 行的方法才是核心:它在构造函数(121 行)和消息处理(171 行)两处被调用。前者按我们的意图定制模板,后者复用已定制的模板。56–67 行定义了两个宏,一个把 C_AdjustTemplate 返回的元素加进系统,另一个把返回的坐标转存进前面那个结构体数组——重点是我们不猜对象在哪,而是问模板它们在哪。
- 行判断是否首次调整模板:不是就走 75 行直接套用;是的话在 73–74 行用循环告诉 C_AdjustTemplate 要抓哪些对象属性。76–81 行逐行下发“对象—属性—值”,82 行提交定制,83 行若属首次再回写数组。87 行让 OBJ_CHART 刷新,视频里的“魔术”就来自这一串调用。
- 行起的函数专门做命中检测:97 行循环遍历 OBJ_CHART 内嵌模板的每个对象,99–102 行给每个对象画一个矩形当作点击区,103 行拿传入坐标比对,命中返回索引,否则返回 MSG_NULL。这就是为什么枚举里必须留 MSG_NULL。
事件处理在 132 行。除了 OBJ_CHART,图表上其实没有第二个真实对象,所以 MetaTrader 5 只会报 OBJ_CHART 被点。我们只在 CHARTEVENT_MOUSE_MOVE 且鼠标非研究态时干活(141 行检查)。空闲态下点击由 143 行分析函数定位,返回的对象名写进静态变量;145 行发现标题被点就跑拖动逻辑。
- 行往后“模拟”了系统原生的 CHARTEVENT_OBJECT_CLICK:一旦确认点中模板内某对象,160–169 行处理事件,170 行清引用,171 行刷模板状态。整套机制让指标看起来满屏都是控件,真实对象却只有 OBJ_CHART 一个。外汇与贵金属品种波动剧烈,这类自绘面板若用于实盘,须先在模拟盘验证点击命中率与刷新延迟。
class="num">001. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">002. class="macro">#class="kw">property copyright "Daniel Jose" class="num">003. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">004. class="macro">#include "../Auxiliar/C_Mouse.mqh" class="num">005. class="macro">#include "C_AdjustTemplate.mqh" class="num">006. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">007. class C_ChartFloatingRAD : class="kw">private C_Terminal class="num">008. { class="num">009. class="kw">private : class="num">010. enum eObjectsIDE {MSG_MAX_MIN, MSG_TITLE_IDE, MSG_DAY_TRADE, MSG_LEVERAGE_VALUE, MSG_TAKE_VALUE, MSG_STOP_VALUE, MSG_BUY_MARKET, MSG_SELL_MARKET, MSG_CLOSE_POSITION, MSG_NULL}; class="num">011. class="kw">struct st00 class="num">012. { class="num">013. class="type">int x, y; class="num">014. class="type">class="kw">string szObj_Chart, class="num">015. szFileNameTemplate;
用 OBJ_CHART 在终端里嵌一块独立画布
想在 MT5 主图表之外再开一块可控区域,最干净的做法是动态创建一个 OBJ_CHART 对象。上面的结构体先把窗口句柄、风控金额、杠杆、是否日内交易等字段收进 m_Info,并用 Regions[MSG_NULL] 预留了坐标矩形数组,方便后面做鼠标命中检测。 CreateWindowRAD 接收 x、y、w、h 四个参数,先把当前终端图表里的对象总数转成字符串当作新对象名,避免重名冲突。ObjectCreate 用 OBJ_CHART 类型在子窗口 0 建出画布,随后用 ObjectSetInteger 把左上角偏移和宽高逐个写进去——这里的 m_Info.x = x 是顺手把入参存回结构体,后续拖拽重绘能直接用。 关掉日期轴和价格轴(OBJPROP_DATE_SCALE / OBJPROP_PRICE_SCALE 设 false)后,这块画布就不会跟着主图缩放乱跳,适合放自定义面板。外汇与贵金属杠杆波动大,这类嵌套窗口若绑定仓位计算,务必先在策略测试器里跑一遍边界值,实盘误触可能倾向引发非预期下单。
class="type">long WinHandle; class="type">class="kw">double FinanceTake, FinanceStop; class="type">int Leverage; class="type">bool IsDayTrade, IsMaximized; class="kw">struct st01 { class="type">int x, y, w, h; }Regions[MSG_NULL]; }m_Info; class=class="str">"cmt">//+------------------------------------------------------------------+ C_Mouse *m_Mouse; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CreateWindowRAD(class="type">int x, class="type">int y, class="type">int w, class="type">int h) { m_Info.szObj_Chart = (class="type">class="kw">string)ObjectsTotal(GetInfoTerminal().ID); ObjectCreate(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJ_CHART, class="num">0, class="num">0, class="num">0); ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_XDISTANCE, m_Info.x = x); ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_YDISTANCE, m_Info.y = y); ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_XSIZE, w); ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_YSIZE, h); ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_DATE_SCALE, false); ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_PRICE_SCALE, false);
「把点数换算成账户盈亏的底层算法」
MQL5 里做面板或 HUD 时,常需要把价格移动的「点数」直接折算成这笔波动在你账户上对应的金额。下面这段 inline 函数就是干这个的,核心在杠杆与最小交易量的耦合。 PointsToFinance() 的返回式是:Points 乘上 (VolumeMinimal + VolumeMinimal*(Leverage-1)) 再乘 AdjustToTrade。也就是说,若券商最小手数是 0.01、杠杆 100,那系数实际是 0.01×100 = 1.0,一个点对余额的影响就被放大到接近 1 个计价单位每标准点。 [CODE] inline double PointsToFinance(const double Points) { return Points * (GetInfoTerminal().VolumeMinimal + (GetInfoTerminal().VolumeMinimal * (m_Info.Leverage - 1))) * GetInfoTerminal().AdjustToTrade; }; [/CODE] 逐行看:第 49 行声明以点数作入参的 inline 双精度函数;第 51 行先取终端最小交易量,用「最小量 + 最小量×(杠杆-1)」把杠杆敞口折成等效仓位系数,再乘 AdjustToTrade(合约大小/汇率调整因子)得出金额。 外汇与贵金属属高杠杆品种,这套换算只是账面浮动估计,实盘滑点会让真实盈亏偏离该数值,开 MT5 把 VolumeMinimal 和 Leverage 打印出来跑一遍就知道偏差量级。
class="kw">inline class="type">class="kw">double PointsToFinance(class="kw">const class="type">class="kw">double Points) { class="kw">return Points * (GetInfoTerminal().VolumeMinimal + (GetInfoTerminal().VolumeMinimal * (m_Info.Leverage - class="num">1))) * GetInfoTerminal().AdjustToTrade; };
◍ 模板坐标与面板字段的回填逻辑
这段逻辑干了两件事:先按枚举把面板各区域的屏幕坐标和尺寸从模板读出来,再在运行时把交易相关的动态文本写回模板对象。 064~066 行在循环里把 A 对应的 pos_y、size_x、size_y 三个字符串转成 int 塞进 m_Info.Regions[A],也就是说面板区块的位置和宽高完全由 tpl 文件里的键值决定,改模板不用动代码。 071~075 行区分首次加载和后续加载:bFirst 为真时从 IDE_RAD.tpl 复制出以终端 ID 命名的独立模板,并对 MSG_CLOSE_POSITION 以内的全部 eObjectsIDE 枚举调用 macro_AddAdjust 写入默认坐标;否则直接读已存在的命名模板。 076~081 行把品种名、杠杆、止盈金额、止损金额、是否日内交易、是否最大化这些运行时变量用 Template.Add 写进 descr 或 state 字段。外汇和贵金属杠杆波动大、滑点随机,面板显示的止损止盈金额仅作参考,实际成交价可能偏离。 想验证很简单:在 MT5 里把 Chart Trade/IDE_RAD.tpl 的 size_x 改大 20,重开面板看区块是否变宽,能变就说明坐标确由模板驱动。
m_Info.Regions[A].y = (class="type">int) StringToInteger((*Template).Get(EnumToString(A), "pos_y")); \ m_Info.Regions[A].w = (class="type">int) StringToInteger((*Template).Get(EnumToString(A), "size_x")); \ m_Info.Regions[A].h = (class="type">int) StringToInteger((*Template).Get(EnumToString(A), "size_y")); \ } C_AdjustTemplate *Template; if (bFirst) { Template = new C_AdjustTemplate("Chart Trade/IDE_RAD.tpl", m_Info.szFileNameTemplate = StringFormat("Chart Trade/%u.tpl", GetInfoTerminal().ID)); for (eObjectsIDE c0 = class="num">0; c0 <= MSG_CLOSE_POSITION; c0++) macro_AddAdjust(EnumToString(c0)); }else Template = new C_AdjustTemplate(m_Info.szFileNameTemplate); Template.Add("MSG_NAME_SYMBOL", "descr", GetInfoTerminal().szSymbol); Template.Add("MSG_LEVERAGE_VALUE", "descr", (class="type">class="kw">string)m_Info.Leverage); Template.Add("MSG_TAKE_VALUE", "descr", (class="type">class="kw">string)m_Info.FinanceTake); Template.Add("MSG_STOP_VALUE", "descr", (class="type">class="kw">string)m_Info.FinanceStop); Template.Add("MSG_DAY_TRADE", "state", (m_Info.IsDayTrade ? "class="num">1" : "class="num">0")); Template.Add("MSG_MAX_MIN", "state", (m_Info.IsMaximized ? "class="num">1" : "class="num">0"));
用坐标命中区判定鼠标落在哪块控件
面板渲染收尾后,真正决定鼠标交互的是 CheckMousePosition 这套坐标比对逻辑。它遍历从 0 到 MSG_CLOSE_POSITION 的所有控件枚举,用面板左上角 m_Info.x / m_Info.y 加上每个区域相对偏移 Regions[c0].x/.y 算出屏幕绝对坐标,再叠上宽高得到右下角边界。 命中条件写在第 103 行:x、y 必须严格落在 xi~xf 与 yi~yf 的开区间内;并且仅当控件是 MSG_MAX_MIN、MSG_TITLE_IDE,或者面板处于最大化态 m_Info.IsMaximized 时,才允许返回该枚举值,其余情况即便坐标重叠也忽略。 没命中任何区域就返回 MSG_NULL,上层据此决定不响应或走默认拖拽。你在 MT5 里改 Regions 的 w、h 数值就能直接改变可点击热区大小,外汇与贵金属图表上挂这类面板时需注意:高频误触可能在高波动时段引发错单,属高风险操作。 下面这段是原文核心函数,逐行对应上面说的逻辑: eObjectsIDE CheckMousePosition(const int x, const int y) { int xi, yi, xf, yf; // 声明控件区域四角临时变量 for (eObjectsIDE c0 = 0; c0 <= MSG_CLOSE_POSITION; c0++) // 遍历全部控件枚举 { xi = m_Info.x + m_Info.Regions[c0].x; // 控件左边界=面板x+区域偏移x yi = m_Info.y + m_Info.Regions[c0].y; // 控件上边界=面板y+区域偏移y xf = xi + m_Info.Regions[c0].w; // 右边界=左边界+宽度 yf = yi + m_Info.Regions[c0].h; // 下边界=上边界+高度
| if ((x > xi) && (y > yi) && (x < xf) && (y < yf) && ((c0 == MSG_MAX_MIN) | (c0 == MSG_TITLE_IDE) | (m_Info.IsMaximized))) return c0; // 坐标在内且控件可见态满足则返回枚举 |
|---|
} return MSG_NULL; // 全不命中返回空标识 }
eObjectsIDE CheckMousePosition(class="kw">const class="type">int x, class="kw">const class="type">int y) { class="type">int xi, yi, xf, yf; for (eObjectsIDE c0 = class="num">0; c0 <= MSG_CLOSE_POSITION; c0++) { xi = m_Info.x + m_Info.Regions[c0].x; yi = m_Info.y + m_Info.Regions[c0].y; xf = xi + m_Info.Regions[c0].w; yf = yi + m_Info.Regions[c0].h; if ((x > xi) && (y > yi) && (x < xf) && (y < yf) && ((c0 == MSG_MAX_MIN) || (c0 == MSG_TITLE_IDE) || (m_Info.IsMaximized))) class="kw">return c0; } class="kw">return MSG_NULL; }