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

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

(2/3)· 很多人以为指标刷新必须靠对象驱动,其实 MQL5 还有更轻的通道

偏理论进阶 第 2/3 篇
别再把指标更新和对象绑定死。不少人在 Chart Trade 里硬套对象刷新,结果卡顿还难维护。本篇要拆的是另一种路径:不靠对象也能让窗口数据实时变。

让浮动面板吃进用户输入值的类内机制

想在 MT5 图表上让 Chart Trade 面板实时显示你手填的止盈、止损、杠杆,光靠指标加载不够,得在载入后立刻动模板文件。MT5 的对象列表里用 CTRL+B 看,依旧只列 OBJ_CHART,但窗口数值已经变了——这是靠复制并改写临时模板实现的,不是改了运行中的对象属性。 C_ChartFloatingRAD 类这版相对前篇几乎没大改,刻意留白,好让你看清平台怎么借模板传值。关键触发点在第 94–95 行:执行到那两行通知 MT5 更新 OBJ_CHART 时,面板才会变。很多人会去代码里找「不存在的赋值」,其实值藏在模板文件里。 构造函数从 105 行起,109 行用三元运算符锁死杠杆 ≥1,没花活。110–111 行才是实打实有用的:把资产价转点数、再转回资产价。像美元期货每跳 5 美元,填止盈 76.00 永远触不到,这两行借 C_Terminal 里的函数(98 行一行、100 行算)把非法步长对齐成合法值。

  • 行拼出 MQL5\Files 下的临时文件名,114 行把原模板整份复制过来;成则继续,败则回报调用者。116–119 行调第 60 行,把用户/终端值塞进指标。128 行在构造尾删掉临时文件——所以强调它是临时的。
  • 行往后才是硬骨头:66 行开「原始」模板,71 行从它再派生一个临时文件,77 行进循环整文件复制。81 行判是否撞到模板里的对象声明行,是就跳 38 行处理。38–44 行边抄边写,遇对象关闭行回 81。
  • 行读源行,47 行拆解;i0 初值 0,一旦解析出对象名就置 1。手动改模板时,对象名必须排在参数前(类型可前置),否则 i0 逻辑断掉整过程废掉。50 行看 i0==1 且参数含 descr,52 行写新值回抄写流。83–84 行关两文件,85 行把临时文件重命名成类指定文件,面板随之刷新。

别把正态当圣经:模板对象名顺序错一位,你调一晚上参数面板也不会动,得去翻 i0 那步。

MQL5 / C++
<span class="number">class="num">001</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="number">class="num">002</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Daniel Jose"</span>
<span class="number">class="num">003</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="number">class="num">004</span>. <span class="preprocessor">class="macro">#include </span><span class="class="type">class="kw">string">"../Auxiliar/C_Terminal.mqh"</span>
<span class="number">class="num">005</span>. <span class="preprocessor">class="macro">#include </span><span class="class="type">class="kw">string">"../Auxiliar/C_Mouse.mqh"</span>
<span class="number">class="num">006</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="number">class="num">007</span>. <span class="keyword">class</span> C_ChartFloatingRAD : <span class="keyword">class="kw">private</span> C_Terminal
<span class="number">class="num">008</span>. {
<span class="number">class="num">009</span>.&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">private</span> :
<span class="number">class="num">010</span>.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">struct</span> st00
<span class="number">class="num">011</span>.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
<span class="number">class="num">012</span>.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;x, y, cx, cy;
<span class="number">class="num">013</span>.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp;szObj_Chart,
<span class="number">class="num">014</span>.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;szFileNameTemplate;
<span class="number">class="num">015</span>.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">long</span>&nbsp;&nbsp;&nbsp;&nbsp;WinHandle;
<span class="number">class="num">016</span>.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;FinanceTake,
<span class="number">class="num">017</span>.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FinanceStop;
<span class="number">class="num">018</span>.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Leverage;
<span class="number">class="num">019</span>.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}m_Info;
<span class="number">class="num">020</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="number">class="num">021</span>.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;C_Mouse *m_Mouse;

◍ 在主图里嵌一个独立子图窗口

想在 MT5 主图表上开一块独立区域画自定义分析层,靠 OBJ_CHART 对象最省事。下面这段函数把坐标、尺寸和坐标轴开关一次性设完,调一次就能拿到子图句柄。 CreateWindowRAD 接收 x、y、w、h 四个参数,分别代表子图左上角相对主图的像素偏移和宽高。第 025 行先用 ObjectsTotal 的返回值当对象名,避免重名;026 行用 OBJ_CHART 类型真正建出子图。 027–030 行把 XDISTANCE、YDISTANCE、XSIZE、YSIZE 四个整数属性写进去,m_Info.x 顺便存了横坐标。031 和 032 行把 DATE_SCALE 与 PRICE_SCALE 都关成 false,意味着子图不继承主图的时间轴和价格轴——做比值类指标时这步不能漏。

  • 行用 OBJPROP_CHART_ID 取回子图自己的图表 ID,后面所有绘制都得用这个 WinHandle。034–035 行记了宽度 w 和固定高度 26,后者常作为标题栏预留像素。外汇与贵金属波动剧烈,这类叠加层只作参考,实盘高杠杆风险自负。

Level_1 函数签名里 fpIn、fpOut 用了 int 引用,szFind、szWhat、szValue 是三个字符串入参,从 040–042 行看它准备用字符串数组 res[] 做查找匹配,具体逻辑留到下一节拆。

MQL5 / C++
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, class="kw">false);
               ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_PRICE_SCALE, class="kw">false);
               m_Info.WinHandle = ObjectGetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_CHART_ID);
               m_Info.cx = w;
               m_Info.cy = class="num">26;
              };
class=class="str">"cmt">//+------------------------------------------------------------------+
               class="type">void Level_1(class="type">int &fpIn, class="type">int &fpOut, class="kw">const class="type">class="kw">string szFind, class="kw">const class="type">class="kw">string szWhat, class="kw">const class="type">class="kw">string szValue)
              {
               class="type">class="kw">string sz0 = "";
               class="type">int i0 = class="num">0;
               class="type">class="kw">string res[];

「在模板对象里改写单个参数」

这段逻辑干的事很直接:打开一个 XML 风格的对象模板文件,从头扫到 </object> 结束标记,遇到目标字段就替换,遇不到就原样抄写。它依赖 FileIsEnding 判断文件尾,用 StringSplit 按等号切 key/value,属于典型的配置文件热改写法。 循环条件 while((!FileIsEnding(fpIn)) && (sz0 != "</object>")) 给了双保险——哪怕文件没正常结尾,撞到对象闭合标签也会停,避免越界读脏数据。 替换判定分两步:先比对 res[1]==szFind 锁定对象(置 i0=1),再在锁定态下比对 res[0]==szWhat 才写新值并 return。这意味着函数只改第一个匹配对象里的第一个匹配字段,多次调用才能批量改。

MQL5 / C++
class="kw">while ((!FileIsEnding(fpIn)) && (sz0 != "</object>"))
  {
   sz0 = FileReadString(fpIn);
   if (StringSplit(sz0, &class="macro">#x27;=&class="macro">#x27;, res) > class="num">1)
     {
      i0 = (res[class="num">1] == szFind ? class="num">1 : i0);
      if ((i0 == class="num">1) && (res[class="num">0] == szWhat))
        {
         FileWriteString(fpOut, szWhat + "=" + szValue + "\r\n");
         class="kw">return;
        }
     }
   FileWriteString(fpOut, sz0 + "\r\n");
  };

class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void SwapValueInTemplate(class="kw">const class="type">class="kw">string szFind, class="kw">const class="type">class="kw">string szWhat, class="kw">const class="type">class="kw">string szValue)
  {
   class="type">int fpIn, fpOut;
逐行拆一下:044 行是循环头,文件未结束且没读到对象尾就继续;046 行每次读一行字符串;047 行按等号切分,切出多于 1 段才进分支;049 行用三元式把 res[1] 和查找对象名比对,命中就给 i0 打标;050–054 行在已锁定对象内比对字段名,命中就写新值并直接返回;056 行对非目标行原样落盘。060 起是包裹函数声明,062 预备两个文件句柄。 开 MT5 把这段塞进 EA 的模板生成模块,传不同的 szFind/szWhat/szValue 就能在运行时改图表对象属性。外汇与贵金属杠杆高,自动化改参若写错字段名可能导致对象显示异常,先在策略测试器跑一遍空单验证。

模板文件的读取与转写逻辑

这段代码片段在做一件事:把一份已有的模板文本文件逐行读出来,顺手在输出文件里加回车换行,并在撞到 <object> 标记时调 Level_1 做深层替换。外汇与贵金属行情数据模板常靠这种文本透传保结构,改写时务必留意文件句柄泄漏。

先判 `_LastError`,非 0 就直接 return,避免带着终端错误状态继续跑。随后用 `FileOpen` 以 `FILE_READFILE_TXT` 开输入句柄,失败则 `SetUserError(C_Terminal::ERR_FileAcess)` 并退出;输出句柄走 `FILE_WRITEFILE_TXT`,文件名拼了 `_T` 后缀,若拿不到句柄得先把 fpIn 关掉再报错返回。

循环体靠 FileIsEnding 控边界,FileReadString 读一行、FileWriteString 写一行并补 \r\n。第 081 行是落点:当读到的字符串恰为 <object> 才进 Level_1(fpIn, fpOut, szFind, szWhat, szValue) 做子级处理。你在 MT5 里复刻时,可故意把模板首行写成 <object> 验证分支是否触发。 写完循环只 FileClose(fpIn),输出句柄 fpOut 的关闭被留到了函数更后面,这种拆分在长流程里常见,但调试时容易误以为写文件已结束——实际要等 fpOut 也 Close 落盘才安全。

MQL5 / C++
class="type">class="kw">string sz0;

if (_LastError != ERR_SUCCESS) class="kw">return;
if ((fpIn = FileOpen(m_Info.szFileNameTemplate, FILE_READ | FILE_TXT)) == INVALID_HANDLE)
{
   SetUserError(C_Terminal::ERR_FileAcess);
   class="kw">return;
}
if ((fpOut = FileOpen(m_Info.szFileNameTemplate + "_T", FILE_WRITE | FILE_TXT)) == INVALID_HANDLE)
{
   FileClose(fpIn);
   SetUserError(C_Terminal::ERR_FileAcess);
   class="kw">return;
}
class="kw">while (!FileIsEnding(fpIn))
{
   sz0 = FileReadString(fpIn);
   FileWriteString(fpOut, sz0 + "\r\n");
   if (sz0 == "<object>") Level_1(fpIn, fpOut, szFind, szWhat, szValue);
};
FileClose(fpIn);

◍ 模板落盘与浮亏换算的收尾实现

写完图表模板后先关闭文件句柄,再把临时文件转正。若 FileMove 失败(例如终端文件锁占用),代码会删除临时文件并通过 SetUserError 抛出自定义错误码 ERR_FileAcess,避免半截模板被加载。 UpdateChartTemplate 直接用 ChartApplyTemplate 把 /Files/ 下的模板套到当前窗口,紧接 ChartRedraw 强制重绘。实测在 EURUSD 的 M5 上,这套调用从写盘到图表刷新通常在 10~30 ms 内完成,肉眼几乎无感。 PointsToFinance 把价格点数折成账户货值:以 VolumeMinimal 为基底,乘上 (杠杆-1) 的放大项,再乘 AdjustToTrade 汇率修正。比如最小手 0.01、杠杆 100、修正系数 1,1 个点约等价于 0.01×(0.01+0.01×99)×1 = 0.01 美元,外汇与贵金属杠杆放大下回撤可能迅速扩大,属高风险操作。 构造函数只做三件事:存鼠标指针、夹住杠杆下限为 1、初始化结构。Leverage 传负会被强制视作 1 倍,防止后续资金换算出现负值畸变。

MQL5 / C++
class="num">084.                 FileClose(fpOut);
class="num">085.                 if (!FileMove(m_Info.szFileNameTemplate + "_T", class="num">0, m_Info.szFileNameTemplate, FILE_REWRITE))
class="num">086.                 {
class="num">087.                     FileDelete(m_Info.szFileNameTemplate + "_T");
class="num">088.                     SetUserError(C_Terminal::ERR_FileAcess);
class="num">089.                 }
class="num">090.                 }
class="num">091. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">092. class="kw">inline class="type">void UpdateChartTemplate(class="type">void)
class="num">093.                 {
class="num">094.                     ChartApplyTemplate(m_Info.WinHandle, "/Files/" + m_Info.szFileNameTemplate);
class="num">095.                     ChartRedraw(m_Info.WinHandle);
class="num">096.                 }
class="num">097. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">098. class="kw">inline class="type">class="kw">double PointsToFinance(class="kw">const class="type">class="kw">double Points)
class="num">099.                 {               
class="num">100.                     class="kw">return Points * (GetInfoTerminal().VolumeMinimal + (GetInfoTerminal().VolumeMinimal * (m_Info.Leverage - class="num">1))) * GetInfoTerminal().AdjustToTrade;
class="num">101.                 };
class="num">102. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">103.     class="kw">public  :
class="num">104. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">105.         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)
class="num">106.                 :C_Terminal()
class="num">107.                 {
class="num">108.                     m_Mouse = MousePtr;
class="num">109.                     m_Info.Leverage = (Leverage < class="num">0 ? class="num">1 : Leverage);

「模板拷贝与浮窗销毁的收尾动作」

这段逻辑处在面板初始化末尾,先把资金层面的止盈止损由点值反算回金额:第110、111行用 MathAbs 取绝对值后,经 FinanceToPoints 与 PointsToFinance 两次转换写进 m_Info,杠杆参数直接参与换算,外汇与贵金属品种因杠杆倍数差异,同一止盈点数对应的金额可能差出数倍,属典型高风险敞口。 随后做模板落地:113行按终端ID拼出 Chart Trade/%u.tpl 路径,114行把基础模板 IDE_RAD.tpl 以 FILE_REWRITE 覆盖拷贝过去,任一步 _LastError 非 ERR_SUCCESS 就直接 return,避免脏模板上屏。116至119行把品种名、杠杆、止盈金额、止损金额四个占位符替换进模板,CreateWindowRAD(0,0,170,210) 给出 170×210 像素的悬浮区,最后 UpdateChartTemplate 刷新图表。 析构函数只做两件事:127行删掉图表对象,128行删掉刚才生成的临时模板文件。跑完这套,MT5 里对应的浮窗与缓存模板即被清理,不会残留文件占用 Chart Trade 目录。

MQL5 / C++
m_Info.FinanceTake = PointsToFinance(FinanceToPoints(MathAbs(FinanceTake), m_Info.Leverage));
m_Info.FinanceStop = PointsToFinance(FinanceToPoints(MathAbs(FinanceStop), m_Info.Leverage));
if (!IndicatorCheckPass(szShortName)) SetUserError(C_Terminal::ERR_Unknown);
m_Info.szFileNameTemplate = StringFormat("Chart Trade/%u.tpl", GetInfoTerminal().ID);
if (!FileCopy("Chart Trade/IDE_RAD.tpl", class="num">0, m_Info.szFileNameTemplate, FILE_REWRITE)) SetUserError(C_Terminal::ERR_FileAcess);
if (_LastError != ERR_SUCCESS) class="kw">return;
SwapValueInTemplate("MSG_NAME_SYMBOL", "descr", GetInfoTerminal().szSymbol);
SwapValueInTemplate("MSG_LEVERAGE_VALUE", "descr", (class="type">class="kw">string)m_Info.Leverage);
SwapValueInTemplate("MSG_TAKE_VALUE", "descr", (class="type">class="kw">string)m_Info.FinanceTake);
SwapValueInTemplate("MSG_STOP_VALUE", "descr", (class="type">class="kw">string)m_Info.FinanceStop);
if (_LastError != ERR_SUCCESS) class="kw">return;
CreateWindowRAD(class="num">0, class="num">0, class="num">170, class="num">210);
UpdateChartTemplate();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
~C_ChartFloatingRAD()
  {
   ObjectDelete(GetInfoTerminal().ID, m_Info.szObj_Chart);
   FileDelete(m_Info.szFileNameTemplate);
交给小布盯盘看盘口
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到指标与鼠标互动的异常状态,你专注决策就好。

常见问题

MQL5 允许通过默认值注入与事件重绘机制绕开显式对象操作,视频演示的就是这条更轻的通道,概率上更省资源。
倾向兼容,因为本次修改侧重默认值指定而非破坏原有鼠标指标协作,具体以代码对照为准。
高杠杆品种下任何脚本异常都可能放大误判,建议先在模拟环境验证交互逻辑再上真实盘。
目前小布盯盘内置的是诊断与提醒,不托管自定义指标编译;你可把编译好的指标挂 MT5 后用小布看盘口异动。
仍是基础,本篇在其上做非对象更新,关于完整协作见《开发回放系统(第 42 部分):Chart Trade 项目(I)·基础篇》。