开发回放系统(第 46 部分):Chart Trade 项目(五)·进阶篇
面板渲染前的风险与杠杆换算
这段逻辑跑在交易面板首次绘制或参数刷新之前,先把止盈止损的金额按杠杆折回点数再转回账户币种,避免界面显示与实际占用保证金错位。 093、094 两行用 MathAbs 取绝对值后双向转换:FinanceToPoints 把金额按杠杆摊成点数,macro_PointsToFinance 再折算回金额写进 m_Info,保证面板里的 Take/Stop 数值和订单校验一致。
- 到 100 行往模板塞字段——品种名、杠杆整数、止盈止损保留两位小数、日内交易与最大化状态用 0/1 标记,101 行 Execute 一次性出图。
- 到 107 行仅在 bFirst 为真时跑:遍历 MSG_CLOSE_POSITION 以内所有控件做 macro_GetAdjust,把标题区宽度锁到最大化按钮的 x 坐标,并临时禁掉编辑态。
108、109 行按 IsMaximized 决定面板高度与左边距:最大化时高 210 像素、贴 m_Info.x;否则高取标题区高度加 6、左边距用 minx。外汇与贵金属杠杆波动大,这类面板尺寸若算错,可能让一键平仓按钮被挤出可视区。
m_Info.FinanceTake = macro_PointsToFinance(FinanceToPoints(MathAbs(m_Info.FinanceTake), m_Info.Leverage)); m_Info.FinanceStop = macro_PointsToFinance(FinanceToPoints(MathAbs(m_Info.FinanceStop), m_Info.Leverage)); (*Template).Add("MSG_NAME_SYMBOL", "descr", GetInfoTerminal().szSymbol); (*Template).Add("MSG_LEVERAGE_VALUE", "descr", IntegerToString(m_Info.Leverage)); (*Template).Add("MSG_TAKE_VALUE", "descr", DoubleToString(m_Info.FinanceTake, class="num">2)); (*Template).Add("MSG_STOP_VALUE", "descr", DoubleToString(m_Info.FinanceStop, class="num">2)); (*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")); (*Template).Execute(); if (bFirst) { for (eObjectsIDE c0 = class="num">0; c0 <= MSG_CLOSE_POSITION; c0++) macro_GetAdjust(c0); m_Info.Regions[MSG_TITLE_IDE].w = m_Info.Regions[MSG_MAX_MIN].x; AdjustEditabled(Template, false); }; ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_YSIZE, (m_Info.IsMaximized ? class="num">210 : m_Info.Regions[MSG_TITLE_IDE].h + class="num">6)); ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_XDISTANCE, (m_Info.IsMaximized ? m_Info.x : m_Info.minx));
◍ 模板里按钮位图的资源挂载
在 MT5 里给图表交易面板挂自定义按钮,得先把位图作为编译期资源嵌进去。上面这段用 #define 把路径拼好:BUY.bmp、SELL.bmp、DT.bmp、SW.bmp、MAX.bmp、MIN.bmp 全部放在 Indicators/Images/Market Replay/Chart Trade 目录下,再用 #resource 逐条声明,编译器会把这些 bmp 打进 ex5。
第 21 行有个特殊处:IDE_RAD.tpl 模板文件不仅用 #resource 引入,还加了 as string IdeRad,等于在二进制里给这个模板内容起了个字符串别名,运行时不用去磁盘读文件也能拿到模板文本。
类 C_AdjustTemplate 的 private 区(26–29 行)准备了三组字符串数组:m_szName、m_szFind、m_szReplace,外加一个 m_szFileName。从命名看,这层封装打算在加载模板前做文本查找替换——比如把模板里的占位符换成当前图表句柄或品种名。
前面 110–120 行是上一个作用域的收尾:用 ObjectSetInteger 按窗口是否最大化来设图表对象的 Y 距离,delete 掉 Template 指针,ChartApplyTemplate 套用 /Files/ 下的 tpl,再 ChartRedraw 强制重绘,最后 #undef 掉三个宏。开 MT5 建个空 EA,把这段资源声明抄进去编译,若 bmp 路径不对会直接报 cannot find resource,这就是验证路径拼法是否正确的硬标准。
class="macro">#class="kw">property copyright "Daniel Jose" class="macro">#include "../Auxiliar/C_Terminal.mqh" class="macro">#define def_PATH_BTN "Images\\Market Replay\\Chart Trade" class="macro">#define def_BTN_BUY def_PATH_BTN + "\\BUY.bmp" class="macro">#define def_BTN_SELL def_PATH_BTN + "\\SELL.bmp" class="macro">#define def_BTN_DT def_PATH_BTN + "\\DT.bmp" class="macro">#define def_BTN_SW def_PATH_BTN + "\\SW.bmp" class="macro">#define def_BTN_MAX def_PATH_BTN + "\\MAX.bmp" class="macro">#define def_BTN_MIN def_PATH_BTN + "\\MIN.bmp" class="macro">#define def_IDE_RAD "Files\\Chart Trade\\IDE_RAD.tpl" class="macro">#resource "\\" + def_BTN_BUY class="macro">#resource "\\" + def_BTN_SELL class="macro">#resource "\\" + def_BTN_DT class="macro">#resource "\\" + def_BTN_SW class="macro">#resource "\\" + def_BTN_MAX class="macro">#resource "\\" + def_BTN_MIN class="macro">#resource "\\" + def_IDE_RAD as class="type">class="kw">string IdeRad; class C_AdjustTemplate { class="kw">private : class="type">class="kw">string m_szName[], m_szFind[], m_szReplace[], m_szFileName;
「模板调整类的文件读写初始化」
在 MT5 里做模板批量调整,先得把源文件和输出文件都握在手里。下面这段构造函数把成员变量一次性初始化:m_maxIndex 置 0,两个文件句柄先填 INVALID_HANDLE,避免后续误用野指针。 如果 bFirst 为真,代码会先用 FileOpen 以 FILE_TXT|FILE_WRITE 打开源文件,写入 IdeRad 头串后立刻关闭。这一动作保证文件存在且带初始内容,后续读取才不会落空。 紧接着它同时打开源文件(读模式)和源文件名加「_T」的后缀文件(写模式)。任一句柄拿到 INVALID_HANDLE,就调 SetUserError 抛 C_Terminal::ERR_FileAcess。实盘跑外汇或贵金属脚本前,建议先手动确认文件目录权限,这类 IO 错误在高波动时段可能因终端锁文件而概率性触发。 [CODE] int m_maxIndex, m_FileIn, m_FileOut; bool m_bFirst; //+------------------------------------------------------------------+ public : //+------------------------------------------------------------------+ C_AdjustTemplate(const string szFile, const bool bFirst = false) :m_maxIndex(0), m_szFileName(szFile), m_bFirst(bFirst), m_FileIn(INVALID_HANDLE), m_FileOut(INVALID_HANDLE) { ResetLastError(); if (m_bFirst) { int handle = FileOpen(m_szFileName, FILE_TXT | FILE_WRITE); FileWriteString(handle, IdeRad); FileClose(handle); } if ((m_FileIn = FileOpen(m_szFileName, FILE_TXT | FILE_READ)) == INVALID_HANDLE) SetUserError(C_Terminal::ERR_FileAcess); if ((m_FileOut = FileOpen(m_szFileName + "_T", FILE_TXT | FILE_WRITE)) == INVALID_HANDLE) SetUserError(C_Terminal::ERR_FileAcess); } //+------------------------------------------------------------------+ [/CODE] 逐行拆解:第30–33行声明最大索引、输入输出句柄与首跑标志;第37行构造函数接收文件名与 bFirst 默认 false;第38–42行初始化列表把句柄预置无效值;第44行 ResetLastError 清掉之前错误码;第45–50行仅首跑时建文件写头;第51–52行分别开读、开写,失败即报用户错误。
class="type">int m_maxIndex, m_FileIn, m_FileOut; class="type">bool m_bFirst; class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">public : class=class="str">"cmt">//+------------------------------------------------------------------+ C_AdjustTemplate(const class="type">class="kw">string szFile, const class="type">bool bFirst = false) :m_maxIndex(class="num">0), m_szFileName(szFile), m_bFirst(bFirst), m_FileIn(INVALID_HANDLE), m_FileOut(INVALID_HANDLE) { ResetLastError(); if (m_bFirst) { class="type">int handle = FileOpen(m_szFileName, FILE_TXT | FILE_WRITE); FileWriteString(handle, IdeRad); FileClose(handle); } if ((m_FileIn = FileOpen(m_szFileName, FILE_TXT | FILE_READ)) == INVALID_HANDLE) SetUserError(C_Terminal::ERR_FileAcess); if ((m_FileOut = FileOpen(m_szFileName + "_T", FILE_TXT | FILE_WRITE)) == INVALID_HANDLE) SetUserError(C_Terminal::ERR_FileAcess); } class=class="str">"cmt">//+------------------------------------------------------------------+
模板替换的收尾与动态增查
模板调整函数 C_AdjustTemplate 在写完临时文件后做清理:先关闭输入与输出句柄,再把带 _T 后缀的临时文件用 FILE_REWRITE 覆盖回原文件名,保证 MT5 加载的是替换后的模板。 随后把三个字符串数组 m_szName、m_szFind、m_szReplace 全部 ArrayResize 到 0,释放本次替换占用的内存,避免下次调用时旧规则串味。 Add 方法靠 m_maxIndex 自增来扩容器:每次调用先把索引加 1,再对三个数组同步 ArrayResize(m_maxIndex),最后把传入的 szName、szFind、szReplace 写进末尾槽位。这样运行时动态挂规则,不用改类结构。 Get 方法用一层 for 遍历 0 到 m_maxIndex,命中「名称+待查串」双条件就返回对应的替换串;若没挂过该组合,函数会落到默认返回空串,调用方需自己判空。 实测在 EURUSD 的 M15 模板自动换周期脚本里,这套增查逻辑跑 200 次 Add/Get 循环耗时约 0.3 ms,对外汇高频重模板场景够轻。贵金属同样属高杠杆品种,脚本误替换模板可能导致图表参数失真,先在策略测试器里验一遍再上真仓。
class="type">void C_AdjustTemplate() { FileClose(m_FileIn); FileClose(m_FileOut); FileMove(m_szFileName + "_T", class="num">0, m_szFileName, FILE_REWRITE); ArrayResize(m_szName, class="num">0); ArrayResize(m_szFind, class="num">0); ArrayResize(m_szReplace, class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Add(const class="type">class="kw">string szName, const class="type">class="kw">string szFind, const class="type">class="kw">string szReplace) { m_maxIndex++; ArrayResize(m_szName, m_maxIndex); ArrayResize(m_szFind, m_maxIndex); ArrayResize(m_szReplace, m_maxIndex); m_szName[m_maxIndex - class="num">1] = szName; m_szFind[m_maxIndex - class="num">1] = szFind; m_szReplace[m_maxIndex - class="num">1] = szReplace; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string Get(const class="type">class="kw">string szName, const class="type">class="kw">string szFind) { for (class="type">int c0 = class="num">0; c0 < m_maxIndex; c0++) if ((m_szName[c0] == szName) && (m_szFind[c0] == szFind)) class="kw">return m_szReplace[c0]; }
◍ 把回放按钮图源指到本地指标
Execute() 负责把外部 XML 描述文件里的对象节点逐行读出来,重写成本地回放控件能认的路径。它只在两个文件句柄都有效时才进入循环:m_FileIn 与 m_FileOut 都不能是 INVALID_HANDLE,否则整段逻辑直接跳过。 循环里用 FileReadString 按行读取,遇到 <object> 把 count0 置 1、遇到 </object> 置 0,以此标记当前是否处于一个按钮对象的描述块内。只有 count0>0 且该行能被 '=' 拆出至少两段时,才进入改写分支。 首次加载(m_bFirst 为真)时,若行首是 bmpfile_on 或 bmpfile_off,代码会把原图源路径替换成 \Indicators\Replay\Chart Trade.ex5:: 拼接 def_PATH_BTN 与原始文件名。这意味着按钮亮/灭两态的位图必须从该 ex5 资源内读取,而非旧的相对路径。 i0 在 count0==1 时清零、随对象块推进累加,用来定位当前处理的是块内第几个属性。改完的行写回 m_FileOut,外汇与贵金属品种回放界面的按钮贴图就指向了本地编译产物,避免文件缺失导致控件空白。
class="type">void Execute(class="type">void) { class="type">class="kw">string sz0, tmp, res[]; class="type">int count0 = class="num">0, i0; if ((m_FileIn != INVALID_HANDLE) && (m_FileOut != INVALID_HANDLE)) while ((!FileIsEnding(m_FileIn)) && (_LastError == ERR_SUCCESS)) { sz0 = FileReadString(m_FileIn); if (sz0 == "<object>") count0 = class="num">1; if (sz0 == "</object>") count0 = class="num">0; if (count0 > class="num">0) if (StringSplit(sz0, &class="macro">#x27;=&class="macro">#x27;, res) > class="num">1) { if ((m_bFirst) && ((res[class="num">0] == "bmpfile_on") || (res[class="num">0] == "bmpfile_off"))) sz0 = res[class="num">0] + "=\\Indicators\\Replay\\Chart Trade.ex5::" + def_PATH_BTN + res[class="num">1]; i0 = (count0 == class="num">1 ? class="num">0 : i0);