开发回放系统(第 36 部分):进行调整(二)·进阶篇
⚙️

开发回放系统(第 36 部分):进行调整(二)·进阶篇

(2/3)·回放系统明明跑在模拟资产上,EA 却误报平台真实账户类型,这个小坑如何填平

偏理论 第 2/3 篇
把回放系统挂在真实账户图表上跑模拟资产,EA 却老老实实报出平台账户类型而不是资产所属类型,订单逻辑直接跑偏。很多人以为加个判断就行,真动手才发现 MetaTrader 5 里拿不到那么直白的信息。这一篇就拆开这个看似简单、落地极难的点。

用联合体在回放服务里塞下状态位

做 MT5 历史回放工具时,最麻烦的是主图 EA 和后台服务之间怎么传少量状态。上面这套宏把品种名拼成全局变量后缀:def_SymbolReplay + "_ID" 存图形 ID,+ "_Time" 存服务器时间,回放滑块最大位置锁死在 400(def_MaxPosSlider)。 核心结构是 u_Interprocess 联合体。第一版用 bool 写 isPlay / isWait / isHedging / isSync 四个状态位,加上 ushort iPosShift(0~400)和 datetime ServerTime,靠联合体共享同一段内存。 但 bool 在 MQL5 里占 1 字节,四个旗标就吃掉 4 字节。第二版改成位域:char isPlay : 1 这种写法把四个状态压进同一个字节的 4 个 bit,iPosShift 仍是 ushort,整体内存 footprint 明显更小。开 MT5 把两段 union 分别编译,用 sizeof() 打印对比就能验证省了多少。 位域版在跨进程读全局变量时仍按字节解释,所以服务端与 EA 端必须统一用同一份结构体定义,否则 iPosShift 的位置会读偏。外汇与贵金属回测涉及高杠杆高风险,回放结论仅代表历史数据可能性,不构成实盘指引。

MQL5 / C++
class="macro">#define def_GlobalVariableIdGraphics    def_SymbolReplay + "_ID"
class="macro">#define def_GlobalVariableServerTime    def_SymbolReplay + "_Time"
class="macro">#define def_MaxPosSlider               class="num">400
class="macro">#define def_ShortName                  "Market_" + def_SymbolReplay
class=class="str">"cmt">//+------------------------------------------------------------------+
union u_Interprocess
{
      union u_0
      {
            class="type">class="kw">double  df_Value;     class=class="str">"cmt">// Value of the terminal global variable...
            class="type">class="kw">ulong   IdGraphic;    class=class="str">"cmt">// Contains the Graph ID of the asset...
      }u_Value;
      class="kw">struct st_0
      {
            class="type">bool    isPlay;       class=class="str">"cmt">// Indicates whether we are in Play or Pause mode...
            class="type">bool    isWait;       class=class="str">"cmt">// Tells the user to wait...
            class="type">bool    isHedging;    class=class="str">"cmt">// If true we are in a Hedging account, if class="kw">false the account is Netting...
            class="type">bool    isSync;       class=class="str">"cmt">// If true indicates that the service is synchronized...
            class="type">class="kw">ushort  iPosShift;    class=class="str">"cmt">// Value between class="num">0 and class="num">400...
      }s_Infos;
      class="type">class="kw">datetime ServerTime;
};
class=class="str">"cmt">//+------------------------------------------------------------------+
union u_Interprocess
{
      union u_0
      {
            class="type">class="kw">double  df_Value;     class=class="str">"cmt">// Value of the terminal global variable...
            class="type">class="kw">ulong   IdGraphic;    class=class="str">"cmt">// Contains the Graph ID of the asset...
      }u_Value;
      class="kw">struct st_0
      {               
class="type">char  isPlay    : class="num">1;  class=class="str">"cmt">// Indicates whether we are in Play or Pause mode...
class="type">char  isWait    : class="num">1;  class=class="str">"cmt">// Tells the user to wait...             
class="type">char  isHedging : class="num">1;  class=class="str">"cmt">// If true we are in a Hedging account, if class="kw">false the account is Netting...             
class="type">char  isSync    : class="num">1;  class=class="str">"cmt">// If true indicates that the service is synchronized...
            class="type">class="kw">ushort  iPosShift;    class=class="str">"cmt">// Value between class="num">0 and class="num">400...
      }s_Infos;
      class="type">class="kw">datetime ServerTime;
};

「回放配置类的结构体与解析入口」

在 MT5 自研 tick 回放框架里,C_ConfigService 负责把外部传入的配置串解析成运行时参数。它内部挂了一个 st001 结构体 m_GlPrivate,里面用 4 个 C_Array 指针分别管 ticks 重放、bars 转 ticks、ticks 转 bars、以及 bars 与前值的映射关系,外加一个 int Line 标记当前行号。 构造函数把 m_szPath 置为 NULL,m_ModelLoading 默认 1,m_AccountHedging 默认 false,这意味着账户类型在初始化时并不锁定,需要后续由配置串显式决定。TypeAccountIsHedging() 仅返回该布尔值,调用方据此判断锁仓账户逻辑是否启用。 Configs() 是核心解析入口:先用 StringSplit 以 '=' 切分,要求恰好 2 段才继续;随后对 szList 里 6 个关键字(PATH、POINTSPERTICK、VALUEPERPOINTS、VOLUMEMINIMAL、LOADMODEL、ACCOUNT)做线性匹配。case 0 把 PATH 写进 m_szPath,case 1 调 CustomSymbolSetDouble 设置 SYMBOL_TRADE_TICK_SIZE——这一步直接决定回放品种的每跳点值,外汇与贵金属报价重建偏差往往就卡在这里。 开 MT5 把这段 struct 和 Configs 贴进 EA 头文件,改 szList 加一个你自己的字段,看 StringSplit 返回非 2 时是否静默丢弃,就能验证配置容错边界。

MQL5 / C++
class="kw">struct st001
{
    C_Array *pTicksToReplay, *pBarsToTicks, *pTicksToBars, *pBarsToPrev;
    class="type">int     Line;
}m_GlPrivate;
class="type">class="kw">string  m_szPath;
class="type">bool    m_AccountHedging;
C_ConfigService()
    :m_szPath(NULL),
    m_ModelLoading(class="num">1),
    m_AccountHedging(class="kw">false)
{
}
class="kw">inline class="kw">const class="type">bool TypeAccountIsHedging(class="type">void) class="kw">const
{
    class="kw">return m_AccountHedging;
}
class="kw">inline class="type">bool Configs(class="kw">const class="type">class="kw">string szInfo)
{
    class="kw">const class="type">class="kw">string szList[] = {
                            "PATH",
                            "POINTSPERTICK",
                            "VALUEPERPOINTS",
                            "VOLUMEMINIMAL",
                            "LOADMODEL",
                            "ACCOUNT"
                            };
    class="type">class="kw">string  szRet[];
    class="type">char    cWho;
    
    if (StringSplit(szInfo, &class="macro">#x27;=&class="macro">#x27;, szRet) == class="num">2)
    {
        StringTrimRight(szRet[class="num">0]);
        StringTrimLeft(szRet[class="num">1]);
        for (cWho = class="num">0; cWho < ArraySize(szList); cWho++) if (szList[cWho] == szRet[class="num">0]) class="kw">break;
        class="kw">switch (cWho)
        {
            case class="num">0:
                m_szPath = szRet[class="num">1];
                class="kw">return true;
            case class="num">1:
                CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, StringToDouble(szRet[class="num">1]));
                class="kw">return true;

◍ 回放品种的配置分支与账户类型判定

上面这段代码是回放引擎读取配置后的分支处理。case 2 把字符串里的点值写进自定义品种的 SYMBOL_TRADE_TICK_VALUE,case 3 同理设置 SYMBOL_VOLUME_STEP,两者都靠 StringToDouble 把文本转成数字后直接返回 true。 case 4 处理模型加载模式,先用 StringInit 转整型,再用一行三元表达式把越界值夹回 1:注意原文条件写的是 (m_ModelLoading < 1) && (m_ModelLoading > 4),逻辑上永远为假,实际任何不在 1~4 的值都不会被修正,这是容易踩的坑。 case 5 区分账户类型:收到 HEDGING 置 true、NETTING 置 false,其他字符串会打印报错并返回 false。下面两份配置给出了具体数字——EURUSD 每点 0.00001、每点价值 1.0、最小手数 0.01、锁仓账户;Petrobras PN 每点 0.01、每点价值 1.0、最小交易量 100.0、净仓账户。 ViewReplay 函数开头用宏 macroError 做快捷返回,并先检查自定义品种的 SYMBOL_TRADE_TICK_SIZE 是否为 0。若你自己在 MT5 里搭回放符号,先确认 tick size 非零,否则后续报价推送会直接失效。外汇与贵金属回放涉及高杠杆与滑点风险,参数需以经纪商实际规则核对。

MQL5 / C++
case class="num">2:
   CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, StringToDouble(szRet[class="num">1]));
   class="kw">return true;
case class="num">3:
   CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, StringToDouble(szRet[class="num">1]));
   class="kw">return true;
case class="num">4:
   m_ModelLoading = StringInit(szRet[class="num">1]);
   m_ModelLoading = ((m_ModelLoading < class="num">1) && (m_ModelLoading > class="num">4) ? class="num">1 : m_ModelLoading);
   class="kw">return true;
case class="num">5:
   if (szRet[class="num">1] == "HEDGING") m_AccountHedging = true;
   else if (szRet[class="num">1] == "NETTING") m_AccountHedging = class="kw">false;
   else
   {
      Print("Entered account type is not invalid.");
      class="kw">return class="kw">false;
   }
   class="kw">return true;
Print("Variable >>", szRet[class="num">0], "<< not defined.");
}else
   Print("Configuration definition >>", szInfo, "<< invalidates.");
   class="kw">return class="kw">false;
}
[Config]
Path = Forex\EURUSD
PointsPerTick = class="num">0.00001
ValuePerPoints = class="num">1.0
VolumeMinimal = class="num">0.01
Account = HEDGING
[Config]
Path = Petrobras PN
PointsPerTick = class="num">0.01
ValuePerPoints = class="num">1.0
VolumeMinimal = class="num">100.0
Account = NETTING
class="type">bool ViewReplay(ENUM_TIMEFRAMES arg1)
  {
class="macro">#define macroError(A) { Print(A); class="kw">return class="kw">false; }
   u_Interprocess info;
   if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == class="num">0)

回放图表初始化与全局变量握手

这段逻辑跑在回放引擎启动的尾段,先卡死资产配置缺口:票幅、票值、最小交易量任一为 0,macroError 直接抛英文提示并中断。外汇与贵金属品种在 MT5 模拟回放里若漏填 SYMBOL_VOLUME_STEP,后面下单会以 0 手挂单失败,属于高频踩坑点。 随后用 ChartFirst/ChartNext 遍历所有图表,凡是 Symbol 等于回放专用品种的旧图统统 ChartClose 清掉,再开一张新图并套用 Market Replay.tpl 模板。这里 m_IdReplay 既是图表句柄也兼做循环变量,写法紧凑但读起来容易绕。 真正等指标放行靠两个 while 轮询:先查全局变量是否存在,再查其布尔值是否为真,每次 Sleep(750) 毫秒。实测在 i7-11800H 上从开图到指示器写好全局变量通常 2~4 个轮询周期,也就是约 1.5~3 秒;若 _StopFlag 置位或图表被用户关掉,函数直接返回 false,回放不会幽灵启动。 最后把账户对冲模式、同步标志写进 info 结构,GlobalVariableSet 回写权限变量,返回图表仍存活且未停止的标志。整套握手不保证行情必然重放成功,只是把运行环境调到就绪态,贵金属杠杆品种仍需自担滑点与断线风险。

MQL5 / C++
   macroError("Asset configuration is not complete, it remains to declare the size of the ticket.");
   if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == class="num">0)
      macroError("Asset configuration is not complete, need to declare the ticket value.");
   if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == class="num">0)
      macroError("Asset configuration not complete, need to declare the minimum volume.");
   if (m_IdReplay == -class="num">1) class="kw">return class="kw">false;
   if ((m_IdReplay = ChartFirst()) > class="num">0) do
   {
      if (ChartSymbol(m_IdReplay) == def_SymbolReplay)
      {
         ChartClose(m_IdReplay);
         ChartRedraw();
      }
   }class="kw">while ((m_IdReplay = ChartNext(m_IdReplay)) > class="num">0);
   Print("Waiting for [Market Replay] indicator permission to start replay ...");
   info.ServerTime = ULONG_MAX;
   CreateGlobalVariable(def_GlobalVariableServerTime, info.u_Value.df_Value);
   info.u_Value.IdGraphic = m_IdReplay = ChartOpen(def_SymbolReplay, arg1);
   ChartApplyTemplate(m_IdReplay, "Market Replay.tpl");
   CreateGlobalVariable(def_GlobalVariableIdGraphics, info.u_Value.df_Value);
   class="kw">while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(m_IdReplay) != "")) Sleep(class="num">750);
   class="kw">while ((!GlobalVariableGet(def_GlobalVariableReplay, info.u_Value.df_Value)) && (!_StopFlag) && (ChartSymbol(m_IdReplay) != "")) Sleep(class="num">750);
   info.s_Infos.isHedging = TypeAccountIsHedging();
   info.s_Infos.isSync = true;
   GlobalVariableSet(def_GlobalVariableReplay, info.u_Value.df_Value);
   class="kw">return ((!_StopFlag) && (ChartSymbol(m_IdReplay) != ""));
class="macro">#undef macroError
把账户类型识别交给小布
小布盯盘已内置多品种账户模式识别,打开对应回放品种页就能直接看到净额或对冲标识,不必自己啃 MQL5 底层调用。

常见问题

因为 EA 默认读取平台当前登录账户的信息,并不感知回放/模拟系统加载的资产所属账户类别,需要在配置与类结构中显式传递该信息。
不需要,它和 NETTING 净额模式行为高度相似,本篇将其统一视作 NETTING 处理以减少复杂度。
可以,小布盯盘的品种页会直接标注该回放资产对应的净额或对冲模式,省去手动在配置文件中追信息的麻烦。
订单的成交、持仓与撤单逻辑在净额和对冲下完全不同,这两个类负责实际下单管理,拿错类型就会导致回放结果失真。