开发回放系统(第 46 部分):Chart Trade 项目(五)·进阶篇
📘

开发回放系统(第 46 部分):Chart Trade 项目(五)·进阶篇

第 2/3 篇

面板渲染前的风险与杠杆换算

这段逻辑跑在交易面板首次绘制或参数刷新之前,先把止盈止损的金额按杠杆折回点数再转回账户币种,避免界面显示与实际占用保证金错位。 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。外汇与贵金属杠杆波动大,这类面板尺寸若算错,可能让一键平仓按钮被挤出可视区。

MQL5 / C++
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,这就是验证路径拼法是否正确的硬标准。

MQL5 / C++
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行分别开读、开写,失败即报用户错误。

MQL5 / C++
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,对外汇高频重模板场景够轻。贵金属同样属高杠杆品种,脚本误替换模板可能导致图表参数失真,先在策略测试器里验一遍再上真仓。

MQL5 / C++
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,外汇与贵金属品种回放界面的按钮贴图就指向了本地编译产物,避免文件缺失导致控件空白。

MQL5 / C++
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);

常见问题

在面板初始化阶段读取账户杠杆与合约大小,按当前品种点值公式预算每手浮亏,渲染前把结果写进风险标签,避免开盘后才发现仓位过重。
检查位图是否以资源名编译进项目,并在挂载代码里用同名字符串引用;若路径写错只会留空块,改对资源名即可恢复。
小布可以读取你的面板配置,比对资源名与模板字段,提示缺失的位图或读写权限问题,省去手动逐个翻文件。
通常是没在启动阶段创建或打开专属配置目录与文件句柄,补上初始化路径与权限判断,读写就能跨会话保留。
在按钮资源绑定处把源地址改为本地指标文件夹下的位图名,确认文件已随项目打包,重新编译后按钮即调用本地图源。