开发回放系统(第 43 部分):Chart Trade 项目(II)·进阶篇
(2/3)· 很多人以为指标刷新必须靠对象驱动,其实 MQL5 还有更轻的通道
让浮动面板吃进用户输入值的类内机制
想在 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 那步。
<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>. <span class="keyword">class="kw">private</span> : <span class="number">class="num">010</span>. <span class="keyword">class="kw">struct</span> st00 <span class="number">class="num">011</span>. { <span class="number">class="num">012</span>. <span class="keyword">class="type">int</span> x, y, cx, cy; <span class="number">class="num">013</span>. <span class="keyword">class="type">class="kw">string</span> szObj_Chart, <span class="number">class="num">014</span>. szFileNameTemplate; <span class="number">class="num">015</span>. <span class="keyword">class="type">long</span> WinHandle; <span class="number">class="num">016</span>. <span class="keyword">class="type">class="kw">double</span> FinanceTake, <span class="number">class="num">017</span>. FinanceStop; <span class="number">class="num">018</span>. <span class="keyword">class="type">int</span> Leverage; <span class="number">class="num">019</span>. }m_Info; <span class="number">class="num">020</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="number">class="num">021</span>. 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[] 做查找匹配,具体逻辑留到下一节拆。
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。这意味着函数只改第一个匹配对象里的第一个匹配字段,多次调用才能批量改。
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;
res[1] 和查找对象名比对,命中就给 i0 打标;050–054 行在已锁定对象内比对字段名,命中就写新值并直接返回;056 行对非目标行原样落盘。060 起是包裹函数声明,062 预备两个文件句柄。
开 MT5 把这段塞进 EA 的模板生成模块,传不同的 szFind/szWhat/szValue 就能在运行时改图表对象属性。外汇与贵金属杠杆高,自动化改参若写错字段名可能导致对象显示异常,先在策略测试器跑一遍空单验证。
模板文件的读取与转写逻辑
这段代码片段在做一件事:把一份已有的模板文本文件逐行读出来,顺手在输出文件里加回车换行,并在撞到 <object> 标记时调 Level_1 做深层替换。外汇与贵金属行情数据模板常靠这种文本透传保结构,改写时务必留意文件句柄泄漏。
| 先判 `_LastError`,非 0 就直接 return,避免带着终端错误状态继续跑。随后用 `FileOpen` 以 `FILE_READ | FILE_TXT` 开输入句柄,失败则 `SetUserError(C_Terminal::ERR_FileAcess)` 并退出;输出句柄走 `FILE_WRITE | FILE_TXT`,文件名拼了 `_T` 后缀,若拿不到句柄得先把 fpIn 关掉再报错返回。 |
|---|
循环体靠 FileIsEnding 控边界,FileReadString 读一行、FileWriteString 写一行并补 \r\n。第 081 行是落点:当读到的字符串恰为 <object> 才进 Level_1(fpIn, fpOut, szFind, szWhat, szValue) 做子级处理。你在 MT5 里复刻时,可故意把模板首行写成 <object> 验证分支是否触发。
写完循环只 FileClose(fpIn),输出句柄 fpOut 的关闭被留到了函数更后面,这种拆分在长流程里常见,但调试时容易误以为写文件已结束——实际要等 fpOut 也 Close 落盘才安全。
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 倍,防止后续资金换算出现负值畸变。
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 目录。
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);