开发多币种 EA 交易(第 21 部分):准备重要实验并优化代码·进阶篇
(2/3)· 当EA没有可调输入参数时,Validate式重优化实验该如何改造才能跑通
◍ 给虚拟顾问挂上品种监视器
CVirtualAdvisor 里原本就驻着几个单例对象:虚拟仓位接收器、风险管理器、用户状态界面。现在要再塞一个 CSymbolsMonitor 指针字段 m_symbols,让 EA 能统一盯住所有交易品种的报价变动。 构造时通过 CSymbolsMonitor::Instance() 静态方法拉起监视器,析构里用 if(!!m_symbols) delete m_symbols 释放,避免 MT5 终端退出时漏内存。 新报价进来走 Tick() 事件,先 UpdateNewBar() 判新柱,再调 m_symbols.Tick() 刷新全品种报价;同时顺手把 ChartEvent 处理壳子也加进类里,目前只转调 m_interface,暂不做实质动作。改完存 VirtualAdvisor.mqh 即可上 MT5 编译验证。 外汇与贵金属杠杆高、滑点跳空频繁,监视器漏掉某个品种报价可能导致虚拟仓位误判,实盘前务必在策略测试器跑多品种样本。
class CVirtualAdvisor : class="kw">public CAdvisor { class="kw">protected: CSymbolsMonitor *m_symbols; class=class="str">"cmt">// Symbol monitor object CVirtualReceiver *m_receiver; class=class="str">"cmt">// Receiver object that brings positions to the market CVirtualInterface *m_interface; class=class="str">"cmt">// Interface object to show the status to the user CVirtualRiskManager *m_riskManager; class=class="str">"cmt">// Risk manager object ... class="kw">public: ... }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CVirtualAdvisor::CVirtualAdvisor(class="type">class="kw">string p_params) { ... class=class="str">"cmt">// If there are no read errors, if(IsValid()) { class=class="str">"cmt">// Create a strategy group CREATE(CVirtualStrategyGroup, p_group, groupParams); class=class="str">"cmt">// Initialize the symbol monitor with a class="kw">static symbol monitor m_symbols = CSymbolsMonitor::Instance(); class=class="str">"cmt">// Initialize the receiver with the class="kw">static receiver m_receiver = CVirtualReceiver::Instance(p_magic); class=class="str">"cmt">// Initialize the interface with the class="kw">static interface m_interface = CVirtualInterface::Instance(p_magic); ... } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CVirtualAdvisor::~CVirtualAdvisor() { if(!!m_symbols) class="kw">delete m_symbols; class=class="str">"cmt">// Remove the symbol monitor if(!!m_receiver) class="kw">delete m_receiver; class=class="str">"cmt">// Remove the recipient if(!!m_interface) class="kw">delete m_interface; class=class="str">"cmt">// Remove the interface if(!!m_riskManager) class="kw">delete m_riskManager; class=class="str">"cmt">// Remove risk manager DestroyNewBar(); class=class="str">"cmt">// Remove the new bar tracking objects } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| OnTick event handler | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CVirtualAdvisor::Tick(class="type">void) { class=class="str">"cmt">// Define a new bar for all required symbols and timeframes class="type">bool isNewBar = UpdateNewBar(); class=class="str">"cmt">// If there is no new bar anywhere, and we only work on new bars, then exit if(!isNewBar && m_useOnlyNewBar) { class="kw">return; } class=class="str">"cmt">// Symbol monitor updates quotes m_symbols.Tick(); class=class="str">"cmt">// Receiver handles class="kw">virtual positions m_receiver.Tick();
策略主循环里的五个关键调用
在 MT5 专家顾问的 OnTick 收尾阶段,有一组固定顺序的调用决定了策略能否正常运转。先看这段实际挂在 tick 事件里的核心片段,它串联起策略推演、风控、成交量修正与界面刷新。 CAdvisor::Tick() 负责把当前行情喂给策略层做信号判断;m_riskManager.Tick() 紧接着处理虚拟持仓的风控逻辑,两者顺序不能颠倒,否则可能用旧信号过了风控。 m_receiver.Correct() 做市场成交量校准,Save() 落盘状态避免重启丢上下文,m_interface.Redraw() 才重绘面板。整套走完一次约在毫秒级,但在低频品种上若 Correct 耗时异常,会拖慢 Redraw 导致界面卡顿。 打开你自己的 EA 源码,核对 OnTick 末尾是否也有这五个环节;缺了 Save 或 Redraw,多半会在实盘重启后表现和回测不一致。外汇与贵金属杠杆高,任何状态不同步都可能放大滑点风险。
class=class="str">"cmt">// Start handling in strategies CAdvisor::Tick(); class=class="str">"cmt">// Risk manager handles class="kw">virtual positions m_riskManager.Tick(); class=class="str">"cmt">// Adjusting market volumes m_receiver.Correct(); class=class="str">"cmt">// Save status Save(); class=class="str">"cmt">// Render the interface m_interface.Redraw(); }
「给虚拟仓位类接上行情监视器」
虚拟仓位类原本自己管交易品种属性,现在把活儿交给 CSymbolsMonitor 和 CSymbolInfo 两个对象。类里加了两个私有指针:m_symbolInfo 指向具体品种信息,m_symbols 指向全局监视器单例。监视器用 CSymbolsMonitor::Instance() 静态方法拿,没有就建一个,析构时若 m_symbolInfo 已建则手动 delete,避免内存漏。 构造函数里不急着给 m_symbolInfo 赋值,因为建仓前不知道跑哪个品种;只有调 CVirtualOrder::Open() 时品种才定,那时再 new 一个 CSymbolInfo 挂上去。Open() 之后,报价刷新全由监视器的 Tick() 统一做,虚拟仓位类不再调 RefreshRates(),省掉重复 IO。 顺手做的性能分析:改完之后 SymbolInfoDouble() 还占 9% 采样耗时,查下来都是取点差用的。其实卖价减买价在 RefreshRates() 里已经有了,直接算差值就能砍掉这批调用。 这轮还顺带加了四样东西:把当前对象传进策略的 OnOpen()/OnClose() 回调、虚拟平仓利润计算、止损止盈的 getter/setter、以及开仓时分配的唯一编号。改动存进 VirtualOrder.mqh 即可,外汇与贵金属回测属高风险,虚拟结果不等于实盘表现。
class CVirtualOrder { class="kw">private: class=class="str">"cmt">//--- Static fields class="kw">static class="type">ulong s_count; class=class="str">"cmt">// Counter of all created CVirtualOrder objects CSymbolInfo *m_symbolInfo; class=class="str">"cmt">// Object for getting symbol properties class=class="str">"cmt">//--- Related recipient objects and strategies CSymbolsMonitor *m_symbols; CVirtualReceiver *m_receiver; CVirtualStrategy *m_strategy; ... } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CVirtualOrder::CVirtualOrder(CVirtualStrategy *p_strategy) : class=class="str">"cmt">// Initialization list m_id(++s_count), class=class="str">"cmt">// New ID = object counter + class="num">1 ... m_point(class="num">0) { PrintFormat(__FUNCTION__ + "#%d | CREATED VirtualOrder", m_id); m_symbolInfo = NULL; m_symbols = CSymbolsMonitor::Instance(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CVirtualOrder::~CVirtualOrder() { if(!!m_symbolInfo) class="kw">delete m_symbolInfo; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Open a class="kw">virtual position(order) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CVirtualOrder::Open(class="type">class="kw">string symbol, class=class="str">"cmt">// Symbol ENUM_ORDER_TYPE type, class=class="str">"cmt">// Type(BUY or SELL) class="type">class="kw">double lot, class=class="str">"cmt">// Volume class="type">class="kw">double price = class="num">0, class=class="str">"cmt">// Open price class="type">class="kw">double sl = class="num">0, class=class="str">"cmt">// StopLoss level(price or points)
◍ 开仓前先卡死重复下单
封装下单逻辑时,第一道闸门必须是持仓状态判定。若 IsOpen() 已返回真,说明同策略仓位在场,直接 PrintFormat 抛错并 return false,避免 MT5 上重复发单把风险敞口翻倍——外汇与贵金属杠杆高,这种低级错单可能在一根秒K里吃掉计划外的保证金。 下面这段是典型入口函数的参数声明与首层拦截。注意 inPoints 这个布尔:它决定 sl、tp 是按价格还是按点数解释,写错会让止损挂在离谱距离。 m_symbolInfo = m_symbols[symbol] 这行从符号监视器取指针,若取不到(!!m_symbolInfo 为假)就走 else 返回 false,说明品种没被纳入监控池,这时候硬开单大概率报价失效。 实盘前建议在策略测试器里故意让 IsOpen() 为真再触发一次,确认日志出现 'ERROR: Order is opened already!' 且不出第二张单,才算这条防护生效。
class="type">class="kw">double tp = class="num">0, class=class="str">"cmt">// TakeProfit level(price or points) class="type">class="kw">string comment = "", class=class="str">"cmt">// Comment class="type">class="kw">datetime expiration = class="num">0, class=class="str">"cmt">// Expiration time class="type">bool inPoints = false class=class="str">"cmt">// Are the SL and TP levels set in points? ) { if(IsOpen()) { class=class="str">"cmt">// If the position is already open, then do nothing PrintFormat(__FUNCTION__ "#%d | ERROR: Order is opened already!", m_id); class="kw">return false; } class=class="str">"cmt">// Get a pointer to the information object for the desired symbol from the symbol monitor m_symbolInfo = m_symbols[symbol]; if(!!m_symbolInfo) { class=class="str">"cmt">// Actions to open ... class="kw">return true; } else { class="kw">return false; } }
把策略类接进品种监视器
要让成交量策略跑在交易品种监视器上,得动两处类定义。第一处是把原来直接存对象的 m_symbolInfo 改成指针,监视器统一管理符号属性,策略只拿引用,避免每个策略实例各存一份行情状态。 构造函数里取消新柱事件的自行注册,改成从 CSymbolsMonitor 单例按品种名取指针。注释掉的那行 IsNewBar 说明:新柱监听已交给监视器,策略不再重复挂钩。 开仓信号检查和下单方法里原有的当前价刷新也删掉,因为监视器每次 tick 已同步最新价。改完存回 SimpleVolumesStrategy.mqh,EA 重载后即生效。外汇与贵金属杠杆高,改完务必在策略测试器用历史数据验证信号触发节奏。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Trading strategy using tick volumes | class=class="str">"cmt">//+------------------------------------------------------------------+ class CSimpleVolumesStrategy : class="kw">public CVirtualStrategy { class="kw">protected: ... CSymbolInfo *m_symbolInfo; class=class="str">"cmt">// Object for getting information about the symbol properties ... class="kw">public: ... }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CSimpleVolumesStrategy::CSimpleVolumesStrategy(class="type">class="kw">string p_params) { ... class=class="str">"cmt">// Register the event handler for a new bar on the minimum timeframe class=class="str">"cmt">//IsNewBar(m_symbol, PERIOD_M1); m_symbolInfo = CSymbolsMonitor::Instance()[m_symbol]; ... }
「加个新柱判断,回测才不漂」
在给 EA 接入交易品种监视器之后,先跑一遍和旧版同一时间段的回测对照。首轮数据:旧版利润 41 990.62、回撤 1 019.49(0.10%)、标准化利润 6 867.78;当前版利润 42 793.27、回撤 1 158.38(0.11%)、标准化利润 6 159.87。两者总体接近,但批交易里互相有对方没有的仓位。 根因在分钟线起始时间错位:测试器以 EURGBP 启动时,该品种新柱落在 mm:00,而 GBPUSD 的新柱可能出现在 mm:00 或 mm:20,导致跨品种信号触发点不一致。外汇与贵金属品种间这种时基差异是高概率坑点,不处理就会让回测结果带噪声。 补一道新柱检查后重测:旧版利润 46 565.39、回撤 1 079.93(0.11%)、标准化利润 7 189.77;当前版利润 47 897.30、回撤 1 051.37(0.10%)、标准化利润 7 596.31。当前版标准化利润反超,改动保留。 下面这段就是加在 Tick 里的关键判断,用 IsNewBar 卡住 M1 新柱再放行订单逻辑,避免同一 tick 内重复或错位开仓。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| "Tick" event handler function |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> CSimpleVolumesStrategy::Tick() <span class="keyword">class="kw">override</span> { <span style="background-class="type">class="kw">color:rgb(class="num">216, class="num">232, class="num">194);"><span class="keyword">if</span>(IsNewBar(m_symbol, <span class="macro">PERIOD_M1</span>)) {</span> <span class="comment">class=class="str">"cmt">// If their number is less than allowed</span> <span class="keyword">if</span>(m_ordersTotal < m_maxCountOfOrders) { <span class="comment">class=class="str">"cmt">// Get an open signal</span> <span class="keyword">class="type">int</span> signal = SignalForOpen(); <span class="keyword">if</span>(signal == <span class="number">class="num">1</span> <span class="comment">class=class="str">"cmt">/* || m_ordersTotal < class="num">1 */</span>) { <span class="comment">class=class="str">"cmt">// If there is a buy signal, then</span> OpenBuyOrder(); <span class="comment">class=class="str">"cmt">// open the BUY_STOP order</span> } <span class="keyword">else</span> <span class="keyword">if</span>(signal == -<span class="number">class="num">1</span>) { <span class="comment">class=class="str">"cmt">// If there is a sell signal, then</span> OpenSellOrder(); <span class="comment">class=class="str">"cmt">// open the SELL_STOP order</span> } } } }