开发回放系统(第 36 部分):进行调整(二)·综合运用
⚙️

开发回放系统(第 36 部分):进行调整(二)·综合运用

(3/3)· 收尾篇聚焦如何让 EA 准确识别回放资产所属账户类型,堵住前篇遗留的识别漏洞

含代码示例偏理论 第 3/3 篇

接上篇,我们继续深挖回放系统的调整细节。前两篇铺垫了基础架构与初步修正,但资产所属账户类型在真实与模拟图表间仍会误报,这个残留问题会让订单逻辑在净额与对冲账户间踩错节拍。

EA 怎么认出当前是哪种账户

回放/模拟系统启动后,MT5 服务先建图、挂控制指标,再由控制指标写一条全局终端变量给回放服务用。紫色箭头那一步才把文件里的配置读进来,EA 必须靠这条信息判断自己跑在 NETTING 还是 HEDGING 账户下。 关键不在「用哪个类存数据」这么简单,而是 C_Orders 类后续也要读账户类型。如果把控制逻辑塞进 C_Manager,C_Orders 就够不到所需数据;放 C_Terminal 里,哪怕以后换类管订单,数据仍可读。初始化顺序错了,系统可能在启动后立刻挂掉。 C_Terminal 里用一个 lock 变量标记账户类型「尚未取得但正在取」。只有 lock 为 false 时,才允许按传入参数写一次账户类型值;写完直到程序结束都不可改。实测中只锁 NETTING 与 HEDGING 两种——EXCHANGE 行为同 NETTING,贴近真实盘已够用。 C_Manager 构造函数改动极小:原先遍历所有账户类型,现在只留两个分支,账户类型信息直接问 C_Terminal 的那个变量。当资产属于回放系统资产时,进入双循环等控制指标建好全局变量;用户终止或服务写好变量才退出外循环,随后把账户类型送回 C_Terminal。 别把正态当圣经:很多教程让你在 Manager 里一次性判账户,这里反着来才不阻断订单类取数。开 MT5 把下面类头抄进 EA,断点跟一遍 lock 从 false 到锁定的过程就懂了。

MQL5 / C++
<span class="keyword">class</span> C_Terminal
{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">protected</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">enum</span> eErrUser {ERR_Unknown, ERR_PointerInvalid};
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">enum</span> eEvents {ev_Update};
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">struct</span> st_Terminal
&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;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="macro">ENUM_SYMBOL_CHART_MODE</span>&nbsp;&nbsp; ChartMode;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="macro">ENUM_ACCOUNT_MARGIN_MODE</span> TypeAccount;
&nbsp;&nbsp;&nbsp;&nbsp;&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;ID;
&nbsp;&nbsp;&nbsp;&nbsp;&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;szSymbol;
&nbsp;&nbsp;&nbsp;&nbsp;&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; Width,
&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;&nbsp;&nbsp;&nbsp;&nbsp;Height,
&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;&nbsp;&nbsp;&nbsp;&nbsp;nDigits;
&nbsp;&nbsp;&nbsp;&nbsp;&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;PointPerTick,
&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;&nbsp;&nbsp;&nbsp;&nbsp;ValuePerPoint,
&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;&nbsp;&nbsp;&nbsp;&nbsp;VolumeMinimal,
&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;&nbsp;&nbsp;&nbsp;&nbsp;AdjustToTrade;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;};
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">private</span> :
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;st_Terminal m_Infos;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">struct</span> mem
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{

◍ 图表与账户参数的初始化接管

在 MT5 自定义类里,构造函数 C_Terminal() 负责把当前图表和交易品种的基础参数一次性抓进内存结构。它先取 ChartID() 写入 m_Infos.ID,并把 m_Mem.AccountLock 置为 false,随后通过 ChartGetInteger 读取图表是否显示对象描述与日期轴,再用 ChartSetInteger 关掉对象描述显示、打开对象创建/删除事件,并隐藏日期轴——这些动作会让图表更干净,也避免手动标注干扰脚本的对象监听。 接着用 SymbolInfoInteger 取 SYMBOL_DIGITS 得到报价小数位,用 ChartGetInteger 取 CHART_WIDTH_IN_PIXELS / CHART_HEIGHT_IN_PIXELS 得到画布像素尺寸;用 SymbolInfoDouble 取 SYMBOL_TRADE_TICK_SIZE 和 SYMBOL_TRADE_TICK_VALUE,两者相除得到 m_Infos.AdjustToTrade,这是把点数换算成金额的关键系数。若当前品种不等于回放专用品种 def_SymbolReplay,还会用 AccountInfoInteger(ACCOUNT_MARGIN_MODE) 判断账户是对冲还是净仓模式。 SetTypeAccount() 用 m_Mem.AccountLock 做一次性写锁:首次调用置锁并返回,之后不再改账户类型,避免运行中被外部切换账户导致逻辑错乱。C_Manager 构造函数则把终端指针、分析指针、颜色与风控参数(止损金额、止盈金额、杠杆、是否日内)传进来,并用 CheckPointer 校验 Terminal 指针有效性,无效就抛自定义错误 ERR_PointerInvalid。 开 MT5 新建 EA 把这段抄进类构造,在 EURUSD 上跑一下,m_Infos.nDigits 应返回 5,AdjustToTrade 通常接近 1.0(标准账户每点约 10 美元/标准手),可验证你的点值换算有没有接对。外汇与贵金属杠杆交易高风险,参数仅作技术验证,不构成任何方向建议。

MQL5 / C++
class="type">long  Show_Descr,
       Show_Date;
class="type">bool   AccountLock;
}m_Mem;
C_Terminal()
  {
   m_Infos.ID = ChartID();
   m_Mem.AccountLock = class="kw">false;
   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, class="num">0, true);
   ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_CREATE, class="num">0, 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;
   m_Infos.ChartMode     = (ENUM_SYMBOL_CHART_MODE) SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_CHART_MODE);
   if(m_Infos.szSymbol != def_SymbolReplay) SetTypeAccount((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE));
   ResetLastError();
  }
class="kw">inline class="type">void SetTypeAccount(class="kw">const ENUM_ACCOUNT_MARGIN_MODE arg)
  {
   if (m_Mem.AccountLock) class="kw">return; else m_Mem.AccountLock = true;
   m_Infos.TypeAccount = (arg == ACCOUNT_MARGIN_MODE_RETAIL_HEDGING ? arg : ACCOUNT_MARGIN_MODE_RETAIL_NETTING);
  }
C_Manager(C_Terminal *arg1, C_Study *arg2, class="type">class="kw">color cPrice, class="type">class="kw">color cStop, class="type">class="kw">color cTake, class="kw">const class="type">class="kw">ulong magic, class="kw">const class="type">class="kw">double FinanceStop, class="kw">const class="type">class="kw">double FinanceTake, class="type">uint Leverage, class="type">bool IsDayTrade)
        :C_ControlOfTime(arg1, magic)
  {
   class="type">class="kw">string szInfo = "HEDGING";
   u_Interprocess info;
   Terminal = arg1;
   Study = arg2;
   if (CheckPointer(Terminal) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid);

「账户模式判定与回放同步的初始化落点」

这段初始化逻辑紧接在指针校验之后,先把风控与品种关联参数写进成员变量:止损额、止盈额、杠杆、是否日内交易,以及默认关闭对冲标记。注意 m_Infos.AccountHedging 先硬编码为 false,真正的账户类型要等后面 switch 分支再翻。 如果当前符号等于回放符号(def_SymbolReplay),代码会进入一个 do-while 等待环:先用 GlobalVariableGet 轮询全局变量,750 毫秒 Sleep 一次,直到拿到值或 _StopFlag 触发;再判断 info.s_Infos.isSync 是否为真才退出。这一段的真实作用是卡住线程,等回放数据源把账户上下文推过来,避免在没同步时误设保证金模式。 离开回放块后,按 def_InfoTerminal.TypeAccount 分流:零售对冲模式会把 m_Infos.AccountHedging 置 true,零售净额模式则把 szInfo 标成 "NETTING"。最后 Print 打出 Detected Account 及对应字样,你在 MT5 Experts 日志里看到这行,才说明初始化跑通了。外汇与贵金属保证金计算受账户模式影响明显,净额与对冲的下单占用逻辑不同,实盘前务必确认日志输出与券商账户类型一致。

MQL5 / C++
if (CheckPointer(Study) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid);
if (_LastError != ERR_SUCCESS) class="kw">return;
m_Infos.FinanceStop     = FinanceStop;
m_Infos.FinanceTake     = FinanceTake;
m_Infos.Leverage        = Leverage;
m_Infos.IsDayTrade      = IsDayTrade;
m_Infos.AccountHedging  = class="kw">false;
m_Objects.corPrice      = cPrice;
m_Objects.corStop       = cStop;
m_Objects.corTake       = cTake;
m_Objects.bCreate       = class="kw">false;
if (def_InfoTerminal.szSymbol == def_SymbolReplay)
{
   do
   {
      class="kw">while ((!GlobalVariableGet(def_GlobalVariableReplay, info.u_Value.df_Value)) && (!_StopFlag)) Sleep(class="num">750);
   }class="kw">while ((!info.s_Infos.isSync) && (!_StopFlag));
   def_AcessTerminal.SetTypeAccount(info.s_Infos.isHedging ? ACCOUNT_MARGIN_MODE_RETAIL_HEDGING : ACCOUNT_MARGIN_MODE_RETAIL_NETTING);
};
class="kw">switch (def_InfoTerminal.TypeAccount)
{
   case ACCOUNT_MARGIN_MODE_RETAIL_HEDGING: m_Infos.AccountHedging = true; class="kw">break;
   case ACCOUNT_MARGIN_MODE_RETAIL_NETTING: szInfo = "NETTING";      class="kw">break;
}
Print("Detected Account ", szInfo);

记住这一条就够了

前面几节把回放/模拟服务的账户类型坑点拆开了讲,核心就落在一件事上:如果你已经在用这套服务,哪怕暂时不做分析,也得把负责回放/模拟的设置文件升到新版。 原包里分了 FOREX(3744 KB)、BOLSA(1358.28 KB)、FUTUROS(11397.55 KB)和 Market_Replay(131.58 KB)四组 ZIP,按你做的品种挑对应的下,别混用。 不更新的话,系统会默默用默认的 HEDGING 账户类型去跑,和某些经纪商的真实账户对不上,后面复盘数据错位的概率很高,排查起来才真头疼。外汇和贵金属本身杠杆高、滑点狠,用错账户类型模拟出来的结果可能直接带偏实盘判断。

把账户类型识别交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到当前回放资产映射的账户模式,你只需专注策略逻辑而非底层判别。

常见问题

早期实现只读取终端当前登录账户类型,未透传回放资产自身的配置。本篇通过在品种配置注入指令并改造 C_Manager 与 C_Orders 类来修正。
因与 NETTING 行为高度相似,代码将其归并视为 NETTING,不单独实现,避免冗余分支。
可以,小布盯盘的品种页已集成该类识别,省去手动核对配置文件的步骤,但外汇贵金属杠杆交易本身高风险,信号仅作参考。
两者平仓与持仓规则不同,EA 必须按资产实际类型切换逻辑,否则可能出现禁止的对冲单或错误平仓,概率上更易触发异常。