开发回放系统(第 40 部分):启动第二阶段(一)·进阶篇
用状态机区分收盘、竞价与实盘
MT5 里想做回放或实盘图形研究,第一步是把市场状态摸清楚。下面这段把市场切成四种:收盘(eCloseMarket)、竞价(eAuction)、正常交易(eInTrading)、回放(eInReplay),靠一个枚举和几个 Update 重载来切换。 带参 Update(const eStatusMarket arg) 直接比较传入状态与现有状态,不同才赋值,避免无谓重绘;无参 Update(void) 里的 switch 按状态写字符串:收盘标 "Closed Market",回放和交易走同一分支去取当前 Bar 时间(TimeToString 到秒),取不到再落回竞价或 ERROR。 另一个重载 Update(const MqlBookInfo &book[]) 更实用:深度簿数组长度为 0 判收盘;符号等于回放专用符号判回放,否则交易;再扫一遍 book,遇到 BOOK_TYPE_BUY_MARKET / SELL_MARKET 就改成竞价状态。外汇与贵金属保证金交易波动剧烈、杠杆风险高,这套状态判定在实盘只作辅助参考,不预示方向。 开 MT5 把这段塞进指标,接上 OnBookEvent 调 Update(book),图表上就能实时看到市场处于哪一态,回放和真盘切换不会混。
enum eStatusMarket {eCloseMarket, eAuction, eInTrading, eInReplay}; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Update(const eStatusMarket arg) class="type">void Update(class="type">void) { class="type">class="kw">datetime dt; class="kw">switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status)) class="kw">switch (m_Info.Status) { case eCloseMarket : m_Info.szInfo = "Closed Market"; break; case eInReplay : case eInTrading : if ((dt = GetBarTime()) < ULONG_MAX) { m_Info.szInfo = TimeToString(dt, TIME_SECONDS); break; } case eAuction : m_Info.szInfo = "Auction"; break; class="kw">default : m_Info.szInfo = "ERROR"; } Draw(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Update(const MqlBookInfo &book[]) { m_Info.Status = (ArraySize(book) == class="num">0 ? eCloseMarket : (def_InfoTerminal.szSymbol == def_SymbolReplay ? eInReplay : eInTrading)); for (class="type">int c0 = class="num">0; (c0 < ArraySize(book)) && (m_Info.Status != eAuction); c0++) if ((book[c0].type == BOOK_TYPE_BUY_MARKET) || (book[c0].type == BOOK_TYPE_SELL_MARKET)) m_Info.Status = eAuction; this.Update(); } class=class="str">"cmt">//+------------------------------------------------------------------+
「鼠标标注指标的初始化与刷新骨架」
这段指标代码负责在 MT5 里挂一个「鼠标研究」层:把盘面状态、价格线、正负研究用三种颜色区分,并在每根 K 线刷新时把标注推到缓冲。核心对象是两个自定义类指针 C_Terminal 和 C_Study,前者管终端/品种信息,后者管绘制逻辑。 OnInit 里先用 CheckPass 校验指标名,失败直接 INIT_FAILED;随后 new 出 Terminal 与 Study,传入 user01~user03 三种颜色(黑/苍绿/浅珊瑚)。若当前品种不等于回放专用品种 def_SymbolReplay,就走 MarketBookAdd + OnBookEvent 并置为 eCloseMarket,否则用 input 的 user00 决定初始状态。最后 SetIndexBuffer(0, m_Buff, INDICATOR_DATA) 把双精度数组绑成指标线,并用 EMPTY_VALUE 初始化——这意味着未标注的柱会留空而非画 0。 OnCalculate 只有三行实质逻辑:m_posBuff = rates_total - 4 把写入位置钉在倒数第 4 根,(*Study).Update(m_Status) 按当前市场状态重绘,return rates_total 告诉内核已处理全部柱。注意 rates_total-4 这个偏移,若你改回放帧率或加载更高帧,可能要同步调这个 4 才能对上鼠标落点。 想验证的话,直接把这段塞进一个空指标工程,配好 Market Replay\Auxiliar\InterProcess.mqh 头文件,切到回放品种看颜色层是否随 user00 切换;外汇与贵金属复盘带杠杆,回测结论仅代表历史概率,实盘仍属高风险。
class="macro">#include <Market Replay\Auxiliar\InterProcess.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ C_Terminal *Terminal = NULL; C_Study *Study = NULL; class=class="str">"cmt">//+------------------------------------------------------------------+ input C_Study::eStatusMarket user00 = C_Study::eAuction; class=class="str">"cmt">//Market Status input class="type">color user01 = clrBlack; class=class="str">"cmt">//Price Line input class="type">color user02 = clrPaleGreen; class=class="str">"cmt">//Positive Study input class="type">color user03 = clrLightCoral; class=class="str">"cmt">//Negative Study class=class="str">"cmt">//+------------------------------------------------------------------+ C_Study::eStatusMarket m_Status; class="type">int m_posBuff = class="num">0; class="type">class="kw">double m_Buff[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { if (!CheckPass("Indicator Mouse Study")) class="kw">return INIT_FAILED; Terminal = new C_Terminal(); Study = new C_Study(Terminal, user01, user02, user03); if ((*Terminal).GetInfoTerminal().szSymbol != def_SymbolReplay) { MarketBookAdd((*Terminal).GetInfoTerminal().szSymbol); OnBookEvent((*Terminal).GetInfoTerminal().szSymbol); m_Status = C_Study::eCloseMarket; }else m_Status = user00; SetIndexBuffer(class="num">0, m_Buff, INDICATOR_DATA); ArrayInitialize(m_Buff, EMPTY_VALUE); class="kw">return INIT_SUCCEEDED; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">int begin, const class="type">class="kw">double &price[]) { m_posBuff = rates_total - class="num">4; (*Study).Update(m_Status); class="kw">return rates_total; } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 图表事件与订单簿状态机怎么接
指标要实时响应鼠标和深度盘口,得靠 OnChartEvent 和 OnBookEvent 两个回调把消息转给研究类。OnChartEvent 里先调 DispatchMessage 派发事件,再 SetBuffer 刷新数据缓冲,最后 ChartRedraw 强制重绘,否则画面可能停在旧帧。 OnBookEvent 的逻辑更硬核:先用 MarketBookGet 把当前品种的深度簿塞进 book[] 数组,数组长度为 0 就判为休市(eCloseMarket),否则为交易中(eInTrading)。接着扫一遍 book,只要出现 BOOK_TYPE_BUY_MARKET 或 BOOK_TYPE_SELL_MARKET 这类市价单,状态就切到竞价(eAuction)。外汇与贵金属市场跳空频繁,深度簿在重大数据前可能瞬间清空,状态机误判概率不低。 OnDeinit 做清理,reason 不是 REASON_INITFAILED 时才释放深度簿订阅并 delete 掉 Study、Terminal 指针,避免内存泄漏。CheckPass 则是单实例守卫:临时改短名后查窗口,若已存在同名指标就删掉临时实例并返回 false,控制台会打印 Only one instance is allowed...。 把下面代码直接贴进 MT5 指标源码,编译后挂 EURUSD 观察盘口,状态切换会在日志里留下轨迹,方便你验证休市/竞价判定是否符合预期。
class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { (*Study).DispatchMessage(id, lparam, dparam, sparam); SetBuffer(); ChartRedraw(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnBookEvent(const class="type">class="kw">string &symbol) { MqlBookInfo book[]; C_Study::eStatusMarket loc = m_Status; if (symbol != (*Terminal).GetInfoTerminal().szSymbol) class="kw">return; MarketBookGet((*Terminal).GetInfoTerminal().szSymbol, book); m_Status = (ArraySize(book) == class="num">0 ? C_Study::eCloseMarket : C_Study::eInTrading); for (class="type">int c0 = class="num">0; (c0 < ArraySize(book)) && (m_Status != C_Study::eAuction); c0++) if ((book[c0].type == BOOK_TYPE_BUY_MARKET) || (book[c0].type == BOOK_TYPE_SELL_MARKET)) m_Status = C_Study::eAuction; if (loc != m_Status) (*Study).Update(m_Status); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { if (reason != REASON_INITFAILED) { if ((*Terminal).GetInfoTerminal().szSymbol != def_SymbolReplay) MarketBookRelease((*Terminal).GetInfoTerminal().szSymbol); class="kw">delete Study; class="kw">delete Terminal; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CheckPass(const class="type">class="kw">string szShortName) { IndicatorSetString(INDICATOR_SHORTNAME, szShortName + "_TMP"); if (ChartWindowFind(ChartID(), szShortName) != -class="num">1) { ChartIndicatorDelete(ChartID(), class="num">0, szShortName + "_TMP"); Print("Only one instance is allowed..."); class="kw">return false; } IndicatorSetString(INDICATOR_SHORTNAME, szShortName);
把鼠标坐标塞进指标缓冲区的写法
这段 inline 函数负责把鼠标在图表上的实时位置写进自定义缓冲数组 m_Buff,供后续绘制或逻辑判断使用。 先纠正写入下标:m_posBuff 若为负数就归零,避免越界写坏数组。随后把鼠标价格直接赋给 m_Buff[m_posBuff+0]。 uCast_Double 这个联合体在这里是关键,它让同一段内存既能存 double 也能拆出 datetime、int 和 char。第108行把鼠标时间存进 Info._datetime,再经 dValue 透传到 m_Buff[m_posBuff+1];X、Y 像素坐标分别写 _int[0]、_int[1],对应落到 +2 位。 按钮状态只在未执行其它研究时记录:ExecStudy 为 eStudyNull 才把 ButtonStatus 转 char 写入 _char[0],否则写0,最终进 +3 位。开 MT5 把这段挂到鼠标事件回调里,移动光标就能在缓冲区看到价格/时间/坐标实时刷新,外汇贵金属行情跳动快,这种高频读位对抓瞬时价位有帮助,但杠杆品种波动剧烈,仅作技术验证、勿直接用于实盘信号。
class="num">099. class="kw">return true; class="num">100. } class="num">101. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">102. class="kw">inline class="type">void SetBuffer(class="type">void) class="num">103. { class="num">104. uCast_Double Info; class="num">105. class="num">106. m_posBuff = (m_posBuff < class="num">0 ? class="num">0 : m_posBuff); class="num">107. m_Buff[m_posBuff + class="num">0] = (*Study).GetInfoMouse().Position.Price; class="num">108. Info._datetime = (*Study).GetInfoMouse().Position.dt; class="num">109. m_Buff[m_posBuff + class="num">1] = Info.dValue; class="num">110. Info._int[class="num">0] = (*Study).GetInfoMouse().Position.X; class="num">111. Info._int[class="num">1] = (*Study).GetInfoMouse().Position.Y; class="num">112. m_Buff[m_posBuff + class="num">2] = Info.dValue; class="num">113. Info._char[class="num">0] = ((*Study).GetInfoMouse().ExecStudy == C_Mouse::eStudyNull ? (class="type">char)(*Study).GetInfoMouse().ButtonStatus : class="num">0); class="num">114. m_Buff[m_posBuff + class="num">3] = Info.dValue; class="num">115. } class="num">116. class=class="str">"cmt">//+------------------------------------------------------------------+
「缓冲区里塞什么、怎么塞」
做跨进程指标交互时,SetBuffer 是把鼠标状态送出去的关键口子。前面第 49 行声明了要往缓冲区放四个 double 值,位置固定在距 rates_total 四个点的地方;回放模式下从零开始,真实行情里 m_posBuff 基本不会负,但模拟资产中可能出现负值。
第 106 行那句 m_posBuff = (m_posBuff < 0 ? 0 : m_posBuff) 不是冗余防御,而是回放态下的保命判断——索引一旦小于零,指标直接断掉。
四个槽位的约定最好一开始就锁死:0 号存鼠标线价格,1 号存时间(借 uCast_Double 转成 double),2 号塞 X/Y 屏幕坐标打包后的 double,3 号把鼠标按钮状态压进最低字节再转 double。顺序可以换,但换了就得同步改所有读这块缓冲的代码,否则拿到的全是错位数据。
下面这段代码就是实际落地,开 MT5 把 Study 指针接上就能直接验证鼠标信息是否正确写入 m_Buff。外汇和贵金属行情跳变快,这类交互指标在实盘高风险环境下可能因 tick 丢失导致缓冲滞后,验证时建议先用模拟账户跑。
class="num">102. class="kw">inline class="type">void SetBuffer(class="type">void) class="num">103. { class="num">104. uCast_Double Info; class="num">105. class="num">106. m_posBuff = (m_posBuff < class="num">0 ? class="num">0 : m_posBuff); class="num">107. m_Buff[m_posBuff + class="num">0] = (*Study).GetInfoMouse().Position.Price; class="num">108. Info._datetime = (*Study).GetInfoMouse().Position.dt; class="num">109. m_Buff[m_posBuff + class="num">1] = Info.dValue; class="num">110. Info._int[class="num">0] = (*Study).GetInfoMouse().Position.X; class="num">111. Info._int[class="num">1] = (*Study).GetInfoMouse().Position.Y; class="num">112. m_Buff[m_posBuff + class="num">2] = Info.dValue; class="num">113. Info._char[class="num">0] = ((*Study).GetInfoMouse().ExecStudy == C_Mouse::eStudyNull ? (class="type">char)(*Study).GetInfoMouse().ButtonStatus : class="num">0); class="num">114. m_Buff[m_posBuff + class="num">3] = Info.dValue; class="num">115. }
◍ 代码稳定前先读懂思路
这套系统的附件里给了编译好的版本,体积 420.65 KB 的 ZIP 包,能直接看运行后的样子;但文章里贴的片段会随开发改动,两者不完全一致。想跟下来的交易者,重点不是背代码,而是搞清每段逻辑在解决什么——等正式版定型,你才不会看不懂。 如果你已经写过几年 EA,建议把文章里的增量片段手动合进工程里跑。改完务必在策略测试器用真实点差回测,外汇和贵金属杠杆高、滑点凶,没验证的改动可能让仓位失控。 下一篇会接着补完没解决的模块,系统的进化还没停。