开发回放系统(第 59 部分):新的未来·进阶篇
⚙️

开发回放系统(第 59 部分):新的未来·进阶篇

(2/3)·控制指标在模板加载后被异步移除,REASON_REMOVE 让图表提前关闭,这一隐蔽时序坑多数回放系统都会踩

案例拆解 第 2/3 篇
很多人以为模板一挂上图表,控制指标就稳稳在位,其实 MetaTrader 5 是异步干掉旧指标的。你看到图表莫名关闭,往往不是模板变了,而是 Deinit 拿到了 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 做贵金属或外汇时高风险,代码不会自动适配合约月份逻辑。

MQL5 / C++
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() 把这几个字段打出来核对,杠杆突变时可能偏差明显,贵金属尤其属于高风险品种。

MQL5 / C++
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) 把传入时间对齐到当前周期边界。外汇与贵金属杠杆品种波动剧烈,这类对齐函数只解决精度与周期错位,不预示任何方向。

MQL5 / C++
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 的互斥选中:

MQL5 / C++
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);
逐行拆一下:114 行按时间阈值返回对齐后的 K 线时间或指定偏移的 K 线时间;119 行用最小交易量乘杠杆差得出等效成交量;121 行把资金额换算成合规报价点数;131-132 行把图表像素尺寸缓存进结构体;135-137 行用静态变量实现单选互斥;140 行在新对象创建时清掉旧选中态。

MQL5 / C++
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 行)。外汇与贵金属市场波动剧烈、杠杆风险高,这类底层封装仅降低代码耦合,不预示任何方向。

MQL5 / C++
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 放好,复制下面这段改自原文的片段,你能验证单实例拦截与鼠标坐标结构是否如预期工作;外汇/贵金属图表上跑这类交互工具仍属高风险,参数异常可能误触发对象堆积。

MQL5 / C++
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;
 };
};
把异步移除日志交给小布
小布盯盘已内置指标 Deinit 原因捕获,打开对应品种页即可看到 REASON_REMOVE 的触发时点,省去你手动加 Print 的功夫。把重复劳动交给小布,你专注决策。

常见问题

因为模板应用是异步的,MetaTrader 5 在过程中主动移除原图表上的控制指标,OnDeinit 拿到的是移除原因而非模板原因,第 58 行消息因此不会打印。
倾向是间接因果:控制指标被异步移除导致图表关闭,若没加延迟模块会强制服务跟着提前终止,插入小延迟可缓解。
可以,小布盯盘的品种页能直接呈现指标 Deinit 事件与原因码,你不用自己埋日志也能复盘图表为何被关。
主要出于性能考量,模板可能带多个指标甚至 EA,同步等待会冻结甚至崩溃平台,所以选择异步处理。
上篇提到模板应用与服务更新间要加延迟,本篇解释了不加延迟时控制指标被异步移除致图表关,从而迫使服务早退的具体机制。