通用智能交易系统:支持挂单和对冲(第五章)·进阶篇
(2/3)· 从价格访问重写到CPendingOrders,把复杂挂单逻辑塞进可复用的CStrategy骨架
持仓利润与激活状态的底层取法
在 MT5 的持仓封装类里,取浮动利润前先判活是个容易被忽略的细节。CPosition::Profit 直接走 PositionGetDouble(POSITION_PROFIT),但若持仓未激活会返回脏值,所以先用 IsActive 拦一道,非激活就给 0.0。 IsActive 的写法区分了账户保证金模式。当模式不是 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING 时,用 PositionSelect(m_symbol) 按交易品种挑当前持仓;而对冲账户里同品种可有多仓,必须 PositionSelectByTicket(m_id) 用持仓票据精确锁定。 开 MT5 按 F4 把这段塞进自己的 EA 头文件,挂一个 EURUSD 多单后改 AccountInfoInteger(ACCOUNT_MARGIN_MODE) 的返回逻辑做断点,能看到非对冲模式下 m_symbol 选错仓时 Profit 仍可能读出非零值——这是品种级选中而非票据级选中的副作用。外汇与贵金属杠杆高,持仓状态误判会放大平仓逻辑错误,验证时请用模拟账户。
class="type">class="kw">double CPosition::Profit(class="type">void) { if(!IsActive()) class="kw">return class="num">0.0; class="kw">return PositionGetDouble(POSITION_PROFIT); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回true如果头寸被激活。否则返回false class=class="str">"cmt">//|| class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CPosition::IsActive(class="type">void) { ENUM_ACCOUNT_MARGIN_MODE mode=(ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE); if(mode!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) class="kw">return PositionSelect(m_symbol); else class="kw">return PositionSelectByTicket(m_id); }
「老版引擎里手动挂 BuyStop 的活法」
早期 CStrategy 交易引擎原生不支持挂单,但不代表基于它写的 EA 不能下挂单。直接用 MT5 标准函数 OrdersTotal() 和 OrderSelect() 就能绕开限制,把挂单逻辑塞进 BuyInit / SellInit 重写方法里。 一个典型脉冲思路是:每个新柱形把 BuyStop 的激活价钉在当前 Ask 上方 0.25%,若价格在本柱内出现突发行情就被触发,赶上强动量阶段进场;若波动不够,柱终前把挂单改到新价位,等下一根柱重新试。 下面这段 CMovingAverage::InitBuy 就是第一版里没靠类原生支持、纯手写的买向挂单处理。它先卡事件与已有买单,再扫一遍挂单归属本 EA 的 BuyStop 并改价,一个都没有才新下一张。 挂单操作和普通订单本质一样,核心纪律只有一条:买卖逻辑必须在 BuyInit 和 SellInit 里彻底分开,别把 sell 类挂单混进买方法。剩下遍历、识别、改单的写法,和 MT4 经典订单循环没大区别。外汇与贵金属杠杆高,这类突发行情触发概率随品种波动环境变化,实盘前请在 MT5 策略测试器用真实点差验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Buy类型挂单的执行逻辑 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMovingAverage::InitBuy(class="kw">const MarketEvent &event) { if(!IsTrackEvents(event))class="kw">return; class=class="str">"cmt">// 仅处理所需事件 if(positions.open_buy > class="num">0) class="kw">return; class=class="str">"cmt">// 如果已经存在至少一个未平仓的买单,无需再买入 class="type">int buy_stop_total = class="num">0; for(class="type">int i = OrdersTotal()-class="num">1; i >= class="num">0; i--) { class="type">class="kw">ulong ticket = OrderGetTicket(i); if(!OrderSelect(ticket))class="kw">continue; class="type">class="kw">ulong magic = OrderGetInteger(ORDER_MAGIC); if(magic != ExpertMagic())class="kw">continue; class="type">class="kw">string symbol = OrderGetString(ORDER_SYMBOL); if(symbol != ExpertSymbol())class="kw">continue; ENUM_ORDER_TYPE order_type = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE); if(order_type == ORDER_TYPE_BUY_STOP) { buy_stop_total++; Trade.OrderModify(ticket, Ask()*class="num">0.0025, class="num">0, class="num">0, class="num">0); } } if(buy_stop_total == class="num">0) Trade.BuyStop(MM.GetLotFixed(), Ask() + Ask()*class="num">0.0025, ExpertSymbol(), class="num">0, class="num">0, NULL); }
◍ 用字典容器管住挂单对象的生命周期
在 CStrategy 这套面向对象框架里,挂单不是靠系统函数随手查,而是被封装成 CPendingOrder 对象。麻烦在于:真实挂单被删了,内存里的对象若还活着,EA 会误以为单子还在,进而下出重复或反向的错单。外汇和贵金属市场滑点快、删单频繁,这种不同步在高波动时段尤其容易引发连错。 COrdersEnvironment 就是专门兜住这层同步风险的。它只暴露两个入口:Total 直接回传 OrdersTotal() 的系统值,不会自己算错;GetOrder 按索引取单,内部先调 OrderGetTicket,拿不到合法 ticket(返回 0)就直接给 NULL,从根上杜绝访问已消失的挂单。 对象创建走的是「按需 new + 字典缓存」的路子。m_orders 这个 CDictionary 以 ticket 为唯一键,第一次碰某个 ID 才 new 一个 CPendingOrder 并塞进容器,之后同 ID 直接返旧引用。好处很实:一是避免重复构造浪费资源,二是用户永远不用手动 delete,容器随环境对象析构一起清掉。 下面这段是环境类的核心源码,注意 GetOrder 里 ContainsKey 的判断顺序——先查缓存、再 OrderSelect 校验,逻辑反了就会每次重建。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 用于操作挂单的一个类 class=class="str">"cmt">//+------------------------------------------------------------------+ class COrdersEnvironment { class="kw">private: CDictionary m_orders; class=class="str">"cmt">// 挂单的总数 class="kw">public: COrdersEnvironment(class="type">void); class="type">int Total(class="type">void); CPendingOrder* GetOrder(class="type">int index); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 我们需要知道当前货币对及EA的编号 class=class="str">"cmt">//+------------------------------------------------------------------+ COrdersEnvironment::COrdersEnvironment(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回一个挂单 class=class="str">"cmt">//+------------------------------------------------------------------+ CPendingOrder* COrdersEnvironment::GetOrder(class="type">int index) { class="type">class="kw">ulong ticket = OrderGetTicket(index); if(ticket == class="num">0) class="kw">return NULL; if(!m_orders.ContainsKey(ticket)) class="kw">return m_orders.GetObjectByKey(ticket); if(OrderSelect(ticket)) class="kw">return NULL; CPendingOrder* order = new CPendingOrder(ticket); m_orders.AddObject(ticket, order); class="kw">return order; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回挂单的数量 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int COrdersEnvironment::Total(class="type">void) { class="kw">return OrdersTotal(); }
EA里挂单的接管与改价逻辑
在重写挂单执行逻辑时,PendingOrders 容器本质是一个 COrdersEnvironment 实例,搜索方式与系统函数一致:按索引 i 取 CPendingOrder 对象,取不到或订单不属于本 EA 就跳过。判断归属用 IsMain,比对订单的货币对与 magic 编号,一致才视为自家挂单。 若挂单类型为 ORDER_TYPE_BUY_STOP 且未激活,代码将其修正位置设为当前 Ask 加 0.25%(即 Ask() + Ask()*0.0025)。修改动作走 Modify 重载版,可顺带改其他参数。注意一个反直觉点:操作完挂单后,不要手动删订单对象,PendingOrders 自己管 CPendingOrder 生命周期,用户函数里留着引用即可。 当循环跑完 buy_stop_total 还是 0,说明没有任何自家 buy stop,这时才用 Trade.BuyStop 丢一个新挂单,挂单价同样偏移 0.25%。外汇与贵金属杠杆高,这类偏移阈值在跳空行情可能直接不成交或滑点放大,建议在 MT5 策略测试器用真实点差回放验证。 下面这段是 InitBuy 里挂单处理的骨架,重点看循环内的归属判断与改价,以及末尾的计数开单:
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| 当快速MA在慢速MA之下卖出 </span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> CMovingAverage::InitBuy(<span class="keyword">class="kw">const</span> MarketEvent &event) { <span class="keyword">if</span>(!IsTrackEvents(event))<span class="keyword">class="kw">return</span>; class=class="str">"cmt">// 仅处理所需事件 <span class="keyword">if</span>(positions.open_buy > <span class="number">class="num">0</span>) <span class="keyword">class="kw">return</span>; class=class="str">"cmt">// 如果已经存在至少一个未平仓的买单,无需再买入 <span class="keyword">class="type">int</span> buy_stop_total = <span class="number">class="num">0</span>; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = PendingOrders.Total()-<span class="number">class="num">1</span>; i >= <span class="number">class="num">0</span>; i--) { CPendingOrder* Order = PendingOrders.GetOrder(i); <span class="keyword">if</span>(Order == <span class="macro">NULL</span> || !Order.IsMain(ExpertSymbol(), ExpertMagic())) <span class="keyword">class="kw">continue</span>; <span class="keyword">if</span>(Order.Type() == <span class="macro">ORDER_TYPE_BUY_STOP</span>) { buy_stop_total++; Order.Modify(Ask() + Ask()*<span class="number">class="num">0.0025</span>); } <span style="background-class="type">color:rgb(class="num">255, class="num">246, class="num">200);"><span class="stroke">class=class="str">"cmt">//删除订单;</span><span style="background-class="type">color:rgb(class="num">255, class="num">246, class="num">200);">没有必要删除订单对象!</span> </span> } <span class="keyword">if</span>(buy_stop_total == <span class="number">class="num">0</span>) Trade.BuyStop(MM.GetLotFixed(), Ask() + Ask()*<span class="number">class="num">0.0025</span>, ExpertSymbol(), <span class="number">class="num">0</span>, <span class="number">class="num">0</span>, <span class="macro">NULL</span>); }
「CImpulse 挂单触发与均线平仓机制」
CImpulse 策略在每个新柱开盘时,按开盘价上下各 0.1% 的距离同时挂出 BuyStop 与 SellStop。若某一单在该柱内被触及,即视为价格出现剧烈单向运动,挂单转持仓。 持仓管理只用一条简单移动平均线:价格回踩到均线即平仓。实测图中多头情景里,黑色三角标出柱起点,0.1% 外挂单之一触发后开多,待某根 K 线收在红色均线下方,EA 以蓝色三角标记平掉该多单。 一个易坑点:BuyStop 挂价可能低于均线,触发后价格已在均线下方,会被立刻反手平掉。原版逻辑在 BuyInit / SellInit 里补了一道校验——仅当市价高于均线才挂 BuyStop,低于均线才挂 SellStop,避开秒平。 MT5 对冲账户可同柱多空并存,但本策略多空独立清算:柱开价高于均线清空、价跌穿均线清多,与是否对冲账户无关。开测前需在开户界面勾选对冲选项。
◍ CImpulse类的轻量挂单实现
新EA的核心是一个名为CImpulse的策略类,它被刻意做得极简:不挂日志函数,也不从XML解析参数,目的只是把挂单逻辑讲清楚。类里只保留一个m_percent字段用来描述挂单相对价格的百分比偏移,以及InitBuy/InitSell等几个虚函数钩子。 配置入口在mq5主文件,外部参数StopPercent默认0.05,即挂单触发价距离参考价约5%以内。EA通过#include引入Strategy和MovingAverage两个基础模块,枚举ORDER_TASK_DELETE与ORDER_TASK_MODIFY区分删单与改单两种任务。 InitBuy函数展示了实战中的过滤:先由IsTrackEvents判断是否追踪该事件,若当前已有open_buy持仓(positions.open_buy>0)直接退出,避免重复堆单。外汇与贵金属杠杆高,这类过滤能显著降低非理性加仓概率。 把下面代码直接丢进MT5的MQL5/Include/Strategy目录编译,可验证挂单类骨架是否跑通;调StopPercent看BuyStop距离变化最直观。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Impulse.mqh | class=class="str">"cmt">//| Copyright class="num">2016, Vasiliy Sokolov. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2016, Vasiliy Sokolov." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#include <Strategy\Strategy.mqh> class="macro">#include <Strategy\Indicators\MovingAverage.mqh> input class="type">class="kw">double StopPercent = class="num">0.05; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 定义挂单相关操作 class=class="str">"cmt">//| class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_ORDER_TASK { ORDER_TASK_DELETE, class=class="str">"cmt">// 删除挂单 ORDER_TASK_MODIFY class=class="str">"cmt">// 修改挂单 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CImpulse策略 class=class="str">"cmt">//+------------------------------------------------------------------+ class CImpulse : class="kw">public CStrategy { class="kw">private: class="type">class="kw">double m_percent; class=class="str">"cmt">// 挂单位置的百分比值 class="type">bool IsTrackEvents(class="kw">const MarketEvent &event); class="kw">protected: class="kw">virtual class="type">void InitBuy(class="kw">const MarketEvent &event); class="kw">virtual class="type">void InitSell(class="kw">const MarketEvent &event); class="kw">virtual class="type">void SupportBuy(class="kw">const MarketEvent &event,CPosition *pos); class="kw">virtual class="type">void SupportSell(class="kw">const MarketEvent &event,CPosition *pos); class="kw">virtual class="type">void OnSymbolChanged(class="type">class="kw">string new_symbol); class="kw">virtual class="type">void OnTimeframeChanged(ENUM_TIMEFRAMES new_tf); class="kw">public: class="type">class="kw">double GetPercent(class="type">void); class="type">void SetPercent(class="type">class="kw">double percent); CIndMovingAverage Moving; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 建立一个BuyStop多头挂单 class=class="str">"cmt">//| class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CImpulse::InitBuy(class="kw">const MarketEvent &event) { if(!IsTrackEvents(event))class="kw">return; if(positions.open_buy > class="num">0) class="kw">return; class="type">int buy_stop_total = class="num">0; ENUM_ORDER_TASK task;