开发回放系统(第 59 部分):新的未来·进阶篇
(2/3)·控制指标在模板加载后被异步移除,REASON_REMOVE 让图表提前关闭,这一隐蔽时序坑多数回放系统都会踩
用结构体把品种属性一次吃进内存
这段声明把终端和品种的静态参数塞进一个 st_Terminal 结构,后面取窗口宽高、报价小数位、每跳点值都直接从成员读,不用每次调 MarketInfo。Width、Height 是图表像素尺寸,nDigits 决定小数精度,SubWin 标记所属子窗口,PointPerTick 与 ValuePerPoint 分别承载最小变动单位和每点金额,VolumeMinimal 是broker允许的最小开仓量,AdjustToTrade 控制是否按交易环境缩放。 私有区又封了一层 mem 结构,存 Show_Descr、Show_Date 两个 long 型开关和 AccountLock 布尔锁,实例 m_Mem 与 m_Infos 随类构造常驻。这样面板重绘时不会反复查账户状态,CPU 占用倾向更平稳。 CurrentSymbol() 负责在初始化时判定当前品种类别。它截取 _Symbol 前三个字符,用枚举 eTypeSymbol 去比对 WIN、IND、WDO、DOL 等前缀,命中后按品种给到期月份代码串 sz1 赋值——比如 DOL/WDO 用 "FGHJKMNQUVXZ",IND/WIN 用 "GJMQVZ",其他直接 return。外汇黄金这类 OTHER 分支不往下走,注意 MT5 做贵金属或外汇时高风险,代码不会自动适配合约月份逻辑。
class="type">int Width, Height, nDigits, SubWin; class="type">class="kw">double PointPerTick, ValuePerPoint, VolumeMinimal, AdjustToTrade; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">private : st_Terminal m_Infos; class="kw">struct mem { class="type">long Show_Descr, Show_Date; class="type">bool AccountLock; }m_Mem; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CurrentSymbol(class="type">void) { class="type">MqlDateTime mdt1; class="type">class="kw">string sz0, sz1; class="type">class="kw">datetime dt = macroGetDate(TimeCurrent(mdt1)); enum eTypeSymbol {WIN, IND, WDO, DOL, OTHER} eTS = OTHER; sz0 = StringSubstr(m_Infos.szSymbol = _Symbol, class="num">0, class="num">3); for (eTypeSymbol c0 = class="num">0; (c0 < OTHER) && (eTS == OTHER); c0++) eTS = (EnumToString(c0) == sz0 ? c0 : eTS); class="kw">switch (eTS) { case DOL : case WDO : sz1 = "FGHJKMNQUVXZ"; class="kw">break; case IND : case WIN : sz1 = "GJMQVZ"; class="kw">break; class="kw">default : class="kw">return; }
◍ 合约到期循环与图表初始化的硬骨头
这段逻辑里最绕的是 055–057 行的 for 循环:它用 i0 在字符串 sz1 上轮询月份字母,i1 从「当前年份减 2000」起步,一旦 i0 回到 0 就给 i1 加 1,相当于逐年向后拼出合约品种名(如 XAUUSDZ25 这种后缀格式),直到 macroGetDate 拿到的到期时间大于 dt 才 break。 061–079 行是 C_Terminal 构造函数,一上来先取 ChartID,再把对象描述、日期轴显隐状态读进 m_Mem 备份,随后强制关掉 CHART_SHOW_OBJECT_DESCR、打开对象增删事件、藏起日期轴——做可视化的面板时这套动作能避免图形对象干扰鼠标交互。 073–079 行把 decimals、像素宽高、tick 尺寸与价值一次性塞进 m_Infos:PointPerTick 与 ValuePerPoint 的比值赋给 AdjustToTrade,外汇和贵金属点值随杠杆与合约规格浮动,实盘前应在 MT5 用 Print() 把这几个字段打出来核对,杠杆突变时可能偏差明显,贵金属尤其属于高风险品种。
for (class="type">int i0 = class="num">0, i1 = mdt1.year - class="num">2000, imax = StringLen(sz1);; i0 = ((++i0) < imax ? i0 : class="num">0), i1 += (i0 == class="num">0 ? class="num">1 : class="num">0)) if (dt < macroGetDate(SymbolInfoInteger(m_Infos.szSymbol = StringFormat("%s%s%d", sz0, StringSubstr(sz1, i0, class="num">1), i1), SYMBOL_EXPIRATION_TIME))) class="kw">break; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">public : class=class="str">"cmt">//+------------------------------------------------------------------+ C_Terminal(const class="type">long id = class="num">0, const class="type">uchar sub = class="num">0) { m_Infos.ID = (id == class="num">0 ? ChartID() : id); m_Mem.AccountLock = class="kw">false; m_Infos.SubWin = (class="type">int) sub; CurrentSymbol(); m_Mem.Show_Descr = ChartGetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR); m_Mem.Show_Date = ChartGetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE); ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, class="kw">false); ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, true); ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_CREATE, true); ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, class="kw">false); m_Infos.nDigits = (class="type">int) SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_DIGITS); m_Infos.Width = (class="type">int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS); m_Infos.Height = (class="type">int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS); m_Infos.PointPerTick = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_SIZE); m_Infos.ValuePerPoint = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_VALUE); m_Infos.VolumeMinimal = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_VOLUME_STEP); m_Infos.AdjustToTrade = m_Infos.ValuePerPoint / m_Infos.PointPerTick;
「终端对象的构造析构与价格时间对齐」
这段 C_Terminal 类的代码片段展示了 MT5 自定义终端封装里几个容易被忽略的细节。构造函数里第 80 行读取了当前品种的图表模式(SYMBOL_CHART_MODE),若当前品种不是回放专用品种 def_SymbolReplay,就用 AccountInfoInteger 拿账户保证金模式并调用 SetTypeAccount 写入内存结构,第 82 行 ResetLastError 清掉前面可能残留的错误码。 析构函数 ~C_Terminal 负责把图表状态还原:第 87–88 行恢复日期刻度和对象描述显示,第 89–90 行关闭图表对象的创建与删除事件监听。如果你在自己的 EA 里挂了对象交互逻辑,忘记在析构时关掉这两个事件开关,可能在手动删线时触发意外回调。 SetTypeAccount 用了一个 AccountLock 标志做一次性写入保护(第 95 行),并且把非对冲账户统一归为 RETAIL_NETTING(第 96 行)——这意味着做 hedge 检测的脚本若跑在净仓账户上会被强制按平仓模式处理。 AdjustPrice 第 106 行用 round(arg / PointPerTick) * PointPerTick 再做 NormalizeDouble,等价于把任意报价吸附到当前品种最小跳动的整数倍;AdjustTime 则通过 PeriodSeconds 和 iTime(PERIOD_CURRENT,0) 把传入时间对齐到当前周期边界。外汇与贵金属杠杆品种波动剧烈,这类对齐函数只解决精度与周期错位,不预示任何方向。
class="num">080. m_Infos.ChartMode = (ENUM_SYMBOL_CHART_MODE) SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_CHART_MODE); class="num">081. if(m_Infos.szSymbol != def_SymbolReplay) SetTypeAccount((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)); class="num">082. ResetLastError(); class="num">083. } class="num">084. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">085. ~C_Terminal() class="num">086. { class="num">087. ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, m_Mem.Show_Date); class="num">088. ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, m_Mem.Show_Descr); class="num">089. ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, class="kw">false); class="num">090. ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_CREATE, class="kw">false); class="num">091. } class="num">092. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">093. class="kw">inline class="type">void SetTypeAccount(const ENUM_ACCOUNT_MARGIN_MODE arg) class="num">094. { class="num">095. if (m_Mem.AccountLock) class="kw">return; else m_Mem.AccountLock = true; class="num">096. m_Infos.TypeAccount = (arg == ACCOUNT_MARGIN_MODE_RETAIL_HEDGING ? arg : ACCOUNT_MARGIN_MODE_RETAIL_NETTING); class="num">097. } class="num">098. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">099. class="kw">inline const st_Terminal GetInfoTerminal(class="type">void) const class="num">100. { class="num">101. class="kw">return m_Infos; class="num">102. } class="num">103. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">104. const class="type">class="kw">double AdjustPrice(const class="type">class="kw">double arg) const class="num">105. { class="num">106. class="kw">return NormalizeDouble(round(arg / m_Infos.PointPerTick) * m_Infos.PointPerTick, m_Infos.nDigits); class="num">107. } class="num">108. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">109. class="kw">inline class="type">class="kw">datetime AdjustTime(const class="type">class="kw">datetime arg) class="num">110. { class="num">111. class="type">int nSeconds= PeriodSeconds(); class="num">112. class="type">class="kw">datetime dt = iTime(m_Infos.szSymbol, PERIOD_CURRENT, class="num">0); class="num">113. }
图表事件与手数换算的内联实现
这段逻辑承接上一节的面板基础结构,把图表尺寸同步和对象点击选择做进了 DispatchMessage 的静态分发里。CHARTEVENT_CHART_CHANGE 触发时直接读 CHART_WIDTH_IN_PIXELS 和 CHART_HEIGHT_IN_PIXELS,把当前图表像素宽高写回 m_Infos,后续绘制坐标计算就不会偏移。 对象点击处理用了一个 static string st_str 记录上一次选中的名字。新对象被点时,先把旧对象 OBJPROP_SELECTED 置 false,再对可选中对象设 true,保证同屏只有一个控件高亮——手动拖指标时这能避免误改隐藏层。 FinanceToPoints 是把手数资金反推成点数距离的内联函数。volume 以 VolumeMinimal 为基,按 (Leverage-1) 倍最小量叠加;用 Finance 除以 volume 再除 AdjustToTrade 后取绝对值,交 AdjustPrice 对齐报价精度。外汇与贵金属杠杆放大盈亏,实盘验证前先把券商最小交易量填对,否则返回点数可能偏离预期。 下面这段可直接贴进 MT5 类里编译,重点看 131-132 的像素读取和 135-137 的互斥选中:
class="num">114. class="kw">return (dt < arg ? ((class="type">class="kw">datetime)(arg / nSeconds) * nSeconds) : iTime(m_Infos.szSymbol, PERIOD_CURRENT, Bars(m_Infos.szSymbol, PERIOD_CURRENT, arg, dt))); class="num">115. } class="num">116. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">117. class="kw">inline class="type">class="kw">double FinanceToPoints(const class="type">class="kw">double Finance, const class="type">uint Leverage) class="num">118. { class="num">119. class="type">class="kw">double volume = m_Infos.VolumeMinimal + (m_Infos.VolumeMinimal * (Leverage - class="num">1)); class="num">120. class="num">121. class="kw">return AdjustPrice(MathAbs(((Finance / volume) / m_Infos.AdjustToTrade))); class="num">122. }; class="num">123. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">124. class="type">void DispatchMessage(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) class="num">125. { class="num">126. class="kw">static class="type">class="kw">string st_str = ""; class="num">127. class="num">128. class="kw">switch (id) class="num">129. { class="num">130. case CHARTEVENT_CHART_CHANGE: class="num">131. m_Infos.Width = (class="type">int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS); class="num">132. m_Infos.Height = (class="type">int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS); class="num">133. class="kw">break; class="num">134. case CHARTEVENT_OBJECT_CLICK: class="num">135. if (st_str != sparam) ObjectSetInteger(m_Infos.ID, st_str, OBJPROP_SELECTED, class="kw">false); class="num">136. if (ObjectGetInteger(m_Infos.ID, sparam, OBJPROP_SELECTABLE) == true) class="num">137. ObjectSetInteger(m_Infos.ID, st_str = sparam, OBJPROP_SELECTED, true); class="num">138. class="kw">break; class="num">139. case CHARTEVENT_OBJECT_CREATE: class="num">140. if (st_str != sparam) ObjectSetInteger(m_Infos.ID, st_str, OBJPROP_SELECTED, class="kw">false);
class="num">114. class="kw">return (dt < arg ? ((class="type">class="kw">datetime)(arg / nSeconds) * nSeconds) : iTime(m_Infos.szSymbol, PERIOD_CURRENT, Bars(m_Infos.szSymbol, PERIOD_CURRENT, arg, dt))); class="num">115. } class="num">116. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">117. class="kw">inline class="type">class="kw">double FinanceToPoints(const class="type">class="kw">double Finance, const class="type">uint Leverage) class="num">118. { class="num">119. class="type">class="kw">double volume = m_Infos.VolumeMinimal + (m_Infos.VolumeMinimal * (Leverage - class="num">1)); class="num">120. class="num">121. class="kw">return AdjustPrice(MathAbs(((Finance / volume) / m_Infos.AdjustToTrade))); class="num">122. }; class="num">123. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">124. class="type">void DispatchMessage(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) class="num">125. { class="num">126. class="kw">static class="type">class="kw">string st_str = ""; class="num">127. class="num">128. class="kw">switch (id) class="num">129. { class="num">130. case CHARTEVENT_CHART_CHANGE: class="num">131. m_Infos.Width = (class="type">int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS); class="num">132. m_Infos.Height = (class="type">int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS); class="num">133. class="kw">break; class="num">134. case CHARTEVENT_OBJECT_CLICK: class="num">135. if (st_str != sparam) ObjectSetInteger(m_Infos.ID, st_str, OBJPROP_SELECTED, class="kw">false); class="num">136. if (ObjectGetInteger(m_Infos.ID, sparam, OBJPROP_SELECTABLE) == true) class="num">137. ObjectSetInteger(m_Infos.ID, st_str = sparam, OBJPROP_SELECTED, true); class="num">138. class="kw">break; class="num">139. case CHARTEVENT_OBJECT_CREATE: class="num">140. if (st_str != sparam) ObjectSetInteger(m_Infos.ID, st_str, OBJPROP_SELECTED, class="kw">false);
◍ 图形对象创建与指标挂载的底层封装
这段内联函数把 MT5 里画对象的繁琐步骤收拢成一行调用。CreateObjectGraphics 先关掉 CHART_EVENT_OBJECT_CREATE 事件(148 行),防止 ObjectCreate 触发多余回调,建完再开回来(156 行),这个开关顺序在批量画线下很关键。 ObjectCreate 用 m_Infos.SubWin 作为子窗口号,若指标跑在主图则 SubWin 为 0;OBJPROP_TOOLTIP 设成 "\n"(150 行)能屏蔽系统默认悬浮提示,避免遮挡价格行为结构。 IndicatorCheckPass 则用来确认指标是否真的挂上图表:先用 _LastError 拦初始化错误(163 行),再把短名加 _TMP 后缀写回 INDICATOR_SHORTNAME(164 行),用 ChartWindowFind 回查子窗口,找不到就回落到 0(165 行)。 如果 ChartIndicatorGet 能拿到原短名句柄(166 行),说明真身已在,临时 _TMP 副本会被 ChartIndicatorDelete 删掉(168 行)。外汇与贵金属市场波动剧烈、杠杆风险高,这类底层封装仅降低代码耦合,不预示任何方向。
class="num">141. st_str = sparam; class="num">142. class="kw">break; class="num">143. } class="num">144. } class="num">145. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">146. class="kw">inline class="type">void CreateObjectGraphics(const class="type">class="kw">string szName, const ENUM_OBJECT obj, const class="type">color cor = clrNONE, const class="type">int zOrder = -class="num">1) const class="num">147. { class="num">148. ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_CREATE, class="num">0, class="kw">false); class="num">149. ObjectCreate(m_Infos.ID, szName, obj, m_Infos.SubWin, class="num">0, class="num">0); class="num">150. ObjectSetString(m_Infos.ID, szName, OBJPROP_TOOLTIP, "\n"); class="num">151. ObjectSetInteger(m_Infos.ID, szName, OBJPROP_BACK, class="kw">false); class="num">152. ObjectSetInteger(m_Infos.ID, szName, OBJPROP_COLOR, cor); class="num">153. ObjectSetInteger(m_Infos.ID, szName, OBJPROP_SELECTABLE, class="kw">false); class="num">154. ObjectSetInteger(m_Infos.ID, szName, OBJPROP_SELECTED, class="kw">false); class="num">155. ObjectSetInteger(m_Infos.ID, szName, OBJPROP_ZORDER, zOrder); class="num">156. ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_CREATE, class="num">0, true); class="num">157. } class="num">158. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">159. class="type">bool IndicatorCheckPass(const class="type">class="kw">string szShortName) class="num">160. { class="num">161. class="type">class="kw">string szTmp = szShortName + "_TMP"; class="num">162. class="num">163. if (_LastError != ERR_SUCCESS) class="kw">return class="kw">false; class="num">164. IndicatorSetString(INDICATOR_SHORTNAME, szTmp); class="num">165. m_Infos.SubWin = ((m_Infos.SubWin = ChartWindowFind(m_Infos.ID, szTmp)) < class="num">0 ? class="num">0 : m_Infos.SubWin); class="num">166. if (ChartIndicatorGet(m_Infos.ID, m_Infos.SubWin, szShortName) != INVALID_HANDLE) class="num">167. { class="num">168. ChartIndicatorDelete(m_Infos.ID, class="num">0, szTmp);
「把鼠标交互封进类里」
这段截取自一个指标/EA 的初始化收尾与鼠标管理类定义,能直接看到单实例限制是怎么落地的。当系统检测到已有同类型实例在跑,会打印「Only one instance is allowed...」并通过 SetUserError 抛出自定的 C_Terminal::ERR_NoMoreInstance 错误码,随后 return false 中止,避免多实例抢图表资源。 初始化通过后,IndicatorSetString(INDICATOR_SHORTNAME, szShortName) 会重写指标短名,ResetLastError() 清掉前面可能的错误尾迹,最后 return true 让 OnInit 走完。 鼠标类 C_Mouse 继承自 C_Terminal,用宏 def_MousePrefixName 拼出带子窗口号的唯一前缀,macro_NameObjectStudy 再叠上 ObjectsTotal(0) 生成不重名对象标识。eBtnMouse 枚举把左键 0x01、右键 0x02、SHIFT 0x04、CTRL 0x08、中键 0x10 做成位掩码,st_Mouse 结构体则同时存了像素坐标(X_Adjusted/Y_Adjusted)、图形坐标、换算价格 Price 和时间 dt,方便后续按按键状态做交互。 开 MT5 把 C_Terminal.mqh 放好,复制下面这段改自原文的片段,你能验证单实例拦截与鼠标坐标结构是否如预期工作;外汇/贵金属图表上跑这类交互工具仍属高风险,参数异常可能误触发对象堆积。
Print("Only one instance is allowed..."); SetUserError(C_Terminal::ERR_NoMoreInstance); class="kw">return class="kw">false; } IndicatorSetString(INDICATOR_SHORTNAME, szShortName); ResetLastError(); class="kw">return true; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "C_Terminal.mqh" class="macro">#define def_MousePrefixName "MouseBase" + (class="type">class="kw">string)GetInfoTerminal().SubWin + "_" class="macro">#define macro_NameObjectStudy(def_MousePrefixName + "T" + (class="type">class="kw">string)ObjectsTotal(class="num">0)) class C_Mouse : class="kw">public C_Terminal { class="kw">public : enum eStatusMarket {eCloseMarket, eAuction, eInTrading, eInReplay}; enum eBtnMouse {eKeyNull = 0x00, eClickLeft = 0x01, eClickRight = 0x02, eSHIFT_Press = 0x04, eCTRL_Press = 0x08, eClickMiddle = 0x10}; class="kw">struct st_Mouse { class="kw">struct st00 { class="type">short X_Adjusted, Y_Adjusted, X_Graphics, Y_Graphics; class="type">class="kw">double Price; class="type">class="kw">datetime dt; }Position; class="type">uchar ButtonStatus; class="type">bool ExecStudy; }; };