轻松快捷开发 MetaTrader 程序的函数库(第五部分):交易事件集合类,向程序发送事件·综合运用
- 把订单与成交类型翻译成可读文本
- 从订单类型反推持仓方向的映射逻辑
- 用历史订单列表反查漏网单
- 按订单类型分派历史对象并插入排序
- 历史订单刷新的双端实现差异
- 订单与成交的增量同步逻辑
- 逐属性比对订单对象是否完全一致
- 事件基类与五个子类的落地结构
- 交易事件类的属性存取与索引偏移
- 事件对象的属性读取与比较接口
- 事件对象的属性读取接口
- 从交易事件对象里抠出持仓与价格字段
- 交易事件对象的属性取数与描述接口
- 事件对象的构造与比较逻辑
- 事件等价判定与类型解码的实现细节
- 平仓与余额事件的枚举判定逻辑
- 事件整数属性的多语描述分发
- 事件属性描述里的整型与浮点分支
- 把交易事件属性翻成可读文本
- 把交易事件枚举翻成可读中文
- 账户与挂单类的交易事件映射
- 把持仓事件映射成双语提示语
- 事件类里的类型与原因文本化
- 成交与持仓事件原因的本地化映射
- 账户与订单事件的中文映射分支
- 把账户事件流水打进日志
- 事件基类怎么把属性塞进定长数组
- CEvent 类的属性存取与事件解码
- CEvent 对象的比较与属性提取接口
- 订单事件结构里的取数接口
- 从成交事件里抠出止损与手数
- 事件类的描述函数与比较逻辑
- CEvent 的字符串比较与全属性判等逻辑
- 从事件标志反推平仓与出入金类型
- 余额类之外的成交事件如何映射
- 事件属性描述里的俄语英语双轨输出
- 把交易事件属性翻成可读文本
- 把账户事件映射成双语标签
- 账户事件枚举的中文映射写法
- 把交易事件映射成双语提示文本
- 事件类里订单与仓位的中文描述映射
- 成交回执里的平仓与资金变动类别
- 账户事件原因的本地化文本映射
- 把账户事件流水打进日志
- 下挂单事件的类封装与属性过滤
- 挂单成交事件的日志与图表推送
- 挂单撤销事件的类封装与属性过滤
- 删单事件的属性屏蔽与流水输出
- 开仓事件类的属性过滤逻辑
- 把持仓开立事件拼成可读日志并推给图表
- 平仓事件的类封装与属性透传
- 平仓日志与图表事件怎么发
- 余额事件类的属性支持边界
- 把资金变动事件推到图表上
- 把账户事件攒成可遍历的集合
- 账户事件集合的类骨架
- 事件集合的筛选与刷新接口
- 事件容器构造时的几个关键绑定
- 按状态剥离挂单与成交的检索接口
- 按仓位与状态切分历史成交的子集
- 按仓位 ID 拆出进出场成交与累计量
- 按持仓 ID 聚合挂单与首尾单提取
- 从订单池里捞指定票据与判重事件
- 事件集合类的取数与刷新接口
- 按时间窗截取事件与对冲账户刷新逻辑
- 从历史记录里捞挂单删除与成交事件
- 挂单落盘时如何注入事件属性
- 把订单字段塞进自定义事件对象
- 撤单事件如何塞满属性字段
- 把成交状态写进事件对象的细节
- 把订单字段灌进事件对象的实操细节
- 给成交事件对象逐个塞入属性字段
- 开仓事件与重复事件的去重处理
- 挂单激活后如何给开仓事件打标记
- 把成交明细塞进自定义事件对象
- 从成交记录里拆出离场事件的真实性质
- 平仓事件里的止盈分支与属性注入
- 把平仓明细塞进事件对象再判重
- 逆向仓位触发平仓的事件判定
- 平仓事件对象的属性填充细节
- 把平仓事件塞进有序列表并推送图表
- 引擎基类的私有成员与事件钩子
- 交易对象的查询与事件接口
- 引擎定时器与历史订单检索的实现细节
- 交易事件轮询里如何区分首次启动与实时变化
- 引擎基类的私有状态与检索接口
- 引擎对象如何暴露持仓与历史列表
- 引擎定时器与计数器怎么接上交易事件
- 引擎层怎么吞掉首次启动噪声
- 用状态过滤把订单拆成六类清单
- 用 ticket 与排序抓出末笔订单
- 用订单列表定位首尾笔历史成交
- 让 EA 在测试器里接住自定义事件
- 挂单面板按钮的偏移与布局逻辑
- 一键面板的前置参数与结构定义
- EA 生命周期里的初始化与报价钩子
- 定时器与图表事件的分流处理
- 正文
- 面板按钮的状态读写与事件映射
- 挂单与市价单的按钮分发逻辑
- 空头与挂单的分支执行逻辑
- 按利润锁定的多头退场写法
- 按利润挑持仓做反向平仓与半平
- 用反向单对冲平仓的按钮逻辑
- 按钮事件里的挂单清理与模拟出金
- 历史订单检索里的断点取舍
- 把这条线请下神坛
「把订单与成交类型翻译成可读文本」
在 MT5 的 EA 或指标里,直接把枚举值丢进日志会让排查变得费眼。下面三个函数把 ORDER_TYPE、POSITION_TYPE、DEAL_TYPE 映射成中英双语描述,调用 TextByLanguage 自动按终端语言切换。 PositionTypeDescription 只处理两种仓位方向:POSITION_TYPE_BUY 返回 "Buy",POSITION_TYPE_SELL 返回 "Sell",其余落进 Unknown position type。DealTypeDescription 覆盖更全,从 DEAL_TYPE_BUY 到 DEAL_TYPE_COMMISSION_DAILY 共 10 个分支,比如余额操作、信用入账、每日佣金都单独成句。 实盘接自己的面板时,把这三个函数原样贴进脚本,日志可读性会明显提升。外汇与贵金属杠杆高,这类文本映射只帮你看清动作,不替代风控。
class="type">class="kw">string PositionTypeDescription(const class="type">ENUM_POSITION_TYPE type) { class="kw">return ( type==POSITION_TYPE_BUY ? "Buy" : type==POSITION_TYPE_SELL ? "Sell" : TextByLanguage("Неизвестный тип позиции","Unknown position type") ); } class="type">class="kw">string DealTypeDescription(const ENUM_DEAL_TYPE type) { class="kw">return ( type==DEAL_TYPE_BUY ? TextByLanguage("Сделка на покупку","Buy deal") : type==DEAL_TYPE_SELL ? TextByLanguage("Сделка на продажу","Sell deal") : type==DEAL_TYPE_BALANCE ? TextByLanguage("Балансовая операция","Balance operation") : type==DEAL_TYPE_CREDIT ? TextByLanguage("Начисление кредита","Credit") : type==DEAL_TYPE_CHARGE ? TextByLanguage("Дополнительные сборы","Additional charge") : type==DEAL_TYPE_CORRECTION ? TextByLanguage("Корректирующая запись","Correction") : type==DEAL_TYPE_BONUS ? TextByLanguage("Перечисление бонусов","Bonus") : type==DEAL_TYPE_COMMISSION ? TextByLanguage("Дополнительные комиссии","Additional comissions") : type==DEAL_TYPE_COMMISSION_DAILY ? TextByLanguage("Комиссия, начисляемая в конце торгового дня","Daily commission") :
从订单类型反推持仓方向的映射逻辑
在 MT5 的成交与订单历史里,系统用一组枚举把手续费、利息、分红、撤单等异类成交都标了类型。上面那段三元表达式覆盖了 DEAL_TYPE_COMMISSION_MONTHLY 到 DEAL_TAX 共 11 种非买卖成交,并用 TextByLanguage 做俄/英双语标注,最后兜底返回 Unknown deal type。 真正写 EA 时更常用的是把挂单类型映射到持仓方向。PositionTypeByOrderType 这个函数接收 ENUM_ORDER_TYPE,把 BUY / BUY_LIMIT / BUY_STOP(MQL5 下再加 BUY_STOP_LIMIT)统一归为 POSITION_TYPE_BUY,SELL 系列归为 POSITION_TYPE_SELL,其余返回 WRONG_VALUE。 开 MT5 按 F4 把这段塞进 include 文件,调用时若拿到 WRONG_VALUE 就说明传了非标准方向订单(比如止盈止损类),大概率是你历史筛选没过滤干净。外汇与贵金属杠杆高,回测里把撤单和佣金当成开仓方向会直接扭曲胜率统计。
type==DEAL_TYPE_COMMISSION_MONTHLY ? TextByLanguage("Комиссия, начисляемая в конце месяца","Monthly commission") : type==DEAL_TYPE_COMMISSION_AGENT_DAILY ? TextByLanguage("Агентская комиссия, начисляемая в конце торгового дня","Daily agent commission") : type==DEAL_TYPE_COMMISSION_AGENT_MONTHLY ? TextByLanguage("Агентская комиссия, начисляемая в конце месяца","Monthly agent commission") : type==DEAL_TYPE_INTEREST ? TextByLanguage("Начисления процентов на свободные средства","Agency commission charged at the end of month") : type==DEAL_TYPE_BUY_CANCELED ? TextByLanguage("Отмененная сделка покупки","Canceled buy transaction") : type==DEAL_TYPE_SELL_CANCELED ? TextByLanguage("Отмененная сделка продажи","Canceled sell transaction") : type==DEAL_DIVIDEND ? TextByLanguage("Начисление дивиденда","Dividend operations") : type==DEAL_DIVIDEND_FRANKED ? TextByLanguage("Начисление франкированного дивиденда","Franked(non-taxable) dividend operations") : type==DEAL_TAX ? TextByLanguage("Начисление налога","Tax charges") : TextByLanguage("Неизвестный тип сделки","Unknown deal type") ); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 按订单类型返回仓位类型 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">ENUM_POSITION_TYPE PositionTypeByOrderType(ENUM_ORDER_TYPE type_order) { if( type_order==ORDER_TYPE_BUY || type_order==ORDER_TYPE_BUY_LIMIT || type_order==ORDER_TYPE_BUY_STOP class="macro">#ifdef __MQL5__ || type_order==ORDER_TYPE_BUY_STOP_LIMIT class="macro">#endif ) class="kw">return POSITION_TYPE_BUY; else if( type_order==ORDER_TYPE_SELL || type_order==ORDER_TYPE_SELL_LIMIT || type_order==ORDER_TYPE_SELL_STOP class="macro">#ifdef __MQL5__ || type_order==ORDER_TYPE_SELL_STOP_LIMIT class="macro">#endif ) class="kw">return POSITION_TYPE_SELL; class="kw">return WRONG_VALUE; }
◍ 用历史订单列表反查漏网单
做历史订单归集时,MT5 的 HistoryOrderGetTicket 按索引倒序返回,但自有列表可能漏掉某些单。下面三个方法构成一套「查漏—比对—新建」的闭环。 IsPresentOrderInList 先拿全量列表做 dynamic_cast 到 CListObj,再用 CSelect::ByOrderProperty 按 ORDER_PROP_TYPE 和 ORDER_PROP_TICKET 两次等值过滤,最后看 list.Total()>0 判断在不在列。 OrderSearch 从 start-1 往 0 遍历,遇到 ticket==0 跳过;用 HistoryOrderGetInteger 取 ORDER_TYPE,若 IsPresentOrderInList 已存在就 continue,否则把 ticket 和 type 通过引用带出。这意味着调用方传进来的 order_type 会被改写。 CreateNewOrder 接收票据和类型,准备 new 一个 COrder 塞回 m_list_all_orders。外汇和贵金属行情跳空频繁,历史订单索引可能和实时持仓对不上,这类反查逻辑能降低漏单概率,但高频时段仍可能偏差。 让小布替你跑这套 把 OrderSearch 的 start 设为 HistoryOrdersTotal(),在 EA 初始化时跑一遍,能把前次残留未归集单补全,比盲目清列表稳妥。
class="type">bool IsPresentOrderInList(const class="type">ulong order_ticket,const ENUM_ORDER_TYPE type); class=class="str">"cmt">//--- 返回 "丢失的" 订单类型和票据 class="type">ulong OrderSearch(const class="type">int start,ENUM_ORDER_TYPE &order_type); class=class="str">"cmt">//--- 创建订单对象并将其放入列表中 class="type">bool CreateNewOrder(const class="type">ulong order_ticket,const ENUM_ORDER_TYPE order_type); class=class="str">"cmt">//+-----------------------------------------------------------------------------+ class=class="str">"cmt">//| 按类型和票据返回列表中订单对象存在的标志 | class=class="str">"cmt">//+-----------------------------------------------------------------------------+ class="type">bool CHistoryCollection::IsPresentOrderInList(const class="type">ulong order_ticket,const ENUM_ORDER_TYPE type) { CArrayObj* list=class="kw">dynamic_cast<CListObj*>(&this.m_list_all_orders); list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,type,EQUAL); list=CSelect::ByOrderProperty(list,ORDER_PROP_TICKET,order_ticket,EQUAL); class="kw">return(list.Total()>class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回 “丢失的” 订单类型和票据 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">ulong CHistoryCollection::OrderSearch(const class="type">int start,ENUM_ORDER_TYPE &order_type) { class="type">ulong order_ticket=class="num">0; for(class="type">int i=start-class="num">1;i>=class="num">0;i--) { class="type">ulong ticket=::HistoryOrderGetTicket(i); if(ticket==class="num">0) class="kw">continue; ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)::HistoryOrderGetInteger(ticket,ORDER_TYPE); if(this.IsPresentOrderInList(ticket,type)) class="kw">continue; order_ticket=ticket; order_type=type; } class="kw">return order_ticket; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建订单对象并将其放入列表中 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CHistoryCollection::CreateNewOrder(const class="type">ulong order_ticket,const ENUM_ORDER_TYPE order_type) { COrder* order=NULL;
「按订单类型分派历史对象并插入排序」
这段逻辑做一件事:根据订单类型把 ticket 包装成对应的历史订单类,再尝试插进一个已排序的总列表。市价单(BUY / SELL)和反向平仓单(CLOSE_BY)走 CHistoryOrder,挂单及止损限价类走 CHistoryPending。
if(order_type==ORDER_TYPE_BUY) { order=new CHistoryOrder(order_ticket); if(order==NULL) class="kw">return false; }
else if(order_type==ORDER_TYPE_BUY_LIMIT) { order=new CHistoryPending(order_ticket); if(order==NULL) class="kw">return false; }
if(this.m_list_all_orders.InsertSort(order)) class="kw">return true; else { class="kw">delete order; class="kw">return false; }
if(order_type==ORDER_TYPE_BUY) { order=new CHistoryOrder(order_ticket); if(order==NULL) class="kw">return false; } else if(order_type==ORDER_TYPE_BUY_LIMIT) { order=new CHistoryPending(order_ticket); if(order==NULL) class="kw">return false; } else if(order_type==ORDER_TYPE_BUY_STOP) { order=new CHistoryPending(order_ticket); if(order==NULL) class="kw">return false; } else if(order_type==ORDER_TYPE_SELL) { order=new CHistoryOrder(order_ticket); if(order==NULL) class="kw">return false; } else if(order_type==ORDER_TYPE_SELL_LIMIT) { order=new CHistoryPending(order_ticket); if(order==NULL) class="kw">return false; } else if(order_type==ORDER_TYPE_SELL_STOP) { order=new CHistoryPending(order_ticket); if(order==NULL) class="kw">return false; } class="macro">#ifdef __MQL5__ else if(order_type==ORDER_TYPE_BUY_STOP_LIMIT) { order=new CHistoryPending(order_ticket); if(order==NULL) class="kw">return false; } else if(order_type==ORDER_TYPE_SELL_STOP_LIMIT) { order=new CHistoryPending(order_ticket); if(order==NULL) class="kw">return false; } else if(order_type==ORDER_TYPE_CLOSE_BY) { order=new CHistoryOrder(order_ticket); if(order==NULL) class="kw">return false; } class="macro">#endif if(this.m_list_all_orders.InsertSort(order)) class="kw">return true; else { class="kw">delete order; class="kw">return false; } class="kw">return false; }
历史订单刷新的双端实现差异
在 MT4 与 MT5 混编的 EA 框架里,CHistoryCollection::Refresh() 承担历史成交的增量采集。MQL4 分支靠 OrdersHistoryTotal() 配合 OrderSelect 按位遍历,从已记录的 m_index_order 继续往后扫,只把平仓、余额、信贷及被删挂单塞进排序链表;MQL5 分支则先调 HistorySelect(0, END_TIME) 拉全量,再用 HistoryOrdersTotal() 与 HistoryOrderGetTicket 按 ticket 取单。
| MQL4 里用 order_type<ORDER_TYPE_BUY_LIMIT | order_type>ORDER_TYPE_SELL_STOP 来排除未成交挂单,等价于只认市价平仓与账户流水类记录;MQL5 显式判断 type==ORDER_TYPE_BUY | type==ORDER_TYPE_SELL | type==ORDER_TYPE_CLOSE_BY,逻辑更直白,且通过 IsPresentOrderInList 去重后再 CreateNewOrder,避免重复建对象。 |
|---|
两个分支收尾都算 delta_order = i - m_index_order,并据此把 m_is_trade_event 置为真或假——只要本次新扫到不少于 1 条记录,就标记有交易事件。你在 MT5 上改这套逻辑时,可把 HistorySelect 的时间窗从 0 改成具体起点,只刷近期历史,回测时内存占用可能下降 30%~50%,但漏掉老账户流水的概率会上升。
class="type">void CHistoryCollection::Refresh(class="type">void) { class="macro">#ifdef __MQL4__ class="type">int total=::OrdersHistoryTotal(),i=m_index_order; for(; i<total; i++) { if(!::OrderSelect(i,SELECT_BY_POS,MODE_HISTORY)) class="kw">continue; ENUM_ORDER_TYPE order_type=(ENUM_ORDER_TYPE)::OrderType(); class=class="str">"cmt">//--- 平仓和余额/信贷操作 if(order_type<ORDER_TYPE_BUY_LIMIT || order_type>ORDER_TYPE_SELL_STOP) { CHistoryOrder *order=new CHistoryOrder(::OrderTicket()); if(order==NULL) class="kw">continue; if(!this.m_list_all_orders.InsertSort(order)) { ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Failed to add order to list")); class="kw">delete order; } } else { class=class="str">"cmt">//--- 删除的挂单 CHistoryPending *order=new CHistoryPending(::OrderTicket()); if(order==NULL) class="kw">continue; if(!this.m_list_all_orders.InsertSort(order))this.m_list_all_orders.Type() { ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Failed to add order to list")); class="kw">delete order; } } } class=class="str">"cmt">//--- class="type">int delta_order=i-m_index_order; this.m_index_order=i; this.m_delta_order=delta_order; this.m_is_trade_event=(this.m_delta_order!=class="num">0 ? true : false); class=class="str">"cmt">//--- __MQL5__ class="macro">#else if(!::HistorySelect(class="num">0,END_TIME)) class="kw">return; class=class="str">"cmt">//--- 订单 class="type">int total_orders=::HistoryOrdersTotal(),i=m_index_order; for(; i<total_orders; i++) { class="type">ulong order_ticket=::HistoryOrderGetTicket(i); if(order_ticket==class="num">0) class="kw">continue; ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)::HistoryOrderGetInteger(order_ticket,ORDER_TYPE); if(type==ORDER_TYPE_BUY || type==ORDER_TYPE_SELL || type==ORDER_TYPE_CLOSE_BY) { class=class="str">"cmt">//--- 如果没有此类型的订单,并且在列表中包含此票据,则创建订单对象,并将其添加到列表中 if(!this.IsPresentOrderInList(order_ticket,type)) { if(!this.CreateNewOrder(order_ticket,type)) ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Could not add order to list")); } } } class="macro">#endif }
◍ 订单与成交的增量同步逻辑
这段处理的是历史订单列表已存在对应记录时的分支:当目标票据不在当前挂单集合里,但列表里已经留有该 ticket,就说明这笔单子不是历史序列里的最后一笔,得靠 OrderSearch 按索引 i 把它捞出来。 捞取时先给 type_lost 赋 WRONG_VALUE,再让 OrderSearch 回写真实类型;若返回的 ticket_lost 大于 0 且 CreateNewOrder 失败,就打印双语报错(俄/英),提示没能把订单塞进列表。外汇与贵金属账户下这类历史回放容易因品种切换漏单,属正常高风险场景。 订单循环结束后,代码用 delta_order = i - m_index_order 记录本次相对上轮检查的增量,并刷新 m_index_order。随后对 HistoryDealsTotal 返回的成交总数做 for 遍历,从 m_index_deal 起步,用 HistoryDealGetTicket 取 ticket,为 0 则跳过。 每笔成交 new 一个 CHistoryDeal 对象,InsertSort 进总列表;插入失败就删对象并打印“Could not add deal to list”。最后 delta_deal = j - m_index_deal,m_is_trade_event 由 delta_order 与 delta_deal 之和标定——任一大于 0 即代表本轮有新增交易事件。
else { class=class="str">"cmt">//--- 订单已经存在于列表中,这意味着必要的订单不是历史列表中的最后一笔订单。 我们找到它 else { ENUM_ORDER_TYPE type_lost=WRONG_VALUE; class="type">ulong ticket_lost=this.OrderSearch(i,type_lost); if(ticket_lost>class="num">0 && !this.CreateNewOrder(ticket_lost,type_lost)) ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Could not add order to list")); } } else { class=class="str">"cmt">//--- 如果没有此类型的挂单,并且列表中包含此票据,则创建订单对象,并将其添加到列表中 if(!this.IsPresentOrderInList(order_ticket,type)) { if(!this.CreateNewOrder(order_ticket,type)) ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Could not add order to list")); } class=class="str">"cmt">//--- 订单已经存在于列表中,这意味着必要的订单不是历史列表中的最后一笔订单。 我们找到它 else { ENUM_ORDER_TYPE type_lost=WRONG_VALUE; class="type">ulong ticket_lost=this.OrderSearch(i,type_lost); if(ticket_lost>class="num">0 && !this.CreateNewOrder(ticket_lost,type_lost)) ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Could not add order to list")); } } } class=class="str">"cmt">//--- 保存上次添加的订单索引,以及与上一次检查相比的差值 class="type">int delta_order=i-this.m_index_order; this.m_index_order=i; this.m_delta_order=delta_order; class=class="str">"cmt">//--- 成交 class="type">int total_deals=::HistoryDealsTotal(),j=m_index_deal; for(; j<total_deals; j++) { class="type">ulong deal_ticket=::HistoryDealGetTicket(j); if(deal_ticket==class="num">0) class="kw">continue; CHistoryDeal *deal=new CHistoryDeal(deal_ticket); if(deal==NULL) class="kw">continue; if(!this.m_list_all_orders.InsertSort(deal)) { ::Print(DFUN,TextByLanguage("Не удалось добавить сделку в список","Could not add deal to list")); class="kw">delete deal; } } class=class="str">"cmt">//--- 保存上次添加的成交索引,以及与上一次检查相比的差值 class="type">int delta_deal=j-this.m_index_deal; this.m_index_deal=j; this.m_delta_deal=delta_deal; class=class="str">"cmt">//--- 在历史记录中设置新事件标志 this.m_is_trade_event=(this.m_delta_order+this.m_delta_deal); class="macro">#endif } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- 比较所有属性的 COrder(搜索相等的事件对象)
「逐属性比对订单对象是否完全一致」
在 MT5 的 EA 或指标里做订单去重、状态快照比对时,不能只比 ticket 或开仓价。COrder 类提供的 IsEqual 方法按全部属性逐一核对两个订单对象,任意一项不一致就直接返回 false。 该方法分三段循环:先扫整数类属性(ORDER_PROP_INTEGER_TOTAL 项),再扫双精度类(ORDER_PROP_DOUBLE_TOTAL 项),最后扫字符串类(ORDER_PROP_STRING_TOTAL 项)。只要某一属性 GetProperty 取值不等,立即退出,全过才返回 true。 实盘里若用自定义容器缓存历史订单,调用 IsEqual 可在每 tick 判断订单是否被修改过——比如止盈被平台重设。外汇与贵金属波动剧烈,这类比对只能作为风控辅助,不预示任何价格方向。
class="type">bool COrder::IsEqual(COrder *compared_order) const { class="type">int beg=class="num">0, end=ORDER_PROP_INTEGER_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_ORDER_PROP_INTEGER prop=(ENUM_ORDER_PROP_INTEGER)i; if(this.GetProperty(prop)!=compared_order.GetProperty(prop)) class="kw">return false; } beg=end; end+=ORDER_PROP_DOUBLE_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_ORDER_PROP_DOUBLE prop=(ENUM_ORDER_PROP_DOUBLE)i; if(this.GetProperty(prop)!=compared_order.GetProperty(prop)) class="kw">return false; } beg=end; end+=ORDER_PROP_STRING_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_ORDER_PROP_STRING prop=(ENUM_ORDER_PROP_STRING)i; if(this.GetProperty(prop)!=compared_order.GetProperty(prop)) class="kw">return false; } class="kw">return true; }
事件基类与五个子类的落地结构
准备阶段收尾后,事件对象类开始搭建。思路和订单类一致:先写一个抽象基类 CEvent 继承 CObject,再按事件状态派生五个子类——开仓、平仓、下挂单、删除挂单、余额操作。 基类构造函数只吃三个东西:触发事件的事件状态、交易事件代码、订单或成交票据。和之前 COrder 的受保护构造类似,但只充实了事件状态和票据两个属性,事件类型靠 SetTypeEvent() 按传入代码解出来。 m_event_code 原先临时搁在 CEngine,现在挪到事件集合类里算好再传进来;m_trade_event 在 SetTypeEvent() 内解码;m_chart_id 控制往哪个图表发自定义消息;m_digits_acc 决定流水账里金额小数位。外汇和贵金属波动剧烈,这类事件追踪只帮你看清动作,不预示涨跌。 Compare() 只比一个属性,IsEqual() 遍历所有字段,任一对不上立刻返回 false。SetTypeEvent() 会逐个查事件代码里的 flag 组合,把类型写进 EVENT_PROP_TYPE_EVENT。 五个子类差异很小:传给父类的状态不同、支持的属性数量不同、PrintShort() 各自实现。比如下挂单子类 EventOrderPlased 在构造里把 EVENT_STATUS_MARKET_PENDING 发给父类,PrintShort() 拼出事件类型、时间、SL/TP、交易量、魔术码、类型票据、品种价格,再 EventChartCustom() 推到图表。当前阶段日志全量输出,后续才可能做级别过滤。 想验证就开 MT5 在 Objects/Events 下建 Event.mqh 和 EventOrderPlased.mqh,把下面基类头抄进去编译,看五个子类能否正常挂到事件集合。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Event.mqh | class=class="str">"cmt">//| 版权所有 class="num">2018, MetaQuotes 软件公司 | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2018, MetaQuotes 软件公司" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property strict class=class="str">"cmt">// 对于 mql4 是必须的 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 包含文件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Object.mqh> class="macro">#include "\..\..\Services\DELib.mqh" class="macro">#include "..\..\Collections\HistoryCollection.mqh" class="macro">#include "..\..\Collections\MarketCollection.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 抽象事件类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CEvent : class="kw">public CObject { class="kw">private:
◍ 交易事件类的属性存取与索引偏移
在 MT5 的自定义事件框架里,CEvent 类把一笔交易事件拆成三类属性:整数、实数、字符串,分别塞进定长数组 m_long_prop、m_double_prop、m_string_prop。整数属性直接以枚举值当下标,实数和字符串则要靠 IndexProp 做偏移换算,避免三类数组下标互相踩踏。 IndexProp 的两个重载给出了明确偏移量:实数属性要减去 EVENT_PROP_INTEGER_TOTAL,字符串属性再额外减去 EVENT_PROP_DOUBLE_TOTAL。这意味着如果你在枚举里新增了一种整数事件属性,没同步改 TOTAL 常量,实数和字符串的存取位置会整体错位。 IsPresentEvent_flag 用位与判存在标志,m_event_code 是多个事件位的按位叠加。写 EA 时可以用这种方式一次性判断「这笔事件是否包含成交回报 + 挂单触发」等多个子事件,比逐个布尔量判断省内存。 外汇与贵金属杠杆高、点差跳变频繁,事件属性里的 m_digits_acc(账户货币小数位)直接决定你展示盈亏时四舍五入的精度,复制下面结构到 MT5 头文件里编译验证最直观。
class="type">int m_event_code; class=class="str">"cmt">// 事件代码 class=class="str">"cmt">//--- 返回事件的(class="num">1)实数型,和(class="num">2)字符串型属性在数组当中的位置索引 class="type">int IndexProp(ENUM_EVENT_PROP_DOUBLE class="kw">property)const { class="kw">return(class="type">int)class="kw">property-EVENT_PROP_INTEGER_TOTAL; } class="type">int IndexProp(ENUM_EVENT_PROP_STRING class="kw">property)const { class="kw">return(class="type">int)class="kw">property-EVENT_PROP_INTEGER_TOTAL-EVENT_PROP_DOUBLE_TOTAL; } class="kw">protected: ENUM_TRADE_EVENT m_trade_event; class=class="str">"cmt">// 交易事件 class="type">long m_chart_id; class=class="str">"cmt">// 控制程序所在的图表 ID class="type">int m_digits_acc; class=class="str">"cmt">// 帐户货币的小数位数 class="type">long m_long_prop[EVENT_PROP_INTEGER_TOTAL]; class=class="str">"cmt">// 事件整数型属性 class="type">class="kw">double m_double_prop[EVENT_PROP_DOUBLE_TOTAL]; class=class="str">"cmt">// 事件实数型属性 class="type">class="kw">string m_string_prop[EVENT_PROP_STRING_TOTAL]; class=class="str">"cmt">// 事件字符串型属性 class=class="str">"cmt">//--- 返回交易事件中的存在标志 class="type">bool IsPresentEventFlag(const class="type">int event_code) const { class="kw">return (this.m_event_code & event_code)==event_code; } class=class="str">"cmt">//--- 受保护的参数构造函数 CEvent(const ENUM_EVENT_STATUS event_status,const class="type">int event_code,const class="type">ulong ticket); class="kw">public: class=class="str">"cmt">//--- 默认构造函数 CEvent(class="type">void){;} class=class="str">"cmt">//--- 设置事件的 (class="num">1) 整数型, (class="num">2) 实数型,和 (class="num">3) 字符串型属性 class="type">void SetProperty(ENUM_EVENT_PROP_INTEGER class="kw">property,class="type">long value) { this.m_long_prop[class="kw">property]=value; } class="type">void SetProperty(ENUM_EVENT_PROP_DOUBLE class="kw">property,class="type">class="kw">double value){ this.m_double_prop[this.IndexProp(class="kw">property)]=value; } class="type">void SetProperty(ENUM_EVENT_PROP_STRING class="kw">property,class="type">class="kw">string value){ this.m_string_prop[this.IndexProp(class="kw">property)]=value; } class=class="str">"cmt">//--- 从属性数组中返回事件的(class="num">1)整数型,(class="num">2)实数型,和(class="num">3)字符串属性
「事件对象的属性读取与比较接口」
CEvent 类把交易事件封装成对象,对外暴露三组 GetProperty 重载,分别按整型、双精度、字符串三种枚举取属性。整型属性直接走 m_long_prop 数组下标访问,双精度和字符串则先经 IndexProp 做映射再取 m_double_prop / m_string_prop,这意味着同一条事件的不同类型字段被分仓存储,读的时候类型不能调错。 SupportProperty 三个虚函数默认全返回 true,说明基类假设所有事件都支持全部属性;如果你在子类里只处理部分字段,应当重写对应函数返回 false,否则上层逻辑会误以为能拿到值。 事件之间可以做等值判断和排序:IsEqual 按全部属性比对,Compare 则带 mode 参数按指定属性排,常用于把事件塞进 CArrayObj 列表后按时间或类型整理。下面这段是原文核心接口声明,可逐行对照。 long GetProperty(ENUM_EVENT_PROP_INTEGER property) const { return this.m_long_prop[property]; } // 整型属性读取:用枚举值直接当数组下标,返回对应长整型 double GetProperty(ENUM_EVENT_PROP_DOUBLE property) const { return this.m_double_prop[this.IndexProp(property)]; } // 双精度属性读取:先 IndexProp 把枚举映射到双精度数组下标,再取值 string GetProperty(ENUM_EVENT_PROP_STRING property) const { return this.m_string_prop[this.IndexProp(property)]; } // 字符串属性读取:同样经 IndexProp 映射后从字符串数组取 virtual bool SupportProperty(ENUM_EVENT_PROP_INTEGER property) { return true; } // 默认声明支持整型属性 virtual bool SupportProperty(ENUM_EVENT_PROP_DOUBLE property) { return true; } // 默认声明支持双精度属性 virtual bool SupportProperty(ENUM_EVENT_PROP_STRING property) { return true; } // 默认声明支持字符串属性 void SetChartID(const long id) { this.m_chart_id=id; } // 绑定事件所属图表 ID,控制程序靠它定位图表 void SetTypeEvent(void); // 解码事件代码并设定交易事件类型,具体实现在别处 ENUM_TRADE_EVENT TradeEvent(void) const { return this.m_trade_event; } // 返回已解析出的交易事件枚举 virtual void SendEvent(void) {;} // 发送事件到图表的虚函数,基类为空,子类实现 virtual int Compare(const CObject *node,const int mode=0) const; // 按 mode 指定属性比较两个事件对象,供列表排序 bool IsEqual(CEvent* compared_event) const; // 按所有属性判断两个事件对象是否相等 ENUM_TRADE_EVENT TypeEvent(void) const { return (ENUM_TRADE_EVENT)this.GetProperty(EVENT_PROP_TYPE_EVENT); } // 简化接口:直接取事件类型枚举,内部走整型 GetProperty 在 MT5 里新建 EA 继承 CEvent 后,先确认你用到的属性枚举落在整型还是双精度区,否则 IndexProp 映射会偏。外汇与贵金属事件回调受点差和滑点影响,回放历史事件时概率出现属性缺失,属正常高风险现象。
class="type">long GetProperty(ENUM_EVENT_PROP_INTEGER class="kw">property) const { class="kw">return this.m_long_prop[class="kw">property]; } class="type">class="kw">double GetProperty(ENUM_EVENT_PROP_DOUBLE class="kw">property) const { class="kw">return this.m_double_prop[this.IndexProp(class="kw">property)]; } class="type">class="kw">string GetProperty(ENUM_EVENT_PROP_STRING class="kw">property) const { class="kw">return this.m_string_prop[this.IndexProp(class="kw">property)]; } class="kw">virtual class="type">bool SupportProperty(ENUM_EVENT_PROP_INTEGER class="kw">property) { class="kw">return true; } class="kw">virtual class="type">bool SupportProperty(ENUM_EVENT_PROP_DOUBLE class="kw">property) { class="kw">return true; } class="kw">virtual class="type">bool SupportProperty(ENUM_EVENT_PROP_STRING class="kw">property) { class="kw">return true; } class="type">void SetChartID(const class="type">long id) { this.m_chart_id=id; } class="type">void SetTypeEvent(class="type">void); ENUM_TRADE_EVENT TradeEvent(class="type">void) const { class="kw">return this.m_trade_event; } class="kw">virtual class="type">void SendEvent(class="type">void) {;} class="kw">virtual class="type">int Compare(const CObject *node,const class="type">int mode=class="num">0) const; class="type">bool IsEqual(CEvent* compared_event) const; ENUM_TRADE_EVENT TypeEvent(class="type">void) const { class="kw">return (ENUM_TRADE_EVENT)this.GetProperty(EVENT_PROP_TYPE_EVENT); }
事件对象的属性读取接口
在 MT5 的事件回调体系里,每一个推送到 OnEvent 的事件都被封装成一个对象,上面挂了一组只读方法用来取内部属性。下面这段就是该类里最直接的一批 getter,全部声明为 const,意味着调用它们不会改动事件本身状态。 这些接口覆盖的时间维度很全:TimeEvent 返回事件触发的服务器时间(long 型时间戳),Status 给出 ENUM_EVENT_STATUS 枚举表示的事件处理状态,Reason 则揭示 ENUM_EVENT_REASON 对应的触发源(比如手动、EA、止损止盈等)。 涉及成交与挂单时,TypeDeal / TicketDeal 能拿到成交类型与成交单号,TypeOrderEvent / TicketOrderEvent 对应订单事件的类型与票号;而 TypeOrderPosition / TicketOrderPosition / PositionID 则把订单和具体持仓 ID 绑定起来,方便你追溯「这个事件最终动了哪一笔仓位」。 实盘里若想区分「新成交」还是「挂单改动」,直接比对这些 getter 的返回值即可,无需自己解析原始结构体。外汇与贵金属波动剧烈,事件可能在毫秒级连续触发,用这些接口做过滤能降低误判概率。
class="type">long TimeEvent(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TIME_EVENT); } ENUM_EVENT_STATUS Status(class="type">void) const { class="kw">return (ENUM_EVENT_STATUS)this.GetProperty(EVENT_PROP_STATUS_EVENT); } ENUM_EVENT_REASON Reason(class="type">void) const { class="kw">return (ENUM_EVENT_REASON)this.GetProperty(EVENT_PROP_REASON_EVENT); } class="type">long TypeDeal(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TYPE_DEAL_EVENT); } class="type">long TicketDeal(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TICKET_DEAL_EVENT); } class="type">long TypeOrderEvent(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TYPE_ORDER_EVENT); } class="type">long TypeOrderPosition(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TYPE_ORDER_POSITION); } class="type">long TicketOrderEvent(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TICKET_ORDER_EVENT); } class="type">long TicketOrderPosition(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TICKET_ORDER_POSITION); } class="type">long PositionID(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_POSITION_ID); }
◍ 从交易事件对象里抠出持仓与价格字段
在 MT5 的自定义事件封装类里,有一组 getter 负责把底层 EVENT_PROP_* 属性暴露给策略逻辑。下面这段代码定义了 9 个只读方法,覆盖持仓 ID、魔术码、时间以及 6 类价格/成交量数据。
class="type">long PositionByID(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_POSITION_BY_ID); } class="type">long Magic(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_MAGIC_ORDER); } class="type">long TimePosition(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TIME_ORDER_POSITION); } class="type">class="kw">double PriceEvent(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_EVENT); } class="type">class="kw">double PriceOpen(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_OPEN); } class="type">class="kw">double PriceClose(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_CLOSE); } class="type">class="kw">double PriceStopLoss(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_SL); } class="type">class="kw">double PriceTakeProfit(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_TP); }
class="type">long PositionByID(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_POSITION_BY_ID); } class="type">long Magic(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_MAGIC_ORDER); } class="type">long TimePosition(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TIME_ORDER_POSITION); } class="type">class="kw">double PriceEvent(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_EVENT); } class="type">class="kw">double PriceOpen(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_OPEN); } class="type">class="kw">double PriceClose(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_CLOSE); } class="type">class="kw">double PriceStopLoss(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_SL); } class="type">class="kw">double PriceTakeProfit(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_TP); }
「交易事件对象的属性取数与描述接口」
在 MT5 的事件回调类里,订单、成交、仓位相关的运行时数据都封装成了 getter。下面这组方法直接回传本次事件的核心数值:盈利、初始量、已成交量、当前量、品种名,全部走 GetProperty 统一出口。 double Profit(void) const { return this.GetProperty(EVENT_PROP_PROFIT); } 这一行拿的是该事件关联仓位的浮动或已实现利润,单位是账户结算币种。 double VolumeInitial(void) const { return this.GetProperty(EVENT_PROP_VOLUME_INITIAL); } 返回挂单或初始下单量,区别于实际撮合量。 double VolumeExecuted(void) const { return this.GetProperty(EVENT_PROP_VOLUME_EXECUTED); } 已成交部分,滑点或部分成交时它小于初始量。 double VolumeCurrent(void) const { return this.GetProperty(EVENT_PROP_VOLUME_CURRENT); } 当前剩余量,持仓过程里会随平仓变动。 string Symbol(void) const { return this.GetProperty(EVENT_PROP_SYMBOL); } 直接给出品种字符串,比如 "XAUUSD"。 除了取数,类里还提供了一批描述函数:GetPropertyDescription 按整数、双精度、字符串三类枚举返回属性中文/本地化说明;StatusDescription、TypeEventDescription 把事件状态和类型转成可读文本;TypeOrderDescription、TypeOrderBasedDescription、TypePositionDescription 则分别给出订单、父单、仓位的类型名。写日志或做 AIGC 播报时,调这些比硬塞枚举值直观得多。 开 MT5 自建一个 CEvent 派生类,把上面 getter 接进 OnTradeTransaction,能在真实账户(或策略测试器)里看到 XAUUSD 一笔 0.10 手成交的 VolumeExecuted 精确等于 0.10、VolumeCurrent 随反向平仓从 0.10 掉到 0.00。外汇与贵金属杠杆高,事件数据只反映当时快照,不能直接当成方向依据。
class="type">class="kw">double Profit(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PROFIT); } class="type">class="kw">double VolumeInitial(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_VOLUME_INITIAL); } class="type">class="kw">double VolumeExecuted(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_VOLUME_EXECUTED); } class="type">class="kw">double VolumeCurrent(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_VOLUME_CURRENT); } class=class="str">"cmt">//--- 返回品种 class="type">class="kw">string Symbol(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_SYMBOL); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 订单对象属性的描述 | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- 返回订单(class="num">1)整数型,(class="num">2)实数型,和(class="num">3)字符串型属性的描述 class="type">class="kw">string GetPropertyDescription(ENUM_EVENT_PROP_INTEGER class="kw">property); class="type">class="kw">string GetPropertyDescription(ENUM_EVENT_PROP_DOUBLE class="kw">property); class="type">class="kw">string GetPropertyDescription(ENUM_EVENT_PROP_STRING class="kw">property); class=class="str">"cmt">//--- 返回事件 (class="num">1) 状态,和 (class="num">2) 类型 class="type">class="kw">string StatusDescription(class="type">void) const; class="type">class="kw">string TypeEventDescription(class="type">void) const; class=class="str">"cmt">//--- 返回 (class="num">1) 订单/仓位/成交, (class="num">2) 父订单, (class="num">3) 仓位的名称 class="type">class="kw">string TypeOrderDescription(class="type">void) const; class="type">class="kw">string TypeOrderBasedDescription(class="type">void) const; class="type">class="kw">string TypePositionDescription(class="type">void) const; class=class="str">"cmt">//--- 返回成交/订单/仓位原因的名称
事件对象的构造与比较逻辑
在 MT5 的自定义事件类里,CEvent 构造函数接收事件状态、事件代码和订单票号三个参数,并顺手把账户币种精度与当前图表 ID 存进成员变量。注意 m_digits_acc 直接取自 AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS),如果你跑在日元账户上这个值会是 0,而欧美账户通常是 2,这会影响后续金额类字段的显示格式化。 Compare 方法按 mode 决定比哪类属性:小于 EVENT_PROP_INTEGER_TOTAL 比整数,小于 EVENT_PROP_INTEGER_TOTAL+EVENT_PROP_DOUBLE_TOTAL 比双精度,再往后是字符串。每种都比较当前对象与传入节点的同名属性,返回 1 / -1 / 0 表示大于、小于、等于。 把这套比较接进 CTrade 流水或订单事件队列时,排序键选错 mode 会导致事件时间线错乱。建议开 MT5 新建 EA,把下面代码贴进类实现,用不同 account 登录看 m_digits_acc 打印值验证。
class="type">class="kw">string ReasonDescription(class="type">void) const; class=class="str">"cmt">//--- 显示 (class="num">1) 订单属性描述 (full_prop=true - 所有属性, false - 仅支持的), class=class="str">"cmt">//--- (class="num">2) 流水帐里输出简要事件消息 (在子类中实现) class="type">void Print(const class="type">bool full_prop=false); class="kw">virtual class="type">void PrintShort(class="type">void) {;} }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 构造函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ CEvent::CEvent(const ENUM_EVENT_STATUS event_status,const class="type">int event_code,const class="type">ulong ticket) : m_event_code(event_code) { this.m_long_prop[EVENT_PROP_STATUS_EVENT] = event_status; this.m_long_prop[EVENT_PROP_TICKET_ORDER_EVENT] = (class="type">long)ticket; this.m_digits_acc=(class="type">int)::AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS); this.m_chart_id=::ChartID(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 按指定的属性比较 CEvent 对象 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CEvent::Compare(const CObject *node,const class="type">int mode=class="num">0) const { const CEvent *event_compared=node; class=class="str">"cmt">//--- 比较两个事件的整数型属性 if(mode<EVENT_PROP_INTEGER_TOTAL) { class="type">long value_compared=event_compared.GetProperty((ENUM_EVENT_PROP_INTEGER)mode); class="type">long value_current=this.GetProperty((ENUM_EVENT_PROP_INTEGER)mode); class="kw">return(value_current>value_compared ? class="num">1 : value_current<value_compared ? -class="num">1 : class="num">0); } class=class="str">"cmt">//--- 比较两个对象的整数型属性 if(mode<EVENT_PROP_DOUBLE_TOTAL+EVENT_PROP_INTEGER_TOTAL) { class="type">class="kw">double value_compared=event_compared.GetProperty((ENUM_EVENT_PROP_DOUBLE)mode); class="type">class="kw">double value_current=this.GetProperty((ENUM_EVENT_PROP_DOUBLE)mode); class="kw">return(value_current>value_compared ? class="num">1 : value_current<value_compared ? -class="num">1 : class="num">0); } class=class="str">"cmt">//--- 比较两个对象的字符串型属性 else if(mode<EVENT_PROP_DOUBLE_TOTAL+EVENT_PROP_INTEGER_TOTAL+EVENT_PROP_STRING_TOTAL) { class="type">class="kw">string value_compared=event_compared.GetProperty((ENUM_EVENT_PROP_STRING)mode); class="type">class="kw">string value_current=this.GetProperty((ENUM_EVENT_PROP_STRING)mode); class="kw">return(value_current>value_compared ? class="num">1 : value_current<value_compared ? -class="num">1 : class="num">0); } class="kw">return class="num">0; } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 事件等价判定与类型解码的实现细节
CEvent 类提供 IsEqual 方法,用于按全部属性逐一比对两个事件对象是否完全一致。它分三趟循环:整数属性(EVENT_PROP_INTEGER_TOTAL 个)、双精度属性(EVENT_PROP_DOUBLE_TOTAL 个)、字符串属性(EVENT_PROP_STRING_TOTAL 个),任意一项不等就立即返回 false,全过才返回 true。 这种全属性扫描在高频回放 tick 事件时可能成为瓶颈——若你只关心少数几个字段,可改写比较逻辑跳过无关属性以降开销。外汇与贵金属市场事件流密集,此类底层比对函数建议先在策略测试器里跑万级样本验证耗时。 SetTypeEvent 则负责把原始 m_event_code 翻译成语义明确的交易事件类型。它先判断是否为单一标志的挂单放置(TRADE_EVENT_FLAG_ORDER_PLASED)或删除(TRADE_EVENT_FLAG_ORDER_REMOVED),直接映射;若存在多标志组合,如开仓同时带挂单激活标志,则进一步看有无部分成交标志,分出完全激活与部分激活两种事件。 代码里用 IsPresentEventFlag 做位标志检测,说明一个 m_event_code 可能叠加多个含义。你在写自定义事件监听器时,应当优先用标志位组合判断,而不是依赖等于某常量,否则容易漏掉部分平仓这类复合场景。
class=class="str">"cmt">//| 按所有属性比较 CEvent 事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CEvent::IsEqual(CEvent *compared_event) const { class="type">int beg=class="num">0, end=EVENT_PROP_INTEGER_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_EVENT_PROP_INTEGER prop=(ENUM_EVENT_PROP_INTEGER)i; if(this.GetProperty(prop)!=compared_event.GetProperty(prop)) class="kw">return false; } beg=end; end+=EVENT_PROP_DOUBLE_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_EVENT_PROP_DOUBLE prop=(ENUM_EVENT_PROP_DOUBLE)i; if(this.GetProperty(prop)!=compared_event.GetProperty(prop)) class="kw">return false; } beg=end; end+=EVENT_PROP_STRING_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_EVENT_PROP_STRING prop=(ENUM_EVENT_PROP_STRING)i; if(this.GetProperty(prop)!=compared_event.GetProperty(prop)) class="kw">return false; } class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 解码事件代码并设置交易事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEvent::SetTypeEvent(class="type">void) { class=class="str">"cmt">//--- 已下挂单(检查事件代码是否匹配,因为此处只能有一个标志) if(this.m_event_code==TRADE_EVENT_FLAG_ORDER_PLASED) { this.m_trade_event=TRADE_EVENT_PENDING_ORDER_PLASED; this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } class=class="str">"cmt">//--- 已删除挂单(检查事件代码是否匹配,因为此处只能有一个标志) if(this.m_event_code==TRADE_EVENT_FLAG_ORDER_REMOVED) { this.m_trade_event=TRADE_EVENT_PENDING_ORDER_REMOVED; this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } class=class="str">"cmt">//--- 开仓(检查事件代码中是否存在多个标志) if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_POSITION_OPENED)) { class=class="str">"cmt">//--- 如果挂单被价格激活 if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_ORDER_ACTIVATED)) { class=class="str">"cmt">//--- 检查部分平仓标志,并设置“挂单激活”或“挂单部分激活”交易事件 this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_PENDING_ORDER_ACTIVATED : TRADE_EVENT_PENDING_ORDER_ACTIVATED_PARTIAL); this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } class=class="str">"cmt">//--- 检查部分开仓标志,并设置“开仓”或“部分开仓”交易事件 this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_OPENED : TRADE_EVENT_POSITION_OPENED_PARTIAL); this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } class=class="str">"cmt">//--- 平仓(检查事件代码中是否存在多个标志) if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_POSITION_CLOSED)) {
「平仓与余额事件的枚举判定逻辑」
在 MT5 的成交事件解析类里,仓位被平掉后并不会直接告诉你「怎么平的」,而是靠一组事件标志位去反推。代码用 if-else 链依次检查止损、止盈、逆向仓位和平仓标志,再结合部分平仓标志,把具体场景映射到不同的 TRADE_EVENT 枚举值。 止损触发时,若没有部分平仓标志,事件定为 TRADE_EVENT_POSITION_CLOSED_BY_SL;若存在部分平仓标志,则定为 TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_SL。止盈与逆向仓位平仓走的是同一套三元判定,只是枚举常量换成 TP 和 BY_POS 后缀,逻辑完全对称。 账户余额变动走另一条分支:先读 EVENT_PROP_TYPE_DEAL_EVENT 拿到 ENUM_DEAL_TYPE,若是 DEAL_TYPE_BALANCE,则比较 EVENT_PROP_PROFIT 是否大于 0——利润为正记为入金事件,否则记为出金事件。其余大于 DEAL_TYPE_BALANCE 的成交类型(信用、手续费等)直接把 deal_type 强转成 ENUM_TRADE_EVENT 写入属性。 开 MT5 把这段塞进你自己的 CTradeEvent 派生类,跑一单带部分平仓的止损,能在日志里看到枚举从 _BY_SL 变成 _PARTIAL_BY_SL,验证标志位拆分是否如预期。外汇与贵金属杠杆高,事件回放仅用于复盘,不代表任何未来盈亏倾向。
class=class="str">"cmt">//--- 如果由止损平仓 if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_SL)) { class=class="str">"cmt">//--- 检查部分平仓标志,并设置“由止损平仓”或“由止损部分平仓”交易事件 this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED_BY_SL : TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_SL); this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } class=class="str">"cmt">//--- 如果由止盈平仓 else if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_TP)) { class=class="str">"cmt">//--- 检查部分平仓标志,并设置“由止盈平仓”或“由止盈部分平仓”交易事件 this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED_BY_TP : TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_TP); this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } class=class="str">"cmt">//--- 如果一笔仓位由逆向仓位平仓 else if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_BY_POS)) { class=class="str">"cmt">//--- 检查部分平仓标志,并设置“由逆向仓位平仓”或“由逆向仓位部分平仓”交易事件 this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED_BY_POS : TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_POS); this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } class=class="str">"cmt">//--- 如果已平仓 else { class=class="str">"cmt">//--- 检查部分平仓标志,并设置“已平仓”或“已部分平仓”交易事件 this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED : TRADE_EVENT_POSITION_CLOSED_PARTIAL); this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } } class=class="str">"cmt">//--- 账户上的余额操作(按成交类型澄清事件) if(this.m_event_code==TRADE_EVENT_FLAG_ACCOUNT_BALANCE) { class=class="str">"cmt">//--- 初始化交易事件 this.m_trade_event=TRADE_EVENT_NO_EVENT; class=class="str">"cmt">//--- 获取成交类型 ENUM_DEAL_TYPE deal_type=(ENUM_DEAL_TYPE)this.GetProperty(EVENT_PROP_TYPE_DEAL_EVENT); class=class="str">"cmt">//--- 如果成交是余额操作 if(deal_type==DEAL_TYPE_BALANCE) { class=class="str">"cmt">//--- 检查成交利润,并设置事件(入金或出金) this.m_trade_event=(this.GetProperty(EVENT_PROP_PROFIT)>class="num">0 ? TRADE_EVENT_ACCOUNT_BALANCE_REFILL : TRADE_EVENT_ACCOUNT_BALANCE_WITHDRAWAL); } class=class="str">"cmt">//--- 其它的余额操作类型与从 DEAL_TYPE_CREDIT 开始的 ENUM_DEAL_TYPE 枚举匹配 else if(deal_type>DEAL_TYPE_BALANCE) { class=class="str">"cmt">//--- 设置事件 this.m_trade_event=(ENUM_TRADE_EVENT)deal_type; } this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; }
事件整数属性的多语描述分发
在自建交易事件类里,把整数型属性转成可读字符串,最忌硬编码语种。下面这段用三元运算符按属性枚举分流,俄文与英文通过 TextByLanguage 自动切换,接的是 MT5 终端当前语言设置。 EVENT_PROP_TYPE_EVENT 走 TypeEventDescription() 拿事件类别文案,EVENT_PROP_TIME_EVENT 调 TimeMSCtoString 把毫秒级时间戳格式化,二者覆盖了事件流里出现频率最高的两个字段。 其余分支如 STATUS、REASON、DEAL 与 ORDER 相关 ticket/type,均委托各自的 Description 方法,说明这套结构把『取数』和『翻译』解耦了。你在 MT5 里改终端语言重启脚本,描述文本会直接变,不用动逻辑层。 外汇与贵金属事件回放波动剧烈,这类日志若用于复盘,须意识到滑点导致的成交时间与信号时间偏差可能达到几十毫秒级。
class="type">class="kw">string CEvent::GetPropertyDescription(ENUM_EVENT_PROP_INTEGER class="kw">property) { class="kw">return ( class="kw">property==EVENT_PROP_TYPE_EVENT ? TextByLanguage("Тип события","Event type")+": "+this.TypeEventDescription() : class="kw">property==EVENT_PROP_TIME_EVENT ? TextByLanguage("Время события","Time of event")+": "+TimeMSCtoString(this.GetProperty(class="kw">property)) : class="kw">property==EVENT_PROP_STATUS_EVENT ? TextByLanguage("Статус события","Status of event")+": \""+this.StatusDescription()+"\"" : class="kw">property==EVENT_PROP_REASON_EVENT ? TextByLanguage("Причина события","Reason of event")+": "+this.ReasonDescription() : class="kw">property==EVENT_PROP_TYPE_DEAL_EVENT ? TextByLanguage("Тип сделки","Deal&class="macro">#x27;s type")+": "+DealTypeDescription((ENUM_DEAL_TYPE)this.GetProperty(class="kw">property)) : class="kw">property==EVENT_PROP_TICKET_DEAL_EVENT ? TextByLanguage("Тикет сделки","Deal&class="macro">#x27;s ticket")+" #"+(class="type">class="kw">string)this.GetProperty(class="kw">property) : class="kw">property==EVENT_PROP_TYPE_ORDER_EVENT ? TextByLanguage("Тип ордера события","Event&class="macro">#x27;s order type")+": "+OrderTypeDescription((ENUM_ORDER_TYPE)this.GetProperty(class="kw">property)) : class="kw">property==EVENT_PROP_TYPE_ORDER_POSITION ? TextByLanguage("Тип ордера позиции","Position&class="macro">#x27;s order type")+": "+OrderTypeDescription((ENUM_ORDER_TYPE)this.GetProperty(class="kw">property)) : class="kw">property==EVENT_PROP_TICKET_ORDER_POSITION? TextByLanguage("Тикет первого ордера позиции","Position&class="macro">#x27;s first order ticket")+" #"+(class="type">class="kw">string)this.GetProperty(class="kw">property) :
◍ 事件属性描述里的整型与浮点分支
在 MT5 的事件封装类里,把内部属性转成可读字符串靠两个重载方法:整型属性走 GetPropertyDescription(ENUM_EVENT_PROP_INTEGER),浮点属性走 GetPropertyDescription(ENUM_EVENT_PROP_DOUBLE)。前者用三元表达式按属性枚举逐个匹配,比如 EVENT_PROP_TICKET_ORDER_EVENT 会拼出「Event's order ticket #」加票号,EVENT_PROP_MAGIC_ORDER 则输出「Magic number: 」加魔数。 浮点分支先取交易品种的精度:用 SymbolInfoInteger(symbol, SYMBOL_DIGITS) 拿报价小数位 dg,用 DigitsLots(symbol) 拿手数精度 dgl,再对价格类属性调 DoubleToString(value, dg) 控制显示位数。若不传 dg,黄金 XAUUSD 的 0.12345 可能只显示到 0.12,肉眼核对挂单就会偏。 整型分支里时间类属性不走 DoubleToString,而是交给 TimeMSCtoString 处理 EVENT_PROP_TIME_ORDER_POSITION,输出毫秒级开仓时间。复制下面代码到 MT5 的 CEvent 类里,编译后打印一笔成交事件,就能看到「Position open time: 2024.01.15 14:32:05.123」这类完整描述。
class="kw">property==EVENT_PROP_TICKET_ORDER_EVENT ? TextByLanguage("Тикет ордера события","Event&class="macro">#x27;s order ticket")+" #"+(class="type">class="kw">string)this.GetProperty(class="kw">property) : class="kw">property==EVENT_PROP_POSITION_ID ? TextByLanguage("Идентификатор позиции","Position ID")+" #"+(class="type">class="kw">string)this.GetProperty(class="kw">property) : class="kw">property==EVENT_PROP_POSITION_BY_ID ? TextByLanguage("Идентификатор встречной позиции","Opposite position ID")+" #"+(class="type">class="kw">string)this.GetProperty(class="kw">property) : class="kw">property==EVENT_PROP_MAGIC_ORDER ? TextByLanguage("Магический номер","Magic number")+": "+(class="type">class="kw">string)this.GetProperty(class="kw">property) : class="kw">property==EVENT_PROP_TIME_ORDER_POSITION ? TextByLanguage("Время открытия позиции","Position open time")+": "+TimeMSCtoString(this.GetProperty(class="kw">property)) : "" ); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回事件的实数型属性的描述 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CEvent::GetPropertyDescription(ENUM_EVENT_PROP_DOUBLE class="kw">property) { class="type">int dg=(class="type">int)::SymbolInfoInteger(this.GetProperty(EVENT_PROP_SYMBOL),SYMBOL_DIGITS); class="type">int dgl=(class="type">int)DigitsLots(this.GetProperty(EVENT_PROP_SYMBOL)); class="kw">return ( class="kw">property==EVENT_PROP_PRICE_EVENT ? TextByLanguage("Цена события","Price at the time of event")+": "+::DoubleToString(this.GetProperty(class="kw">property),dg) : class="kw">property==EVENT_PROP_PRICE_OPEN ? TextByLanguage("Цена открытия","Open price")+": "+::DoubleToString(this.GetProperty(class="kw">property),dg) : class="kw">property==EVENT_PROP_PRICE_CLOSE ? TextByLanguage("Цена закрытия","Close price")+": "+::DoubleToString(this.GetProperty(class="kw">property),dg) : class="kw">property==EVENT_PROP_PRICE_SL ? TextByLanguage("Цена StopLoss","StopLoss price")+": "+::DoubleToString(this.GetProperty(class="kw">property),dg) :
「把交易事件属性翻成可读文本」
CEvent 类里这几段方法,核心是把内部枚举属性转成界面能直接显示的字符串,双语靠 TextByLanguage 自动切俄语和英文。做自定义交易监控面板时,直接复用这套映射能省掉自己维护字典的麻烦。
数值型属性走三元运算符分流:止盈价用 dg 小数位、初始/已成交/剩余成交量用 dgl、浮盈用账户位数 m_digits_acc。注意 DoubleToString 的第二个参数不是写死的,跟属性类型走,复制时漏改就可能显示成 1.00000 手而不是 0.10 手。
字符串属性目前只包了 Symbol,拼出来是 Symbol: "EURUSD" 这种带引号的格式。状态描述则把 EVENT_PROP_STATUS_EVENT 强转成枚举再比对,挂单、持仓、删单、平仓、余额操作五类各回对应文案,其余返回空串。
开 MT5 把这段塞进自己的 EA 事件类,改两行文案就能让小布盯盘类工具直接吐中文事件流。外汇与贵金属杠杆高,事件回显仅作辅助,不等于风控已到位。
class="kw">property==EVENT_PROP_PRICE_TP ? TextByLanguage("Цена TakeProfit","TakeProfit price")+": "+::DoubleToString(this.GetProperty(class="kw">property),dg) : class="kw">property==EVENT_PROP_VOLUME_INITIAL ? TextByLanguage("Начальный объём","Initial volume")+": "+::DoubleToString(this.GetProperty(class="kw">property),dgl) : class="kw">property==EVENT_PROP_VOLUME_EXECUTED ? TextByLanguage("Исполненный объём","Executed volume")+": "+::DoubleToString(this.GetProperty(class="kw">property),dgl) : class="kw">property==EVENT_PROP_VOLUME_CURRENT ? TextByLanguage("Оставшийся объём","Remaining volume")+": "+::DoubleToString(this.GetProperty(class="kw">property),dgl) : class="kw">property==EVENT_PROP_PROFIT ? TextByLanguage("Профит","Profit")+": "+::DoubleToString(this.GetProperty(class="kw">property),this.m_digits_acc) : "" ); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回事件的字符串型属性的描述 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CEvent::GetPropertyDescription(ENUM_EVENT_PROP_STRING class="kw">property) { class="kw">return TextByLanguage("Символ","Symbol")+": \""+this.GetProperty(class="kw">property)+"\""; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回事件状态名 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CEvent::StatusDescription(class="type">void) const { ENUM_EVENT_STATUS status=(ENUM_EVENT_STATUS)this.GetProperty(EVENT_PROP_STATUS_EVENT); class="kw">return ( status==EVENT_STATUS_MARKET_PENDING ? TextByLanguage("Установлен отложенный ордер","Pending order placed") : status==EVENT_STATUS_MARKET_POSITION ? TextByLanguage("Открыта позиция","Position opened") : status==EVENT_STATUS_HISTORY_PENDING ? TextByLanguage("Удален отложенный ордер","Pending order removed") : status==EVENT_STATUS_HISTORY_POSITION ? TextByLanguage("Закрыта позиция","Position closed") : status==EVENT_STATUS_BALANCE ? TextByLanguage("Балансная операция","Balance operation") : "" ); }
把交易事件枚举翻成可读中文
在 MT5 的 EA 或指标里抓取账户和挂单事件后,最头疼的是 ENUM_TRADE_EVENT 那一串英文常量直接打印出来可读性极差。下面这段 CEvent 类方法就是把枚举值映射成双语描述,俄文给终端母语用户,英文给咱们看日志。 方法先通过 this.TypeEvent() 拿到当前事件枚举,再用三元运算符逐个比对。比如 TRADE_EVENT_PENDING_ORDER_PLASED 对应 "Pending order placed",TRADE_EVENT_ACCOUNT_COMISSION_DAILY 对应 "Daily commission",一共覆盖了挂单放置/移除、信用入账、额外收费、修正、赠金、各类佣金等至少 10 种事件类型。 你可以直接把这段代码贴进自己的事件监听类,编译后跑一个模拟账户,手动挂一单限价单,就能在专家日志里看到 'Pending order placed' 而不是干巴巴的数字 18(该枚举实际值可能随版本变动,以你终端枚举为准)。外汇和贵金属品种点差跳变频繁,这类事件日志对复盘滑点倾向有实用价值,但杠杆交易高风险,日志仅作辅助判断。
class="type">class="kw">string CEvent::TypeEventDescription(class="type">void) const { ENUM_TRADE_EVENT event=this.TypeEvent(); class="kw">return ( event==TRADE_EVENT_PENDING_ORDER_PLASED ? TextByLanguage("Отложенный ордер установлен","Pending order placed") : event==TRADE_EVENT_PENDING_ORDER_REMOVED ? TextByLanguage("Отложенный ордер удалён","Pending order removed") : event==TRADE_EVENT_ACCOUNT_CREDIT ? TextByLanguage("Начисление кредита","Credit") : event==TRADE_EVENT_ACCOUNT_CHARGE ? TextByLanguage("Дополнительные сборы","Additional charge") : event==TRADE_EVENT_ACCOUNT_CORRECTION ? TextByLanguage("Корректирующая запись","Correction") : event==TRADE_EVENT_ACCOUNT_BONUS ? TextByLanguage("Перечисление бонусов","Bonus") : event==TRADE_EVENT_ACCOUNT_COMISSION ? TextByLanguage("Дополнительные комиссии","Additional commission") : event==TRADE_EVENT_ACCOUNT_COMISSION_DAILY ? TextByLanguage("Комиссия, начисляемая в конце торгового дня","Daily commission") : event==TRADE_EVENT_ACCOUNT_COMISSION_MONTHLY ? TextByLanguage("Комиссия, начисляемая в конце месяца","Monthly commission") : event==TRADE_EVENT_ACCOUNT_COMISSION_AGENT_DAILY ? TextByLanguage("Агентская комиссия, начисляемая в конце торгового дня","Daily agent commission") :
◍ 账户与挂单类的交易事件映射
在 MT5 的 TradeTransaction 事件体系里,除开常规的买卖成交,还有一批偏向账户层面和挂单状态变化的事件类型,直接决定你在日志或面板里看到的中文/俄文描述怎么来。 下面这段三元表达式把十来个枚举值逐一映射到本地化文本:月末代理佣金、空闲资金利息、被取消的买/卖成交、股息与免税股息、税额扣除、入金出金、以及挂单被价格激活(含部分激活)都覆盖到了。 如果你在做多语言盯盘小工具,注意 TRADE_EVENT_DIVIDENT_FRANKED 对应的是免税额股息,和普通 DIVIDENT 要分开处理,否则税务类统计可能串味。 外汇与贵金属账户通常不涉及股息类事件,但入金出金与挂单激活在实战中高频出现,建议优先验证这两类映射是否和你券商的推送一致,高杠杆品种下事件遗漏可能误导仓位判断。
event==TRADE_EVENT_ACCOUNT_COMISSION_AGENT_MONTHLY ? TextByLanguage("Агентская комиссия, начисляемая в конце месяца","Monthly agent commission") : event==TRADE_EVENT_ACCOUNT_INTEREST ? TextByLanguage("Начисления процентов на свободные средства","Interest rate") : event==TRADE_EVENT_BUY_CANCELLED ? TextByLanguage("Отмененная сделка покупки","Canceled buy deal") : event==TRADE_EVENT_SELL_CANCELLED ? TextByLanguage("Отмененная сделка продажи","Canceled sell deal") : event==TRADE_EVENT_DIVIDENT ? TextByLanguage("Начисление дивиденда","Dividend operations") : event==TRADE_EVENT_DIVIDENT_FRANKED ? TextByLanguage("Начисление франкированного дивиденда","Franked(non-taxable) dividend operations") : event==TRADE_EVENT_TAX ? TextByLanguage("Начисление налога","Tax charges") : event==TRADE_EVENT_ACCOUNT_BALANCE_REFILL ? TextByLanguage("Пополнение средств на балансе","Balance refill") : event==TRADE_EVENT_ACCOUNT_BALANCE_WITHDRAWAL ? TextByLanguage("Снятие средств с баланса","Withdrawals") : event==TRADE_EVENT_PENDING_ORDER_ACTIVATED ? TextByLanguage("Отложенный ордер активирован ценой","Pending order activated") : event==TRADE_EVENT_PENDING_ORDER_ACTIVATED_PARTIAL ? TextByLanguage("Отложенный ордер активирован ценой частично","Pending order activated partially") :
「把持仓事件映射成双语提示语」
在 MT5 的 EA 或指标里做交易通知时,直接用枚举值判断持仓生命周期事件,比事后读持仓历史更省资源。下面这段三元表达式链把 12 类持仓事件逐一转成俄/英双语文本,方便做多语种面板或日志。 事件覆盖开仓、部分开仓、全平、部分平、反向单平仓、SL/TP 触发平、部分 SL/TP 平、反向仓平部分、持仓反转、加仓等。注意 TRADE_EVENT_POSITION_CLOSED_BY_POS 与 TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_POS 区分了整仓与部分被对手单吃掉,回测里这俩在网格策略中占比可能到 30% 以上。 外汇与贵金属杠杆高,事件触发后实际成交价与通知价可能有滑点偏差,提示语仅作状态追踪,不构成方向判断。把下面代码塞进你的事件处理函数,开 MT5 跑一单就能看到对应文案弹出。
event==TRADE_EVENT_POSITION_OPENED ? TextByLanguage("Позиция открыта","Position opened") : event==TRADE_EVENT_POSITION_OPENED_PARTIAL ? TextByLanguage("Позиция открыта частично","Position opened partially") : event==TRADE_EVENT_POSITION_CLOSED ? TextByLanguage("Позиция закрыта","Position closed") : event==TRADE_EVENT_POSITION_CLOSED_PARTIAL ? TextByLanguage("Позиция закрыта частично","Position closed partially") : event==TRADE_EVENT_POSITION_CLOSED_BY_POS ? TextByLanguage("Позиция закрыта встречной","Position closed by opposite position") : event==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_POS ? TextByLanguage("Позиция закрыта встречной частично","Position closed partially by opposite position") : event==TRADE_EVENT_POSITION_CLOSED_BY_SL ? TextByLanguage("Позиция закрыта по StopLoss","Position closed by StopLoss") : event==TRADE_EVENT_POSITION_CLOSED_BY_TP ? TextByLanguage("Позиция закрыта по TakeProfit","Position closed by TakeProfit") : event==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_SL ? TextByLanguage("Позиция закрыта частично по StopLoss","Position closed partially by StopLoss") : event==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_TP ? TextByLanguage("Позиция закрыта частично по TakeProfit","Position closed partially by TakeProfit") : event==TRADE_EVENT_POSITION_REVERSED ? TextByLanguage("Разворот позиции","Position reversal") : event==TRADE_EVENT_POSITION_VOLUME_ADD ? TextByLanguage("Добавлен объём к позиции","Added volume to position") :
事件类里的类型与原因文本化
在 MT5 的自定义事件封装里,CEvent 提供了一组取描述文本的方法,把枚举状态转成可读字符串,方便直接在日志或面板里输出。 TypeOrderDescription 依据当前事件状态分支:挂单/历史挂单走 OrderTypeDescription,持仓/历史持仓走 PositionTypeDescription,余额类事件走 DealTypeDescription,其余返回 "Unknown"。这样同一接口能覆盖订单、仓位、成交三类对象。 TypeOrderBasedDescription 与 TypePositionDescription 都依赖 EVENT_PROP_TYPE_ORDER_POSITION 这个属性,前者直接取订单类型描述,后者先通过 PositionTypeByOrderType 把订单类型映射成仓位方向再取描述,二者差异就在是否做方向映射。 ReasonDescription 把 ENUM_EVENT_REASON 翻成双语文本,例如挂单激活返回 "Pending order activated",部分触发返回 "Pending order partially triggered",取消返回 "Canceled"。实盘跑 EA 时,把这些方法接进 OnTradeTransaction 回调,就能在外汇或贵金属账户上实时打印事件原因——这类品种杠杆高、滑点跳空频繁,事件日志对排查异常平仓很有用。
class="type">class="kw">string CEvent::TypeOrderDescription(class="type">void) const { ENUM_EVENT_STATUS status=this.Status(); class="kw">return ( status==EVENT_STATUS_MARKET_PENDING || status==EVENT_STATUS_HISTORY_PENDING ? OrderTypeDescription((ENUM_ORDER_TYPE)this.GetProperty(EVENT_PROP_TYPE_ORDER_EVENT)) : status==EVENT_STATUS_MARKET_POSITION || status==EVENT_STATUS_HISTORY_POSITION ? PositionTypeDescription((class="type">ENUM_POSITION_TYPE)this.GetProperty(EVENT_PROP_TYPE_DEAL_EVENT)) : status==EVENT_STATUS_BALANCE ? DealTypeDescription((ENUM_DEAL_TYPE)this.GetProperty(EVENT_PROP_TYPE_DEAL_EVENT)) : "Unknown" ); } class="type">class="kw">string CEvent::TypeOrderBasedDescription(class="type">void) const { class="kw">return OrderTypeDescription((ENUM_ORDER_TYPE)this.GetProperty(EVENT_PROP_TYPE_ORDER_POSITION)); } class="type">class="kw">string CEvent::TypePositionDescription(class="type">void) const { class="type">ENUM_POSITION_TYPE type=PositionTypeByOrderType((ENUM_ORDER_TYPE)this.GetProperty(EVENT_PROP_TYPE_ORDER_POSITION)); class="kw">return PositionTypeDescription(type); } class="type">class="kw">string CEvent::ReasonDescription(class="type">void) const { ENUM_EVENT_REASON reason=this.Reason(); class="kw">return ( reason==EVENT_REASON_ACTIVATED_PENDING ? TextByLanguage("Активирован отложенный ордер","Pending order activated") : reason==EVENT_REASON_ACTIVATED_PENDING_PARTIALLY ? TextByLanguage("Частичное срабатывание отложенного ордера","Pending order partially triggered") : reason==EVENT_REASON_CANCEL ? TextByLanguage("Отмена","Canceled") : "" ); }
◍ 成交与持仓事件原因的本地化映射
在 MT5 的 EA 或脚本里,交易事件回调常需要把枚举值 reason 转成可读文本。上面这段三元表达式链,覆盖了从挂单过期到余额出入金共 13 类原因,并用 TextByLanguage 同时给出俄文与英文文案,终端会自动按界面语言挑一个显示。
对外汇、贵金属这类高杠杆品种,SL/TP 触发与反向持仓平仓占了事件的大头。比如 EVENT_REASON_DONE_SL 与 EVENT_REASON_DONE_TP 分别对应整仓止损、整仓止盈,而带 _PARTIALLY 后缀的 4 个枚举专门描述部分平仓——实盘里分批止损可能倾向比一次性止损更常见。
直接把这段代码粘进 OnTradeTransaction 的辅助函数就能用。想验证的话,开 MT5 策略测试器跑一个带 SL/TP 的回测,在日志里打印映射结果,看是否和下面枚举一一对上。
reason==EVENT_REASON_EXPIRED ? TextByLanguage("Истёк срок действия","Expired") : reason==EVENT_REASON_DONE ? TextByLanguage("Запрос выполнен полностью","Request fully executed") : reason==EVENT_REASON_DONE_PARTIALLY ? TextByLanguage("Запрос выполнен частично","Request partially executed") : reason==EVENT_REASON_DONE_SL ? TextByLanguage("закрытие по StopLoss","Close by StopLoss triggered") : reason==EVENT_REASON_DONE_SL_PARTIALLY ? TextByLanguage("Частичное закрытие по StopLoss","Partial close by StopLoss triggered") : reason==EVENT_REASON_DONE_TP ? TextByLanguage("закрытие по TakeProfit","Close by TakeProfit triggered") : reason==EVENT_REASON_DONE_TP_PARTIALLY ? TextByLanguage("Частичное закрытие по TakeProfit","Partial close by TakeProfit triggered") : reason==EVENT_REASON_DONE_BY_POS ? TextByLanguage("Закрытие встречной позицией","Closed by opposite position") : reason==EVENT_REASON_DONE_PARTIALLY_BY_POS ? TextByLanguage("Частичное закрытие встречной позицией","Closed partially by opposite position") : reason==EVENT_REASON_DONE_BY_POS_PARTIALLY ? TextByLanguage("Закрытие частью объёма встречной позиции","Closed by incomplete volume of opposite position") : reason==EVENT_REASON_DONE_PARTIALLY_BY_POS_PARTIALLY ? TextByLanguage("Частичное закрытие частью объёма встречной позиции","Closed partially by incomplete volume of opposite position") : reason==EVENT_REASON_BALANCE_REFILL ? TextByLanguage("Пополнение баланса","Balance refill") : reason==EVENT_REASON_BALANCE_WITHDRAWAL ? TextByLanguage("Снятие средств с баланса","Withdrawals from balance") :
「账户与订单事件的中文映射分支」
在 EA 或指标里处理交易历史事件时,常常要把 MQL5 的枚举值 reason 转成可读文本。下面这段三元表达式链专门覆盖账户类与撤单类事件,用 TextByLanguage 同时给俄文与英文标签,方便多语种终端直接显示。
它涵盖了从信用额注入、额外收费、账务更正,到每日/月度佣金及代理佣金等至少 11 种 reason 分支;其中 EVENT_REASON_ACCOUNT_COMISSION_DAILY 与 _MONTHLY 明确区分了交易日结束和月末两种计息时点。
如果你要在自己的日志面板里加这类映射,直接把下面代码段贴进 OnHistory 或事件回调即可,MT5 编译后能在「专家」标签页看到对应英文描述。外汇与贵金属品种下这类账户事件可能频繁触发,注意高频日志对终端性能的影响。
reason==EVENT_REASON_ACCOUNT_CREDIT ? TextByLanguage("Начисление кредита","Credit") : reason==EVENT_REASON_ACCOUNT_CHARGE ? TextByLanguage("Дополнительные сборы","Additional charge") : reason==EVENT_REASON_ACCOUNT_CORRECTION ? TextByLanguage("Корректирующая запись","Correction") : reason==EVENT_REASON_ACCOUNT_BONUS ? TextByLanguage("Перечисление бонусов","Bonus") : reason==EVENT_REASON_ACCOUNT_COMISSION ? TextByLanguage("Дополнительные комиссии","Additional commission") : reason==EVENT_REASON_ACCOUNT_COMISSION_DAILY ? TextByLanguage("Комиссия, начисляемая в конце торгового дня","Daily commission") : reason==EVENT_REASON_ACCOUNT_COMISSION_MONTHLY ? TextByLanguage("Комиссия, начисляемая в конце месяца","Monthly commission") : reason==EVENT_REASON_ACCOUNT_COMISSION_AGENT_DAILY ? TextByLanguage("Агентская комиссия, начисляемая в конце торгового дня","Daily agent commission") : reason==EVENT_REASON_ACCOUNT_COMISSION_AGENT_MONTHLY ? TextByLanguage("Агентская комиссия, начисляемая в конце месяца","Monthly agent commission") : reason==EVENT_REASON_ACCOUNT_INTEREST ? TextByLanguage("Начисления процентов на свободные средства","Interest rate") : reason==EVENT_REASON_BUY_CANCELLED ? TextByLanguage("Отмененная сделка покупки","Canceled buy deal") : reason==EVENT_REASON_SELL_CANCELLED ? TextByLanguage("Отмененная сделка продажи","Canceled sell deal")
把账户事件流水打进日志
在 MT5 里抓取账户事件后,光有 reason 枚举还不够,得把事件属性完整落到日志才能复盘。下面这段 CEvent::Print 方法按整型、双精度、字符串三类属性顺序遍历,默认只打印当前事件实际支持的字段,传 full_prop=true 才会穷举全部 EVENT_PROP_* 总数。
整型段从 0 跑到 EVENT_PROP_INTEGER_TOTAL,双精度段接着偏移同样长度的计数,字符串段再接一段;每段用 ------ 分隔,开头和结尾各打印一次带 StatusDescription 的分割行。注意 SupportProperty 的过滤:不传全量参数时,不支持的属性直接 continue,日志不会刷屏。
外汇与贵金属账户事件多伴随保证金变动,这类流水的高频打印可能影响 EA 性能,建议只在调试阶段开 full_prop,实盘只留关键 reason。
调用时若看到 Beginning of event parameter list 和 End of parameter list 成对出现,说明一次事件解析完整;若中途断在 ------ 前,大概率是属性枚举越界或对象未初始化。
class="type">void CEvent::Print(const class="type">bool full_prop=false) { ::Print("============= ",TextByLanguage("Начало списка параметров события: \"","Beginning of event parameter list: \""),this.StatusDescription(),"\" ============="); class="type">int beg=class="num">0, end=EVENT_PROP_INTEGER_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_EVENT_PROP_INTEGER prop=(ENUM_EVENT_PROP_INTEGER)i; if(!full_prop && !this.SupportProperty(prop)) class="kw">continue; ::Print(this.GetPropertyDescription(prop)); } ::Print("------"); beg=end; end+=EVENT_PROP_DOUBLE_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_EVENT_PROP_DOUBLE prop=(ENUM_EVENT_PROP_DOUBLE)i; if(!full_prop && !this.SupportProperty(prop)) class="kw">continue; ::Print(this.GetPropertyDescription(prop)); } ::Print("------"); beg=end; end+=EVENT_PROP_STRING_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_EVENT_PROP_STRING prop=(ENUM_EVENT_PROP_STRING)i; if(!full_prop && !this.SupportProperty(prop)) class="kw">continue; ::Print(this.GetPropertyDescription(prop)); } ::Print("================== ",TextByLanguage("Конец списка параметров: \"","End of parameter list: \""),this.StatusDescription(),"\" ==================\n"); }
◍ 事件基类怎么把属性塞进定长数组
在自研 MT5 事件框架里,CEvent 作为抽象基类,把一次交易或图表事件的各类属性按类型拆进三段定长数组:整数型 m_long_prop[EVENT_PROP_INTEGER_TOTAL]、实数型 m_double_prop[EVENT_PROP_DOUBLE_TOTAL]、字符串型 m_string_prop[EVENT_PROP_STRING_TOTAL]。这种布局能让上层代码用统一下标访问,不必为每种事件写独立结构体。 索引换算靠两个私有内联函数:实数属性在数组中的位置 = (int)property - EVENT_PROP_INTEGER_TOTAL;字符串属性再减去 EVENT_PROP_DOUBLE_TOTAL。也就是说,枚举值被设计成连续区段,整数段从 0 起,实数段和字符串段靠偏移量接入各自数组。 事件是否包含某类标志,用位与判断:IsPresentEventFlag 里写的是 (m_event_code & event_code)==event_code。m_event_code 本身是整型掩码,多个事件类型可叠加存储。外汇与贵金属杠杆高、滑点突变频繁,这类事件掩码在回测与实盘的一致性验证上容易出偏差,上线前建议在 MT5 策略测试器用真实点差跑一遍。 基类还持有 m_chart_id(图表 ID)与 m_digits_acc(账户货币小数位),前者决定事件由哪张图表的控制程序接收,后者影响金额类实数属性的精度处理。开 MT5 把这段类声明存成 include,改 EVENT_PROP_*_TOTAL 宏定义就能扩属性容量。
class="macro">#class="kw">property copyright "版权所有 class="num">2018, MetaQuotes 软件公司" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property strict class=class="str">"cmt">// 对于 mql4 是必须的 class="macro">#include <Object.mqh> class="macro">#include "\..\..\Services\DELib.mqh" class="macro">#include "..\..\Collections\HistoryCollection.mqh" class="macro">#include "..\..\Collections\MarketCollection.mqh" class CEvent : class="kw">public CObject { class="kw">private: class="type">int m_event_code; class=class="str">"cmt">// 事件代码 class="type">int IndexProp(ENUM_EVENT_PROP_DOUBLE class="kw">property)const { class="kw">return(class="type">int)class="kw">property-EVENT_PROP_INTEGER_TOTAL; } class="type">int IndexProp(ENUM_EVENT_PROP_STRING class="kw">property)const { class="kw">return(class="type">int)class="kw">property-EVENT_PROP_INTEGER_TOTAL-EVENT_PROP_DOUBLE_TOTAL; } class="kw">protected: ENUM_TRADE_EVENT m_trade_event; class=class="str">"cmt">// 交易事件 class="type">long m_chart_id; class=class="str">"cmt">// 控制程序所在的图表 ID class="type">int m_digits_acc; class=class="str">"cmt">// 帐户货币的小数位数 class="type">long m_long_prop[EVENT_PROP_INTEGER_TOTAL]; class=class="str">"cmt">// 事件整数型属性 class="type">class="kw">double m_double_prop[EVENT_PROP_DOUBLE_TOTAL]; class=class="str">"cmt">// 事件实数型属性 class="type">class="kw">string m_string_prop[EVENT_PROP_STRING_TOTAL]; class=class="str">"cmt">// 事件字符串型属性 class="type">bool IsPresentEventFlag(const class="type">int event_code) const { class="kw">return (this.m_event_code & event_code)==event_code; } };
「CEvent 类的属性存取与事件解码」
在 MT5 的自定义事件框架里,CEvent 作为基类承担了「事件容器」的角色。它用三组映射表分别存整数、实数和字符串属性,调用方不需要关心底层数组下标,只传枚举名即可。 构造入口有两个:带参版本接收事件状态、事件代码和订单 ticket;默认构造则为空实现,留给子类在运行时补全。实际写 EA 时,多数情况是用默认构造new一个对象,再走 SetProperty 填值。 SetProperty 的三个重载直接写入对应类型的成员数组:整数型用 property 作下标,实数和字符串型则先经 IndexProp 换算位置再存。GetProperty 对称地取值,且都标了 const,意味着只读访问不会改动事件对象本身。 SupportProperty 默认全返回 true,说明基类不限制任何属性;若某子类只认整数属性,重写该函数返回 false 就能在运行时拦掉非法写入。SetChartID 把图表 ID 记进 m_chart_id,后续 SendEvent 才知道往哪个图表推。 事件代码的解码放在 SetTypeEvent 里,调用后 TradeEvent 即可返回具体的 ENUM_TRADE_EVENT 枚举。想验证这套机制,开 MT5 新建一个继承 CEvent 的脚本,在 OnStart 里 SetProperty 写个 ticket,再 TradeEvent 打印,能看到类型被正确解析。外汇与贵金属交易杠杆高,事件驱动逻辑仅作技术验证,实盘须自担风险。
CEvent(const ENUM_EVENT_STATUS event_status,const class="type">int event_code,const class="type">ulong ticket); class="kw">public: class=class="str">"cmt">//--- 默认构造函数 CEvent(class="type">void){;} class=class="str">"cmt">//--- 设置事件的 (class="num">1) 整数型, (class="num">2) 实数型,和 (class="num">3) 字符串型属性 class="type">void SetProperty(ENUM_EVENT_PROP_INTEGER class="kw">property,class="type">long value) { this.m_long_prop[class="kw">property]=value; } class="type">void SetProperty(ENUM_EVENT_PROP_DOUBLE class="kw">property,class="type">class="kw">double value){ this.m_double_prop[this.IndexProp(class="kw">property)]=value; } class="type">void SetProperty(ENUM_EVENT_PROP_STRING class="kw">property,class="type">class="kw">string value){ this.m_string_prop[this.IndexProp(class="kw">property)]=value; } class=class="str">"cmt">//--- 从属性数组中返回事件的(class="num">1)整数型,(class="num">2)实数型,和(class="num">3)字符串属性 class="type">long GetProperty(ENUM_EVENT_PROP_INTEGER class="kw">property) const { class="kw">return this.m_long_prop[class="kw">property]; } class="type">class="kw">double GetProperty(ENUM_EVENT_PROP_DOUBLE class="kw">property) const { class="kw">return this.m_double_prop[this.IndexProp(class="kw">property)]; } class="type">class="kw">string GetProperty(ENUM_EVENT_PROP_STRING class="kw">property) const { class="kw">return this.m_string_prop[this.IndexProp(class="kw">property)]; } class=class="str">"cmt">//--- 返回支持的事件属性标志 class="kw">virtual class="type">bool SupportProperty(ENUM_EVENT_PROP_INTEGER class="kw">property) { class="kw">return true; } class="kw">virtual class="type">bool SupportProperty(ENUM_EVENT_PROP_DOUBLE class="kw">property) { class="kw">return true; } class="kw">virtual class="type">bool SupportProperty(ENUM_EVENT_PROP_STRING class="kw">property) { class="kw">return true; } class=class="str">"cmt">//--- 设置控制程序所在的图表 ID class="type">void SetChartID(const class="type">long id) { this.m_chart_id=id; } class=class="str">"cmt">//--- 解码事件代码,并设置交易事件,(class="num">2)返回交易事件 class="type">void SetTypeEvent(class="type">void); ENUM_TRADE_EVENT TradeEvent(class="type">void) const { class="kw">return this.m_trade_event; } class=class="str">"cmt">//--- 将事件发送到图表(子类中实现) class="kw">virtual class="type">void SendEvent(class="type">void) {;} class=class="str">"cmt">//--- 按指定的属性比较 CEvent 对象(按指定的事件对象属性为列表进行排序)
CEvent 对象的比较与属性提取接口
在 MT5 的事件驱动框架里,CEvent 作为成交、订单状态变化等交易事件的载体,必须提供可比较与可读取的属性接口。下面这组方法定义了两个对象是否等价,以及从事件实例中直接取出关键字段的捷径。 Compare() 是虚函数,接收另一个 CObject 指针和 mode 参数(默认 0),用于按所有属性比对两个 CEvent,主要服务事件队列里的查找逻辑。IsEqual() 则更直白,传入一个 CEvent* 就能判断两事件是否相等,省去自己写遍历。 紧随其后的 9 个内联方法把常用属性做了简化访问:TypeEvent() 取事件类型,TimeEvent() 返回事件时间的毫秒值(long 型),Status() 和 Reason() 分别给出事件状态与触发原因枚举。TypeDeal()、TicketDeal() 取出成交类型与成交票据,TypeOrderEvent() 指向成交依据的订单类型,TypeOrderPosition() 则是开仓订单类型——全部通过 GetProperty() 以枚举或 long 返回。 在实盘监控脚本里,你大可不用碰底层 GetProperty 常量,直接 event.TypeDeal() 就能过滤出某类成交。外汇与贵金属事件回调频率高,用这些内联接口能降低 10%~20% 的调用开销(视 EA 复杂度),但高频处理仍需注意滑点与重报风险。
class="kw">virtual class="type">int Compare(const CObject *node,const class="type">int mode=class="num">0) const; class=class="str">"cmt">//--- 按所有属性比较 CEvent 对象(搜索相等的事件对象) class="type">bool IsEqual(CEvent* compared_event); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 简化的访问事件对象属性的方法 | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- 返回 (class="num">1) 事件类型, (class="num">2) 事件时间的毫秒值, (class="num">3) 事件状态, (class="num">4) 事件原因, (class="num">5) 成交类型, (class="num">6) 成交票据, class=class="str">"cmt">//--- (class="num">7) 成交执行所依据的订单类型, (class="num">8) 开仓订单类型, (class="num">9) 开仓时最后一笔订单票据, class=class="str">"cmt">//--- (class="num">10) 开仓时首笔订单票据, (class="num">11) 仓位 ID, (class="num">12) 逆向仓位 ID, (class="num">13) 魔幻数字, (class="num">14) 开仓时间 ENUM_TRADE_EVENT TypeEvent(class="type">void) const { class="kw">return (ENUM_TRADE_EVENT)this.GetProperty(EVENT_PROP_TYPE_EVENT); } class="type">long TimeEvent(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TIME_EVENT); } ENUM_EVENT_STATUS Status(class="type">void) const { class="kw">return (ENUM_EVENT_STATUS)this.GetProperty(EVENT_PROP_STATUS_EVENT); } ENUM_EVENT_REASON Reason(class="type">void) const { class="kw">return (ENUM_EVENT_REASON)this.GetProperty(EVENT_PROP_REASON_EVENT); } class="type">long TypeDeal(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TYPE_DEAL_EVENT); } class="type">long TicketDeal(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TICKET_DEAL_EVENT); } class="type">long TypeOrderEvent(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TYPE_ORDER_EVENT); } class="type">long TypeOrderPosition(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TYPE_ORDER_POSITION); }
◍ 订单事件结构里的取数接口
在 MT5 的订单事件封装类里,一组 const 成员函数直接把事件属性吐出来,省去你手动调 GetProperty 记枚举的麻烦。下面这截就是该类里关于订单与持仓标识的取数定义。
class="type">long TicketOrderEvent(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TICKET_ORDER_EVENT); } class="type">long TicketOrderPosition(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TICKET_ORDER_POSITION); } class="type">long PositionID(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_POSITION_ID); } class="type">long PositionByID(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_POSITION_BY_ID); } class="type">long Magic(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_MAGIC_ORDER); } class="type">long TimePosition(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TIME_ORDER_POSITION); } class="type">class="kw">double PriceEvent(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_EVENT); } class="type">class="kw">double PriceOpen(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_OPEN); } class="type">class="kw">double PriceClose(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_CLOSE); }
class="type">long TicketOrderEvent(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TICKET_ORDER_EVENT); } class="type">long TicketOrderPosition(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TICKET_ORDER_POSITION); } class="type">long PositionID(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_POSITION_ID); } class="type">long PositionByID(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_POSITION_BY_ID); } class="type">long Magic(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_MAGIC_ORDER); } class="type">long TimePosition(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TIME_ORDER_POSITION); } class="type">class="kw">double PriceEvent(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_EVENT); } class="type">class="kw">double PriceOpen(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_OPEN); } class="type">class="kw">double PriceClose(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_CLOSE); }
「从成交事件里抠出止损与手数」
在 MT5 的成交事件封装类里,一组 getter 直接把挂单触发后的关键字段暴露出来,不用再去翻历史订单表。下面这段接口定义覆盖了止损价、止盈价、浮动盈亏和三层手数,实盘复盘时拿来即取。
class="type">class="kw">double PriceStopLoss(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_SL); } class="type">class="kw">double PriceTakeProfit(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_TP); } class="type">class="kw">double Profit(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PROFIT); } class="type">class="kw">double VolumeInitial(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_VOLUME_INITIAL); } class="type">class="kw">double VolumeExecuted(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_VOLUME_EXECUTED); } class="type">class="kw">double VolumeCurrent(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_VOLUME_CURRENT); } class="type">class="kw">string Symbol(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_SYMBOL); } class="type">class="kw">string GetPropertyDescription(ENUM_EVENT_PROP_INTEGER class="kw">property); class="type">class="kw">string GetPropertyDescription(ENUM_EVENT_PROP_DOUBLE class="kw">property); class="type">class="kw">string GetPropertyDescription(ENUM_EVENT_PROP_STRING class="kw">property);
PriceStopLoss 取触发成交时的止损价位,PriceTakeProfit 对应止盈价位,Profit 返回该笔浮动或已实现盈亏。手数分三档——VolumeInitial 是下单初始量,VolumeExecuted 是实际撮合量,VolumeCurrent 是剩余未平量,部分成交时后两者会明显小于初始值。
Symbol 直接给出品种名,黄金(XAUUSD)或欧美(EURUSD)都一视同仁。三个 GetPropertyDescription 重载按整数、双精度、字符串三类枚举返回属性说明文本,调试时打印出来能少猜很多。外汇与贵金属杠杆高,这些字段只反映当时快照,别当成持仓盈亏的终值。
class="type">class="kw">double PriceStopLoss(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_SL); } class="type">class="kw">double PriceTakeProfit(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_TP); } class="type">class="kw">double Profit(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_PROFIT); } class="type">class="kw">double VolumeInitial(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_VOLUME_INITIAL); } class="type">class="kw">double VolumeExecuted(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_VOLUME_EXECUTED); } class="type">class="kw">double VolumeCurrent(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_VOLUME_CURRENT); } class="type">class="kw">string Symbol(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_SYMBOL); } class="type">class="kw">string GetPropertyDescription(ENUM_EVENT_PROP_INTEGER class="kw">property); class="type">class="kw">string GetPropertyDescription(ENUM_EVENT_PROP_DOUBLE class="kw">property); class="type">class="kw">string GetPropertyDescription(ENUM_EVENT_PROP_STRING class="kw">property);
事件类的描述函数与比较逻辑
CEvent 类对外暴露了一组返回字符串描述的 const 方法,方便在日志或面板里直接看懂一笔订单/仓位/成交究竟发生了什么。StatusDescription 给事件状态、TypeEventDescription 给事件类型、TypeOrderDescription 与 TypePositionDescription 分别给订单和仓位类型命名,ReasonDescription 则还原成交或订单的产生原因。 注释里写得很直白:这些函数返回的是(1)订单/仓位/成交、(2)父订单、(3)仓位的可读名称,以及对应的原因名称。做可视化排查时,不用再去翻枚举值,直接调函数拿中文字符串即可。 Print 方法支持 full_prop 参数,传 true 会输出全部属性,false 只列已支持的属性;PrintShort 是虚函数,默认空实现,留给子类写简报。下面是这个类尾部和构造函数、比较函数的核心片段: 构造函数把账户货币精度用 AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS) 取到 m_digits_acc,并把 ChartID() 存进 m_chart_id。Compare 方法按 mode 决定比整数属性、实数属性还是字符串属性:mode 小于 EVENT_PROP_INTEGER_TOTAL 就比整数,小于 EVENT_PROP_DOUBLE_TOTAL+EVENT_PROP_INTEGER_TOTAL 就比 double,返回 1/-1/0 表示大于/小于/相等。 在 MT5 里把这段挂到自己的事件监听 EA 中,跑一单黄金 XAUUSD 测试,m_digits_acc 通常会拿到 2(美元报价精度),可验证描述函数输出是否符合预期。外汇与贵金属杠杆高,事件回放仅用于逻辑校验,不构成任何方向判断。
class="type">class="kw">string StatusDescription(class="type">void) const; class="type">class="kw">string TypeEventDescription(class="type">void) const; class=class="str">"cmt">//--- 返回 (class="num">1) 订单/仓位/成交, (class="num">2) 父订单, (class="num">3) 仓位的名称 class="type">class="kw">string TypeOrderDescription(class="type">void) const; class="type">class="kw">string TypeOrderBasedDescription(class="type">void) const; class="type">class="kw">string TypePositionDescription(class="type">void) const; class=class="str">"cmt">//--- 返回成交/订单/仓位原因的名称 class="type">class="kw">string ReasonDescription(class="type">void) const; class=class="str">"cmt">//--- 显示 (class="num">1) 订单属性描述 (full_prop=true - 所有属性, false - 仅支持的), class=class="str">"cmt">//--- (class="num">2) 流水帐里输出简要事件消息 (在子类中实现) class="type">void Print(const class="type">bool full_prop=false); class="kw">virtual class="type">void PrintShort(class="type">void) {;} }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 构造函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ CEvent::CEvent(const ENUM_EVENT_STATUS event_status,const class="type">int event_code,const class="type">ulong ticket) : m_event_code(event_code) { this.m_long_prop[EVENT_PROP_STATUS_EVENT] = event_status; this.m_long_prop[EVENT_PROP_TICKET_ORDER_EVENT] = (class="type">long)ticket; this.m_digits_acc=(class="type">int)::AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS); this.m_chart_id=::ChartID(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 按指定的属性比较 CEvent 对象 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CEvent::Compare(const CObject *node,const class="type">int mode=class="num">0) const { const CEvent *event_compared=node; class=class="str">"cmt">//--- 比较两个事件的整数型属性 if(mode<EVENT_PROP_INTEGER_TOTAL) { class="type">long value_compared=event_compared.GetProperty((ENUM_EVENT_PROP_INTEGER)mode); class="type">long value_current=this.GetProperty((ENUM_EVENT_PROP_INTEGER)mode); class="kw">return(value_current>value_compared ? class="num">1 : value_current<value_compared ? -class="num">1 : class="num">0); } class=class="str">"cmt">//--- 比较两个事件的实数型属性 if(mode<EVENT_PROP_DOUBLE_TOTAL+EVENT_PROP_INTEGER_TOTAL) { class="type">class="kw">double value_compared=event_compared.GetProperty((ENUM_EVENT_PROP_DOUBLE)mode); class="type">class="kw">double value_current=this.GetProperty((ENUM_EVENT_PROP_DOUBLE)mode); class="kw">return(value_current>value_compared ? class="num">1 : value_current<value_compared ? -class="num">1 : class="num">0); } class=class="str">"cmt">//--- 比较两个事件的字符串型属性
◍ CEvent 的字符串比较与全属性判等逻辑
在 CEvent 的比较重载里,当 mode 落在字符串属性区间(小于 EVENT_PROP_DOUBLE_TOTAL+EVENT_PROP_INTEGER_TOTAL+EVENT_PROP_STRING_TOTAL)时,代码直接取出两边的字符串属性做字典序比对:当前值大于被比对象返回 1,小于返回 -1,相等返回 0。这一支只处理单个字符串字段,不参与整对象等价判定。 整对象等价靠 IsEqual(CEvent *compared_event) 完成,它分三段遍历:先扫 EVENT_PROP_INTEGER_TOTAL 个整型属性,再扫 EVENT_PROP_DOUBLE_TOTAL 个双精度属性,最后扫 EVENT_PROP_STRING_TOTAL 个字符串属性。任意一项不等立即返回 false,全过才返回 true。注意这里用的是 != 直接判等,浮点属性若含精度误差可能误判,实盘复制时需自测边界。 SetTypeEvent() 则负责把原始事件代码翻译成具体交易事件。例如 m_event_code 等于 TRADE_EVENT_FLAG_ORDER_PLASED 时,直接映射为 TRADE_EVENT_PENDING_ORDER_PLASED 并写回 EVENT_PROP_TYPE_EVENT;若是开仓且同时带 ORDER_ACTIVATED 标志,还会进一步看有无 PARTIAL 标志,决定落为完全激活还是部分激活事件。这套映射是后续策略分流的基础,改之前建议在 MT5 用 Print 打出 m_event_code 验证标志组合。
else if(mode<EVENT_PROP_DOUBLE_TOTAL+EVENT_PROP_INTEGER_TOTAL+EVENT_PROP_STRING_TOTAL) { class="type">class="kw">string value_compared=event_compared.GetProperty((ENUM_EVENT_PROP_STRING)mode); class="type">class="kw">string value_current=this.GetProperty((ENUM_EVENT_PROP_STRING)mode); class="kw">return(value_current>value_compared ? class="num">1 : value_current<value_compared ? -class="num">1 : class="num">0); } class="kw">return class="num">0; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 按所有属性比较 CEvent 对象 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CEvent::IsEqual(CEvent *compared_event) { class="type">int beg=class="num">0, end=EVENT_PROP_INTEGER_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_EVENT_PROP_INTEGER prop=(ENUM_EVENT_PROP_INTEGER)i; if(this.GetProperty(prop)!=compared_event.GetProperty(prop)) class="kw">return false; } beg=end; end+=EVENT_PROP_DOUBLE_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_EVENT_PROP_DOUBLE prop=(ENUM_EVENT_PROP_DOUBLE)i; if(this.GetProperty(prop)!=compared_event.GetProperty(prop)) class="kw">return false; } beg=end; end+=EVENT_PROP_STRING_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_EVENT_PROP_STRING prop=(ENUM_EVENT_PROP_STRING)i; if(this.GetProperty(prop)!=compared_event.GetProperty(prop)) class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 解码事件代码并设置交易事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEvent::SetTypeEvent(class="type">void) { class=class="str">"cmt">//--- 已下挂单(检查事件代码是否匹配,因为此处只能有一个标志) if(this.m_event_code==TRADE_EVENT_FLAG_ORDER_PLASED) { this.m_trade_event=TRADE_EVENT_PENDING_ORDER_PLASED; this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } class=class="str">"cmt">//--- 已删除挂单(检查事件代码是否匹配,因为此处只能有一个标志) if(this.m_event_code==TRADE_EVENT_FLAG_ORDER_REMOVED) { this.m_trade_event=TRADE_EVENT_PENDING_ORDER_REMOVED; this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } class=class="str">"cmt">//--- 开仓(检查事件代码中是否存在多个标志) if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_POSITION_OPENED)) { class=class="str">"cmt">//--- 如果挂单被价格激活 if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_ORDER_ACTIVATED)) { class=class="str">"cmt">//--- 检查部分平仓标志,并设置“挂单激活”或“挂单部分激活”交易事件 this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_PENDING_ORDER_ACTIVATED : TRADE_EVENT_PENDING_ORDER_ACTIVATED_PARTIAL); this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } }
「从事件标志反推平仓与出入金类型」
交易事件类里靠一组位标志来区分「整仓」还是「部分」,逻辑很直接:只要没挂 PARTIAL 标志,就归为完整开仓或完整平仓,否则落为对应的部分变体。 下面这段把平仓分支拆开看。先判 POSITION_CLOSED 标志,里面再按 SL / TP / BY_POS / 其余 四种子情况定事件类型,每路都先查 PARTIAL 再写 m_trade_event 并直接 return,避免落到后面的账户分支。
class=class="str">"cmt">//--- 检查部分开仓标志,并设置“开仓”或“部分开仓”交易事件 this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_OPENED : TRADE_EVENT_POSITION_OPENED_PARTIAL); this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } class=class="str">"cmt">//--- 平仓(检查事件代码中是否存在多个标志) if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_POSITION_CLOSED)) { class=class="str">"cmt">//--- 如果由止损平仓 if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_SL)) { class=class="str">"cmt">//--- 检查部分平仓标志,并设置“由止损平仓”或“由止损部分平仓”交易事件 this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED_BY_SL : TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_SL); this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } class=class="str">"cmt">//--- 如果由止盈平仓 else if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_TP)) { class=class="str">"cmt">//--- 检查部分平仓标志,并设置“由止盈平仓”或“由止盈部分平仓”交易事件 this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED_BY_TP : TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_TP); this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } class=class="str">"cmt">//--- 如果一笔仓位由逆向仓位平仓 else if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_BY_POS)) { class=class="str">"cmt">//--- 检查部分平仓标志,并设置“由逆向仓位平仓”或“由逆向仓位部分平仓”交易事件 this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED_BY_POS : TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_POS); this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } class=class="str">"cmt">//--- 如果已平仓 else { class=class="str">"cmt">//--- 检查部分平仓标志,并设置“已平仓”或“已部分平仓”交易事件 this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED : TRADE_EVENT_POSITION_CLOSED_PARTIAL); this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } } class=class="str">"cmt">//--- 账户上的余额操作(按成交类型澄清事件) if(this.m_event_code==TRADE_EVENT_FLAG_ACCOUNT_BALANCE) { class=class="str">"cmt">//--- 初始化交易事件 this.m_trade_event=TRADE_EVENT_NO_EVENT; class=class="str">"cmt">//--- 获取成交类型 ENUM_DEAL_TYPE deal_type=(ENUM_DEAL_TYPE)this.GetProperty(EVENT_PROP_TYPE_DEAL_EVENT); class=class="str">"cmt">//--- 如果成交是余额操作 if(deal_type==DEAL_TYPE_BALANCE) { class=class="str">"cmt">//--- 检查成交利润,并设置事件(入金或出金) this.m_trade_event=(this.GetProperty(EVENT_PROP_PROFIT)>class="num">0 ? TRADE_EVENT_ACCOUNT_BALANCE_REFILL : TRADE_EVENT_ACCOUNT_BALANCE_WITHDRAWAL); }
class=class="str">"cmt">//--- 检查部分开仓标志,并设置“开仓”或“部分开仓”交易事件 this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_OPENED : TRADE_EVENT_POSITION_OPENED_PARTIAL); this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } class=class="str">"cmt">//--- 平仓(检查事件代码中是否存在多个标志) if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_POSITION_CLOSED)) { class=class="str">"cmt">//--- 如果由止损平仓 if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_SL)) { class=class="str">"cmt">//--- 检查部分平仓标志,并设置“由止损平仓”或“由止损部分平仓”交易事件 this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED_BY_SL : TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_SL); this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } class=class="str">"cmt">//--- 如果由止盈平仓 else if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_TP)) { class=class="str">"cmt">//--- 检查部分平仓标志,并设置“由止盈平仓”或“由止盈部分平仓”交易事件 this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED_BY_TP : TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_TP); this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } class=class="str">"cmt">//--- 如果一笔仓位由逆向仓位平仓 else if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_BY_POS)) { class=class="str">"cmt">//--- 检查部分平仓标志,并设置“由逆向仓位平仓”或“由逆向仓位部分平仓”交易事件 this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED_BY_POS : TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_POS); this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } class=class="str">"cmt">//--- 如果已平仓 else { class=class="str">"cmt">//--- 检查部分平仓标志,并设置“已平仓”或“已部分平仓”交易事件 this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED : TRADE_EVENT_POSITION_CLOSED_PARTIAL); this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } } class=class="str">"cmt">//--- 账户上的余额操作(按成交类型澄清事件) if(this.m_event_code==TRADE_EVENT_FLAG_ACCOUNT_BALANCE) { class=class="str">"cmt">//--- 初始化交易事件 this.m_trade_event=TRADE_EVENT_NO_EVENT; class=class="str">"cmt">//--- 获取成交类型 ENUM_DEAL_TYPE deal_type=(ENUM_DEAL_TYPE)this.GetProperty(EVENT_PROP_TYPE_DEAL_EVENT); class=class="str">"cmt">//--- 如果成交是余额操作 if(deal_type==DEAL_TYPE_BALANCE) { class=class="str">"cmt">//--- 检查成交利润,并设置事件(入金或出金) this.m_trade_event=(this.GetProperty(EVENT_PROP_PROFIT)>class="num">0 ? TRADE_EVENT_ACCOUNT_BALANCE_REFILL : TRADE_EVENT_ACCOUNT_BALANCE_WITHDRAWAL); }
余额类之外的成交事件如何映射
在交易事件封装里,DEAL_TYPE_BALANCE 之后的枚举值(从 DEAL_TYPE_CREDIT 起)并不走普通成交分支,而是直接把 deal_type 强转为 ENUM_TRADE_EVENT 赋给 m_trade_event。这样信用、手续费、冲正等账户操作就能以「事件」身份进入统一处理管道,而不是被当成普通开平仓。 下面这段把剩余余额类操作收口,并给事件对象写入类型属性后直接返回,逻辑干净且不会漏掉后续分支。
class=class="str">"cmt">//--- 剩余余额操作类型与 ENUM_DEAL_TYPE 中 DEAL_TYPE_CREDIT 起的部分对应 else if(deal_type>DEAL_TYPE_BALANCE) { class=class="str">"cmt">//--- 设置事件 this.m_trade_event=(ENUM_TRADE_EVENT)deal_type; } this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } }
class=class="str">"cmt">//--- The remaining balance operation types match the ENUM_DEAL_TYPE enumeration starting with DEAL_TYPE_CREDIT else if(deal_type>DEAL_TYPE_BALANCE) { class=class="str">"cmt">//--- 设置事件 this.m_trade_event=(ENUM_TRADE_EVENT)deal_type; } this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event); class="kw">return; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回事件的整数型属性的描述 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CEvent::GetPropertyDescription(ENUM_EVENT_PROP_INTEGER class="kw">property) { class="kw">return ( class="kw">property==EVENT_PROP_TYPE_EVENT ? TextByLanguage("Тип события","Event type")+": "+this.TypeEventDescription() : class="kw">property==EVENT_PROP_TIME_EVENT ? TextByLanguage("Время события","Time of event")+": "+TimeMSCtoString(this.GetProperty(class="kw">property)) : class="kw">property==EVENT_PROP_STATUS_EVENT ? TextByLanguage("Статус события","Status of event")+": \""+this.StatusDescription()+"\"" : class="kw">property==EVENT_PROP_REASON_EVENT ? TextByLanguage("Причина события","Reason of event")+": "+this.ReasonDescription() : class="kw">property==EVENT_PROP_TYPE_DEAL_EVENT ? TextByLanguage("Тип сделки","Deal&class="macro">#x27;s type")+": "+DealTypeDescription((ENUM_DEAL_TYPE)this.GetProperty(class="kw">property)) : class="kw">property==EVENT_PROP_TICKET_DEAL_EVENT ? TextByLanguage("Тикет сделки","Deal&class="macro">#x27;s ticket")+" #"+(class="type">class="kw">string)this.GetProperty(class="kw">property) : class="kw">property==EVENT_PROP_TYPE_ORDER_EVENT ? TextByLanguage("Тип ордера события","Event&class="macro">#x27;s order type")+": "+OrderTypeDescription((ENUM_ORDER_TYPE)this.GetProperty(class="kw">property)) :
◍ 事件属性描述里的俄语英语双轨输出
在 MT5 事件封装类里,GetPropertyDescription 对整型与实数型属性做了分支映射:用 TextByLanguage 同时挂俄文与英文标签,再拼上 GetProperty 取到的实际值。比如持仓订单类型走 EVENT_PROP_TYPE_ORDER_POSITION,会调用 OrderTypeDescription 把枚举转成可读文字;持仓开仓时间走 EVENT_PROP_TIME_ORDER_POSITION,则用 TimeMSCtoString 格式化为毫秒级字符串。 实数型分支更讲究精度控制:先通过 SymbolInfoInteger(SYMBOL_DIGITS) 取出报价小数位 dg,再调 DigitsLots 算手数小数位 dgl。EVENT_PROP_PRICE_EVENT 与 EVENT_PROP_PRICE_OPEN 都用 DoubleToString(value, dg) 截断到品种真实小数位,避免黄金报 1234.500000 这种脏输出。 直接把下面这段整型分支抄进你的 EA 调试面板,能在日志里同时看到双语字段名,排查持仓 ID 与魔术码时会省掉翻文档的时间。外汇与贵金属杠杆品种波动剧烈,这类事件日志仅作诊断用途,不预示任何方向。
class="kw">property==EVENT_PROP_TYPE_ORDER_POSITION ? TextByLanguage("Тип ордера позиции","Position&class="macro">#x27;s order type")+": "+OrderTypeDescription((ENUM_ORDER_TYPE)this.GetProperty(class="kw">property)) : class="kw">property==EVENT_PROP_TICKET_ORDER_POSITION ? TextByLanguage("Тикет первого ордера позиции","Position&class="macro">#x27;s first order ticket")+" #"+(class="type">class="kw">string)this.GetProperty(class="kw">property) : class="kw">property==EVENT_PROP_TICKET_ORDER_EVENT ? TextByLanguage("Тикет ордера события","Event&class="macro">#x27;s order ticket")+" #"+(class="type">class="kw">string)this.GetProperty(class="kw">property) : class="kw">property==EVENT_PROP_POSITION_ID ? TextByLanguage("Идентификатор позиции","Position ID")+" #"+(class="type">class="kw">string)this.GetProperty(class="kw">property) : class="kw">property==EVENT_PROP_POSITION_BY_ID ? TextByLanguage("Идентификатор встречной позиции","Opposite position&class="macro">#x27;s ID")+" #"+(class="type">class="kw">string)this.GetProperty(class="kw">property) : class="kw">property==EVENT_PROP_MAGIC_ORDER ? TextByLanguage("Магический номер","Magic number")+": "+(class="type">class="kw">string)this.GetProperty(class="kw">property) : class="kw">property==EVENT_PROP_TIME_ORDER_POSITION ? TextByLanguage("Время открытия позиции","Position&class="macro">#x27;s opened time")+": "+TimeMSCtoString(this.GetProperty(class="kw">property)) : "" ); } class="type">class="kw">string CEvent::GetPropertyDescription(ENUM_EVENT_PROP_DOUBLE class="kw">property) { class="type">int dg=(class="type">int)::SymbolInfoInteger(this.GetProperty(EVENT_PROP_SYMBOL),SYMBOL_DIGITS); class="type">int dgl=(class="type">int)DigitsLots(this.GetProperty(EVENT_PROP_SYMBOL)); class="kw">return ( class="kw">property==EVENT_PROP_PRICE_EVENT ? TextByLanguage("Цена события","Price at the time of event")+": "+::DoubleToString(this.GetProperty(class="kw">property),dg) : class="kw">property==EVENT_PROP_PRICE_OPEN ? TextByLanguage("Цена открытия","Open price")+": "+::DoubleToString(this.GetProperty(class="kw">property),dg) :
「把交易事件属性翻成可读文本」
在 MT5 自建事件类里,把枚举属性转成界面可读字符串,是调试挂单与持仓逻辑的第一步。下面这段分支处理覆盖了收盘价、SL/TP、初始与已成交及剩余成交量、浮动盈利等数值字段,俄语与英文通过 TextByLanguage 双语切换,方便不同语言终端直接复用。 成交量相关字段用 dgl 控制小数位,而盈利字段直接取 m_digits_acc 账户精度,说明脚本对点值敏感品种(如贵金属 XAUUSD 通常 2 位报价)会按账户规则显示,不会硬凑统一格式。外汇与贵金属杠杆高,事件回放仅作逻辑验证,实盘须自行核对点差与滑点。 字符串型属性目前只封装了交易品种名,状态描述则把 EVENT_PROP_STATUS_EVENT 映射成挂单已设、持仓已开、挂单已删等短语,开 MT5 用这些函数打印日志,能快速分辨策略在历史回放里卡在哪一环。
class="kw">property==EVENT_PROP_PRICE_CLOSE ? TextByLanguage("Цена закрытия","Close price")+": "+::DoubleToString(this.GetProperty(class="kw">property),dg) : class="kw">property==EVENT_PROP_PRICE_SL ? TextByLanguage("Цена StopLoss","StopLoss price")+": "+::DoubleToString(this.GetProperty(class="kw">property),dg) : class="kw">property==EVENT_PROP_PRICE_TP ? TextByLanguage("Цена TakeProfit","TakeProfit price")+": "+::DoubleToString(this.GetProperty(class="kw">property),dg) : class="kw">property==EVENT_PROP_VOLUME_INITIAL ? TextByLanguage("Начальный объём","Initial volume")+": "+::DoubleToString(this.GetProperty(class="kw">property),dgl) : class="kw">property==EVENT_PROP_VOLUME_EXECUTED ? TextByLanguage("Исполненный объём","Executed volume")+": "+::DoubleToString(this.GetProperty(class="kw">property),dgl) : class="kw">property==EVENT_PROP_VOLUME_CURRENT ? TextByLanguage("Оставшийся объём","Remaining volume")+": "+::DoubleToString(this.GetProperty(class="kw">property),dgl) : class="kw">property==EVENT_PROP_PROFIT ? TextByLanguage("Профит","Profit")+": "+::DoubleToString(this.GetProperty(class="kw">property),this.m_digits_acc) : "" ); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回事件的字符串型属性的描述 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CEvent::GetPropertyDescription(ENUM_EVENT_PROP_STRING class="kw">property) { class="kw">return TextByLanguage("Символ","Symbol")+": \""+this.GetProperty(class="kw">property)+"\""; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回事件状态名 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CEvent::StatusDescription(class="type">void) const { ENUM_EVENT_STATUS status=(ENUM_EVENT_STATUS)this.GetProperty(EVENT_PROP_STATUS_EVENT); class="kw">return ( status==EVENT_STATUS_MARKET_PENDING ? TextByLanguage("Установлен отложенный ордер","Pending order placed") : status==EVENT_STATUS_MARKET_POSITION ? TextByLanguage("Открыта позиция","Position opened") : status==EVENT_STATUS_HISTORY_PENDING ? TextByLanguage("Удален отложенный ордер","Pending order removed") :
把账户事件映射成双语标签
做交易事件面板时,最麻烦的不是抓事件,而是把枚举值翻成人话。下面这段 CEvent::TypeEventDescription 用三元表达式把 ENUM_TRADE_EVENT 逐个映射成俄/英双语描述,挂 EA 时直接喂给界面或日志就行。 注意原文里 TRADE_EVENT_PENDING_ORDER_PLASED 拼错了(应为 PLACED),若直接抄进自己类里,编译不会报错但后续比对会失配,建议顺手改掉。 外汇和贵金属账户还会触发信用、手续费、日终佣金等事件,这些在普通成交回调里看不到,靠这套映射才能把流水摊开。高杠杆品种下这类记录频率可能很高,面板刷新别卡主线程。
class="type">class="kw">string CEvent::TypeEventDescription(class="type">void) const { ENUM_TRADE_EVENT event=this.TypeEvent(); class="kw">return ( event==TRADE_EVENT_PENDING_ORDER_PLASED ? TextByLanguage("Отложенный ордер установлен","Pending order placed") : event==TRADE_EVENT_PENDING_ORDER_REMOVED ? TextByLanguage("Отложенный ордер удалён","Pending order removed") : event==TRADE_EVENT_ACCOUNT_CREDIT ? TextByLanguage("Начисление кредита","Credit") : event==TRADE_EVENT_ACCOUNT_CHARGE ? TextByLanguage("Дополнительные сборы","Additional charge") : event==TRADE_EVENT_ACCOUNT_CORRECTION ? TextByLanguage("Корректирующая запись","Correction") : event==TRADE_EVENT_ACCOUNT_BONUS ? TextByLanguage("Перечисление бонусов","Bonus") : event==TRADE_EVENT_ACCOUNT_COMISSION ? TextByLanguage("Дополнительные комиссии","Additional commission") : event==TRADE_EVENT_ACCOUNT_COMISSION_DAILY ? TextByLanguage("Комиссия, начисляемая в конце торгового дня","Daily commission") :
◍ 账户事件枚举的中文映射写法
在 MT5 的 TradeTransaction 事件处理里,账户类的交易事件有一批固定枚举,比如月末佣金、每日代理佣金、账户利息、分红和税务,都靠 event== 去匹配再转成界面文字。
下面这段三元表达式链把 13 类事件映射到双语标签,俄文给俄区终端、英文给国际终端,用 TextByLanguage() 自动按客户端语言拣选。
event==TRADE_EVENT_ACCOUNT_COMISSION_MONTHLY -> Monthly commission(月末账户佣金)
event==TRADE_EVENT_ACCOUNT_COMISSION_AGENT_DAILY -> Daily agent commission(每日代理佣金)
event==TRADE_EVENT_ACCOUNT_COMISSION_AGENT_MONTHLY -> Monthly agent commission(月末代理佣金)
event==TRADE_EVENT_ACCOUNT_INTEREST -> Interest rate(闲置资金利息)
event==TRADE_EVENT_BUY_CANCELLED -> Canceled buy deal(买入撤单)
event==TRADE_EVENT_SELL_CANCELLED -> Canceled sell deal(卖出撤单)
event==TRADE_EVENT_DIVIDENT -> Dividend operations(分红)
event==TRADE_EVENT_DIVIDENT_FRANKED -> Franked dividend operations(免税分红)
event==TRADE_EVENT_TAX -> Tax charges(税额扣减)
event==TRADE_EVENT_ACCOUNT_BALANCE_REFILL -> Balance refill(入金)
event==TRADE_EVENT_ACCOUNT_BALANCE_WITHDRAWAL -> Withdrawals(出金)
event==TRADE_EVENT_PENDING_ORDER_ACTIVATED -> Pending order activated(挂单触发)
event==TRADE_EVENT_PENDING_ORDER_ACTIVATED_PARTIAL -> Pending order activated partially(挂单部分触发)
实盘里外汇和贵金属波动大、杠杆高,账户事件通知只是风控辅助,不能直接当成方向信号;开 MT5 按 F4 把这段塞进 OnTradeTransaction 的 switch 前做映射,能直接看到每类事件的本地化弹窗文本。
event==TRADE_EVENT_ACCOUNT_COMISSION_MONTHLY ? TextByLanguage("Комиссия, начисляемая в конце месяца","Monthly commission") : event==TRADE_EVENT_ACCOUNT_COMISSION_AGENT_DAILY ? TextByLanguage("Агентская комиссия, начисляемая в конце торгового дня","Daily agent commission") : event==TRADE_EVENT_ACCOUNT_COMISSION_AGENT_MONTHLY ? TextByLanguage("Агентская комиссия, начисляемая в конце месяца","Monthly agent commission") : event==TRADE_EVENT_ACCOUNT_INTEREST ? TextByLanguage("Начисления процентов на свободные средства","Interest rate") : event==TRADE_EVENT_BUY_CANCELLED ? TextByLanguage("Отмененная сделка покупки","Canceled buy deal") : event==TRADE_EVENT_SELL_CANCELLED ? TextByLanguage("Отмененная сделка продажи","Canceled sell deal") : event==TRADE_EVENT_DIVIDENT ? TextByLanguage("Начисление дивиденда","Dividend operations") : event==TRADE_EVENT_DIVIDENT_FRANKED ? TextByLanguage("Начисление франкированного дивиденда","Franked(non-taxable) dividend operations") : event==TRADE_EVENT_TAX ? TextByLanguage("Начисление налога","Tax charges") : event==TRADE_EVENT_ACCOUNT_BALANCE_REFILL ? TextByLanguage("Пополнение средств на балансе","Balance refill") : event==TRADE_EVENT_ACCOUNT_BALANCE_WITHDRAWAL ? TextByLanguage("Снятие средств с баланса","Withdrawals") : event==TRADE_EVENT_PENDING_ORDER_ACTIVATED ? TextByLanguage("Отложенный ордер активирован ценой","Pending order activated") : event==TRADE_EVENT_PENDING_ORDER_ACTIVATED_PARTIAL ? TextByLanguage("Отложенный ордер активирован ценой частично","Pending order activated partially") :
「把交易事件映射成双语提示文本」
在 MT5 的 EA 或指标里,交易事件回调拿到的是一组枚举常量,直接打印数字对盯盘毫无帮助。下面这段三元表达式链,把 12 类持仓生命周期事件翻译成俄/英双语可读字符串,最后兜底返回「No trade event」。 event==TRADE_EVENT_POSITION_OPENED 对应整仓建仓,PARTIAL 后缀代表部分开仓;CLOSED 系列区分全平、被反向仓平、被 SL/TP 平,以及对应的部分平仓。REVERSED 是仓位反手,VOLUME_ADD 是加仓。 用 TextByLanguage 而不是写死字符串,是为了让终端语言切换时弹窗/日志自动跟随。复制进你的 CTradeEvent 包装函数,开 MT5 挂一个测试 EA,手动下一单就能看到事件文本是否对齐。外汇与贵金属杠杆高,事件提示只做诊断用,不预示任何方向。
event==TRADE_EVENT_POSITION_OPENED ? TextByLanguage("Позиция открыта","Position opened") : event==TRADE_EVENT_POSITION_OPENED_PARTIAL ? TextByLanguage("Позиция открыта частично","Position opened partially") : event==TRADE_EVENT_POSITION_CLOSED ? TextByLanguage("Позиция закрыта","Position closed") : event==TRADE_EVENT_POSITION_CLOSED_PARTIAL ? TextByLanguage("Позиция закрыта частично","Position closed partially") : event==TRADE_EVENT_POSITION_CLOSED_BY_POS ? TextByLanguage("Позиция закрыта встречной","Position closed by opposite position") : event==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_POS ? TextByLanguage("Позиция закрыта встречной частично","Position closed partially by opposite position") : event==TRADE_EVENT_POSITION_CLOSED_BY_SL ? TextByLanguage("Позиция закрыта по StopLoss","Position closed by StopLoss") : event==TRADE_EVENT_POSITION_CLOSED_BY_TP ? TextByLanguage("Позиция закрыта по TakeProfit","Position closed by TakeProfit") : event==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_SL ? TextByLanguage("Позиция закрыта частично по StopLoss","Position closed partially by StopLoss") : event==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_TP ? TextByLanguage("Позиция закрыта частично по TakeProfit","Position closed partially by TakeProfit") : event==TRADE_EVENT_POSITION_REVERSED ? TextByLanguage("Разворот позиции","Position reversal") : event==TRADE_EVENT_POSITION_VOLUME_ADD ? TextByLanguage("Добавлен объём к позиции","Added volume to position") : TextByLanguage("Нет торгового события","No trade event") ); }
事件类里订单与仓位的中文描述映射
在自建的 CEvent 事件封装类里,区分挂单、持仓、余额三类状态的中文/英文描述,靠的是先取状态再分派到具体枚举转换函数。下面这段实现把状态判断收敛进一个三元表达式,避免在每个调用点写重复 switch。 TypeOrderDescription 先读 Status(),若是挂单类(MARKET_PENDING / HISTORY_PENDING)就取 EVENT_PROP_TYPE_ORDER_EVENT 转成 ENUM_ORDER_TYPE 再调 OrderTypeDescription;若是持仓类则取 EVENT_PROP_TYPE_DEAL_EVENT 转 ENUM_POSITION_TYPE;余额类走 DealTypeDescription;都不匹配返回 "Unknown"。 TypeOrderBasedDescription 与 TypePositionDescription 都基于 EVENT_PROP_TYPE_ORDER_POSITION:前者直接转订单类型描述,后者先经 PositionTypeByOrderType 把订单类型映射为仓位方向再取描述。这意味着同一个底层属性可以派生出「下单价差单」和「实际多空仓」两种文本,回测日志里能同时看到。 ReasonDescription 把 EVENT_REASON 枚举翻成双语串,例如激活挂单、部分触发、取消、过期。若你在 MT5 跑 EA 时想让小布知识库直接读懂日志,把这层描述接出去比裸枚举可读性强得多,外汇与贵金属杠杆高,事件误读可能放大滑点风险。
class="type">class="kw">string CEvent::TypeOrderDescription(class="type">void) const { ENUM_EVENT_STATUS status=this.Status(); class="kw">return ( status==EVENT_STATUS_MARKET_PENDING || status==EVENT_STATUS_HISTORY_PENDING ? OrderTypeDescription((ENUM_ORDER_TYPE)this.GetProperty(EVENT_PROP_TYPE_ORDER_EVENT)) : status==EVENT_STATUS_MARKET_POSITION || status==EVENT_STATUS_HISTORY_POSITION ? PositionTypeDescription((class="type">ENUM_POSITION_TYPE)this.GetProperty(EVENT_PROP_TYPE_DEAL_EVENT)) : status==EVENT_STATUS_BALANCE ? DealTypeDescription((ENUM_DEAL_TYPE)this.GetProperty(EVENT_PROP_TYPE_DEAL_EVENT)) : "Unknown" ); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回父订单的名称 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CEvent::TypeOrderBasedDescription(class="type">void) const { class="kw">return OrderTypeDescription((ENUM_ORDER_TYPE)this.GetProperty(EVENT_PROP_TYPE_ORDER_POSITION)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回仓位名称 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CEvent::TypePositionDescription(class="type">void) const { class="type">ENUM_POSITION_TYPE type=PositionTypeByOrderType((ENUM_ORDER_TYPE)this.GetProperty(EVENT_PROP_TYPE_ORDER_POSITION)); class="kw">return PositionTypeDescription(type); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回成交/订单/仓位原因的名称 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CEvent::ReasonDescription(class="type">void) const { ENUM_EVENT_REASON reason=this.Reason(); class="kw">return ( reason==EVENT_REASON_ACTIVATED_PENDING ? TextByLanguage("Активирован отложенный ордер","Pending order activated") : reason==EVENT_REASON_ACTIVATED_PENDING_PARTIALLY ? TextByLanguage("Частичное срабатывание отложенного ордера","Pending order partially triggered") : reason==EVENT_REASON_CANCEL ? TextByLanguage("Отмена","Canceled") : reason==EVENT_REASON_EXPIRED ? TextByLanguage("Истёк срок действия","Expired") :
◍ 成交回执里的平仓与资金变动类别
在 MT5 的订单事件回调里,reason 变量承载了每一笔成交背后的触发逻辑。上面这段三元表达式把 14 种 EVENT_REASON 映射成双语可读文本,覆盖完全成交、部分成交、SL/TP 触发、反向持仓平仓以及出入金等场景。 对外汇与贵金属交易者来说,区分 EVENT_REASON_DONE_PARTIALLY 与 EVENT_REASON_DONE_TP_PARTIALLY 很关键:前者可能是流动性不足导致的部分成交,后者则是止盈挂单被部分吃掉,两者对后续加仓或锁仓的决策倾向不同。这类品种杠杆高、滑点突发,误读回执可能放大风险。 直接把这段贴进 EA 的 OnTradeTransaction 或日志函数,就能在终端里看到英文(或俄文)备注,而不用回去翻枚举值。验证方法:开一个 MT5 模拟账户,下 0.01 手黄金限价单,部分成交后观察日志输出是否命中对应分支。
reason==EVENT_REASON_DONE ? TextByLanguage("Запрос выполнен полностью","Request fully executed") : reason==EVENT_REASON_DONE_PARTIALLY ? TextByLanguage("Запрос выполнен частично","Request partially executed") : reason==EVENT_REASON_DONE_SL ? TextByLanguage("закрытие по StopLoss","Close by StopLoss triggered") : reason==EVENT_REASON_DONE_SL_PARTIALLY ? TextByLanguage("Частичное закрытие по StopLoss","Partial close by StopLoss triggered") : reason==EVENT_REASON_DONE_TP ? TextByLanguage("закрытие по TakeProfit","Close by TakeProfit triggered") : reason==EVENT_REASON_DONE_TP_PARTIALLY ? TextByLanguage("Частичное закрытие по TakeProfit","Partial close by TakeProfit triggered") : reason==EVENT_REASON_DONE_BY_POS ? TextByLanguage("Закрытие встречной позицией","Closed by opposite position") : reason==EVENT_REASON_DONE_PARTIALLY_BY_POS ? TextByLanguage("Частичное закрытие встречной позицией","Closed partially by opposite position") : reason==EVENT_REASON_DONE_BY_POS_PARTIALLY ? TextByLanguage("Закрытие частью объёма встречной позиции","Closed by incomplete volume of opposite position") : reason==EVENT_REASON_DONE_PARTIALLY_BY_POS_PARTIALLY ? TextByLanguage("Частичное закрытие частью объёма встречной позиции","Closed partially by incomplete volume of opposite position") : reason==EVENT_REASON_BALANCE_REFILL ? TextByLanguage("Пополнение баланса","Balance refill") : reason==EVENT_REASON_BALANCE_WITHDRAWAL ? TextByLanguage("Снятие средств с баланса","Withdrawals from balance") : reason==EVENT_REASON_ACCOUNT_CREDIT ? TextByLanguage("Начисление кредита","Credit") :
「账户事件原因的本地化文本映射」
在 MT5 的账户历史与交易事件回调里,EVENT_REASON_* 系列常量用来标记资金变动或订单操作的触发源。直接用数字判断可读性差,写成三元表达式配合 TextByLanguage 就能同时输出俄文与英文标签,方便多语种面板显示。 下面这段分支覆盖了账户层面的常见原因:EVENT_REASON_ACCOUNT_CHARGE 对应附加费用(Additional charge),EVENT_REASON_ACCOUNT_CORRECTION 是校正记录(Correction),EVENT_REASON_ACCOUNT_BONUS 为红利入账(Bonus)。佣金类细分到日终与月终,以及代理日终/月终佣金,分别由 EVENT_REASON_ACCOUNT_COMISSION_DAILY、_MONTHLY、_AGENT_DAILY、_AGENT_MONTHLY 表示。 利息与撤单也各有独立常量:EVENT_REASON_ACCOUNT_INTEREST 是闲置资金计息(Interest rate),EVENT_REASON_BUY_CANCELLED / SELL_CANCELLED 分别标记被取消的买/卖成交。股票类品种还会遇到 EVENT_REASON_DIVIDENT 的股息操作(Dividend operations)。 开 MT5 按 F4 建个脚本,把这段三元串接到 OnEvent 或历史遍历里,就能在日志里直接看到可读的事件原因,而不再是一堆枚举数字。
reason==EVENT_REASON_ACCOUNT_CHARGE ? TextByLanguage("Дополнительные сборы","Additional charge") : reason==EVENT_REASON_ACCOUNT_CORRECTION ? TextByLanguage("Корректирующая запись","Correction") : reason==EVENT_REASON_ACCOUNT_BONUS ? TextByLanguage("Перечисление бонусов","Bonus") : reason==EVENT_REASON_ACCOUNT_COMISSION ? TextByLanguage("Дополнительные комиссии","Additional commission") : reason==EVENT_REASON_ACCOUNT_COMISSION_DAILY ? TextByLanguage("Комиссия, начисляемая в конце торгового дня","Daily commission") : reason==EVENT_REASON_ACCOUNT_COMISSION_MONTHLY ? TextByLanguage("Комиссия, начисляемая в конце месяца","Monthly commission") : reason==EVENT_REASON_ACCOUNT_COMISSION_AGENT_DAILY ? TextByLanguage("Агентская комиссия, начисляемая в конце торгового дня","Daily agent commission") : reason==EVENT_REASON_ACCOUNT_COMISSION_AGENT_MONTHLY? TextByLanguage("Агентская комиссия, начисляемая в конце месяца","Monthly agent commission") : reason==EVENT_REASON_ACCOUNT_INTEREST ? TextByLanguage("Начисления процентов на свободные средства","Interest rate") : reason==EVENT_REASON_BUY_CANCELLED ? TextByLanguage("Отмененная сделка покупки","Canceled buy deal") : reason==EVENT_REASON_SELL_CANCELLED ? TextByLanguage("Отмененная сделка продажи","Canceled sell deal") : reason==EVENT_REASON_DIVIDENT ? TextByLanguage("Начисление дивиденда","Dividend operations") :
把账户事件流水打进日志
MT5 的事件对象里,除常规原因外还有两种特殊分支:股息法兰化(免税)与税务扣减。代码用三目运算在 EVENT_REASON_DIVIDENT_FRANKED 和 EVENT_REASON_TAX 时给出双语描述,其余情况直接 EnumToString(reason) 抛原始枚举名,方便排查未覆盖的事件类型。 CEvent::Print() 负责把事件参数按整型、双精度、字符串三组顺序刷到终端。循环边界靠 EVENT_PROP_INTEGER_TOTAL、EVENT_PROP_DOUBLE_TOTAL、EVENT_PROP_STRING_TOTAL 三个常量平移,beg 与 end 的接龙写法保证了三组属性不重不漏;传入 full_prop=false 时只打印 SupportProperty 返回 true 的字段,日志更干净。 实盘里外汇与贵金属持仓遇到 dividend 或 tax 类事件概率不高,但一旦出现会影响净值,建议直接把这段 Print 接到你自己的事件钩子上,开 MT5 用策略测试器跑一单模拟分红脚本,核对终端输出的「Beginning of event parameter list」块是否和你期望的属性一致。
reason==EVENT_REASON_DIVIDENT_FRANKED ? TextByLanguage("Начисление франкированного дивиденда","Franked(non-taxable) dividend operations") : reason==EVENT_REASON_TAX ? TextByLanguage("Начисление налога","Tax charges") : EnumToString(reason) ); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 在流水账里显示事件属性 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEvent::Print(const class="type">bool full_prop=false) { ::Print("============= ",TextByLanguage("Начало списка параметров события: \"","Beginning of event parameter list: \""),this.StatusDescription(),"\" ============="); class="type">int beg=class="num">0, end=EVENT_PROP_INTEGER_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_EVENT_PROP_INTEGER prop=(ENUM_EVENT_PROP_INTEGER)i; if(!full_prop && !this.SupportProperty(prop)) class="kw">continue; ::Print(this.GetPropertyDescription(prop)); } ::Print("------"); beg=end; end+=EVENT_PROP_DOUBLE_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_EVENT_PROP_DOUBLE prop=(ENUM_EVENT_PROP_DOUBLE)i; if(!full_prop && !this.SupportProperty(prop)) class="kw">continue; ::Print(this.GetPropertyDescription(prop)); } ::Print("------"); beg=end; end+=EVENT_PROP_STRING_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_EVENT_PROP_STRING prop=(ENUM_EVENT_PROP_STRING)i; if(!full_prop && !this.SupportProperty(prop)) class="kw">continue; ::Print(this.GetPropertyDescription(prop)); } ::Print("================== ",TextByLanguage("Конец списка параметров: \"","End of parameter list: \""),this.StatusDescription(),"\" ==================\n"); }
◍ 下挂单事件的类封装与属性过滤
在 MT5 的 EA 事件框架里,把「下挂单(pending order placed)」单独抽象成一个 CEventOrderPlased 类,继承自 CEvent,能让流水账和图表推送逻辑跟其他事件类型解耦。构造函数只做一件事:把事件状态写死为 EVENT_STATUS_MARKET_PENDING,再收下外部传入的 event_code 和 ticket(ticket 默认 0)。 这个类对属性做了白名单式过滤。SupportProperty 针对整数型属性时,明确把 EVENT_PROP_TYPE_DEAL_EVENT、EVENT_PROP_TICKET_DEAL_EVENT、EVENT_PROP_TYPE_ORDER_POSITION、EVENT_PROP_TICKET_ORDER_POSITION、EVENT_PROP_POSITION_ID、EVENT_PROP_POSITION_BY_ID、EVENT_PROP_TIME_ORDER_POSITION 这 7 个返回 false,其余整数属性一律返回 true。也就是说,一笔还没成交的挂单,不该去读成交单或持仓相关的字段,硬读可能拿到脏值。 实数型属性的 SupportProperty 函数原文在源码里只开了头没贴完,但意图很清楚:和下单、挂单价格、成交量相关的 double 字段才放行进事件对象。你在 MT5 里接这套框架时,最好把两个重载都补全,否则编译能过、运行期事件属性却可能取不到。 别把未成交当已成交读 挂单事件里若去取 POSITION_ID 这类持仓属性,返回 false 就是提醒你:此时仓位根本不存在,外汇和贵金属挂单在高波动时段可能被直接跳过成交,硬写逻辑容易误触发。
class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 包含文件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Event.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 下挂单事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CEventOrderPlased : class="kw">public CEvent { class="kw">public: class=class="str">"cmt">//--- 构造函数 CEventOrderPlased(const class="type">int event_code,const class="type">ulong ticket=class="num">0) : CEvent(EVENT_STATUS_MARKET_PENDING,event_code,ticket) {} class=class="str">"cmt">//--- 支持的订单属性 (class="num">1) 实数型,和 (class="num">2) 整数型 class="kw">virtual class="type">bool SupportProperty(ENUM_EVENT_PROP_INTEGER class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_EVENT_PROP_DOUBLE class="kw">property); class=class="str">"cmt">//--- (class="num">1) 在流水帐中显示有关事件的摘要消息, (class="num">2) 将事件发送到图表 class="kw">virtual class="type">void PrintShort(class="type">void); class="kw">virtual class="type">void SendEvent(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ CEventOrderPlased(const class="type">int event_code,const class="type">ulong ticket=class="num">0) : CEvent(EVENT_STATUS_MARKET_PENDING,event_code,ticket) {} class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回 "true",如果支持传递的事件 class=class="str">"cmt">//| 整数型属性,否则返回 &class="macro">#x27;false&class="macro">#x27; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CEventOrderPlased::SupportProperty(ENUM_EVENT_PROP_INTEGER class="kw">property) { if(class="kw">property==EVENT_PROP_TYPE_DEAL_EVENT || class="kw">property==EVENT_PROP_TICKET_DEAL_EVENT || class="kw">property==EVENT_PROP_TYPE_ORDER_POSITION || class="kw">property==EVENT_PROP_TICKET_ORDER_POSITION || class="kw">property==EVENT_PROP_POSITION_ID || class="kw">property==EVENT_PROP_POSITION_BY_ID || class="kw">property==EVENT_PROP_TIME_ORDER_POSITION ) class="kw">return false; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回 "true",如果支持传递的事件 class=class="str">"cmt">//| 实数型属性,否则返回 &class="macro">#x27;false&class="macro">#x27; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CEventOrderPlased::SupportProperty(ENUM_EVENT_PROP_DOUBLE class="kw">property) {
「挂单成交事件的日志与图表推送」
CEventOrderPlased 类里用 PrintShort 把成交流水压缩成一行可读文本,方便在 MT5 专家日志里快速扫一眼就知道哪张单、什么价、带没带 sl/tp。 代码先拼 head:事件类型描述加毫秒级时间,再判断止损止盈大于 0 才追加字符串,避免无谓的 ', sl 0.00000' 噪音;成交量用 DigitsLots 按品种精度格式化,magic 非 0 才显示。 SendEvent 里先调 PrintShort 落日志,随后用 EventChartCustom 把订单事件推到图表:图表 ID、交易事件枚举转 ushort、订单 ticket、开盘价、品种名五个参数一并发出,EA 或指标可在 OnChartEvent 里接这个自定义事件做标记。 实盘接这套时注意,外汇与贵金属杠杆高、滑点可能让 PriceOpen 偏离预期,图表标记仅作辅助,不等于执行无误。
if(class="kw">property==EVENT_PROP_PRICE_CLOSE || class="kw">property==EVENT_PROP_PROFIT ) class="kw">return false; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 在流水帐中显示简要事件消息 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEventOrderPlased::PrintShort(class="type">void) { class="type">class="kw">string head="- "+this.TypeEventDescription()+": "+TimeMSCtoString(this.TimePosition())+" -\n"; class="type">class="kw">string sl=(this.PriceStopLoss()>class="num">0 ? ", sl "+::DoubleToString(this.PriceStopLoss(),(class="type">int)::SymbolInfoInteger(this.Symbol(),SYMBOL_DIGITS)) : ""); class="type">class="kw">string tp=(this.PriceTakeProfit()>class="num">0 ? ", tp "+::DoubleToString(this.PriceTakeProfit(),(class="type">int)::SymbolInfoInteger(this.Symbol(),SYMBOL_DIGITS)) : ""); class="type">class="kw">string vol=::DoubleToString(this.VolumeInitial(),DigitsLots(this.Symbol())); class="type">class="kw">string magic=(this.Magic()!=class="num">0 ? TextByLanguage(", магик ",", magic ")+(class="type">class="kw">string)this.Magic() : ""); class="type">class="kw">string type=this.TypeOrderDescription()+" #"+(class="type">class="kw">string)this.TicketOrderEvent(); class="type">class="kw">string price=TextByLanguage(" по цене "," at price ")+::DoubleToString(this.PriceOpen(),(class="type">int)::SymbolInfoInteger(this.Symbol(),SYMBOL_DIGITS)); class="type">class="kw">string txt=head+this.Symbol()+" "+vol+" "+type+price+sl+tp+magic; ::Print(txt); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 将事件发送到图表 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEventOrderPlased::SendEvent(class="type">void) { this.PrintShort(); ::EventChartCustom(this.m_chart_id,(class="type">class="kw">ushort)this.m_trade_event,this.TicketOrderEvent(),this.PriceOpen(),this.Symbol()); } class=class="str">"cmt">//+------------------------------------------------------------------+
挂单撤销事件的类封装与属性过滤
在 MT5 的事件驱动框架里,挂单被移除(pending order removed)可以单独抽象成一个事件类 CEventOrderRemoved,它继承自基类 CEvent,构造时固定传入 EVENT_STATUS_HISTORY_PENDING 作为状态标识,并允许外部给一个 event_code 与 ticket 参数(ticket 默认 0)。 这个类只重写了四个虚函数:两个 SupportProperty 分别接管整数型和实数型属性判断,PrintShort 负责在日志打印简短摘要,SendEvent 把事件推到图表。其余行为沿用基类。 整数型属性过滤逻辑很直接:如果属性是 EVENT_PROP_TYPE_DEAL_EVENT、EVENT_PROP_TICKET_DEAL_EVENT、EVENT_PROP_TYPE_ORDER_POSITION、EVENT_PROP_TICKET_ORDER_POSITION 或 EVENT_PROP_TIME_ORDER_POSITION,直接返回 false,意味着挂单撤销事件不携带成交流水或持仓时间类字段;其余整数属性一律返回 true。 实数型 SupportProperty 在函数声明后原文未给出实现体,实际编译时若基类无默认 true 返回,需要补一个 return true 或按需求过滤,否则链接期可能报未定义引用。开 MT5 新建 EA 把下面代码贴进自定义 include 验证继承链是否通。
class=class="str">"cmt">//| EventOrderRemoved.mqh | class=class="str">"cmt">//| 版权所有 class="num">2018, MetaQuotes 软件公司 | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2018, MetaQuotes 软件公司" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 包含文件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Event.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 下挂单事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CEventOrderRemoved : class="kw">public CEvent { class="kw">public: class=class="str">"cmt">//--- 构造函数 CEventOrderRemoved(const class="type">int event_code,const class="type">ulong ticket=class="num">0) : CEvent(EVENT_STATUS_HISTORY_PENDING,event_code,ticket) {} class=class="str">"cmt">//--- 支持的订单属性 (class="num">1) 实数型,和 (class="num">2) 整数型 class="kw">virtual class="type">bool SupportProperty(ENUM_EVENT_PROP_INTEGER class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_EVENT_PROP_DOUBLE class="kw">property); class=class="str">"cmt">//--- (class="num">1) 在流水帐中显示有关事件的摘要消息, (class="num">2) 将事件发送到图表 class="kw">virtual class="type">void PrintShort(class="type">void); class="kw">virtual class="type">void SendEvent(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回 "true",如果支持传递的事件 | class=class="str">"cmt">//| 整数型属性,否则返回 &class="macro">#x27;false&class="macro">#x27; | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CEventOrderRemoved::SupportProperty(ENUM_EVENT_PROP_INTEGER class="kw">property) { if(class="kw">property==EVENT_PROP_TYPE_DEAL_EVENT || class="kw">property==EVENT_PROP_TICKET_DEAL_EVENT || class="kw">property==EVENT_PROP_TYPE_ORDER_POSITION || class="kw">property==EVENT_PROP_TICKET_ORDER_POSITION || class="kw">property==EVENT_PROP_TIME_ORDER_POSITION ) class="kw">return false; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回 "true",如果支持传递的事件 | class=class="str">"cmt">//| 实数型属性,否则返回 &class="macro">#x27;false&class="macro">#x27; | class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 删单事件的属性屏蔽与流水输出
CEventOrderRemoved 类在 SupportProperty 里对 EVENT_PROP_PROFIT 直接返回 false,意思是被删除的挂单本身不产生已实现盈亏属性,其余双精度属性一律放行。写回测或事件解析器时若误读 profit 字段,会得到脏数据,这一点在 MT5 事件流里容易被忽略。 PrintShort 把删单事件拼成一行可读文本:时间(毫秒)、品种、初始手数、订单类型与票号、开价,以及非零的 sl/tp/magic。手数精度用 DigitsLots 取,价格位数走 SymbolInfoInteger 的 SYMBOL_DIGITS,所以同一段代码在 XAUUSD(3位) 和 EURUSD(5位) 上打印位数自动不同。 SendEvent 先调 PrintShort 落日志,再用 EventChartCustom 把票号、开价、品种推到图表端,自定义事件 id 取自 m_trade_event。想验证就挂一个 EA 监听 OnChartEvent,删掉一笔挂单看图表是否收到对应 custom id,外汇与贵金属波动剧烈,事件回放仅用于逻辑核对,实盘仍属高风险。
class="type">bool CEventOrderRemoved::SupportProperty(ENUM_EVENT_PROP_DOUBLE class="kw">property) { class="kw">return(class="kw">property==EVENT_PROP_PROFIT ? false : true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 在流水帐中显示简要事件消息 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEventOrderRemoved::PrintShort(class="type">void) { class="type">class="kw">string head="- "+this.TypeEventDescription()+": "+TimeMSCtoString(this.TimePosition())+" -\n"; class="type">class="kw">string sl=(this.PriceStopLoss()>class="num">0 ? ", sl "+::DoubleToString(this.PriceStopLoss(),(class="type">int)::SymbolInfoInteger(this.Symbol(),SYMBOL_DIGITS)) : ""); class="type">class="kw">string tp=(this.PriceTakeProfit()>class="num">0 ? ", tp "+::DoubleToString(this.PriceTakeProfit(),(class="type">int)::SymbolInfoInteger(this.Symbol(),SYMBOL_DIGITS)) : ""); class="type">class="kw">string vol=::DoubleToString(this.VolumeInitial(),DigitsLots(this.Symbol())); class="type">class="kw">string magic=(this.Magic()!=class="num">0 ? TextByLanguage(", магик ",", magic ")+(class="type">class="kw">string)this.Magic() : ""); class="type">class="kw">string type=this.TypeOrderDescription()+" #"+(class="type">class="kw">string)this.TicketOrderEvent(); class="type">class="kw">string price=TextByLanguage(" по цене "," at price ")+::DoubleToString(this.PriceOpen(),(class="type">int)::SymbolInfoInteger(this.Symbol(),SYMBOL_DIGITS)); class="type">class="kw">string txt=head+this.Symbol()+" "+vol+" "+type+price+sl+tp+magic; ::Print(txt); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 将事件发送到图表 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEventOrderRemoved::SendEvent(class="type">void) { this.PrintShort(); ::EventChartCustom(this.m_chart_id,(class="type">class="kw">ushort)this.m_trade_event,this.TicketOrderEvent(),this.PriceOpen(),this.Symbol()); }
「开仓事件类的属性过滤逻辑」
在 MT5 的 EA 事件框架里,开仓动作被封装成 CEventPositionOpen 类,它继承自通用事件基类 CEvent,构造时固定传入 EVENT_STATUS_MARKET_POSITION 作为事件状态类型,并允许用 ticket 参数绑定具体持仓编号(默认 0 表示不绑定)。 这个类对属性做了显式白名单/黑名单控制:整数型属性里,只有 EVENT_PROP_POSITION_BY_ID 被拒绝(返回 false),其余一律支持;实数型属性中,EVENT_PROP_PRICE_CLOSE 与 EVENT_PROP_PROFIT 被挡掉,因为开仓瞬间平仓价和浮盈尚未成立,硬取容易读到脏值。 PrintShort 方法把事件类型和持仓时间拼成一行流水账前缀,格式为「- 类型描述: 毫秒级时间 -\n」,方便在 Experts 日志里快速扫读哪根毫秒刻度触发了哪类开仓。 外汇与贵金属杠杆交易高风险,这类事件封装仅用于复盘与告警,不代表任何方向判断。
class="macro">#class="kw">property copyright "版权所有 class="num">2018, MetaQuotes 软件公司" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 包含文件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Event.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 开仓事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CEventPositionOpen : class="kw">public CEvent { class="kw">public: class=class="str">"cmt">//--- 构造函数 CEventPositionOpen(const class="type">int event_code,const class="type">ulong ticket=class="num">0) : CEvent(EVENT_STATUS_MARKET_POSITION,event_code,ticket) {} class=class="str">"cmt">//--- 支持的订单属性 (class="num">1) 实数型,和 (class="num">2) 整数型 class="kw">virtual class="type">bool SupportProperty(ENUM_EVENT_PROP_INTEGER class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_EVENT_PROP_DOUBLE class="kw">property); class=class="str">"cmt">//--- (class="num">1) 在流水帐中显示有关事件的摘要消息, (class="num">2) 将事件发送到图表 class="kw">virtual class="type">void PrintShort(class="type">void); class="kw">virtual class="type">void SendEvent(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回 "true",如果支持传递的事件整数型属性,否则返回 &class="macro">#x27;false&class="macro">#x27; | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CEventPositionOpen::SupportProperty(ENUM_EVENT_PROP_INTEGER class="kw">property) { class="kw">return(class="kw">property==EVENT_PROP_POSITION_BY_ID ? false : true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回 &class="macro">#x27;true&class="macro">#x27; 如果订单支持所传递的实数型属性,否则返回 &class="macro">#x27;false&class="macro">#x27; | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CEventPositionOpen::SupportProperty(ENUM_EVENT_PROP_DOUBLE class="kw">property) { if(class="kw">property==EVENT_PROP_PRICE_CLOSE || class="kw">property==EVENT_PROP_PROFIT ) class="kw">return false; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 在流水帐中显示简要事件消息 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEventPositionOpen::PrintShort(class="type">void) { class="type">class="kw">string head="- "+this.TypeEventDescription()+": "+TimeMSCtoString(this.TimePosition())+" -\n";
把持仓开立事件拼成可读日志并推给图表
持仓被打开时,最忌讳的是终端只弹一句‘trade ok’却没带单号、价格和点位。下面这段把一笔 PositionOpen 事件的所有关键字段拼成一行字符串,直接 Print 到专家日志,肉眼扫一眼就能核对。 字符串拼接逻辑很直白:若挂单被激活,就补上订单号与类型描述;SL/TP 大于 0 才追加,避免对无止损单打印‘sl 0.00000’的噪音;成交量用 DigitsLots 按品种精度格式化,magic 非 0 才显示。最终串是 ‘品种 手数 类型#ID [激活信息] [#订单号] at price X, sl Y, tp Z, magic N’ 的结构。 SendEvent 在打印后调用 EventChartCustom,把持仓 ID、开仓价、品种名发到图表端。图表上挂的 EA 可以用 OnChartEvent 捕获这个自定义事件,做弹窗或画线——外汇与贵金属波动剧烈,这类即时事件推送对秒级风控有帮助,但信号本身不预示方向,仅作执行追踪。 复制下面代码到 MT5 的 mqh 里,建一个 CEventPositionOpen 对象并在持仓回调里调 SendEvent,你能在‘专家’标签页看到完整一行日志,验证字段是否如预期拼接。
class="type">class="kw">string order=(this.IsPresentEventFlag(TRADE_EVENT_FLAG_ORDER_ACTIVATED) ? " #"+((class="type">class="kw">string)this.TicketOrderPosition()) : ""); class="type">class="kw">string activated=(this.IsPresentEventFlag(TRADE_EVENT_FLAG_ORDER_ACTIVATED) ? TextByLanguage(" активацией ордера "," by ")+this.TypeOrderBasedDescription() : ""); class="type">class="kw">string sl=(this.PriceStopLoss()>class="num">0 ? ", sl "+::DoubleToString(this.PriceStopLoss(),(class="type">int)::SymbolInfoInteger(this.Symbol(),SYMBOL_DIGITS)) : ""); class="type">class="kw">string tp=(this.PriceTakeProfit()>class="num">0 ? ", tp "+::DoubleToString(this.PriceTakeProfit(),(class="type">int)::SymbolInfoInteger(this.Symbol(),SYMBOL_DIGITS)) : ""); class="type">class="kw">string vol=::DoubleToString(this.VolumeInitial(),DigitsLots(this.Symbol())); class="type">class="kw">string magic=(this.Magic()!=class="num">0 ? TextByLanguage(", магик ",", magic ")+(class="type">class="kw">string)this.Magic() : ""); class="type">class="kw">string type=this.TypePositionDescription()+" #"+(class="type">class="kw">string)this.PositionID(); class="type">class="kw">string price=TextByLanguage(" по цене "," at price ")+::DoubleToString(this.PriceOpen(),(class="type">int)::SymbolInfoInteger(this.Symbol(),SYMBOL_DIGITS)); class="type">class="kw">string txt=head+this.Symbol()+" "+vol+" "+type+activated+order+price+sl+tp+magic; ::Print(txt); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 将事件发送到图表 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEventPositionOpen::SendEvent(class="type">void) { this.PrintShort(); ::EventChartCustom(this.m_chart_id,(class="type">class="kw">ushort)this.m_trade_event,this.PositionID(),this.PriceOpen(),this.Symbol()); }
◍ 平仓事件的类封装与属性透传
在 MT5 的 EA 事件体系里,平仓动作需要单独建模。下面这段把平仓事件抽象成 CEventPositionClose 类,继承自基础事件类 CEvent,构造时固定传入 EVENT_STATUS_HISTORY_POSITION 状态,并允许用 ticket 指定具体持仓编号(默认 0 表示不绑定)。 类里重载了两个 SupportProperty 方法,分别接收整数型与实数型属性枚举。当前实现直接 return true,意味着该事件对外声明:无论是整数属性(如成交单号、魔术码)还是实数属性(如平仓价、成交量)都照单全收,调用方无需担心属性类型被拒。 PrintShort 负责把事件压缩成一行流水账。它拼接了事件类型描述、毫秒级时间字符串,以及是否由某张订单触发的信息(TRADE_EVENT_FLAG_BY_POS 标志位命中时追加订单类型与 PositionByID),最后用 DoubleToString 按品种手数精度格式化已平成交量。 开 MT5 新建一个 include 文件把这段塞进去,再在 EA 里 #include 它,就能在回测历史里直接打印每笔平仓的精确时间与手数——外汇与贵金属波动大、滑点随机,这类毫秒级记录对复盘剥头皮策略很有用,但任何历史还原都只是概率参考。
class=class="str">"cmt">//| 包含文件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Event.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 开仓事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CEventPositionClose : class="kw">public CEvent { class="kw">public: class=class="str">"cmt">//--- 构造函数 CEventPositionClose(const class="type">int event_code,const class="type">ulong ticket=class="num">0) : CEvent(EVENT_STATUS_HISTORY_POSITION,event_code,ticket) {} class=class="str">"cmt">//--- 支持的订单属性 (class="num">1) 实数型,和 (class="num">2) 整数型 class="kw">virtual class="type">bool SupportProperty(ENUM_EVENT_PROP_INTEGER class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_EVENT_PROP_DOUBLE class="kw">property); class=class="str">"cmt">//--- (class="num">1) 在流水帐中显示有关事件的摘要消息, (class="num">2) 将事件发送到图表 class="kw">virtual class="type">void PrintShort(class="type">void); class="kw">virtual class="type">void SendEvent(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回 "true",如果支持传递的事件 | class=class="str">"cmt">//| 整数型属性,否则返回 &class="macro">#x27;false&class="macro">#x27; | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CEventPositionClose::SupportProperty(ENUM_EVENT_PROP_INTEGER class="kw">property) { class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回 "true",如果支持传递的事件 | class=class="str">"cmt">//| 实数型属性,否则返回 &class="macro">#x27;false&class="macro">#x27; | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CEventPositionClose::SupportProperty(ENUM_EVENT_PROP_DOUBLE class="kw">property) { class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 在流水帐中显示有关事件的简要消息 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEventPositionClose::PrintShort(class="type">void) { class="type">class="kw">string head="- "+this.TypeEventDescription()+": "+TimeMSCtoString(this.TimePosition())+" -\n"; class="type">class="kw">string opposite=(this.IsPresentEventFlag(TRADE_EVENT_FLAG_BY_POS) ? " by "+this.TypeOrderDescription()+" #"+(class="type">class="kw">string)this.PositionByID() : ""); class="type">class="kw">string vol=::DoubleToString(this.VolumeExecuted(),DigitsLots(this.Symbol()));
「平仓日志与图表事件怎么发」
平仓动作落地后,先拼一条多语言日志再推到图表,是自动化盯盘里最实用的小闭环。下面这段代码把魔术码、品种、手数、平仓价、浮盈全部串进一行 Print,俄语和英文环境都能直接读。
class="type">class="kw">string magic=(this.Magic()!=class="num">0 ? TextByLanguage(", магик ",", magic ")+(class="type">class="kw">string)this.Magic() : ""); class="type">class="kw">string type=this.TypePositionDescription()+" #"+(class="type">class="kw">string)this.PositionID()+opposite; class="type">class="kw">string price=TextByLanguage(" по цене "," at price ")+::DoubleToString(this.PriceClose(),(class="type">int)::SymbolInfoInteger(this.Symbol(),SYMBOL_DIGITS)); class="type">class="kw">string profit=TextByLanguage(", профит: ",", profit: ")+::DoubleToString(this.Profit(),this.m_digits_acc)+" "+::AccountInfoString(ACCOUNT_CURRENCY); class="type">class="kw">string txt=head+this.Symbol()+" "+vol+" "+type+price+magic+profit; ::Print(txt); }
class="type">class="kw">string magic=(this.Magic()!=class="num">0 ? TextByLanguage(", магик ",", magic ")+(class="type">class="kw">string)this.Magic() : ""); class="type">class="kw">string type=this.TypePositionDescription()+" #"+(class="type">class="kw">string)this.PositionID()+opposite; class="type">class="kw">string price=TextByLanguage(" по цене "," at price ")+::DoubleToString(this.PriceClose(),(class="type">int)::SymbolInfoInteger(this.Symbol(),SYMBOL_DIGITS)); class="type">class="kw">string profit=TextByLanguage(", профит: ",", profit: ")+::DoubleToString(this.Profit(),this.m_digits_acc)+" "+::AccountInfoString(ACCOUNT_CURRENCY); class="type">class="kw">string txt=head+this.Symbol()+" "+vol+" "+type+price+magic+profit; ::Print(txt); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 将事件发送到图表 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEventPositionClose::SendEvent(class="type">void) { this.PrintShort(); ::EventChartCustom(this.m_chart_id,(class="type">class="kw">ushort)this.m_trade_event,this.PositionID(),this.PriceClose(),this.Symbol()); }
余额事件类的属性支持边界
CEventBalanceOperation 作为余额类事件的封装,构造时直接绑定 EVENT_STATUS_BALANCE 状态并透传事件码与订单 ticket,默认 ticket 为 0。它对外暴露三组 SupportProperty 重载,分别接管整数、实数与字符串型属性,这是事件系统做属性路由时的判断入口。 整数属性里有一串明确被拒的枚举:EVENT_PROP_TYPE_ORDER_EVENT、EVENT_PROP_TYPE_ORDER_POSITION、EVENT_PROP_TICKET_ORDER_EVENT、EVENT_PROP_TICKET_ORDER_POSITION、EVENT_PROP_POSITION_ID、EVENT_PROP_POSITION_BY_ID、EVENT_PROP_MAGIC_ORDER、EVENT_PROP_TIME_ORDER_POSITION,命中即返回 false,其余整数属性一律 true。 实数属性只认 EVENT_PROP_PROFIT 一个,返回 true,其他全部 false;字符串属性干脆整体返回 false。写自定义事件监听时,若向余额事件要持仓 magic 或下单时间,会直接拿不到值,得绕回订单历史去查。 外汇与贵金属杠杆高,这类事件封装只解决状态记录,不预示盈亏方向,实盘接事件流时仍要以成交回报为准。
CEventBalanceOperation(const class="type">int event_code,const class="type">ulong ticket=class="num">0) : CEvent(EVENT_STATUS_BALANCE,event_code,ticket) {} class=class="str">"cmt">//--- 支持的订单属性 (class="num">1) 实数型,和 (class="num">2) 整数型 class="kw">virtual class="type">bool SupportProperty(ENUM_EVENT_PROP_INTEGER class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_EVENT_PROP_DOUBLE class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_EVENT_PROP_STRING class="kw">property); class=class="str">"cmt">//--- (class="num">1) 在流水帐中显示有关事件的摘要消息, (class="num">2) 将事件发送到图表 class="kw">virtual class="type">void PrintShort(class="type">void); class="kw">virtual class="type">void SendEvent(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回 "true",如果支持传递的事件整数型属性,否则返回 &class="macro">#x27;false&class="macro">#x27; | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CEventBalanceOperation::SupportProperty(ENUM_EVENT_PROP_INTEGER class="kw">property) { if(class="kw">property==EVENT_PROP_TYPE_ORDER_EVENT || class="kw">property==EVENT_PROP_TYPE_ORDER_POSITION || class="kw">property==EVENT_PROP_TICKET_ORDER_EVENT || class="kw">property==EVENT_PROP_TICKET_ORDER_POSITION || class="kw">property==EVENT_PROP_POSITION_ID || class="kw">property==EVENT_PROP_POSITION_BY_ID || class="kw">property==EVENT_PROP_POSITION_ID || class="kw">property==EVENT_PROP_MAGIC_ORDER || class="kw">property==EVENT_PROP_TIME_ORDER_POSITION ) class="kw">return false; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回 "true",如果支持传递的事件实数型属性,否则返回 &class="macro">#x27;false&class="macro">#x27; | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CEventBalanceOperation::SupportProperty(ENUM_EVENT_PROP_DOUBLE class="kw">property) { class="kw">return(class="kw">property==EVENT_PROP_PROFIT ? true : false); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回 "true",如果支持传递的事件字符串型属性,否则返回 &class="macro">#x27;false&class="macro">#x27; | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CEventBalanceOperation::SupportProperty(ENUM_EVENT_PROP_STRING class="kw">property) { class="kw">return false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 在流水帐中显示有关事件的简要消息 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEventBalanceOperation::PrintShort(class="type">void) {
◍ 把资金变动事件推到图表上
在 MT5 的 EA 或指标里,光靠 Print 往日志写一笔余额变动往往不够,真要盯盘还得把事件丢到图表对象层。下面这段就是典型的「先拼头部、再发自定义事件」写法。 head 那行把状态描述、持仓时间和换行符拼成前缀,状态与时间是分开取的,TimeMSCtoString 拿到的是毫秒级时间戳,比普通 TimeToString 更细。紧接着 PrintShort 里用 DoubleToString 按账户小数位 m_digits_acc 格式化盈亏,货币单位直接取 ACCOUNT_CURRENCY,所以多币种账户也不会串。 SendEvent 先调 PrintShort 落日志,再用 EventChartCustom 把事件号 m_trade_event、类型 TypeEvent()、盈亏值和货币名推到指定 chart_id。图表上挂的脚本只要监听同事件号,就能实时画标记——外汇和贵金属波动快、杠杆高,这类自定义事件适合做轻量预警,但信号本身不预示方向,只是把已发生的账面变动可视化。
class="type">class="kw">string head="- "+this.StatusDescription()+": "+TimeMSCtoString(this.TimePosition())+" -\n"; ::Print(head+this.TypeEventDescription()+": "+::DoubleToString(this.Profit(),this.m_digits_acc)+" "+::AccountInfoString(ACCOUNT_CURRENCY)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 将事件发送到图表 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEventBalanceOperation::SendEvent(class="type">void) { this.PrintShort(); ::EventChartCustom(this.m_chart_id,(class="type">class="kw">ushort)this.m_trade_event,this.TypeEvent(),this.Profit(),::AccountInfoString(ACCOUNT_CURRENCY)); } class=class="str">"cmt">//+------------------------------------------------------------------+
「把账户事件攒成可遍历的集合」
早期版本里账户事件只能追最新一笔,且逻辑塞在 CEngine 基类。单一循环里若同时删挂单、平多仓,旧理论一次只处理若干个最新事件中的一个,会漏掉并发动作。正确做法是单独建一个 CEventsCollection 类(基类 CListObj),把启动后所有事件存进列表,按事件时间排序,再一次性处置;后续还能借这类方法重走账户历史,从开户起重建发生过的事。 在 DoEasy\Collections 下新建 EventsCollection.mqh。构造函数初始化清单里要重置交易事件、清集合、设按时间排序、设集合 ID、置对冲账户标志,并把控制程序图表 ID 绑成当前图表。私有成员 m_list_events 基于 CListObj 存全部事件;m_is_hedge 记账户类型决定事件处理模块;m_chart_id 收自定义事件并回传图表;m_trade_event 存末次事件;m_event_instance 是按属性搜样本用的特殊对象。 类里取列表的方法很直白:取在场挂单前先校验列表类型,非入场集合就报错返空;取已删挂单/成交/平仓单则校验后从历史集按状态筛。按仓位 ID 取订单时,先按指针形成子列表,删掉成交返剩余;取入场成交留 DEAL_ENTRY_IN、取离场成交留 DEAL_ENTRY_OUT,调用方必须判 NULL。离场成交量要搜两次——成交里汇总 DEAL_ENTRY_OUT,再扫历史平仓单按 ORDER_PROP_POSITION_BY_ID 补逆向仓位部分,两者相加才是真实平仓量。 因逆向平仓可能出现两笔相同事件(两仓平但共用一平仓单、两成交),写入集合前要用 IsEqual() 比一遍,避免复刻。Refresh() 是总入口,目前仅适配 MQL5 对冲账户:收订单/成交/仓位各集合指针及增减数量,循环按量调 CreateNewEvent() 塞进列表,并向图表发自定义消息;m_trade_event 记末次事件,公有 GetLastTradeEvent() 可读,也有类似 ResetLastError() 的清事件法。 CEngine 要瘦身:删 m_trade_event_code 及 SetTradeEvent/IsTradeEventFlag/WorkWithHedgeCollections/WorkWithNettoCollections/TradeEventCode 等私有项,改含 EventsCollection 文件、声明事件集对象、加 TradeEventsControl()、把 GetListHistoryDeals() 改名 GetListDeals()。构造函数里加毫秒定时器推进,失败写日记。定时器里订单成交仓位集解暂停后调 TradeEventsControl(),它比旧 WorkWithHedgeCollections 短很多,逻辑仅为收集合事件并派发。
账户事件集合的类骨架
在 MT5 的 EA 工程里,把账户层面的交易动作(挂单、成交、平仓、余额变动)做统一抽象,常见做法是继承 CListObj 写一个 CEventsCollection 类。它内部挂了一个 m_list_events 事件清单,并用 m_is_hedge 标记当前账户是对冲模式还是净头寸模式——这点直接影响后续事件归类逻辑。 类里还保留了 m_chart_id(EA 所依附的图表 ID)和 m_trade_event(当前触发的交易事件枚举),以及一个 m_event_instance 用作按属性检索时的临时对象。 私有方法暴露了事件重建的入口:CreateNewEvent 根据订单状态把历史与实时订单数组合并后生成新事件;其余 GetList* 系列负责从总列表里筛出在场挂单、已删挂单、成交、按仓位 ID 归类的订单与成交等子集。 下面这段是类声明头部与私有成员、方法签名的原文,可直接贴进 MQH 文件验证编译:
class="macro">#class="kw">property copyright "版权所有 class="num">2018, MetaQuotes 软件公司" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include "ListObj.mqh" class="macro">#include "..\Services\Select.mqh" class="macro">#include "..\Objects\Orders\Order.mqh" class="macro">#include "..\Objects\Events\EventBalanceOperation.mqh" class="macro">#include "..\Objects\Events\EventOrderPlaced.mqh" class="macro">#include "..\Objects\Events\EventOrderRemoved.mqh" class="macro">#include "..\Objects\Events\EventPositionOpen.mqh" class="macro">#include "..\Objects\Events\EventPositionClose.mqh" class CEventsCollection : class="kw">public CListObj { class="kw">private: CListObj m_list_events; class=class="str">"cmt">// 事件清单 class="type">bool m_is_hedge; class=class="str">"cmt">// 对冲账户标志 class="type">long m_chart_id; class=class="str">"cmt">// 控制程序所在的图表 ID ENUM_TRADE_EVENT m_trade_event; class=class="str">"cmt">// 账户交易事件 CEvent m_event_instance; class=class="str">"cmt">// 按属性搜索的事件对象 class=class="str">"cmt">//--- 根据订单状态创建交易事件 class="type">void CreateNewEvent(COrder* order,CArrayObj* list_history,CArrayObj* list_market); class=class="str">"cmt">//--- 选择并返回在场挂单列表 CArrayObj* GetListMarketPendings(CArrayObj* list); class=class="str">"cmt">//--- 选择并返回历史(class="num">1)已删除挂单,(class="num">2)成交,(class="num">3)所有已平订单的列表 CArrayObj* GetListHistoryPendings(CArrayObj* list); CArrayObj* GetListDeals(CArrayObj* list); CArrayObj* GetListCloseByOrders(CArrayObj* list); class=class="str">"cmt">//--- 选择并返回(class="num">1)所有仓位订单的 ID 列表,(class="num">2)所有成交仓位的 ID class=class="str">"cmt">//--- (class="num">3) 按仓位 ID 排序的所有入场成交,(class="num">4)按仓位 ID 排序的所有离场成交 CArrayObj* GetListAllOrdersByPosID(CArrayObj* list,const class="type">ulong position_id); CArrayObj* GetListAllDealsByPosID(CArrayObj* list,const class="type">ulong position_id); CArrayObj* GetListAllDealsInByPosID(CArrayObj* list,const class="type">ulong position_id);
◍ 事件集合的筛选与刷新接口
做仓位级复盘时,光拿到原始成交列表还不够,得能按仓位 ID 把 IN/OUT 成交量拆开算。GetListAllDealsOutByPosID 与 SummaryVolumeDealsOutByPosID 这类方法就是干这个的:前者返回某仓位所有平仓成交对象指针数组,后者直接给出该仓位平仓总交易量(double),回测时用来核对净敞口很实用。 事件集合类对外暴露的 GetList 有多态版本。无参版直接返回内部 m_list_events 的地址;带属性参数的版本则按整数、实数、字符串三种属性类型配合比较符(默认 EQUAL)做过滤,底层走 CSelect::ByEventProperty。 时间窗筛选用 GetListByTime(begin_time, end_time),两个 datetime 参数默认值都是 0,意味着不传就拉全量。Refresh 方法靠外部传入历史与实时两个列表,以及新增订单计数来增量更新事件集,避免每次全量重建——在 MT5 实盘高频事件流里,这种增量刷新可能明显降低主线程开销。
CArrayObj* GetListAllDealsOutByPosID(CArrayObj* list,const class="type">ulong position_id); class=class="str">"cmt">//--- 按仓位 ID 返回所有成交的总交易量(class="num">1)IN,(class="num">2)OUT class="type">class="kw">double SummaryVolumeDealsInByPosID(CArrayObj* list,const class="type">ulong position_id); class="type">class="kw">double SummaryVolumeDealsOutByPosID(CArrayObj* list,const class="type">ulong position_id); class=class="str">"cmt">//--- 返回 (class="num">1) 首笔, (class="num">2) 最后一笔,和 (class="num">3) 从所有仓位订单列表中的平仓订单, (class="num">4) 按票据的订单 COrder* GetFirstOrderFromList(CArrayObj* list,const class="type">ulong position_id); COrder* GetLastOrderFromList(CArrayObj* list,const class="type">ulong position_id); COrder* GetCloseByOrderFromList(CArrayObj* list,const class="type">ulong position_id); COrder* GetOrderByTicket(CArrayObj* list,const class="type">ulong order_ticket); class=class="str">"cmt">//--- 返回事件列表中事件对象存在的标志 class="type">bool IsPresentEventInList(CEvent* compared_event); class="kw">public: class=class="str">"cmt">//--- 选择集合中从 begin_time 到 end_time 时间范围内的事件 CArrayObj *GetListByTime(const class="type">class="kw">datetime begin_time=class="num">0,const class="type">class="kw">datetime end_time=class="num">0); class=class="str">"cmt">//--- 按“原样”返回完整的事件集合列表 CArrayObj *GetList(class="type">void) { class="kw">return &this.m_list_events; } class=class="str">"cmt">//--- 返回符合所选择的(class="num">1)整数型,(class="num">2)实数型,和(class="num">3)字符串型属性比较标准的列表 CArrayObj *GetList(ENUM_EVENT_PROP_INTEGER class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByEventProperty(this.GetList(),class="kw">property,value,mode); } CArrayObj *GetList(ENUM_EVENT_PROP_DOUBLE class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByEventProperty(this.GetList(),class="kw">property,value,mode); } CArrayObj *GetList(ENUM_EVENT_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByEventProperty(this.GetList(),class="kw">property,value,mode); } class=class="str">"cmt">//--- 更新事件列表 class="type">void Refresh(CArrayObj* list_history, CArrayObj* list_market, const class="type">bool is_history_event, const class="type">bool is_market_event, const class="type">int new_history_orders, const class="type">int new_market_pendings,
「事件容器构造时的几个关键绑定」
CEventsCollection 的构造函数里做了几件影响后续逻辑走向的初始化,值得在 MT5 里单步跟一遍。 m_trade_event 被显式置为 TRADE_EVENT_NO_EVENT,意味着对象刚建起来时账户交易事件处于“空”状态,必须靠后续 OnTradeTransaction 回调去填充,避免用残留值误判。 构造函数顺手把账户对冲属性读进 m_is_hedge:用 AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING 判断,零售对冲账户会返回 true。这直接决定后面挂单和仓位配对逻辑是否走对冲分支。 图表 ID 通过 ChartID() 绑到 m_chart_id,控制面板或图形对象就知道自己贴在哪张图上;若用 SetChartID 改绑,事件高亮可能跳去别的图表。 事件列表 m_list_events 在构造时 Clear 后做了 Sort(SORT_BY_EVENT_TIME_EVENT) 和 Type(COLLECTION_EVENTS_ID),保证后续插入的事件按时间有序且带集合类型标记,省得每次追加再排。
CEventsCollection::CEventsCollection(class="type">void) : m_trade_event(TRADE_EVENT_NO_EVENT) { this.m_list_events.Clear(); this.m_list_events.Sort(SORT_BY_EVENT_TIME_EVENT); this.m_list_events.Type(COLLECTION_EVENTS_ID); this.m_is_hedge=class="type">bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING); this.m_chart_id=::ChartID(); }
按状态剥离挂单与成交的检索接口
在 MT5 的 CEventsCollection 体系里,挂单和成交被统一塞进 CArrayObj 列表,但实盘盯盘时你往往只关心某一类。下面这组方法就是按状态把列表切开:GetListMarketPendings 取在场挂单,GetListHistoryPendings 取已删除的历史挂单,GetListDeals 直接拿成交。 核心拦截逻辑在类型校验。若传入的 list.Type() 不等于 COLLECTION_MARKET_ID 或 COLLECTION_HISTORY_ID,函数立刻 Print 报错并返回 NULL,避免把历史集合当市场集合筛。 [CODE] 里的 GetListHistoryPendings 给出完整实现:先判类型,失败返回 NULL;通过才调用 CSelect::ByOrderProperty 按 ORDER_PROP_STATUS == ORDER_STATUS_HISTORY_PENDING 过滤。开 MT5 把这段代码贴进 EA 调试,传一个历史集合进去,应能拿回纯已删挂单子列表。 外汇与贵金属杠杆高,历史挂单统计仅作仓位复盘参考,不预示后续价格走向。
CArrayObj* CEventsCollection::GetListHistoryPendings(CArrayObj* list) { if(list.Type()!=COLLECTION_HISTORY_ID) { Print(DFUN,TextByLanguage("Ошибка. Список не является списком исторической коллекции","Error. The list is not a list of the history collection")); class="kw">return NULL; } CArrayObj* list_orders=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_HISTORY_PENDING,EQUAL); class="kw">return list_orders; }
◍ 按仓位与状态切分历史成交的子集
在 MT5 历史容器里捞数据,第一步必须确认传入的 list 是 COLLECTION_HISTORY_ID 类型,否则直接 Print 报错并返回 NULL,这是后面所有筛选的前提。 GetListDeals 用 CSelect::ByOrderProperty 按 ORDER_PROP_STATUS 等于 ORDER_STATUS_DEAL 过滤,拿到的是已成交状态的订单子集;GetListCloseByOrders 则按 ORDER_PROP_TYPE 等于 ORDER_TYPE_CLOSE_BY 把反向 CloseBy 平仓单单独拎出来。 按仓位 ID 拆子集时,GetListAllOrdersByPosID 先按 POSITION_ID 相等筛选,再叠加 STATUS 不等于 DEAL 的条件,留下的就是该仓位下尚未成成交形态的挂单与活动单;GetListAllDealsByPosID 同理但保留 STATUS 等于 DEAL,只取真实成交。 GetListAllDealsInByPosID 直接复用 GetListAllDealsByPosID 的结果,调用方只需传 position_id 即可拿到某仓位的所有入场成交。外汇与贵金属品种的历史回放涉及真实资金逻辑,复盘验证时须认知其高风险属性,参数筛选结果仅代表历史统计倾向。
CArrayObj* CEventsCollection::GetListDeals(CArrayObj* list) { if(list.Type()!=COLLECTION_HISTORY_ID) { Print(DFUN,TextByLanguage("Ошибка. Список не является списком исторической коллекции","Error. The list is not a list of the history collection")); class="kw">return NULL; } CArrayObj* list_deals=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_DEAL,EQUAL); class="kw">return list_deals; } CArrayObj* CEventsCollection::GetListCloseByOrders(CArrayObj *list) { if(list.Type()!=COLLECTION_HISTORY_ID) { Print(DFUN,TextByLanguage("Ошибка. Список не является списком исторической коллекции","Error. The list is not a list of the history collection")); class="kw">return NULL; } CArrayObj* list_orders=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,ORDER_TYPE_CLOSE_BY,EQUAL); class="kw">return list_orders; } CArrayObj* CEventsCollection::GetListAllOrdersByPosID(CArrayObj* list,const class="type">ulong position_id) { CArrayObj* list_orders=CSelect::ByOrderProperty(list,ORDER_PROP_POSITION_ID,position_id,EQUAL); list_orders=CSelect::ByOrderProperty(list_orders,ORDER_PROP_STATUS,ORDER_STATUS_DEAL,NO_EQUAL); class="kw">return list_orders; } CArrayObj* CEventsCollection::GetListAllDealsByPosID(CArrayObj *list,const class="type">ulong position_id) { if(list.Type()!=COLLECTION_HISTORY_ID) { Print(DFUN,TextByLanguage("Ошибка. Список не является списком исторической коллекции","Error. The list is not a list of the history collection")); class="kw">return NULL; } CArrayObj* list_deals=CSelect::ByOrderProperty(list,ORDER_PROP_POSITION_ID,position_id,EQUAL); list_deals=CSelect::ByOrderProperty(list_deals,ORDER_PROP_STATUS,ORDER_STATUS_DEAL,EQUAL); class="kw">return list_deals; } CArrayObj* CEventsCollection::GetListAllDealsInByPosID(CArrayObj *list,const class="type">ulong position_id) { CArrayObj* list_deals=this.GetListAllDealsByPosID(list,position_id);
「按仓位 ID 拆出进出场成交与累计量」
在 MT5 的成交事件集合里,按 position_id 把某仓相关的所有 deal 抓出来后,还得再切一刀:进场(DEAL_ENTRY_IN)和离场(DEAL_ENTRY_OUT)要分清单列,否则后面算净仓和回吐量会串味。 下面这段直接给出两个取子集的方法,核心都靠 CSelect::ByOrderProperty 按 ORDER_PROP_DEAL_ENTRY 做等值过滤,返回的是新的 CArrayObj 指针,原清单不被破坏。 CArrayObj* CEventsCollection::GetListAllDealsOutByPosID(CArrayObj *list,const ulong position_id) { CArrayObj* list_deals=this.GetListAllDealsByPosID(list,position_id); list_deals=CSelect::ByOrderProperty(list_deals,ORDER_PROP_DEAL_ENTRY,DEAL_ENTRY_OUT,EQUAL); return list_deals; } 真正有用的不是清单本身,而是成交量汇总。SummaryVolumeDealsInByPosID 先取 IN 清单,空指针直接返 0,再跑循环把每笔 deal.Volume() 累加;OUT 那边逻辑对称,但额外调了 GetListCloseByOrders 去覆盖逆向仓平仓带来的成交量(源码片段在循环后截断,说明 OUT 量统计会把 Close-By 单子也纳入)。 实盘验证时,EURUSD 一个 0.5 手净仓若被反向 0.3 手 close-by 掉,OUT 汇总量会包含这 0.3,而非只看显式平仓单。外汇与贵金属杠杆高,用这类统计做仓位审计前,先在策略测试器跑历史成交核对一遍代码路径。
CArrayObj* CEventsCollection::GetListAllDealsOutByPosID(CArrayObj *list,const class="type">ulong position_id) { CArrayObj* list_deals=this.GetListAllDealsByPosID(list,position_id); list_deals=CSelect::ByOrderProperty(list_deals,ORDER_PROP_DEAL_ENTRY,DEAL_ENTRY_OUT,EQUAL); class="kw">return list_deals; } class="type">class="kw">double CEventsCollection::SummaryVolumeDealsInByPosID(CArrayObj *list,const class="type">ulong position_id) { class="type">class="kw">double vol=class="num">0.0; CArrayObj* list_in=this.GetListAllDealsInByPosID(list,position_id); if(list_in==NULL) class="kw">return class="num">0; for(class="type">int i=class="num">0;i<list_in.Total();i++) { COrder* deal=list_in.At(i); if(deal==NULL) class="kw">continue; vol+=deal.Volume(); } class="kw">return vol; } class="type">class="kw">double CEventsCollection::SummaryVolumeDealsOutByPosID(CArrayObj *list,const class="type">ulong position_id) { class="type">class="kw">double vol=class="num">0.0; CArrayObj* list_out=this.GetListAllDealsOutByPosID(list,position_id); if(list_out!=NULL) { for(class="type">int i=class="num">0;i<list_out.Total();i++) { COrder* deal=list_out.At(i); if(deal==NULL) class="kw">continue; vol+=deal.Volume(); } } CArrayObj* list_by=this.GetListCloseByOrders(list);
按持仓 ID 聚合挂单与首尾单提取
在订单事件集合类里,常需要把一个持仓关联的所有订单(含开仓、加仓、平仓、close_by)从总列表里筛出来再统计。下面这段逻辑先遍历传入的订单列表,只要订单的 PositionID 或 PositionByID 命中目标 position_id,就把它的成交量累加进 vol,空指针直接跳过。 提取首笔与末笔订单时,先调用 GetListAllOrdersByPosID 拿到该持仓的全部订单对象数组,判空后按 ORDER_TIME_OPEN_MSC(毫秒级开单时间)排序。首单取 At(0),末单取 At(Total()-1),这样即便同一持仓被多次部分平仓,时间序也不会乱。 close_by 订单的提取稍有不同:在拿到持仓订单列表后,再用 CSelect::ByOrderProperty 按 ORDER_PROP_TYPE 过滤出 ORDER_TYPE_CLOSE_BY 类型,排序后取最后一条。外汇与贵金属保证金交易杠杆高,持仓被 close_by 对冲时成交节奏快,这种按 ID 聚合的方式能在回测或实盘监控里快速定位某一仓的完整生命周期。 把这几段直接塞进你的 EA 调试工程,用 Print() 打出 vol 和首末单的 Ticket,就能在 MT5 策略测试器里验证聚合是否漏单。
if(list_by!=NULL) { for(class="type">int i=class="num">0;i<list_by.Total();i++) { COrder* order=list_by.At(i); if(order==NULL) class="kw">continue; if(order.PositionID()==position_id || order.PositionByID()==position_id) { vol+=order.Volume(); } } } class="kw">return vol; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 从所有仓位订单列表中返回首笔订单 | class=class="str">"cmt">//+------------------------------------------------------------------+ COrder* CEventsCollection::GetFirstOrderFromList(CArrayObj* list,const class="type">ulong position_id) { CArrayObj* list_orders=this.GetListAllOrdersByPosID(list,position_id); if(list_orders==NULL || list_orders.Total()==class="num">0) class="kw">return NULL; list_orders.Sort(SORT_BY_ORDER_TIME_OPEN_MSC); COrder* order=list_orders.At(class="num">0); class="kw">return(order!=NULL ? order : NULL); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 从所有仓位订单列表中返回最后一笔订单 | class=class="str">"cmt">//+------------------------------------------------------------------+ COrder* CEventsCollection::GetLastOrderFromList(CArrayObj* list,const class="type">ulong position_id) { CArrayObj* list_orders=this.GetListAllOrdersByPosID(list,position_id); if(list_orders==NULL || list_orders.Total()==class="num">0) class="kw">return NULL; list_orders.Sort(SORT_BY_ORDER_TIME_OPEN_MSC); COrder* order=list_orders.At(list_orders.Total()-class="num">1); class="kw">return(order!=NULL ? order : NULL); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回最后的平仓订单 | class=class="str">"cmt">//| 从所有仓位订单里 | class=class="str">"cmt">//+------------------------------------------------------------------+ COrder* CEventsCollection::GetCloseByOrderFromList(CArrayObj *list,const class="type">ulong position_id) { CArrayObj* list_orders=this.GetListAllOrdersByPosID(list,position_id); list_orders=CSelect::ByOrderProperty(list_orders,ORDER_PROP_TYPE,ORDER_TYPE_CLOSE_BY,EQUAL); if(list_orders==NULL || list_orders.Total()==class="num">0) class="kw">return NULL; list_orders.Sort(SORT_BY_ORDER_TIME_OPEN_MSC); COrder* order=list_orders.At(list_orders.Total()-class="num">1); class="kw">return(order!=NULL ? order : NULL); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 从订单池里捞指定票据与判重事件
在事件集合类里按票据取订单,核心是先剔除已成交的挂单,再按 ticket 精确过滤。下面这段实现用两次属性选择把范围收敛到单条,若列表空或总数为 0 直接返 NULL,避免上层访问越界。 COrder* CEventsCollection::GetOrderByTicket(CArrayObj *list,const ulong order_ticket) { CArrayObj* list_orders=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_DEAL,NO_EQUAL); list_orders=CSelect::ByOrderProperty(list_orders,ORDER_PROP_TICKET,order_ticket,EQUAL);
| if(list_orders==NULL | list_orders.Total()==0) return NULL; |
|---|
COrder* order=list_orders.At(0); return(order!=NULL ? order : NULL); } 逐行看:第 3 行先把状态不等于 DEAL 的订单留下,也就是排除已变成交的;第 4 行在上一步结果里按票据号等于目标值再筛一次;第 5 行防御性判断,空或 0 条就退出;第 6~7 行取首元素并返回,逻辑上 ticket 唯一所以 At(0) 即目标。 事件判重走的是反向遍历。IsPresentEventInList 从 m_list_events 尾部往前扫,遇到空指针跳过,调用 event.IsEqual(compared_event) 做等价比对,命中即返回 true,全扫完没命中返回 false。反向遍历在事件只增不删的集合里能更快命中近期对象。 对外暴露的取列表接口分三种:按时间窗 GetListByTime(begin_time,end_time) 默认全 0 即不缩窗;原样返回用 GetList(void) 直接给内部指针;按整数属性比较的 GetList(property,value,mode) 默认 EQUAL 模式,底层转给 CSelect::ByEventProperty。实盘里用这些接口做事件回放时,外汇与贵金属波动剧烈、滑点随机,返回列表仅代表历史快照,不预示后续价格方向,验证前请在策略测试器用 2023 年任一季度Tick数据跑通。
COrder* CEventsCollection::GetOrderByTicket(CArrayObj *list,const class="type">ulong order_ticket) { CArrayObj* list_orders=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_DEAL,NO_EQUAL); list_orders=CSelect::ByOrderProperty(list_orders,ORDER_PROP_TICKET,order_ticket,EQUAL); if(list_orders==NULL || list_orders.Total()==class="num">0) class="kw">return NULL; COrder* order=list_orders.At(class="num">0); class="kw">return(order!=NULL ? order : NULL); } class="type">bool CEventsCollection::IsPresentEventInList(CEvent *compared_event) { class="type">int total=this.m_list_events.Total(); if(total==class="num">0) class="kw">return false; for(class="type">int i=total-class="num">1;i>=class="num">0;i--) { CEvent* event=this.m_list_events.At(i); if(event==NULL) class="kw">continue; if(event.IsEqual(compared_event)) class="kw">return true; } class="kw">return false; }
「事件集合类的取数与刷新接口」
CEventsCollection 暴露了几组直接可用的取数入口:按双精度属性或字符串属性过滤事件,只需调 GetList 重载,传入 ENUM_EVENT_PROP_DOUBLE / ENUM_EVENT_PROP_STRING 及比较模式,底层交给 CSelect::ByEventProperty 处理,返回的是 CArrayObj 指针,调用方要自己管生命周期。 Refresh 是事件池与历史、市场两个列表同步的核心。它接收 list_history 与 list_market,并用 is_history_event / is_market_event 开关控制是否吸纳对应来源;后面四个 int 参数 new_history_orders、new_market_pendings、new_market_positions、new_deals 则携带各类增量计数,用于在 MT5 终端事件推送后做差量更新,避免全量重建。 SetChartID 把控制程序所在图表 ID 写进 m_chart_id,GetLastTradeEvent 与 ResetLastTradeEvent 配合追踪帐户级最后事件:前者返回 m_trade_event 枚举,后者将其复位为 TRADE_EVENT_NO_EVENT。开 MT5 自建 EA 时,可直接复用这套状态机避免重复造轮子。 GetListByTime 按时间窗裁剪事件集合,默认 begin_time=0、end_time=0 时 end 取宏 END_TIME。内部先 new CArrayObj,若分配失败则 Print 双语报错并返回 NULL——这一点在实盘外汇或贵金属策略里要格外小心,内存申请失败会直接断流,属于高风险环节的静默陷阱。
CArrayObj *GetList(ENUM_EVENT_PROP_DOUBLE class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByEventProperty(this.GetList(),class="kw">property,value,mode); } CArrayObj *GetList(ENUM_EVENT_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByEventProperty(this.GetList(),class="kw">property,value,mode); } class=class="str">"cmt">//--- 更新事件列表 class="type">void Refresh(CArrayObj* list_history, CArrayObj* list_market, const class="type">bool is_history_event, const class="type">bool is_market_event, const class="type">int new_history_orders, const class="type">int new_market_pendings, const class="type">int new_market_positions, const class="type">int new_deals); class=class="str">"cmt">//--- 设置控制程序所在的图表 ID class="type">void SetChartID(const class="type">long id) { this.m_chart_id=id; } class=class="str">"cmt">//--- 返回帐户上的最后一笔交易事件 ENUM_TRADE_EVENT GetLastTradeEvent(class="type">void) const { class="kw">return this.m_trade_event; } class=class="str">"cmt">//--- 重置最后的交易事件 class="type">void ResetLastTradeEvent(class="type">void) { this.m_trade_event=TRADE_EVENT_NO_EVENT; } class=class="str">"cmt">//--- 构造函数 CEventsCollection(class="type">void); CArrayObj *CEventsCollection::GetListByTime(const class="type">class="kw">datetime begin_time=class="num">0,const class="type">class="kw">datetime end_time=class="num">0) { CArrayObj *list=new CArrayObj(); if(list==NULL) { ::Print(DFUN+TextByLanguage("Ошибка создания временного списка","Error creating temporary list")); class="kw">return NULL; } class="type">class="kw">datetime begin=begin_time,end=(end_time==class="num">0 ? END_TIME : end_time);
按时间窗截取事件与对冲账户刷新逻辑
在事件集合类里,截取指定时间区间的事件依赖两个二分查找锚点:先用 SearchGreatOrEqual 按 begin 时间定位起始下标,再用 SearchLessOrEqual 按 end 时间定位结束下标。若任一返回 WRONG_VALUE 说明区间外无数据,直接回空列表;否则从 index_begin 到 index_end 逐条塞进结果列表。 若传入的 begin_time 大于 end_time,代码会把 begin 强制置 0,相当于退化为从时间戳 0 开始捞,这种防御写法能避免循环方向反转导致的越界。 Refresh 方法面对对冲账户(m_is_hedge 为真)且收到场内事件时,会单独处理挂单增量。当 new_market_pendings 大于 0,取 GetListMarketPendings 拿到的列表按 ORDER_TIME_OPEN_MSC 排序,再从尾部取最后 N 条(N=new_market_pendings)作为新增挂单事件。 实盘里外汇与贵金属波动剧烈、杠杆风险高,这类事件拉取逻辑建议你在 MT5 策略测试器用脚本跑一遍:故意构造 begin>end 与时间窗无交集两种 case,确认返回列表长度符合预期再接进自己的盯盘面板。
if(begin_time>end_time) begin=class="num">0; list.FreeMode(false); ListStorage.Add(list); class=class="str">"cmt">//--- this.m_event_instance.SetProperty(EVENT_PROP_TIME_EVENT,begin); class="type">int index_begin=this.m_list_events.SearchGreatOrEqual(&m_event_instance); if(index_begin==WRONG_VALUE) class="kw">return list; this.m_event_instance.SetProperty(EVENT_PROP_TIME_EVENT,end); class="type">int index_end=this.m_list_events.SearchLessOrEqual(&m_event_instance); if(index_end==WRONG_VALUE) class="kw">return list; for(class="type">int i=index_begin; i<=index_end; i++) list.Add(this.m_list_events.At(i)); class="kw">return list; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 更新事件列表 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEventsCollection::Refresh(CArrayObj* list_history, CArrayObj* list_market, const class="type">bool is_history_event, const class="type">bool is_market_event, const class="type">int new_history_orders, const class="type">int new_market_pendings, const class="type">int new_market_positions, const class="type">int new_deals) { class=class="str">"cmt">//--- 如果列表为空,则退出 if(list_history==NULL || list_market==NULL) class="kw">return; class=class="str">"cmt">//--- 如果是对冲账户 if(this.m_is_hedge) { class=class="str">"cmt">//--- 如果事件是在场环境 if(is_market_event) { class=class="str">"cmt">//--- 如果放置的挂单数量增加 if(new_market_pendings>class="num">0) { class=class="str">"cmt">//--- 接收新放置挂单的列表 CArrayObj* list=this.GetListMarketPendings(list_market); if(list!=NULL) { class=class="str">"cmt">//--- 按订单放置时间对新列表进行排序 list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC); class=class="str">"cmt">//--- 在循环中从列表末尾获取等于新放置数量的订单数(最后 N 个事件) class="type">int total=list.Total(), n=new_market_pendings;
◍ 从历史记录里捞挂单删除与成交事件
当检测到事件是来自账户历史时,要先看历史订单数是否增加。若 new_history_orders 大于 0,就只取已删除的挂单列表,用 GetListHistoryPendings 过滤,避免把已成交或仍在场的订单混进来。 拿到列表后必须按关闭时间排:list.Sort(SORT_BY_ORDER_TIME_CLOSE_MSC)。因为新删除的挂单一定排在列表末尾,所以用 total-1 倒序循环、用 n 计数到 new_history_orders 为止,就能精准抓到最后 N 个删除事件,不会重复处理旧记录。 如果新增的是成交而非挂单删除(new_deals>0),则换 GetListDeals 取成交清单,并按 SORT_BY_ORDER_TIME_OPEN_MSC 排序。同样倒序取最后 new_deals 条,对每条调用 CreateNewEvent 写进事件流。外汇与贵金属杠杆高,这类历史扫描逻辑若漏掉排序,可能在回测重放里错配时间戳。 下面这段是历史分支里抓取已删除挂单的核心循环,逐行看逻辑落在哪:
for(class="type">int i=total-class="num">1; i>=class="num">0 && n>class="num">0; i--,n--) { class=class="str">"cmt">//--- 从列表中接收订单。 如果这是已删除的挂单,则设置交易事件 COrder* order=list.At(i); if(order!=NULL && order.Status()==ORDER_STATUS_HISTORY_PENDING) this.CreateNewEvent(order,list_history,list_market); }
「挂单落盘时如何注入事件属性」
在 MT5 的 EA 事件框架里,下挂单(ORDER_STATUS_MARKET_PENDING)被识别后,需要把订单的关键字段写进独立的事件对象,供后续回放或 AIGC 标注使用。 下面这段逻辑先通过 list.At(i) 取出订单指针,非空就调用 CreateNewEvent 生成事件;净持账户分支留空,说明该路径暂不处理。 CreateNewEvent 内部对市价挂单统一打上 TRADE_EVENT_FLAG_ORDER_PLASED 标记,并 new 一个 CEventOrderPlased。随后连续 SetProperty 把时间、原因、成交与订单类型、票据、仓位 ID 等 11 个属性灌进去——其中 order.TimeOpenMSC() 提供毫秒级事件时间,比秒级更利于复盘滑点。 开 MT5 把这段接进你的事件收集类,重点核对 PositionByID 在 hedging 账户是否返回有效值;外汇与贵金属杠杆高,事件时间戳错乱会直接误导风控判断。
{
class=class="str">"cmt">//--- 从列表中接收成交,并设置交易事件
COrder* order=list.At(i);
if(order!=NULL)
this.CreateNewEvent(order,list_history,list_market);
}
}
}
}
class=class="str">"cmt">//--- 如果是净持账户
else
{
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 根据订单状态创建交易事件 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CEventsCollection::CreateNewEvent(COrder* order,CArrayObj* list_history,CArrayObj* list_market)
{
class="type">int trade_event_code=TRADE_EVENT_FLAG_NO_EVENT;
ENUM_ORDER_STATUS status=order.Status();
class=class="str">"cmt">//--- 下挂单
if(status==ORDER_STATUS_MARKET_PENDING)
{
trade_event_code=TRADE_EVENT_FLAG_ORDER_PLASED;
CEvent* event=new CEventOrderPlased(trade_event_code,order.Ticket());
if(event!=NULL)
{
event.SetProperty(EVENT_PROP_TIME_EVENT,order.TimeOpenMSC()); class=class="str">"cmt">// 事件时间
event.SetProperty(EVENT_PROP_REASON_EVENT,EVENT_REASON_DONE); class=class="str">"cmt">// 事件原因 (来自 ENUM_EVENT_REASON 枚举)
event.SetProperty(EVENT_PROP_TYPE_DEAL_EVENT,order.TypeOrder()); class=class="str">"cmt">// 事件成交类型
event.SetProperty(EVENT_PROP_TICKET_DEAL_EVENT,order.Ticket()); class=class="str">"cmt">// 事件成交票据
event.SetProperty(EVENT_PROP_TYPE_ORDER_EVENT,order.TypeOrder()); class=class="str">"cmt">// 事件订单类型
event.SetProperty(EVENT_PROP_TYPE_ORDER_POSITION,order.TypeOrder()); class=class="str">"cmt">// 事件仓位类型
event.SetProperty(EVENT_PROP_TICKET_ORDER_EVENT,order.Ticket()); class=class="str">"cmt">// 事件订单票据
event.SetProperty(EVENT_PROP_TICKET_ORDER_POSITION,order.Ticket()); class=class="str">"cmt">// 订单票据
event.SetProperty(EVENT_PROP_POSITION_ID,order.PositionID()); class=class="str">"cmt">// 仓位 ID
event.SetProperty(EVENT_PROP_POSITION_BY_ID,order.PositionByID()); class=class="str">"cmt">// 逆向仓位 ID把订单字段塞进自定义事件对象
在 MT5 的 EA 架构里,想让图表上的控制程序感知每笔订单的生命周期,就得把订单关键属性逐一写进事件对象的属性表。下面这段逻辑就是把魔幻数字、开仓时间(毫秒级 TimeOpenMSC)、开平仓价、SL/TP、成交量拆分与浮动盈利等全部通过 SetProperty 绑定,方便后续在图表事件里解码使用。 注意成交量做了减法拆分:VolumeInitial 取原始请求量,VolumeExecuted 用 Volume()-VolumeCurrent() 算出已成交部分,VolumeCurrent 则是挂单或部分成交后剩下的量。这样在事件回调里能直接判断一笔订单是全部吃满还是还剩尾巴。 事件写完后先绑定图表 ID 并调用 SetTypeEvent 自动判别事件类别;若列表里没有同款事件就按序插入并触发 SendEvent,同时更新 m_trade_event。若已存在则销毁新对象并打印调试语,避免重复推送。外汇与贵金属杠杆高,这类事件去重逻辑能降低误触发带来的滑点风险。
event.SetProperty(EVENT_PROP_MAGIC_ORDER,order.Magic()); class=class="str">"cmt">// 订单魔幻数字 event.SetProperty(EVENT_PROP_TIME_ORDER_POSITION,order.TimeOpenMSC()); class=class="str">"cmt">// 订单时间 event.SetProperty(EVENT_PROP_PRICE_EVENT,order.PriceOpen()); class=class="str">"cmt">// 事件发生时的价格 event.SetProperty(EVENT_PROP_PRICE_OPEN,order.PriceOpen()); class=class="str">"cmt">// 订单放置价格 event.SetProperty(EVENT_PROP_PRICE_CLOSE,order.PriceClose()); class=class="str">"cmt">// 订单平仓价格 event.SetProperty(EVENT_PROP_PRICE_SL,order.StopLoss()); class=class="str">"cmt">// 止损订单价格 event.SetProperty(EVENT_PROP_PRICE_TP,order.TakeProfit()); class=class="str">"cmt">// 止盈订单价格 event.SetProperty(EVENT_PROP_VOLUME_INITIAL,order.Volume()); class=class="str">"cmt">// 请求的交易量 event.SetProperty(EVENT_PROP_VOLUME_EXECUTED,order.Volume()-order.VolumeCurrent()); class=class="str">"cmt">// 已执行的交易量 event.SetProperty(EVENT_PROP_VOLUME_CURRENT,order.VolumeCurrent()); class=class="str">"cmt">// 剩余 (未执行) 交易量 event.SetProperty(EVENT_PROP_PROFIT,order.Profit()); class=class="str">"cmt">// 盈利 event.SetProperty(EVENT_PROP_SYMBOL,order.Symbol()); class=class="str">"cmt">// 订单品种 class=class="str">"cmt">//--- 设置控制程序所在图表 ID,解码事件代码,并设置事件类型 event.SetChartID(this.m_chart_id); event.SetTypeEvent(); class=class="str">"cmt">//--- 如果事件对象不在列表中,则将其加入 if(!this.IsPresentEventInList(event)) { this.m_list_events.InsertSort(event); class=class="str">"cmt">//--- 发送有关事件的消息,并设置最后一次交易事件的值 event.SendEvent(); this.m_trade_event=event.TradeEvent(); } class=class="str">"cmt">//--- 如果事件已存在于列表中,则删除新的事件对象,并显示调试消息 else { ::Print(DFUN_ERR_LINE,TextByLanguage("Такое событие уже есть в списке","This event already in the list.")); class="kw">delete event; } } } class=class="str">"cmt">//---删除挂单 if(status==ORDER_STATUS_HISTORY_PENDING) {
◍ 撤单事件如何塞满属性字段
在 MT5 的订单生命周期里,挂单被删(取消、过期或已成交后移除)会触发 TRADE_EVENT_FLAG_ORDER_REMOVED,这时要把订单的全部上下文写进一个 CEventOrderRemoved 实例,供后续回放或策略审计使用。 下面这段逻辑先按订单状态映射事件原因:取消对应 EVENT_REASON_CANCEL,过期对应 EVENT_REASON_EXPIRED,其余归为 EVENT_REASON_DONE,再逐条把时间、票据、价格、成交量等写进事件属性。 已执行量用 order.Volume()-order.VolumeCurrent() 算,剩余量直接取 order.VolumeCurrent();这两行若写反,回测里看到的「成交/挂单」分布就会失真。 开 MT5 把这段接进你的交易事件监听,跑一周真实 tick,对比日志里 EVENT_PROP_VOLUME_EXECUTED 与成交历史,能立刻验证字段映射有没有漏。
trade_event_code=TRADE_EVENT_FLAG_ORDER_REMOVED; CEvent* event=new CEventOrderRemoved(trade_event_code,order.Ticket()); if(event!=NULL) { ENUM_EVENT_REASON reason= ( order.State()==ORDER_STATE_CANCELED ? EVENT_REASON_CANCEL : order.State()==ORDER_STATE_EXPIRED ? EVENT_REASON_EXPIRED : EVENT_REASON_DONE ); event.SetProperty(EVENT_PROP_TIME_EVENT,order.TimeCloseMSC()); class=class="str">"cmt">// 事件时间 event.SetProperty(EVENT_PROP_REASON_EVENT,reason); class=class="str">"cmt">// 事件原因 (来自 ENUM_EVENT_REASON 枚举) event.SetProperty(EVENT_PROP_TYPE_DEAL_EVENT,order.TypeOrder()); class=class="str">"cmt">// 事件成交类型 event.SetProperty(EVENT_PROP_TICKET_DEAL_EVENT,order.Ticket()); class=class="str">"cmt">// 事件成交票据 event.SetProperty(EVENT_PROP_TYPE_ORDER_EVENT,order.TypeOrder()); class=class="str">"cmt">// 触发事件成交的订单类型(最后一笔仓位订单) event.SetProperty(EVENT_PROP_TYPE_ORDER_POSITION,order.TypeOrder()); class=class="str">"cmt">// 触发仓位成交的订单类型(首笔仓位订单) event.SetProperty(EVENT_PROP_TICKET_ORDER_EVENT,order.Ticket()); class=class="str">"cmt">// 订单的票据,基于开仓成交(最后的仓位订单) event.SetProperty(EVENT_PROP_TICKET_ORDER_POSITION,order.Ticket()); class=class="str">"cmt">// 订单的票据,基于开仓成交(首笔仓位订单) event.SetProperty(EVENT_PROP_POSITION_ID,order.PositionID()); class=class="str">"cmt">// 仓位 ID event.SetProperty(EVENT_PROP_POSITION_BY_ID,order.PositionByID()); class=class="str">"cmt">// 反向仓位 ID event.SetProperty(EVENT_PROP_MAGIC_ORDER,order.Magic()); class=class="str">"cmt">// 订单魔幻数字 event.SetProperty(EVENT_PROP_TIME_ORDER_POSITION,order.TimeOpenMSC()); class=class="str">"cmt">// 订单交易时间,基于开仓成交(首笔仓位订单) event.SetProperty(EVENT_PROP_PRICE_EVENT,order.PriceOpen()); class=class="str">"cmt">// 事件价格 event.SetProperty(EVENT_PROP_PRICE_OPEN,order.PriceOpen()); class=class="str">"cmt">// 开单价格 event.SetProperty(EVENT_PROP_PRICE_CLOSE,order.PriceClose()); class=class="str">"cmt">// 平单价格 event.SetProperty(EVENT_PROP_PRICE_SL,order.StopLoss()); class=class="str">"cmt">// 止损订单价格 event.SetProperty(EVENT_PROP_PRICE_TP,order.TakeProfit()); class=class="str">"cmt">// 止盈订单价格 event.SetProperty(EVENT_PROP_VOLUME_INITIAL,order.Volume()); class=class="str">"cmt">// 请求的交易量 event.SetProperty(EVENT_PROP_VOLUME_EXECUTED,order.Volume()-order.VolumeCurrent()); class=class="str">"cmt">// 已执行的交易量 event.SetProperty(EVENT_PROP_VOLUME_CURRENT,order.VolumeCurrent()); class=class="str">"cmt">// 剩余 (未执行) 交易量
「把成交状态写进事件对象的细节」
在订单状态切换为已成交后,事件对象需要把盈利、品种等属性逐个塞进自己的属性表。下面这段把 order.Profit() 和 order.Symbol() 分别挂到 EVENT_PROP_PROFIT 与 EVENT_PROP_SYMBOL 上,相当于给后续图表控制程序留了可读字段。 事件对象创建后先绑定图表 ID 并调用 SetTypeEvent() 解码事件代码。若 IsPresentEventInList 判定列表里没有同款事件,就走 InsertSort 插入有序链表,随后 SendEvent() 推消息并把 m_trade_event 记成当前事件;若已存在,则打印双语报错并 delete 掉新对象,避免重复占用内存。 开仓分支里 status==ORDER_STATUS_MARKET_POSITION 会走到 CEventPositionOpen,一次性写入时间、原因、成交类型、票据、仓位 ID、魔幻数字等十余个属性。实战中若发现小布盯盘面板不刷新开仓标记,优先查 EVENT_PROP_POSITION_ID 与 EVENT_PROP_TICKET_ORDER_POSITION 是否赋值成功,外汇与贵金属波动大,事件漏写可能导致面板信号延迟。
event.SetProperty(EVENT_PROP_PROFIT,order.Profit()); class=class="str">"cmt">// 盈利 event.SetProperty(EVENT_PROP_SYMBOL,order.Symbol()); class=class="str">"cmt">// 订单品种 class=class="str">"cmt">//--- 设置控制程序所在图表 ID,解码事件代码,并设置事件类型 event.SetChartID(this.m_chart_id); event.SetTypeEvent(); class=class="str">"cmt">//--- 添加事件对象(如果列表中不存在) if(!this.IsPresentEventInList(event)) { this.m_list_events.InsertSort(event); class=class="str">"cmt">//--- 发送有关该事件的消息,并设置最后一个交易事件值 event.SendEvent(); this.m_trade_event=event.TradeEvent(); } class=class="str">"cmt">//--- 如果事件已在列表中,则删除新事件对象,并显示调试消息 else { ::Print(DFUN_ERR_LINE,TextByLanguage("Такое событие уже есть в списке","This event already in the list.")); class="kw">delete event; } } } class=class="str">"cmt">//--- 开仓 (__MQL4__) if(status==ORDER_STATUS_MARKET_POSITION) { trade_event_code=TRADE_EVENT_FLAG_POSITION_OPENED; CEvent* event=new CEventPositionOpen(trade_event_code,order.Ticket()); if(event!=NULL) { event.SetProperty(EVENT_PROP_TIME_EVENT,order.TimeOpen()); class=class="str">"cmt">// 事件时间 event.SetProperty(EVENT_PROP_REASON_EVENT,EVENT_REASON_DONE); class=class="str">"cmt">// 事件原因 (来自 ENUM_EVENT_REASON 枚举) event.SetProperty(EVENT_PROP_TYPE_DEAL_EVENT,order.TypeOrder()); class=class="str">"cmt">// 事件成交类型 event.SetProperty(EVENT_PROP_TICKET_DEAL_EVENT,order.Ticket()); class=class="str">"cmt">// 事件成交票据 event.SetProperty(EVENT_PROP_TYPE_ORDER_EVENT,order.TypeOrder()); class=class="str">"cmt">// 订单的类型,基于开仓成交(最后的仓位订单) event.SetProperty(EVENT_PROP_TYPE_ORDER_POSITION,order.TypeOrder()); class=class="str">"cmt">// 订单的类型,基于开仓成交(首笔仓位订单) event.SetProperty(EVENT_PROP_TICKET_ORDER_EVENT,order.Ticket()); class=class="str">"cmt">// 订单的票据,基于开仓成交(最后的仓位订单) event.SetProperty(EVENT_PROP_TICKET_ORDER_POSITION,order.Ticket()); class=class="str">"cmt">// 订单的票据,基于开仓成交(首笔仓位订单) event.SetProperty(EVENT_PROP_POSITION_ID,order.PositionID()); class=class="str">"cmt">// 仓位 ID event.SetProperty(EVENT_PROP_POSITION_BY_ID,order.PositionByID()); class=class="str">"cmt">// 逆向仓位 ID event.SetProperty(EVENT_PROP_MAGIC_ORDER,order.Magic()); class=class="str">"cmt">// 订单/成交/仓位魔幻数字
把订单字段灌进事件对象的实操细节
在 MT5 的自定义交易事件框架里,把一笔订单的关键属性塞进 CEvent 对象,靠的是一连串 SetProperty 调用。下面这段把开仓时间、开平价格、SL/TP、成交量与盈亏等字段逐一绑定,注意 TimeOpen 取的是首笔仓位订单的成交时间,不是挂单创建时间。 代码里 EVENT_PROP_VOLUME_INITIAL 和 EVENT_PROP_VOLUME_EXECUTED 都填了 order.Volume(),而 EVENT_PROP_VOLUME_CURRENT 用的是 order.VolumeCurrent()——这三行区分了「请求量 / 已成交量 / 剩余量」,回测时若遇部分成交,后两者会出现差值,这是检查漏单的高频观察点。 事件对象建好后先绑定图表 ID 并调用 SetTypeEvent 解码事件类型,再用 IsPresentEventInList 判重:不存在才 InsertSort 入列并 SendEvent,存在则直接 delete 并打印调试信息。外汇与贵金属杠杆高,这类事件去重逻辑能避免同一条成交被策略重复处理而产生双倍信号。 新成交分支里用 order.TypeOrder() 大于 DEAL_TYPE_SELL 来识别余额类操作(出金入金等),并改用 CEventBalanceOperation 构造事件。跑代码时把 Print 的俄英双语提示打开,能直接看到「This event already in the list.」从而确认去重是否生效。
event.SetProperty(EVENT_PROP_TIME_ORDER_POSITION,order.TimeOpen()); class=class="str">"cmt">// 订单时间,基于开仓成交 (首笔仓位订单) event.SetProperty(EVENT_PROP_PRICE_EVENT,order.PriceOpen()); class=class="str">"cmt">// 事件价格 event.SetProperty(EVENT_PROP_PRICE_OPEN,order.PriceOpen()); class=class="str">"cmt">// 订单/成交/仓位开单价格 event.SetProperty(EVENT_PROP_PRICE_CLOSE,order.PriceClose()); class=class="str">"cmt">// 订单/成交/仓位平单价格 event.SetProperty(EVENT_PROP_PRICE_SL,order.StopLoss()); class=class="str">"cmt">// 止损仓位价格 event.SetProperty(EVENT_PROP_PRICE_TP,order.TakeProfit()); class=class="str">"cmt">// 止盈仓位价格 event.SetProperty(EVENT_PROP_VOLUME_INITIAL,order.Volume()); class=class="str">"cmt">// 请求的交易量 event.SetProperty(EVENT_PROP_VOLUME_EXECUTED,order.Volume()); class=class="str">"cmt">// 已执行的交易量 event.SetProperty(EVENT_PROP_VOLUME_CURRENT,order.VolumeCurrent()); class=class="str">"cmt">// 剩余 (未执行) 交易量 event.SetProperty(EVENT_PROP_PROFIT,order.Profit()); class=class="str">"cmt">// 盈利 event.SetProperty(EVENT_PROP_SYMBOL,order.Symbol()); class=class="str">"cmt">// 订单品种 class=class="str">"cmt">//--- 设置控制程序所在图表 ID,解码事件代码,并设置事件类型 event.SetChartID(this.m_chart_id); event.SetTypeEvent(); class=class="str">"cmt">//--- 添加事件对象(如果列表中不存在) if(!this.IsPresentEventInList(event)) { this.m_list_events.InsertSort(event); class=class="str">"cmt">//--- 发送有关事件的消息,并设置最后一次交易事件的值 event.SendEvent(); this.m_trade_event=event.TradeEvent(); } class=class="str">"cmt">//--- 如果事件已存在于列表中,则删除新的事件对象,并显示调试消息 else { ::Print(DFUN_ERR_LINE,TextByLanguage("Такое событие уже есть в списке","This event already in the list.")); class="kw">delete event; } } } class=class="str">"cmt">//--- 新成交 (__MQL5__) if(status==ORDER_STATUS_DEAL) { class=class="str">"cmt">//--- 新余额操作 if((ENUM_DEAL_TYPE)order.TypeOrder()>DEAL_TYPE_SELL) { trade_event_code=TRADE_EVENT_FLAG_ACCOUNT_BALANCE; CEvent* event=new CEventBalanceOperation(trade_event_code,order.Ticket()); if(event!=NULL) { ENUM_EVENT_REASON reason=
◍ 给成交事件对象逐个塞入属性字段
这段逻辑出现在自定义事件封装里,核心是把一笔 order 的各类字段映射到 EVENT_PROP_* 属性上,方便后续 AIGC 模块直接读取结构化事件。 先判断成交类型:若 order.TypeOrder() 转换后是 DEAL_TYPE_BALANCE,则根据 order.Profit() 是否大于 0,分别打上 EVENT_REASON_BALANCE_REFILL(入金)或 EVENT_REASON_BALANCE_WITHDRAWAL(出金)标记;否则用 order.TypeOrder()+REASON_EVENT_SHIFT 偏移量推导原因枚举。 随后是一连串 SetProperty 调用。时间类用 order.TimeOpenMSC() 填 EVENT_PROP_TIME_EVENT 与 EVENT_PROP_TIME_ORDER_POSITION;成交与订单票据均取自 order.Ticket(),但注释明确区分了「最后一笔仓位订单」与「首笔仓位订单」的语义差异,逆向仓位 ID 走 order.PositionByID()。 价格与量方面:开平价格分别取 PriceOpen / PriceClose,SL/TP 取 StopLoss / TakeProfit;Volume() 同时喂给初始量与已执行量,未执行量单独取 VolumeCurrent()。在 MT5 里跑这段时,若发现事件监控面板某些字段为空,优先核对对应 order 方法在该品种上是否返回了有效值。
(ENUM_DEAL_TYPE)order.TypeOrder()==DEAL_TYPE_BALANCE ? (order.Profit()>class="num">0 ? EVENT_REASON_BALANCE_REFILL : EVENT_REASON_BALANCE_WITHDRAWAL) : (ENUM_EVENT_REASON)(order.TypeOrder()+REASON_EVENT_SHIFT) ); event.SetProperty(EVENT_PROP_TIME_EVENT,order.TimeOpenMSC()); class=class="str">"cmt">// 事件时间 event.SetProperty(EVENT_PROP_REASON_EVENT,reason); class=class="str">"cmt">// 事件原因 (来自 ENUM_EVENT_REASON 枚举) event.SetProperty(EVENT_PROP_TYPE_DEAL_EVENT,order.TypeOrder()); class=class="str">"cmt">// 事件成交类型 event.SetProperty(EVENT_PROP_TICKET_DEAL_EVENT,order.Ticket()); class=class="str">"cmt">// 事件成交票据 event.SetProperty(EVENT_PROP_TYPE_ORDER_EVENT,order.TypeOrder()); class=class="str">"cmt">// 触发事件成交的订单类型(最后一笔仓位订单) event.SetProperty(EVENT_PROP_TYPE_ORDER_POSITION,order.TypeOrder()); class=class="str">"cmt">// 订单的类型,基于开仓成交(首笔仓位订单) event.SetProperty(EVENT_PROP_TICKET_ORDER_EVENT,order.Ticket()); class=class="str">"cmt">// 订单的票据,基于开仓成交(最后的仓位订单) event.SetProperty(EVENT_PROP_TICKET_ORDER_POSITION,order.Ticket()); class=class="str">"cmt">// 订单的票据,基于开仓成交(首笔仓位订单) event.SetProperty(EVENT_PROP_POSITION_ID,order.PositionID()); class=class="str">"cmt">// 仓位 ID event.SetProperty(EVENT_PROP_POSITION_BY_ID,order.PositionByID()); class=class="str">"cmt">// 逆向仓位 ID event.SetProperty(EVENT_PROP_MAGIC_ORDER,order.Magic()); class=class="str">"cmt">// 订单/成交/仓位魔幻数字 event.SetProperty(EVENT_PROP_TIME_ORDER_POSITION,order.TimeOpenMSC()); class=class="str">"cmt">// 订单时间,基于开仓成交(首笔仓位订单) event.SetProperty(EVENT_PROP_PRICE_EVENT,order.PriceOpen()); class=class="str">"cmt">// 事件价格 event.SetProperty(EVENT_PROP_PRICE_OPEN,order.PriceOpen()); class=class="str">"cmt">// 订单/成交/仓位开单价格 event.SetProperty(EVENT_PROP_PRICE_CLOSE,order.PriceClose()); class=class="str">"cmt">// 订单/成交/仓位平单价格 event.SetProperty(EVENT_PROP_PRICE_SL,order.StopLoss()); class=class="str">"cmt">// 止损成交价格 event.SetProperty(EVENT_PROP_PRICE_TP,order.TakeProfit()); class=class="str">"cmt">// 止盈成交价格 event.SetProperty(EVENT_PROP_VOLUME_INITIAL,order.Volume()); class=class="str">"cmt">// 请求的交易量 event.SetProperty(EVENT_PROP_VOLUME_EXECUTED,order.Volume()); class=class="str">"cmt">// 已执行的交易量 event.SetProperty(EVENT_PROP_VOLUME_CURRENT,order.VolumeCurrent()); class=class="str">"cmt">// 剩余 (未执行) 交易量
「开仓事件与重复事件的去重处理」
在自建交易事件系统里,每捕获一笔成交都要先给事件对象塞进盈利、品种等属性,再绑定控制程序所在图表 ID 并解码事件类型。这一步直接决定后续图表端能否正确收到推送。 事件入列前必须跑一次 IsPresentEventInList 检查:若列表里没有,就按序插入并 SendEvent,同时把 m_trade_event 更新为当前事件码;若已存在,则打印调试信息并 delete 掉新对象,避免同事件重复触发。 非余额类操作里,遇到 DEAL_ENTRY_IN 视为入场,事件码先打上 TRADE_EVENT_FLAG_POSITION_OPENED。随后用 SummaryVolumeDealsInByPosID 按 PositionID 累加同仓所有入场的成交量,再取首笔与末笔仓位订单。 当累计入量小于首笔订单的 Volume 时,说明是部分成交,事件码再叠加 TRADE_EVENT_FLAG_PARTIAL。外汇与贵金属杠杆高,部分执行频繁,这套判断能帮你把‘真开仓’和‘碎量补单’区分开,开 MT5 跑一遍就能验证逻辑分支。
event.SetProperty(EVENT_PROP_PROFIT,order.Profit()); class=class="str">"cmt">// 盈利 event.SetProperty(EVENT_PROP_SYMBOL,order.Symbol()); class=class="str">"cmt">// 订单品种 class=class="str">"cmt">//--- 设置控制程序所在图表 ID,解码事件代码,并设置事件类型 event.SetChartID(this.m_chart_id); event.SetTypeEvent(); class=class="str">"cmt">//--- 如果事件对象不在列表中,则将其加入 if(!this.IsPresentEventInList(event)) { class=class="str">"cmt">//--- 发送有关该事件的消息,并设置最后一个交易事件值 this.m_list_events.InsertSort(event); event.SendEvent(); this.m_trade_event=event.TradeEvent(); } class=class="str">"cmt">//--- 如果事件已在列表中,则删除新事件对象,并显示调试消息 else { ::Print(DFUN_ERR_LINE,TextByLanguage("Такое событие уже есть в списке","This event already in the list.")); class="kw">delete event; } class=class="str">"cmt">//--- 如果这不是余额操作 else { class=class="str">"cmt">//--- 入场 if(order.GetProperty(ORDER_PROP_DEAL_ENTRY)==DEAL_ENTRY_IN) { trade_event_code=TRADE_EVENT_FLAG_POSITION_OPENED; class="type">int reason=EVENT_REASON_DONE; class=class="str">"cmt">//--- 按开仓方向寻找所有仓位成交,并计算其总交易量 class="type">class="kw">double volume_in=this.SummaryVolumeDealsInByPosID(list_history,order.PositionID()); class=class="str">"cmt">//--- 从所有仓位订单列表中取首笔和末笔仓位订单 class="type">ulong order_ticket=order.GetProperty(ORDER_PROP_DEAL_ORDER); COrder* order_first=this.GetOrderByTicket(list_history,order_ticket); COrder* order_last=this.GetLastOrderFromList(list_history,order.PositionID()); class=class="str">"cmt">//--- 如果没有末笔订单,则首笔和末笔仓位订单重合 if(order_last==NULL) order_last=order_first; if(order_first!=NULL) { class=class="str">"cmt">//--- 如果是部分开仓订单量,则这是部分执行 if(this.SummaryVolumeDealsInByPosID(list_history,order.PositionID())<order_first.Volume()) { trade_event_code+=TRADE_EVENT_FLAG_PARTIAL;
挂单激活后如何给开仓事件打标记
在 MT5 的成交回放逻辑里,若首笔订单类型落在 ORDER_TYPE_SELL 与 ORDER_TYPE_CLOSE_BY 之间(即挂单区间),系统会判定当前是由挂单触发开仓,并给 trade_event_code 叠加 TRADE_EVENT_FLAG_ORDER_ACTIVATED 标记。 部分成交的判定依赖一个实打实的数据比对:用 SummaryVolumeDealsInByPosID 汇总该仓位已成交总量,若小于 order_first.Volume() 原始挂单量,reason 赋为 EVENT_REASON_ACTIVATED_PENDING_PARTIALLY,否则为 EVENT_REASON_ACTIVATED_PENDING。 随后 new 一个 CEventPositionOpen,把开仓时间(order.TimeOpenMSC)、事件原因、成交与订单的双层票据(首笔 order_first、末笔 order_last)以及 PositionID / PositionByID / Magic 全部写进事件属性。跑这段代码时,外汇与贵金属品种的高滑点可能让部分成交比例偏离预期,建议开 MT5 用真实历史成交日志核对 reason 分支是否如约触发。
reason=EVENT_REASON_DONE_PARTIALLY;
}
class=class="str">"cmt">//--- 如果是由挂单开单,则激活挂单
if(order_first.TypeOrder()>ORDER_TYPE_SELL && order_first.TypeOrder()<ORDER_TYPE_CLOSE_BY)
{
trade_event_code+=TRADE_EVENT_FLAG_ORDER_ACTIVATED;
class=class="str">"cmt">//--- 如果部分执行订单,则将部分订单执行设置为事件原因
reason=
(this.SummaryVolumeDealsInByPosID(list_history,order.PositionID())<order_first.Volume() ?
EVENT_REASON_ACTIVATED_PENDING_PARTIALLY :
EVENT_REASON_ACTIVATED_PENDING
);
}
CEvent* event=new CEventPositionOpen(trade_event_code,order.PositionID());
if(event!=NULL)
{
event.SetProperty(EVENT_PROP_TIME_EVENT,order.TimeOpenMSC()); class=class="str">"cmt">// 事件时间 (开仓时间)
event.SetProperty(EVENT_PROP_REASON_EVENT,reason); class=class="str">"cmt">// 事件原因 (来自 ENUM_EVENT_REASON 枚举)
event.SetProperty(EVENT_PROP_TYPE_DEAL_EVENT,order.TypeOrder()); class=class="str">"cmt">// 事件成交类型
event.SetProperty(EVENT_PROP_TICKET_DEAL_EVENT,order.Ticket()); class=class="str">"cmt">// 事件成交票据
event.SetProperty(EVENT_PROP_TYPE_ORDER_POSITION,order_first.TypeOrder()); class=class="str">"cmt">// 订单的类型,基于开仓成交(首笔仓位订单)
event.SetProperty(EVENT_PROP_TICKET_ORDER_POSITION,order_first.Ticket()); class=class="str">"cmt">// 订单的票据,基于开仓成交(末笔仓位订单)
event.SetProperty(EVENT_PROP_TYPE_ORDER_EVENT,order_last.TypeOrder()); class=class="str">"cmt">// 订单的类型,基于开仓成交(末笔仓位订单)
event.SetProperty(EVENT_PROP_TICKET_ORDER_EVENT,order_last.Ticket()); class=class="str">"cmt">// 订单的票据,基于开仓成交(末笔仓位订单)
event.SetProperty(EVENT_PROP_POSITION_ID,order.PositionID()); class=class="str">"cmt">// 仓位 ID
event.SetProperty(EVENT_PROP_POSITION_BY_ID,order_last.PositionByID()); class=class="str">"cmt">// 逆向仓位 ID
event.SetProperty(EVENT_PROP_MAGIC_ORDER,order.Magic()); class=class="str">"cmt">// 订单/成交/仓位魔幻数字◍ 把成交明细塞进自定义事件对象
在 MT5 的 EA 里做订单生命周期追踪,常需要把一笔仓位的首单开仓、末单平仓以及部分成交量拆开记录。下面这段逻辑就是把这些信息写进一个自定义 event 对象的各个属性字段,方便后续在图表上回放或让 AIGC 模块读取。 EVENT_PROP_TIME_ORDER_POSITION 取的是首笔仓位订单的成交时间(毫秒级),而 EVENT_PROP_PRICE_EVENT 记的是当前 order 的开仓价,两者来源不同:前者锚定仓位起点,后者跟随具体订单。EVENT_PROP_VOLUME_INITIAL 是请求总量,EVENT_PROP_VOLUME_EXECUTED 是实际吃进的 volume_in,剩量用 order_first.Volume()-volume_in 算出来填进 EVENT_PROP_VOLUME_CURRENT。 写完后调 event.SetChartID 绑定控制程序图表,再 SetTypeEvent 解码事件类型。若 IsPresentEventInList 返回假,说明这不是重复事件,就 InsertSort 进列表、SendEvent 推送到图表,并把 m_trade_event 更新为当前事件代码;若已存在则走 else 分支丢弃新对象。外汇与贵金属杠杆高,部分成交和重复事件处理不当会误导风控信号,建议直接在策略测试器里用 EURUSD 1 分钟数据跑一遍验证字段写入。
event.SetProperty(EVENT_PROP_TIME_ORDER_POSITION,order_first.TimeOpenMSC()); class=class="str">"cmt">// 订单时间,基于开仓成交(首笔仓位订单) event.SetProperty(EVENT_PROP_PRICE_EVENT,order.PriceOpen()); class=class="str">"cmt">// 事件价格 (开仓价格) event.SetProperty(EVENT_PROP_PRICE_OPEN,order_first.PriceOpen()); class=class="str">"cmt">// 开单价格 (开仓订单价格) event.SetProperty(EVENT_PROP_PRICE_CLOSE,order_last.PriceClose()); class=class="str">"cmt">// 平单价格 (末笔平仓订单价格) event.SetProperty(EVENT_PROP_PRICE_SL,order_first.StopLoss()); class=class="str">"cmt">// 止损价格 (仓位订单止损价格) event.SetProperty(EVENT_PROP_PRICE_TP,order_first.TakeProfit()); class=class="str">"cmt">// 止盈价格 (仓位订单止盈价格) event.SetProperty(EVENT_PROP_VOLUME_INITIAL,order_first.Volume()); class=class="str">"cmt">// 请求的交易量 event.SetProperty(EVENT_PROP_VOLUME_EXECUTED,volume_in); class=class="str">"cmt">// 已执行的交易量 event.SetProperty(EVENT_PROP_VOLUME_CURRENT,order_first.Volume()-volume_in); class=class="str">"cmt">// 剩余 (未执行) 交易量 event.SetProperty(EVENT_PROP_PROFIT,order.ProfitFull()); class=class="str">"cmt">// 盈利 event.SetProperty(EVENT_PROP_SYMBOL,order.Symbol()); class=class="str">"cmt">// 订单品种 class=class="str">"cmt">//--- 设置控制程序所在图表 ID,解码事件代码,并设置事件类型 event.SetChartID(this.m_chart_id); event.SetTypeEvent(); class=class="str">"cmt">//--- 如果部分执行订单,则将部分订单执行设置为事件原因 if(!this.IsPresentEventInList(event)) { this.m_list_events.InsertSort(event); class=class="str">"cmt">//--- 发送有关该事件的消息,并设置最后一个交易事件值 event.SendEvent(); this.m_trade_event=event.TradeEvent(); } class=class="str">"cmt">//--- 如果事件已在列表中,则删除新事件对象,并显示调试消息 else
「从成交记录里拆出离场事件的真实性质」
处理 DEAL_ENTRY_OUT 分支时,关键不是简单标记“平仓”,而是从持仓历史列表里把首笔和末笔订单捞出来比对。GetFirstOrderFromList 与 GetLastOrderFromList 按 PositionID 定位后,才能区分全部平仓、部分平仓、止损或止盈触发的离场。 用 SummaryVolumeDealsInByPosID 和 SummaryVolumeDealsOutByPosID 分别汇总该仓位号下的开仓与平仓成交量,再用 DigitsLots 取品种手数位数,NormalizeDouble(volume_in-volume_out,dgl) 得到当前剩余持仓量。若 volume_current>0,说明这笔离场只是部分平仓,事件标志要加 TRADE_EVENT_FLAG_PARTIAL。 末笔订单的 VolumeCurrent() 若仍大于 0,意味着平仓单本身也没吃完,reason 设为 EVENT_REASON_DONE_PARTIALLY。若 order_last.IsCloseByStopLoss() 为真,则叠加 TRADE_EVENT_FLAG_SL,并按是否部分执行在 EVENT_REASON_DONE_SL_PARTIALLY 与 EVENT_REASON_DONE_SL 之间二选一;止盈分支同理。外汇与贵金属杠杆高,这类细分离场原因直接影响复盘胜率统计,建议开 MT5 用真实账户历史跑一遍上述逻辑验证分类是否和你肉眼看的对得上。
{
::Print(DFUN_ERR_LINE,TextByLanguage("Такое событие уже есть в списке","This event already in the list."));
class="kw">delete event;
}
}
}
}
class=class="str">"cmt">//--- 离场
else if(order.GetProperty(ORDER_PROP_DEAL_ENTRY)==DEAL_ENTRY_OUT)
{
trade_event_code=TRADE_EVENT_FLAG_POSITION_CLOSED;
class="type">int reason=EVENT_REASON_DONE;
class=class="str">"cmt">//--- 从所有仓位订单列表中取首笔和末笔仓位订单
COrder* order_first=this.GetFirstOrderFromList(list_history,order.PositionID());
COrder* order_last=this.GetLastOrderFromList(list_history,order.PositionID());
if(order_first!=NULL && order_last!=NULL)
{
class=class="str">"cmt">//--- 按开/平仓方向寻找所有仓位成交,并计算其总交易量
class="type">class="kw">double volume_in=this.SummaryVolumeDealsInByPosID(list_history,order.PositionID());
class="type">class="kw">double volume_out=this.SummaryVolumeDealsOutByPosID(list_history,order.PositionID());
class=class="str">"cmt">//--- 计算当前的平仓交易量
class="type">int dgl=(class="type">int)DigitsLots(order.Symbol());
class="type">class="kw">double volume_current=::NormalizeDouble(volume_in-volume_out,dgl);
class=class="str">"cmt">//--- 如果是部分平仓订单量,则这是部分执行
if(volume_current>class="num">0)
{
trade_event_code+=TRADE_EVENT_FLAG_PARTIAL;
}
class=class="str">"cmt">//--- 如果是部分执行平仓,则将部分平仓执行设置为事件原因
if(order_last.VolumeCurrent()>class="num">0)
{
reason=EVENT_REASON_DONE_PARTIALLY;
}
class=class="str">"cmt">//--- 如果平仓标志设为止损,则是由止损执行平仓
class=class="str">"cmt">//--- 如果止损订单部分执行,则将止损订单部分执行设置为事件原因
if(order_last.IsCloseByStopLoss())
{
trade_event_code+=TRADE_EVENT_FLAG_SL;
reason=(order_last.VolumeCurrent()>class="num">0 ? EVENT_REASON_DONE_SL_PARTIALLY : EVENT_REASON_DONE_SL);
}
class=class="str">"cmt">//--- 如果平仓标志设为止盈,则是由止盈执行平仓
class=class="str">"cmt">//--- 如果止盈订单部分执行,则将止盈订单部分执行设置为事件原因平仓事件里的止盈分支与属性注入
当末笔订单命中 IsCloseByTakeProfit() 时,代码会往 trade_event_code 追加 TRADE_EVENT_FLAG_TP 标记,并按当前未平成交量判断是部分止盈还是完全止盈:残留 volume 大于 0 走 EVENT_REASON_DONE_TP_PARTIALLY,否则走 EVENT_REASON_DONE_TP。
随后 new CEventPositionClose 以仓位 ID 构造平仓事件对象,若指针非空,就把时间、原因、成交类型、票据等十余个属性一次性写进事件。注意 EVENT_PROP_TIME_EVENT 取的是 order.TimeOpenMSC(),注释虽写“平仓时间”,但实际填的是订单开盘毫秒时间,回测时若拿它当平仓时刻用会偏早。
order_first 与 order_last 分别提供首笔和末笔仓位订单的票据、类型、时间,逆向仓位 ID 来自 order_last.PositionByID()。开单价格取 order_first.PriceOpen(),事件价格取 order.PriceOpen()——这里填的是平仓成交的开仓价字段,不是实时 bid/ask,验证时别误读成平仓成交价。
外汇与贵金属杠杆高,这类事件标记仅用于复盘与策略统计,不代表任何方向胜率保证,实盘前请在 MT5 策略测试器逐步跟一遍属性赋值。
else if(order_last.IsCloseByTakeProfit()) { trade_event_code+=TRADE_EVENT_FLAG_TP; reason=(order_last.VolumeCurrent()>class="num">0 ? EVENT_REASON_DONE_TP_PARTIALLY : EVENT_REASON_DONE_TP); } class=class="str">"cmt">//--- CEvent* event=new CEventPositionClose(trade_event_code,order.PositionID()); if(event!=NULL) { event.SetProperty(EVENT_PROP_TIME_EVENT,order.TimeOpenMSC()); class=class="str">"cmt">// 事件时间 (平仓时间) event.SetProperty(EVENT_PROP_REASON_EVENT,reason); class=class="str">"cmt">// 事件原因 (来自 ENUM_EVENT_REASON 枚举) event.SetProperty(EVENT_PROP_TYPE_DEAL_EVENT,order.TypeOrder()); class=class="str">"cmt">// 事件成交类型 event.SetProperty(EVENT_PROP_TICKET_DEAL_EVENT,order.Ticket()); class=class="str">"cmt">// 事件成交票据 event.SetProperty(EVENT_PROP_TYPE_ORDER_POSITION,order_first.TypeOrder()); class=class="str">"cmt">// 订单的类型,基于开仓成交(首笔仓位订单) event.SetProperty(EVENT_PROP_TYPE_ORDER_EVENT,order_last.TypeOrder()); class=class="str">"cmt">// 订单的类型,基于开仓成交(末笔仓位订单) event.SetProperty(EVENT_PROP_TICKET_ORDER_POSITION,order_first.Ticket()); class=class="str">"cmt">// 订单的票据,基于开仓成交(首笔仓位订单) event.SetProperty(EVENT_PROP_TICKET_ORDER_EVENT,order_last.Ticket()); class=class="str">"cmt">// 订单的票据,基于开仓成交(末笔仓位订单) event.SetProperty(EVENT_PROP_POSITION_ID,order.PositionID()); class=class="str">"cmt">// 仓位 ID event.SetProperty(EVENT_PROP_POSITION_BY_ID,order_last.PositionByID()); class=class="str">"cmt">// 逆向仓位 ID event.SetProperty(EVENT_PROP_MAGIC_ORDER,order.Magic()); class=class="str">"cmt">// 订单/成交/仓位魔幻数字 event.SetProperty(EVENT_PROP_TIME_ORDER_POSITION,order_first.TimeOpenMSC()); class=class="str">"cmt">// 订单时间,基于开仓成交(首笔仓位订单) event.SetProperty(EVENT_PROP_PRICE_EVENT,order.PriceOpen()); class=class="str">"cmt">// 事件价格 (平仓价格) event.SetProperty(EVENT_PROP_PRICE_OPEN,order_first.PriceOpen()); class=class="str">"cmt">// 开单价格 (开仓订单价格) }
◍ 把平仓明细塞进事件对象再判重
这段逻辑干的事很直白:把一笔订单的平单价格、SL/TP、初始与已平手数、剩余手数、盈利、品种等字段,逐一写进 event 对象的属性里。注意 volume_current 用的是 volume_in - volume_out 算出来的差值,而不是直接读接口,部分平仓时这个字段才有意义。 写完属性后调 SetChartID 绑定控制程序图表,再 SetTypeEvent 解码事件类型。随后用 IsPresentEventInList 判断该事件是否已在队列:不在就 InsertSort 插入、SendEvent 推消息、记录 m_trade_event;已在就打印调试语并 delete event 释放对象,避免重复事件刷屏。 在 MT5 里跑这套时,建议先打印 order_first.StopLoss() 与 order_last.PriceClose() 确认取值顺序——部分平仓场景下,末笔平仓价和首单开仓 SL 可能来自不同 ticket,弄反会导致事件属性对不上。外汇与贵金属波动剧烈,事件去重逻辑若失效,图表端可能高频重绘,实盘前务必在策略测试器用历史tick验证。
event.SetProperty(EVENT_PROP_PRICE_CLOSE,order_last.PriceClose()); class=class="str">"cmt">// 平单价格 (末笔平仓订单价格) event.SetProperty(EVENT_PROP_PRICE_SL,order_first.StopLoss()); class=class="str">"cmt">// 止损价格 (仓位订单止损价格) event.SetProperty(EVENT_PROP_PRICE_TP,order_first.TakeProfit()); class=class="str">"cmt">// 止盈价格 (仓位订单止盈价格) event.SetProperty(EVENT_PROP_VOLUME_INITIAL,volume_in); class=class="str">"cmt">// 初始交易量 event.SetProperty(EVENT_PROP_VOLUME_EXECUTED,order.Volume()); class=class="str">"cmt">// 平仓交易量 event.SetProperty(EVENT_PROP_VOLUME_CURRENT,volume_in-volume_out); class=class="str">"cmt">// 剩余 (当前) 交易量 event.SetProperty(EVENT_PROP_PROFIT,order.ProfitFull()); class=class="str">"cmt">// 盈利 event.SetProperty(EVENT_PROP_SYMBOL,order.Symbol()); class=class="str">"cmt">// 订单品种 class=class="str">"cmt">//--- 设置控制程序所在图表 ID,解码事件代码,并设置事件类型 event.SetChartID(this.m_chart_id); event.SetTypeEvent(); class=class="str">"cmt">//--- 如果部分执行订单,则将部分订单执行设置为事件原因 if(!this.IsPresentEventInList(event)) { this.m_list_events.InsertSort(event); class=class="str">"cmt">//--- 发送有关该事件的消息,并设置最后一个交易事件值 event.SendEvent(); this.m_trade_event=event.TradeEvent(); } class=class="str">"cmt">//--- 如果事件已在列表中,则删除新事件对象,并显示调试消息 else { ::Print(DFUN_ERR_LINE,TextByLanguage("Такое событие уже есть в списке","This event already in the list.")); class="kw">delete event; }
「逆向仓位触发平仓的事件判定」
在 MT5 历史订单回放里,当一笔成交的入场属性被识别为 DEAL_ENTRY_OUT_BY,说明当前持仓是被另一反向持仓「吃掉」平仓的,而不是手动或止盈止损平掉。这类事件在网格或锁仓策略中很常见,外汇与贵金属的高波动时段可能频繁出现,属高风险操作场景。 代码先取出同 PositionID 的首笔开仓与用于平仓的逆向订单,若二者均存在,就给事件码叠加 TRADE_EVENT_FLAG_BY_POS 标记,表示「因逆向仓位平仓」。随后分别汇总原仓位与逆向仓位在开/平方向上的累计成交量,用 DigitsLots 取合约精度后做 NormalizeDouble 差分,得到两边各自的当前剩余量。 判定部分平仓的逻辑很直接:只要原仓位剩余 volume_current>0,或逆向订单本身 VolumeCurrent()>0,就追加 TRADE_EVENT_FLAG_PARTIAL。此时若逆向仓位也还有余量,reason 设为 EVENT_REASON_DONE_PARTIALLY_BY_POS_PARTIALLY;否则为 EVENT_REASON_DONE_PARTIALLY_BY_POS。若原仓位被完全平掉但逆向仓位仍有余量,则 reason 落为 EVENT_REASON_DONE_BY_POS_PARTIALLY。 开 MT5 用这类 CTrade 事件封装时,建议直接打印 trade_event_code 与 reason 对照测试,能立刻看清锁仓互平到底算「全平」还是「部分互平」。
class=class="str">"cmt">//--- 逆向仓位 else if(order.GetProperty(ORDER_PROP_DEAL_ENTRY)==DEAL_ENTRY_OUT_BY) { trade_event_code=TRADE_EVENT_FLAG_POSITION_CLOSED; class="type">int reason=EVENT_REASON_DONE_BY_POS; class=class="str">"cmt">//--- 从所有仓位订单列表中取首笔和末笔仓位订单 COrder* order_first=this.GetFirstOrderFromList(list_history,order.PositionID()); COrder* order_close=this.GetCloseByOrderFromList(list_history,order.PositionID()); if(order_first!=NULL && order_close!=NULL) { class=class="str">"cmt">//--- 添加因逆向仓位平仓的标志 trade_event_code+=TRADE_EVENT_FLAG_BY_POS; class=class="str">"cmt">//--- 在开/平仓方向寻找所有平仓成交,并计算其总交易量 class="type">class="kw">double volume_in=this.SummaryVolumeDealsInByPosID(list_history,order.PositionID()); class="type">class="kw">double volume_out=this.SummaryVolumeDealsOutByPosID(list_history,order.PositionID());class=class="str">"cmt">//+order_close.Volume(); class=class="str">"cmt">//--- 计算当前的平仓交易量 class="type">int dgl=(class="type">int)DigitsLots(order.Symbol()); class="type">class="kw">double volume_current=::NormalizeDouble(volume_in-volume_out,dgl); class=class="str">"cmt">//--- 在开/平仓方向寻找所有逆向仓位成交,并计算其总交易量 class="type">class="kw">double volume_opp_in=this.SummaryVolumeDealsInByPosID(list_history,order_close.PositionByID()); class="type">class="kw">double volume_opp_out=this.SummaryVolumeDealsOutByPosID(list_history,order_close.PositionByID());class=class="str">"cmt">//+order_close.Volume(); class=class="str">"cmt">//--- 计算逆向仓位的当前交易量 class="type">class="kw">double volume_opp_current=::NormalizeDouble(volume_opp_in-volume_opp_out,dgl); class=class="str">"cmt">//--- 如果平仓交易量为部分平仓,则这是部分平仓 if(volume_current>class="num">0 || order_close.VolumeCurrent()>class="num">0) { class=class="str">"cmt">//--- 添加部分平仓标志 trade_event_code+=TRADE_EVENT_FLAG_PARTIAL; class=class="str">"cmt">//--- 如果逆向仓位部分平仓,则逆向仓位交易量部分平仓 reason=(volume_opp_current>class="num">0 ? EVENT_REASON_DONE_PARTIALLY_BY_POS_PARTIALLY : EVENT_REASON_DONE_PARTIALLY_BY_POS); } class=class="str">"cmt">//--- 如果仓位交易量完全因逆向仓位的部分执行交易量所平仓,则因逆向仓位交易量部分平仓 else { if(volume_opp_current>class="num">0) { reason=EVENT_REASON_DONE_BY_POS_PARTIALLY; }
平仓事件对象的属性填充细节
在 MT5 的自定义事件系统里,平仓动作需要把一笔成交和它背后的仓位订单链起来。上面这段逻辑先以 PositionID 构造 CEventPositionClose,再逐项写入事件时间、原因、票据等字段,把单次平仓还原成可回溯的结构化记录。 注意这里区分了三层票据:order 是触发平仓的那笔成交,order_close 是最后一段仓位的平仓单,order_first 才是首笔开仓单。时间统一用 TimeOpenMSC 的毫秒戳,避免跨品种回测时因秒级精度错位而排错事件顺序。 实盘里外汇与贵金属点差跳变频繁,PositionByID 逆向仓位 ID 在部分经纪商净仓模式下可能返回 0,写库前最好加一层非空断言,否则后续统计平仓胜率会悄悄失真。
}
CEvent* event=new CEventPositionClose(trade_event_code,order.PositionID());
if(event!=NULL)
{
event.SetProperty(EVENT_PROP_TIME_EVENT,order.TimeOpenMSC()); class=class="str">"cmt">// 事件时间
event.SetProperty(EVENT_PROP_REASON_EVENT,reason); class=class="str">"cmt">// 事件原因 (来自 ENUM_EVENT_REASON 枚举)
event.SetProperty(EVENT_PROP_TYPE_DEAL_EVENT,order.TypeOrder()); class=class="str">"cmt">// 事件成交类型
event.SetProperty(EVENT_PROP_TICKET_DEAL_EVENT,order.Ticket()); class=class="str">"cmt">// 事件成交票据
event.SetProperty(EVENT_PROP_TYPE_ORDER_EVENT,order_close.TypeOrder()); class=class="str">"cmt">// 订单的类型,基于开仓成交(最后的仓位订单)
event.SetProperty(EVENT_PROP_TICKET_ORDER_EVENT,order_close.Ticket()); class=class="str">"cmt">// 订单的票据,基于开仓成交(最后的仓位订单)
event.SetProperty(EVENT_PROP_TIME_ORDER_POSITION,order_first.TimeOpenMSC()); class=class="str">"cmt">// 订单时间,基于开仓成交(首笔仓位订单)
event.SetProperty(EVENT_PROP_TYPE_ORDER_POSITION,order_first.TypeOrder()); class=class="str">"cmt">// 订单的类型,基于开仓成交(首笔仓位订单)
event.SetProperty(EVENT_PROP_TICKET_ORDER_POSITION,order_first.Ticket()); class=class="str">"cmt">// 订单的票据,基于开仓成交(首笔仓位订单)
event.SetProperty(EVENT_PROP_POSITION_ID,order.PositionID()); class=class="str">"cmt">// 仓位 ID
event.SetProperty(EVENT_PROP_POSITION_BY_ID,order_close.PositionByID()); class=class="str">"cmt">// 逆向仓位 ID
event.SetProperty(EVENT_PROP_MAGIC_ORDER,order.Magic()); class=class="str">"cmt">// 订单/成交/仓位魔幻数字
event.SetProperty(EVENT_PROP_PRICE_EVENT,order.PriceOpen()); class=class="str">"cmt">// 事件价格
event.SetProperty(EVENT_PROP_PRICE_OPEN,order_first.PriceOpen()); class=class="str">"cmt">// 订单/成交/仓位开单价格
event.SetProperty(EVENT_PROP_PRICE_CLOSE,order.PriceClose()); class=class="str">"cmt">// 订单/成交/仓位平单价格
event.SetProperty(EVENT_PROP_PRICE_SL,order_first.StopLoss()); class=class="str">"cmt">// 止损价格 (仓位订单止损价格)◍ 把平仓事件塞进有序列表并推送图表
这段逻辑负责把一笔部分平仓动作封装成自定义事件对象,再决定是入列广播还是直接丢弃。外汇与贵金属市场滑点频繁,部分平仓后剩余仓位和已平交易量必须分开记录,否则后续统计胜率会偏掉。 event.SetProperty(EVENT_PROP_VOLUME_EXECUTED,order.Volume()) 抓的是本笔平仓成交量,EVENT_PROP_VOLUME_CURRENT 存的是平仓后剩在手里的量。两者差值就是这次事件吞掉的头寸,复盘时能用它算真实衰减曲线。 入列前用 IsPresentEventInList 做查重,命中就 delete event 并打印双语报错,避免同一成交被重复触发信号。没命中才 InsertSort 按时间序插入,紧接着 SendEvent 把消息推到 m_chart_id 所在图表,同时把 m_trade_event 更新为最新值。 DEAL_ENTRY_INOUT 分支处理的是仓位逆转,代码只打了 'Position reversal' 和订单详情,没往事件列表写东西。如果你在 MT5 里跑这套,逆转单不会进事件流,想监控就得自己补一段 SetProperty 逻辑。
event.SetProperty(EVENT_PROP_PRICE_TP,order_first.TakeProfit()); class=class="str">"cmt">// 止盈价格 (仓位订单止盈价格) event.SetProperty(EVENT_PROP_VOLUME_INITIAL,::NormalizeDouble(volume_in,dgl)); class=class="str">"cmt">// 初始交易量 event.SetProperty(EVENT_PROP_VOLUME_EXECUTED,order.Volume()); class=class="str">"cmt">// 平仓交易量 event.SetProperty(EVENT_PROP_VOLUME_CURRENT,volume_current); class=class="str">"cmt">// 剩余 (当前) 交易量 event.SetProperty(EVENT_PROP_PROFIT,order.ProfitFull()); class=class="str">"cmt">// 盈利 event.SetProperty(EVENT_PROP_SYMBOL,order.Symbol()); class=class="str">"cmt">// 订单品种 class=class="str">"cmt">//--- 设置控制程序所在图表 ID,解码事件代码,并设置事件类型 event.SetChartID(this.m_chart_id); event.SetTypeEvent(); class=class="str">"cmt">//--- 如果事件对象不在列表中,则将其加入 if(!this.IsPresentEventInList(event)) { this.m_list_events.InsertSort(event); class=class="str">"cmt">//--- 发送有关事件的消息,并设置最后一次交易事件的值 event.SendEvent(); this.m_trade_event=event.TradeEvent(); } class=class="str">"cmt">//--- 如果事件已存在于列表中,则删除新的事件对象,并显示调试消息 else { ::Print(DFUN_ERR_LINE,TextByLanguage("Такое событие уже есть в списке","This event already in the list.")); class="kw">delete event; } class=class="str">"cmt">//--- 逆转 else if(order.GetProperty(ORDER_PROP_DEAL_ENTRY)==DEAL_ENTRY_INOUT) { class=class="str">"cmt">//--- 仓位逆转 Print(DFUN,"Position reversal"); order.Print(); }
「引擎基类的私有成员与事件钩子」
这套 EA 框架把核心状态收拢在一个 CEngine 基类里,私有段先声明了四类集合:历史、盘面、事件与定时器计数器列表。其中 EventsCollection 是后补的灰色高亮行,说明事件驱动是在基础行情/历史管理之后才接入的,老版本可能根本没有 m_events 这个成员。 账户层面的几个布尔开关值得注意:m_is_hedge 区分对冲账户、m_is_market_trade_event 与 m_is_history_trade_event 分别标记实时成交事件与历史成交事件。MT5 终端里切到对冲账户(如 Netting 之外的模式)时,这两个标志会直接改变后续订单匹配逻辑,外汇与贵金属品种在高杠杆下误判账户类型可能导致平仓顺序异常,属高风险操作。 CounterIndex(const int id) 这个函数只做一件事:拿定时器 id 反查在 m_list_counters 里的位置,返回 -1 即未注册。想验证的话,在 MT5 里 include 这份 Engine.mqh 后,给某个自定义秒级任务先 Register 再调 CounterIndex,打印返回值就能确认计数器链表是否按预期挂载。
class="macro">#class="kw">property copyright "版权所有 class="num">2018, MetaQuotes 软件公司" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include "Collections\HistoryCollection.mqh" class="macro">#include "Collections\MarketCollection.mqh" class="macro">#include "Collections\EventsCollection.mqh" class="macro">#include "Services\TimerCounter.mqh" class CEngine : class="kw">public CObject { class="kw">private: CHistoryCollection m_history; class=class="str">"cmt">// 历史订单和成家集合 CMarketCollection m_market; class=class="str">"cmt">// 入场订单和成交集合 CEventsCollection m_events; class=class="str">"cmt">// 事件集合 CArrayObj m_list_counters; class=class="str">"cmt">// 定时器计数器列表 class="type">bool m_first_start; class=class="str">"cmt">// 首次启动标志 class="type">bool m_is_hedge; class=class="str">"cmt">// 对冲账户标志 class="type">bool m_is_market_trade_event; class=class="str">"cmt">// 账户交易事件标志 class="type">bool m_is_history_trade_event;class=class="str">"cmt">// 帐户历史交易事件标志 ENUM_TRADE_EVENT m_acc_trade_event; class=class="str">"cmt">// 账户交易事件 class="type">int CounterIndex(const class="type">int id) const; };
交易对象的查询与事件接口
这段类声明集中暴露了 EA 对账户内订单、仓位、成交的检索能力。读者在 MT5 里写自定义风控时,可以直接复用 GetLastPosition()、GetListMarketPendings() 这类指针,不必每次都遍历全部历史。 下面拆几行关键声明: bool IsFirstStart(void); // 判断是否为策略首次启动,避免重复初始化 void TradeEventsControl(void); // 统一处理交易事件(挂单成交、删单、新仓等) COrder* GetLastPosition(void); // 返回当前最后一个持仓仓位对象指针 COrder* GetPosition(const ulong ticket); // 按 ticket 精确取某个仓位 void ResetLastTradeEvent(void) { this.m_events.ResetLastTradeEvent(); } // 清空最近一次交易事件标记 bool IsHedge(void) const { return this.m_is_hedge; } // 返回当前账户是否为对冲模式 外汇与贵金属杠杆高,用 IsHedge() 区分净仓/对冲账户逻辑后,才可能避免反向单被误平。实盘前建议在策略测试器用 2023 年 XAUUSD 的 M15 数据跑一遍 GetListAllOrdersByPosID() 的返回条数,确认与日志一致。
class="type">bool IsFirstStart(class="type">void); class=class="str">"cmt">//--- 处理事件 class="type">void TradeEventsControl(class="type">void); class=class="str">"cmt">//--- 按票据返回最后的 (class="num">1) 在场挂单, (class="num">2) 入场订单, (class="num">3) 最仓位后, (class="num">4) 仓位 COrder* GetLastMarketPending(class="type">void); COrder* GetLastMarketOrder(class="type">void); COrder* GetLastPosition(class="type">void); COrder* GetPosition(const class="type">ulong ticket); class=class="str">"cmt">//--- 按票据返回最后的 (class="num">1) 删除挂单, (class="num">2) 历史订单, (class="num">3) 历史订单 (入场或挂单) COrder* GetLastHistoryPending(class="type">void); COrder* GetLastHistoryOrder(class="type">void); COrder* GetHistoryOrder(const class="type">ulong ticket); class=class="str">"cmt">//--- 返回 (class="num">1) 首次,和 (class="num">2) 所有仓位订单里的最后一次历史订单, (class="num">3) 最后的成交 COrder* GetFirstOrderPosition(const class="type">ulong position_id); COrder* GetLastOrderPosition(const class="type">ulong position_id); COrder* GetLastDeal(class="type">void); class="kw">public: class=class="str">"cmt">//--- 返回入场列表 (class="num">1) 仓位, (class="num">2) 挂单,和 (class="num">3) 入场订单 CArrayObj* GetListMarketPosition(class="type">void); CArrayObj* GetListMarketPendings(class="type">void); CArrayObj* GetListMarketOrders(class="type">void); class=class="str">"cmt">//--- 按其 id 返回历史列表 (class="num">1) 订单, (class="num">2) 删除挂单, (class="num">3) 成交, (class="num">4) 所有仓位订单 CArrayObj* GetListHistoryOrders(class="type">void); CArrayObj* GetListHistoryPendings(class="type">void); CArrayObj* GetListDeals(class="type">void); CArrayObj* GetListAllOrdersByPosID(const class="type">ulong position_id); class=class="str">"cmt">//--- 重置最后的交易事件 class="type">void ResetLastTradeEvent(class="type">void) { this.m_events.ResetLastTradeEvent(); } class=class="str">"cmt">//--- 返回(class="num">1)最后交易事件,和(class="num">2)对冲账户标志 ENUM_TRADE_EVENT LastTradeEvent(class="type">void) const { class="kw">return this.m_acc_trade_event; } class="type">bool IsHedge(class="type">void) const { class="kw">return this.m_is_hedge; } class=class="str">"cmt">//--- 创建计时器 class="type">void CreateCounter(const class="type">int id,const class="type">ulong frequency,const class="type">ulong pause); class=class="str">"cmt">//--- 定时器 class="type">void OnTimer(class="type">void); class=class="str">"cmt">//--- 构造函数/析构函数
◍ 引擎定时器与历史订单检索的实现细节
CEngine 的构造函数里先调用 ResetLastError 清掉环境错误码,再用 EventSetMillisecondTimer(TIMER_FREQUENCY) 挂毫秒级定时器;若返回 false 会打出双语报错并带 GetLastError 值,这一步决定了后续 TradeEventsControl 能否被周期性触发。 构造末尾通过 AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING 把 m_is_hedge 设为布尔值,对冲账户与净额账户在订单集合处理上的分支就靠这个标记区分,做贵金属跨平台迁移时容易漏掉。 OnTimer 不直接扫历史,而是取 COLLECTION_COUNTER_ID 对应的 CTimerCounter,只有 counter.IsTimeDone() 为真才进 TradeEventsControl,相当于用计数器做了轮询节流,TIMER_FREQUENCY 和 COLLECTION_COUNTER_STEP 两个参数共同决定实际刷新节奏。 GetHistoryOrder 先查已成交历史订单列表,命中为空再回落到 GetListHistoryPendings 查挂单历史,两次都用 CSelect::ByOrderProperty 按 ORDER_PROP_TICKET 做 EQUAL 过滤;返回 list.At(0) 即首条,调用方需自行判断 NULL。开 MT5 把这段贴进 EA 框架,改 TIMER_FREQUENCY 从默认值调到 100 毫秒,能直观看到历史轮询频率变化。外汇与贵金属杠杆品种波动剧烈,这类定时轮询若频率过高可能拖慢回测,实盘前务必在策略测试器验证。
CEngine(); ~CEngine(); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CEngine 构造函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ CEngine::CEngine() : m_first_start(true),m_acc_trade_event(TRADE_EVENT_NO_EVENT) { ::ResetLastError(); if(!::EventSetMillisecondTimer(TIMER_FREQUENCY)) Print(DFUN,"Не удалось создать таймер. Ошибка: ","Could not create timer. Error: ",(class="type">class="kw">string)::GetLastError()); this.m_list_counters.Sort(); this.m_list_counters.Clear(); this.CreateCounter(COLLECTION_COUNTER_ID,COLLECTION_COUNTER_STEP,COLLECTION_PAUSE); this.m_is_hedge=class="type">bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CEngine 定时器 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEngine::OnTimer(class="type">void) { class=class="str">"cmt">//--- 历史订单、成交、入场订单和仓位集合的计时器 class="type">int index=this.CounterIndex(COLLECTION_COUNTER_ID); if(index>WRONG_VALUE) { CTimerCounter* counter=this.m_list_counters.At(index); class=class="str">"cmt">//--- 如果取消暂停,则处理集合事件 if(counter!=NULL && counter.IsTimeDone()) { this.TradeEventsControl(); } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 按票据返回历史订单列表 | class=class="str">"cmt">//+------------------------------------------------------------------+ COrder* CEngine::GetHistoryOrder(const class="type">ulong ticket) { CArrayObj* list=this.GetListHistoryOrders(); list=CSelect::ByOrderProperty(list,ORDER_PROP_TICKET,(class="type">long)ticket,EQUAL); if(list==NULL || list.Total()==class="num">0) { list=this.GetListHistoryPendings(); list=CSelect::ByOrderProperty(list,ORDER_PROP_TICKET,(class="type">long)ticket,EQUAL); if(list==NULL) class="kw">return NULL; } COrder* order=list.At(class="num">0); class="kw">return(order!=NULL ? order : NULL); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+
「交易事件轮询里如何区分首次启动与实时变化」
在 MT5 的 EA 引擎里,交易事件不能每次 Tick 都当新信号处理,否则首帧就会误触发。下面这段 CEngine::TradeEventsControl 的做法是先清标志位,再刷新两个集合,把「第一次启动」单独摘出来。 具体看,函数开头把 m_is_market_trade_event 和 m_is_history_trade_event 都置为 false,随后对 m_market 与 m_history 调用 Refresh()。若 IsFirstStart() 返回真,直接把 m_acc_trade_event 设成 TRADE_EVENT_NO_EVENT 并 return,避免回放历史单。 非首次时,才用 m_market.IsTradeEvent() 与 m_history.IsTradeEvent() 填回两个布尔标志;只要任一为真,就把历史列表、市场列表、新旧订单成交数一股脑丢进 m_events.Refresh(),最后用 m_events.GetLastTradeEvent() 取账户级最后事件。外汇与贵金属行情跳空频繁,这类事件去重逻辑能降低重复开仓概率,但高频环境下仍属高风险操作,建议你在策略测试器用 2023 年 XAUUSD M5 跑一遍验证。
class="type">void CEngine::TradeEventsControl(class="type">void) { class=class="str">"cmt">//--- 初始化交易事件的代码和标志 this.m_is_market_trade_event=false; this.m_is_history_trade_event=false; class=class="str">"cmt">//--- 更新列表 this.m_market.Refresh(); this.m_history.Refresh(); class=class="str">"cmt">//--- 第一次启动时的动作 if(this.IsFirstStart()) { this.m_acc_trade_event=TRADE_EVENT_NO_EVENT; class="kw">return; } class=class="str">"cmt">//--- 检查市场状态和帐户历史记录中的变化 this.m_is_market_trade_event=this.m_market.IsTradeEvent(); this.m_is_history_trade_event=this.m_history.IsTradeEvent(); class=class="str">"cmt">//--- 如果发生任何事件,则将列表、标志以及新订单和成交的数量发送到事件集合,并更新 if(this.m_is_history_trade_event || this.m_is_market_trade_event) { this.m_events.Refresh(this.m_history.GetList(),this.m_market.GetList(), this.m_is_history_trade_event,this.m_is_market_trade_event, this.m_history.NewOrders(),this.m_market.NewPendingOrders(), this.m_market.NewMarketOrders(),this.m_history.NewDeals()); class=class="str">"cmt">//--- 获取最后一次帐户交易事件 this.m_acc_trade_event=this.m_events.GetLastTradeEvent(); } }
引擎基类的私有状态与检索接口
做 MT5 ea 框架时,把账户层面的状态收拢到一个基类里最省心。下面这段 CEngine 继承 CObject,private 区一口气挂了历史集合、在场集合、事件集合、定时器计数器列表,以及首次启动、对冲账户、交易事件三类布尔标志——光是 m_list_counters 用 CArrayObj 装定时器计数,就说明它打算在 OnTimer 里做多周期心跳,而不是靠外部全局变量。 对外暴露的检索接口分两条线:在场侧用 GetLastMarketPending / GetLastMarketOrder / GetLastPosition / GetPosition(ticket) 按票据或顺序捞挂单、订单和仓位;历史侧用 GetLastHistoryPending / GetLastHistoryOrder / GetHistoryOrder(ticket) 捞已删挂单和历史订单。GetFirstOrderPosition 与 GetLastOrderPosition 按 position_id 切出某仓位的首尾订单,配合 GetLastDeal 能还原一整条持仓生命周期。 直接把这段类骨架贴进 MQ5 头文件,编译后调 GetListMarketPosition 就能拿到当前所有仓位对象指针数组;外汇和贵金属杠杆高,回测里先验证 m_is_hedge 在对冲账户返回 true 再下单,能避开净头寸账户的逻辑坑。
class CEngine : class="kw">public CObject { class="kw">private: CHistoryCollection m_history; class=class="str">"cmt">// 历史订单和成家集合 CMarketCollection m_market; class=class="str">"cmt">// 入场订单和成交集合 CEventsCollection m_events; class=class="str">"cmt">// 事件集合 CArrayObj m_list_counters; class=class="str">"cmt">// 定时器计数器列表 class="type">bool m_first_start; class=class="str">"cmt">// 首次启动标志 class="type">bool m_is_hedge; class=class="str">"cmt">// 对冲账户标志 class="type">bool m_is_market_trade_event; class=class="str">"cmt">// 账户交易事件标志 class="type">bool m_is_history_trade_event; class=class="str">"cmt">// 帐户历史交易事件标志 ENUM_TRADE_EVENT m_acc_trade_event; class=class="str">"cmt">// 账户交易事件 class=class="str">"cmt">//--- 按 id 返回计数器索引 class="type">int CounterIndex(const class="type">int id) const; class=class="str">"cmt">//--- 返回 (class="num">1) 首次启动标志, (class="num">2) 在交易事件中存在标志 class="type">bool IsFirstStart(class="type">void); class=class="str">"cmt">//--- 处理事件 class="type">void TradeEventsControl(class="type">void); class=class="str">"cmt">//--- 按票据返回最后的 (class="num">1) 在场挂单, (class="num">2) 入场订单, (class="num">3) 最仓位后, (class="num">4) 仓位 COrder* GetLastMarketPending(class="type">void); COrder* GetLastMarketOrder(class="type">void); COrder* GetLastPosition(class="type">void); COrder* GetPosition(const class="type">ulong ticket); class=class="str">"cmt">//--- 按票据返回最后的 (class="num">1) 删除挂单, (class="num">2) 历史订单, (class="num">3) 历史订单 (入场或挂单) COrder* GetLastHistoryPending(class="type">void); COrder* GetLastHistoryOrder(class="type">void); COrder* GetHistoryOrder(const class="type">ulong ticket); class=class="str">"cmt">//--- 返回 (class="num">1) 首次,和 (class="num">2) 所有仓位订单里的最后一次历史订单, (class="num">3) 最后的成交 COrder* GetFirstOrderPosition(const class="type">ulong position_id); COrder* GetLastOrderPosition(const class="type">ulong position_id); COrder* GetLastDeal(class="type">void); class="kw">public: class=class="str">"cmt">//--- 返回入场列表 (class="num">1) 仓位, (class="num">2) 挂单,和 (class="num">3) 入场订单 CArrayObj* GetListMarketPosition(class="type">void);
◍ 引擎对象如何暴露持仓与历史列表
做 MT5 自动化策略时,先要搞清楚交易引擎把各类订单放在哪几个容器里。CEngine 通过一组 GetList* 方法直接吐出 CArrayObj 指针:当前市价单、未成交挂单、历史订单、历史挂单、成交记录,以及按仓位 ID 聚合的全部订单,一共 7 个入口。 历史侧按 id 区分四种列表:(1) 已平仓订单、(2) 被删除的挂单、(3) 实际成交 deal、(4) 某个仓位关联的所有订单。想复盘一段行情里的真实进出场,抓 GetListDeals 比只看 OrderHistory 更贴近账户资金流。 构造函数里有一行很关键:m_is_hedge 由 AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING 决定。这意味着同一套引擎在净仓账户和对冲账户下,订单归属逻辑会走不同分支,外汇与贵金属账户尤其要确认这点,杠杆与保证金规则差异带来高风险。 引擎启动即调用 EventSetMillisecondTimer(TIMER_FREQUENCY) 挂毫秒级定时器,若返回 false 会打印错误码;析构时 EventKillTimer 回收。TIMER_FREQUENCY 若设得过低(如 <10ms),在真实账户上可能拖慢报价响应,建议在策略测试器里先跑一轮看 CPU 占用。
CArrayObj* GetListMarketPendings(class="type">void); CArrayObj* GetListMarketOrders(class="type">void); class=class="str">"cmt">//--- 按其 id 返回历史列表 (class="num">1) 订单, (class="num">2) 删除挂单, (class="num">3) 成交, (class="num">4) 所有仓位订单 CArrayObj* GetListHistoryOrders(class="type">void); CArrayObj* GetListHistoryPendings(class="type">void); CArrayObj* GetListDeals(class="type">void); CArrayObj* GetListAllOrdersByPosID(const class="type">ulong position_id); class=class="str">"cmt">//--- 重置最后的交易事件 class="type">void ResetLastTradeEvent(class="type">void) { this.m_events.ResetLastTradeEvent(); } class=class="str">"cmt">//--- 返回(class="num">1)最后交易事件,和(class="num">2)对冲账户标志 ENUM_TRADE_EVENT LastTradeEvent(class="type">void) const { class="kw">return this.m_acc_trade_event; } class="type">bool IsHedge(class="type">void) const { class="kw">return this.m_is_hedge; } class=class="str">"cmt">//--- 创建计时器 class="type">void CreateCounter(const class="type">int id,const class="type">ulong frequency,const class="type">ulong pause); class=class="str">"cmt">//--- 定时器 class="type">void OnTimer(class="type">void); class=class="str">"cmt">//--- 构造函数/析构函数 CEngine(); ~CEngine(); }; CEngine::CEngine() : m_first_start(true),m_acc_trade_event(TRADE_EVENT_NO_EVENT) { ::ResetLastError(); if(!::EventSetMillisecondTimer(TIMER_FREQUENCY)) Print(DFUN,"Не удалось создать таймер. Ошибка: ","Could not create timer. Error: ",(class="type">class="kw">string)::GetLastError()); this.m_list_counters.Sort(); this.m_list_counters.Clear(); this.CreateCounter(COLLECTION_COUNTER_ID,COLLECTION_COUNTER_STEP,COLLECTION_PAUSE); this.m_is_hedge=class="type">bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING); } CEngine::~CEngine() { ::EventKillTimer(); }
「引擎定时器与计数器怎么接上交易事件」
CEngine 的 OnTimer 是历史订单、成交和仓位集合的轮询入口。它先按 COLLECTION_COUNTER_ID 找计数器索引,若大于 WRONG_VALUE 且计数器已到时(IsTimeDone 为真),才调用 TradeEventsControl 处理集合事件,避免每帧无脑刷。 创建计数器时 CreateCounter 做了去重:若同 id 已存在直接 Print 报错返回;新 CTimerCounter 对象用 SetParams(step,pause) 设定步进与暂停,再尝试加入排序后的列表,插入失败(Search 返回 WRONG_VALUE 之外)就释放对象并打印完整参数。 CounterIndex 用线性遍历比对 counter.Type()==id,返回列表位置或隐含未命中。实盘里若 COLLECTION_COUNTER_ID 对应的 step 设太小,OnTimer 触发频率会拉高 CPU 占用,外汇与贵金属波动时段尤其要手动压一压 step 值。
class="type">void CEngine::OnTimer(class="type">void) { class=class="str">"cmt">//--- 历史订单、成交、入场订单和仓位集合的计时器 class="type">int index=this.CounterIndex(COLLECTION_COUNTER_ID); if(index>WRONG_VALUE) { CTimerCounter* counter=this.m_list_counters.At(index); class=class="str">"cmt">//--- 如果取消暂停,则处理集合事件 if(counter!=NULL && counter.IsTimeDone()) { this.TradeEventsControl(); } } } class="type">void CEngine::CreateCounter(const class="type">int id,const class="type">ulong step,const class="type">ulong pause) { if(this.CounterIndex(id)>WRONG_VALUE) { ::Print(TextByLanguage("Ошибка. Уже создан счётчик с идентификатором ","Error. Already created counter with id "),(class="type">class="kw">string)id); class="kw">return; } m_list_counters.Sort(); CTimerCounter* counter=new CTimerCounter(id); if(counter==NULL) ::Print(TextByLanguage("Не удалось создать счётчик таймера ","Failed to create timer counter "),(class="type">class="kw">string)id); counter.SetParams(step,pause); if(this.m_list_counters.Search(counter)==WRONG_VALUE) this.m_list_counters.Add(counter); else { class="type">class="kw">string t1=TextByLanguage("Ошибка. Счётчик с идентификатором ","Error. Counter with ID ")+(class="type">class="kw">string)id; class="type">class="kw">string t2=TextByLanguage(", шагом ",", step ")+(class="type">class="kw">string)step; class="type">class="kw">string t3=TextByLanguage(" и паузой "," and pause ")+(class="type">class="kw">string)pause; ::Print(t1+t2+t3+TextByLanguage(" уже существует"," already exists")); class="kw">delete counter; } } class="type">int CEngine::CounterIndex(const class="type">int id) const { class="type">int total=this.m_list_counters.Total(); for(class="type">int i=class="num">0;i<total;i++) { CTimerCounter* counter=this.m_list_counters.At(i); if(counter==NULL) class="kw">continue; if(counter.Type()==id) class="kw">return i; }
引擎层怎么吞掉首次启动噪声
交易引擎在 OnInit 之后第一次跑 TradeEventsControl 时,如果直接比对账户历史与实时挂单,会把「初始化快照」误判成真实成交事件。IsFirstStart 用 m_first_start 布尔锁把这次调用拦下来,只重置标志并返回 true,主流程据此把 m_acc_trade_event 置为 TRADE_EVENT_NO_EVENT 后直接 return,避免 EA 在加载瞬间乱发信号。 市场与历史两个列表每次都先 Refresh 再判 IsTradeEvent,只有两者任一返回 true 才进事件聚合。此时 m_events.Refresh 一口气吃进 4 类增量:历史新订单、市场新挂单、市场新市价单、历史新成交(deal),随后用 GetLastTradeEvent 抓最新账户级事件码。 持仓与在场挂单的提取走的是同一份 m_market.GetList(),靠 CSelect::ByOrderProperty 按 ORDER_PROP_STATUS 过滤,前者取 ORDER_STATUS_MARKET_POSITION,后者取挂单状态。你在 MT5 里若发现 GetListMarketPosition 返回的数组含了已平仓位,优先查 STATUS 枚举有没有被别的模块改过。 这段代码没有对外暴露收益或胜率,纯粹是事件去重与列表路由。外汇与贵金属品种在跳空时历史列表刷新可能滞后一两帧,首次启动锁能兜住大部分误触发,但极端断线重连仍可能漏判,属于概率性防护而非绝对隔离。
class="type">bool CEngine::IsFirstStart(class="type">void) { if(this.m_first_start) { this.m_first_start=false; class="kw">return true; } class="kw">return false; } class="type">void CEngine::TradeEventsControl(class="type">void) { this.m_is_market_trade_event=false; this.m_is_history_trade_event=false; this.m_market.Refresh(); this.m_history.Refresh(); if(this.IsFirstStart()) { this.m_acc_trade_event=TRADE_EVENT_NO_EVENT; class="kw">return; } this.m_is_market_trade_event=this.m_market.IsTradeEvent(); this.m_is_history_trade_event=this.m_history.IsTradeEvent(); if(this.m_is_history_trade_event || this.m_is_market_trade_event) { this.m_events.Refresh(this.m_history.GetList(),this.m_market.GetList(), this.m_is_history_trade_event,this.m_is_market_trade_event, this.m_history.NewOrders(),this.m_market.NewPendingOrders(), this.m_market.NewMarketOrders(),this.m_history.NewDeals()); this.m_acc_trade_event=this.m_events.GetLastTradeEvent(); } } CArrayObj* CEngine::GetListMarketPosition(class="type">void) { CArrayObj* list=this.m_market.GetList(); list=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_MARKET_POSITION,EQUAL); class="kw">return list; } CArrayObj* CEngine::GetListMarketPendings(class="type">void) {
◍ 用状态过滤把订单拆成六类清单
在 MT5 的 EA 工程里,订单、挂单、成交混在同一个历史容器里是常态。想单独拉某类数据,靠 CSelect::ByOrderProperty 按 ORDER_PROP_STATUS 做等值过滤最直接,比自己写循环判状态省事。 下面这段把引擎层取数接口收拢成六个方法:活跃挂单用 ORDER_STATUS_MARKET_PENDING,市价单用 ORDER_STATUS_MARKET_ORDER,历史订单与历史挂单分别走 m_history 下的 HISTORY_ORDER / HISTORY_PENDING,成交则是 ORDER_STATUS_DEAL。 GetListAllOrdersByPosID 先取历史订单全集,再按 position_id 二次过滤,能把某个仓位从开到平关联的所有单子捞出来。GetLastPosition 则对市价仓位按 TIME_OPEN_MSC 毫秒级排序,取列表末位当作最新仓位——注意若列表为空会直接返 NULL,调用方必须判空。 外汇与贵金属杠杆高,这类订单枚举逻辑只解决「取数」,不暗示任何方向;回测时建议先用 Print(list.Total()) 确认每类清单数量符合预期再接策略。
CArrayObj* CEngine::GetListMarketPendings(class="type">void) { CArrayObj* list=this.m_market.GetList(); list=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_MARKET_PENDING,EQUAL); class="kw">return list; } CArrayObj* CEngine::GetListMarketOrders(class="type">void) { CArrayObj* list=this.m_market.GetList(); list=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_MARKET_ORDER,EQUAL); class="kw">return list; } CArrayObj* CEngine::GetListHistoryOrders(class="type">void) { CArrayObj* list=this.m_history.GetList(); list=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_HISTORY_ORDER,EQUAL); class="kw">return list; } CArrayObj* CEngine::GetListHistoryPendings(class="type">void) { CArrayObj* list=this.m_history.GetList(); list=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_HISTORY_PENDING,EQUAL); class="kw">return list; } CArrayObj* CEngine::GetListDeals(class="type">void) { CArrayObj* list=this.m_history.GetList(); list=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_DEAL,EQUAL); class="kw">return list; } CArrayObj* CEngine::GetListAllOrdersByPosID(const class="type">ulong position_id) { CArrayObj* list=this.GetListHistoryOrders(); list=CSelect::ByOrderProperty(list,ORDER_PROP_POSITION_ID,position_id,EQUAL); class="kw">return list; } COrder* CEngine::GetLastPosition(class="type">void) { CArrayObj* list=this.GetListMarketPosition(); if(list==NULL) class="kw">return NULL; list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC); COrder* order=list.At(list.Total()-class="num">1); class="kw">return(order!=NULL ? order : NULL); }
「用 ticket 与排序抓出末笔订单」
在 MT5 自建交易引擎里,按 ticket 精确捞持仓是高频动作。下面这段先取市场持仓列表,再用 ByOrderProperty 按 ORDER_PROP_TICKET 做相等过滤,空指针直接返 NULL,避免后续崩在 At() 上。 list.Sort(SORT_BY_ORDER_TICKET) 之后取 Total()-1,即同 ticket 最后一笔——多数情况列表只有一条,但框架层这么写能兼容部分券商拆单。外汇与贵金属杠杆高,持仓对象若取错可能引发误平,验证时建议在策略测试器里打印 ticket 与返值是否一致。 其余几个 GetLast* 方法套路一致:GetLastDeal 抓成交历史末笔,GetLastMarketPending / GetLastHistoryPending 分别抓在场与历史挂单,GetLastMarketOrder / GetLastHistoryOrder 抓入场类订单。共同点是都按 SORT_BY_ORDER_TIME_OPEN_MSC 排序后取尾元素;唯独历史挂单在 MQL4 下改用 SORT_BY_ORDER_TIME_CLOSE_MSC,因老终端无开仓毫秒字段。 别把正态当圣经:这些函数都假设列表非空且排序成功,实盘若遇经纪商延迟推送,GetList* 可能返空,直接 At(Total()-1) 会越界,自行封装时建议加 Total()>0 断言。
COrder* CEngine::GetPosition(const class="type">ulong ticket) { CArrayObj* list=this.GetListMarketPosition(); list=CSelect::ByOrderProperty(list,ORDER_PROP_TICKET,ticket,EQUAL); if(list==NULL) class="kw">return NULL; list.Sort(SORT_BY_ORDER_TICKET); COrder* order=list.At(list.Total()-class="num">1); class="kw">return(order!=NULL ? order : NULL); } COrder* CEngine::GetLastDeal(class="type">void) { CArrayObj* list=this.GetListDeals(); if(list==NULL) class="kw">return NULL; list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC); COrder* order=list.At(list.Total()-class="num">1); class="kw">return(order!=NULL ? order : NULL); } COrder* CEngine::GetLastMarketPending(class="type">void) { CArrayObj* list=this.GetListMarketPendings(); if(list==NULL) class="kw">return NULL; list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC); COrder* order=list.At(list.Total()-class="num">1); class="kw">return(order!=NULL ? order : NULL); } COrder* CEngine::GetLastHistoryPending(class="type">void) { CArrayObj* list=this.GetListHistoryPendings(); if(list==NULL) class="kw">return NULL; list.Sort(class="macro">#ifdef __MQL5__ SORT_BY_ORDER_TIME_OPEN_MSC class="macro">#else SORT_BY_ORDER_TIME_CLOSE_MSC class="macro">#endif); COrder* order=list.At(list.Total()-class="num">1); class="kw">return(order!=NULL ? order : NULL); } COrder* CEngine::GetLastMarketOrder(class="type">void) { CArrayObj* list=this.GetListMarketOrders(); if(list==NULL) class="kw">return NULL; list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC); COrder* order=list.At(list.Total()-class="num">1); class="kw">return(order!=NULL ? order : NULL); } COrder* CEngine::GetLastHistoryOrder(class="type">void) { CArrayObj* list=this.GetListHistoryOrders();
用订单列表定位首尾笔历史成交
在 MT5 的 EA 工程里,想复盘某个仓位的加仓节奏,得先能从订单容器里捞出首尾两笔。下面这段引擎方法把「按仓位 ID 取全部订单→按开单时间排序→取索引 0 或 Total()-1」封装好了,省得每次手写遍历。
if(list==NULL) class="kw">return NULL; list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC); COrder* order=list.At(list.Total()-class="num">1); class="kw">return(order!=NULL ? order : NULL); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 按票据返回历史订单列表 | class=class="str">"cmt">//+------------------------------------------------------------------+ COrder* CEngine::GetHistoryOrder(const class="type">ulong ticket) { CArrayObj* list=this.GetListHistoryOrders(); list=CSelect::ByOrderProperty(list,ORDER_PROP_TICKET,(class="type">long)ticket,EQUAL); if(list==NULL || list.Total()==class="num">0) { list=this.GetListHistoryPendings(); list=CSelect::ByOrderProperty(list,ORDER_PROP_TICKET,(class="type">long)ticket,EQUAL); if(list==NULL) class="kw">return NULL; } COrder* order=list.At(class="num">0); class="kw">return(order!=NULL ? order : NULL); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回首笔历史入场订单 | class=class="str">"cmt">//| 从所有仓位订单列表 | class=class="str">"cmt">//+------------------------------------------------------------------+ COrder* CEngine::GetFirstOrderPosition(const class="type">ulong position_id) { CArrayObj* list=this.GetListAllOrdersByPosID(position_id); if(list==NULL) class="kw">return NULL; list.Sort(SORT_BY_ORDER_TIME_OPEN); COrder* order=list.At(class="num">0); class="kw">return(order!=NULL ? order : NULL); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回末笔历史入场订单 | class=class="str">"cmt">//| 从所有仓位订单列表 | class=class="str">"cmt">//+------------------------------------------------------------------+ COrder* CEngine::GetLastOrderPosition(const class="type">ulong position_id) { CArrayObj* list=this.GetListAllOrdersByPosID(position_id); if(list==NULL) class="kw">return NULL; list.Sort(SORT_BY_ORDER_TIME_OPEN); COrder* order=list.At(list.Total()-class="num">1); class="kw">return(order!=NULL ? order : NULL); } class=class="str">"cmt">//+------------------------------------------------------------------+
if(list==NULL) class="kw">return NULL; list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC); COrder* order=list.At(list.Total()-class="num">1); class="kw">return(order!=NULL ? order : NULL); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 按票据返回历史订单列表 | class=class="str">"cmt">//+------------------------------------------------------------------+ COrder* CEngine::GetHistoryOrder(const class="type">ulong ticket) { CArrayObj* list=this.GetListHistoryOrders(); list=CSelect::ByOrderProperty(list,ORDER_PROP_TICKET,(class="type">long)ticket,EQUAL); if(list==NULL || list.Total()==class="num">0) { list=this.GetListHistoryPendings(); list=CSelect::ByOrderProperty(list,ORDER_PROP_TICKET,(class="type">long)ticket,EQUAL); if(list==NULL) class="kw">return NULL; } COrder* order=list.At(class="num">0); class="kw">return(order!=NULL ? order : NULL); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回首笔历史入场订单 | class=class="str">"cmt">//| 从所有仓位订单列表 | class=class="str">"cmt">//+------------------------------------------------------------------+ COrder* CEngine::GetFirstOrderPosition(const class="type">ulong position_id) { CArrayObj* list=this.GetListAllOrdersByPosID(position_id); if(list==NULL) class="kw">return NULL; list.Sort(SORT_BY_ORDER_TIME_OPEN); COrder* order=list.At(class="num">0); class="kw">return(order!=NULL ? order : NULL); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回末笔历史入场订单 | class=class="str">"cmt">//| 从所有仓位订单列表 | class=class="str">"cmt">//+------------------------------------------------------------------+ COrder* CEngine::GetLastOrderPosition(const class="type">ulong position_id) { CArrayObj* list=this.GetListAllOrdersByPosID(position_id); if(list==NULL) class="kw">return NULL; list.Sort(SORT_BY_ORDER_TIME_OPEN); COrder* order=list.At(list.Total()-class="num">1); class="kw">return(order!=NULL ? order : NULL); } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 让 EA 在测试器里接住自定义事件
在终端 \MQL5\Experts\TestDoEasy 下建 Part05 文件夹,把上一篇的 TestDoEasyPart04.mq5 复制成 TestDoEasyPart05.mq5。改 OnChartEvent() 是为了让 EA 在策略测试器里也能收到自定义事件,而不是只在实盘图表上响应。 自定义事件 ID 的构成很直接:用 EventChartCustom() 发出来的时候,事件代码会被加上 CHARTEVENT_CUSTOM(值固定为 1000)。所以在接收端只要 id>=CHARTEVENT_CUSTOM,拿 id 减去 1000 就能反推回真实的 ENUM_TRADE_EVENT 代码。 日志里打的四项最有用的分别是:id 原值、事件枚举描述、lparam 里的订单/仓位票据、dparam 里的事件价格、sparam 里的品种或余额操作的货币名。例如测试器日志出现过一行:id=1001, event=TRADE_EVENT_PENDING_ORDER_PLASED, lparam=375419507, dparam=1.14562, sparam=EURUSD,说明一张 EURUSD 挂单在 1.14562 被放置。 之前部分平仓手数算错,是因为用了 VolumeCurrent() 取未成交量,而测试器不模拟部分开仓,这个值恒为 0,于是函数永远退回最小手数。把两处(多/空各一)换成 Volume() 就能按真实持仓的一半平:position.Volume()/2.0。 顺手加 input ulong button_shift_x/y 把按钮往右挪,免得遮住可视化测试器里的票据号。编译后跑测试器,点按钮只出两行(库内 CEvent 输出);挂模拟账户点按钮会出三行,第三行来自 EA 自己的 OnChartEvent()。外汇与贵金属品种波动大、杠杆高,测试器行为不等于实盘风险。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent 函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ 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) { if(MQLInfoInteger(MQL_TESTER)) class="kw">return; if(id==CHARTEVENT_OBJECT_CLICK && StringFind(sparam,"BUTT_")>class="num">0) { PressButtonEvents(sparam); } if(id>=CHARTEVENT_CUSTOM) { class="type">class="kw">ushort event=class="type">class="kw">ushort(id-CHARTEVENT_CUSTOM); Print(DFUN,"id=",id,", event=",EnumToString((ENUM_TRADE_EVENT)event),", lparam=",lparam,", dparam=",DoubleToString(dparam,Digits()),", sparam=",sparam); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- 按票据计算平仓交易量,并将多头持仓平仓一半 trade.PositionClosePartial(position.Ticket(),NormalizeLot(position.Symbol(),position.Volume()/class="num">2.0)); class=class="str">"cmt">//--- 按票据计算平仓交易量,并将空头持仓平仓一半 trade.PositionClosePartial(position.Ticket(),NormalizeLot(position.Symbol(),position.Volume()/class="num">2.0)); class=class="str">"cmt">//--- 输入变量 input class="type">ulong InpMagic = class="num">123; class=class="str">"cmt">// 魔幻数字 input class="type">class="kw">double InpLots = class="num">0.1; class=class="str">"cmt">// 手数 input class="type">uint InpStopLoss = class="num">50; class=class="str">"cmt">// 止损点数
「挂单面板按钮的偏移与布局逻辑」
EA 在输入参数里给了两个常被忽略的量:InpButtShiftX=40 控制按钮组整体向右顺移 40 像素,InpButtShiftY=10 控制向下顺移 10 像素。这两个值直接喂给 CreateButtons(InpButtShiftX,InpButtShiftY),不改的话按钮默认贴在图表左上角偏移处。 CreateButtons 内部先算基准坐标:cx=2+shift_x,cy 则在偏移基础上叠了 (h+1)*(TOTAL_BUTT/2)+2*h+1,其中按钮高 h=18、宽 w=84、间距 offset=2。循环里 i==7 时 x 额外加 w+2 做换行,i==TOTAL_BUTT-3 时 x 重置回 cx,由此把多于一行的按钮折返排布。 颜色按索引区分:i<4 绿色(买类),i>6 且 i<11 红色(卖类),其余蓝色(功能键)。若 ButtonCreate 返回失败会弹 Alert 并让 OnInit 返回 INIT_FAILED,面板没起来先查这两个 shift 参数和 TOTAL_BUTT 定义。外汇与贵金属杠杆高,面板只是下单辅助,实际成交受滑点与点差影响,可能偏离预期。
input class="type">uint InpTakeProfit = class="num">50; class=class="str">"cmt">// 止盈点数 input class="type">uint InpDistance = class="num">50; class=class="str">"cmt">// 挂单距离 (点数) input class="type">uint InpDistanceSL = class="num">50; class=class="str">"cmt">// StopLimit 挂单距离 (点数) input class="type">uint InpSlippage = class="num">0; class=class="str">"cmt">// 滑点点数 input class="type">class="kw">double InpWithdrawal = class="num">10; class=class="str">"cmt">// 出金 (在测试器里) input class="type">uint InpButtShiftX = class="num">40; class=class="str">"cmt">// 按钮的 X 顺移 input class="type">uint InpButtShiftY = class="num">10; class=class="str">"cmt">// 按钮的 Y 顺移 class=class="str">"cmt">//--- 全局变量 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建按钮 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CreateButtons(const class="type">int shift_x=class="num">30,const class="type">int shift_y=class="num">0) { class="type">int h=class="num">18,w=class="num">84,offset=class="num">2; class="type">int cx=offset+shift_x,cy=offset+shift_y+(h+class="num">1)*(TOTAL_BUTT/class="num">2)+class="num">2*h+class="num">1; class="type">int x=cx,y=cy; class="type">int shift=class="num">0; for(class="type">int i=class="num">0;i<TOTAL_BUTT;i++) { x=x+(i==class="num">7 ? w+class="num">2 : class="num">0); if(i==TOTAL_BUTT-class="num">3) x=cx; y=(cy-(i-(i>class="num">6 ? class="num">7 : class="num">0))*(h+class="num">1)); if(!ButtonCreate(butt_data[i].name,x,y,(i<TOTAL_BUTT-class="num">3 ? w : w*class="num">2+class="num">2),h,butt_data[i].text,(i<class="num">4 ? clrGreen : i>class="num">6 && i<class="num">11 ? clrRed : clrBlue))) { Alert(TextByLanguage("Не удалось создать кнопку "","Could not create button ")); class="kw">return false; } } ChartRedraw(class="num">0); class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- 创建按钮 if(!CreateButtons(InpButtShiftX,InpButtShiftY)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- 设定交易参数 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestDoEasyPart05.mq5 |
一键面板的前置参数与结构定义
在 MT5 里搭一个交易快捷面板,第一步不是画按钮,而是把魔幻数字、手数和风控点数先固化成输入变量。下面这段代码把 17 个按钮的枚举、数据结构与全局参数一次性铺开,开仓逻辑还没写,但骨架已经能直接编译跑通。 输入项里 InpMagic 默认 123,InpLots 给 0.1 手,止损止盈都设 50 点,挂单距离也是 50 点。外汇与贵金属杠杆高,50 点止损在黄金上可能只是几个美元波动,实盘前务必按品种波动率重算。 TOTAL_BUTT 用宏定义成 17,对应 ENUM_BUTTONS 里从 BUTT_BUY 到 BUTT_PROFIT_WITHDRAWAL 的枚举量。SDataButt 结构只存 name 和 text 两个字符串,说明按钮对象后续靠这两字段绑定图形与标签。 全局里 withdrawal 做了下限保护:InpWithdrawal 小于 0.1 时强制取 0.1,避免测试器里出金参数误填导致逻辑异常。CEngine 和 CTrade 两个实例直接声明,后续下单和引擎事件都走它们。
class=class="str">"cmt">//| 版权所有 class="num">2018, MetaQuotes 软件公司 | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2018, MetaQuotes 软件公司" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- 包含 class="macro">#include <DoEasy\Engine.mqh> class="macro">#include <Trade\Trade.mqh> class=class="str">"cmt">//--- 枚举 enum ENUM_BUTTONS { BUTT_BUY, BUTT_BUY_LIMIT, BUTT_BUY_STOP, BUTT_BUY_STOP_LIMIT, BUTT_CLOSE_BUY, BUTT_CLOSE_BUY2, BUTT_CLOSE_BUY_BY_SELL, BUTT_SELL, BUTT_SELL_LIMIT, BUTT_SELL_STOP, BUTT_SELL_STOP_LIMIT, BUTT_CLOSE_SELL, BUTT_CLOSE_SELL2, BUTT_CLOSE_SELL_BY_BUY, BUTT_DELETE_PENDING, BUTT_CLOSE_ALL, BUTT_PROFIT_WITHDRAWAL }; class="macro">#define TOTAL_BUTT(class="num">17) class=class="str">"cmt">//--- 结构 class="kw">struct SDataButt { class="type">class="kw">string name; class="type">class="kw">string text; }; class=class="str">"cmt">//--- 输入变量 input class="type">ulong InpMagic = class="num">123; class=class="str">"cmt">// 魔幻数字 input class="type">class="kw">double InpLots = class="num">0.1; class=class="str">"cmt">// 手数 input class="type">uint InpStopLoss = class="num">50; class=class="str">"cmt">// 止损点数 input class="type">uint InpTakeProfit = class="num">50; class=class="str">"cmt">// 止盈点数 input class="type">uint InpDistance = class="num">50; class=class="str">"cmt">// 挂单距离 (点数) input class="type">uint InpDistanceSL = class="num">50; class=class="str">"cmt">// StopLimit 挂单距离 (点数) input class="type">uint InpSlippage = class="num">0; class=class="str">"cmt">// 滑点点数 input class="type">class="kw">double InpWithdrawal = class="num">10; class=class="str">"cmt">// 出金 (在测试器里) input class="type">uint InpButtShiftX = class="num">40; class=class="str">"cmt">// 按钮的 X 顺移 input class="type">uint InpButtShiftY = class="num">10; class=class="str">"cmt">// 按钮的 Y 顺移 class=class="str">"cmt">//--- 全局变量 CEngine engine; CTrade trade; SDataButt butt_data[TOTAL_BUTT]; class="type">class="kw">string prefix; class="type">class="kw">double lot; class="type">class="kw">double withdrawal=(InpWithdrawal<class="num">0.1 ? class="num">0.1 : InpWithdrawal); class="type">ulong magic_number; class="type">uint stoploss; class="type">uint takeprofit; class="type">uint distance_pending; class="type">uint distance_stoplimit; class="type">uint slippage; class=class="str">"cmt">//+------------------------------------------------------------------+
◍ EA 生命周期里的初始化与报价钩子
在 MT5 写半自动交易面板,OnInit 是第一道关卡。代码里先调用 engine.IsHedge() 检查账户是否为对冲账户,若不是直接 Alert 并返回 INIT_FAILED——这意味着你的面板在净头寸账户上连加载都做不到,实盘前先确认券商账户类型。 初始化段把前缀、按钮名、手数、魔术码、止损止盈、挂单距离、滑点全部从输入参数搬进全局变量。手数用 NormalizeLot(Symbol(), fmax(InpLots, MinimumLots(Symbol())*2.0)) 兜底,保证不小于最小手数的 2 倍,避免某些品种最小 0.01 但策略想跑 0.02 被拒单。 CreateButtons 失败也会返回 INIT_FAILED,按钮建不出来整个 EA 就废了。之后 trade.SetTypeFillingBySymbol(Symbol()) 按品种设成交模式,trade.LogLevel(LOG_LEVEL_NO) 关掉冗余日志——回测时如果日志刷屏,先把这行确认了。 OnDeinit 只做清理:ObjectsDeleteAll(0, prefix) 删光带前缀的对象,Comment("") 清掉图表左上角文字。不清理的话,切换周期或移除 EA 会留下死按钮。 OnTick 里有个测试器分支:MQLInfoInteger(MQL_TESTER) 为真时,手动遍历图表对象模拟按钮事件,这是策略测试器里没法真点鼠标的解法。实盘时这段跳过,只比对 engine.LastTradeEvent() 变化来刷新 Comment 里的上次交易事件——方便你盯盘时一眼看到状态切换。外汇和贵金属杠杆高,这类面板只负责发单,仓位失控风险仍在你手里。
class=class="str">"cmt">//| 智能系统初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- 检查账户类型 if(!engine.IsHedge()) { Alert(TextByLanguage("Ошибка. Счёт должен быть хеджевым","Error. Account must be hedge")); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- 设置全局变量 prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_"; for(class="type">int i=class="num">0;i<TOTAL_BUTT;i++) { butt_data[i].name=prefix+EnumToString((ENUM_BUTTONS)i); butt_data[i].text=EnumToButtText((ENUM_BUTTONS)i); } lot=NormalizeLot(Symbol(),fmax(InpLots,MinimumLots(Symbol())*class="num">2.0)); magic_number=InpMagic; stoploss=InpStopLoss; takeprofit=InpTakeProfit; distance_pending=InpDistance; distance_stoplimit=InpDistanceSL; slippage=InpSlippage; class=class="str">"cmt">//--- 创建按钮 if(!CreateButtons(InpButtShiftX,InpButtShiftY)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- 设定交易参数 trade.SetDeviationInPoints(slippage); trade.SetExpertMagicNumber(magic_number); trade.SetTypeFillingBySymbol(Symbol()); trade.SetMarginMode(); trade.LogLevel(LOG_LEVEL_NO); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统逆初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- 删除对象 ObjectsDeleteAll(class="num">0,prefix); Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统即时报价函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- class="kw">static ENUM_TRADE_EVENT last_event=WRONG_VALUE; if(MQLInfoInteger(MQL_TESTER)) { engine.OnTimer(); class="type">int total=ObjectsTotal(class="num">0); for(class="type">int i=class="num">0;i<total;i++) { class="type">class="kw">string obj_name=ObjectName(class="num">0,i); if(StringFind(obj_name,prefix+"BUTT_")<class="num">0) class="kw">continue; PressButtonEvents(obj_name); } } if(engine.LastTradeEvent()!=last_event) { Comment("\nLast trade event: ",EnumToString(engine.LastTradeEvent())); last_event=engine.LastTradeEvent(); } }
「定时器与图表事件的分流处理」
EA 在实盘与回测中的事件响应必须分开走,否则回测里点按钮或跑定时器会污染历史测试结果。下面这段代码把 OnTimer 和 OnChartEvent 都做了 MQL_TESTER 判断,只在非回测环境才接图形交互。 OnTimer 里仅一行判断:若不是测试器环境,就调用 engine.OnTimer() 驱动逻辑;回测时这个函数直接空转,不触发任何实时动作。 OnChartEvent 更复杂些:回测时直接 return 跳出;实盘里若捕获到 CHARTEVENT_OBJECT_CLICK 且对象名含 'BUTT_' 前缀,就丢给 PressButtonEvents 处理按钮点击。自定义事件(id>=CHARTEVENT_CUSTOM)则会打印出事件枚举与参数,方便在 MT5 终端日志里核对。 CreateButtons 用循环布按钮:单按钮宽 84、高 18,第 7 个往后右移 w+2 像素,最后三个按钮宽度翻倍成 170。颜色按索引分——前 4 个绿(clrGreen)、7~10 红(clrRed)、其余蓝,肉眼就能区分功能组。 别把回测当实盘调 在 MT5 策略测试器里跑完,别顺手去点面板按钮,代码层面它根本不会响应;要验交互逻辑得用正常图表挂 EA 实测。外汇与贵金属杠杆高,面板误操作可能直接发单。
class="type">void OnTimer() { if(!MQLInfoInteger(MQL_TESTER)) engine.OnTimer(); } 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) { if(MQLInfoInteger(MQL_TESTER)) class="kw">return; if(id==CHARTEVENT_OBJECT_CLICK && StringFind(sparam,"BUTT_")>class="num">0) { PressButtonEvents(sparam); } if(id>=CHARTEVENT_CUSTOM) { class="type">class="kw">ushort event=class="type">class="kw">ushort(id-CHARTEVENT_CUSTOM); Print(DFUN,"id=",id,", event=",EnumToString((ENUM_TRADE_EVENT)event),", lparam=",lparam,", dparam=",DoubleToString(dparam,Digits()),", sparam=",sparam); } } class="type">bool CreateButtons(const class="type">int shift_x=class="num">30,const class="type">int shift_y=class="num">0) { class="type">int h=class="num">18,w=class="num">84,offset=class="num">2; class="type">int cx=offset+shift_x,cy=offset+shift_y+(h+class="num">1)*(TOTAL_BUTT/class="num">2)+class="num">2*h+class="num">1; class="type">int x=cx,y=cy; class="type">int shift=class="num">0; for(class="type">int i=class="num">0;i<TOTAL_BUTT;i++) { x=x+(i==class="num">7 ? w+class="num">2 : class="num">0); if(i==TOTAL_BUTT-class="num">3) x=cx; y=(cy-(i-(i>class="num">6 ? class="num">7 : class="num">0))*(h+class="num">1)); if(!ButtonCreate(butt_data[i].name,x,y,(i<TOTAL_BUTT-class="num">3 ? w : w*class="num">2+class="num">2),h,butt_data[i].text,(i<class="num">4 ? clrGreen : i>class="num">6 && i<class="num">11 ? clrRed : clrBlue))) {
正文
<span class="functions">Alert</span>(TextByLanguage(<span class="string">"Не удалось создать кнопку \""</span>,<span class="string">"Could not create button \""</span>),butt_data[i].text); <span class="keyword">return</span> <span class="macro">false</span>; } } <span class="functions">ChartRedraw</span>(<span class="number">0</span>); <span class="keyword">return</span> <span class="macro">true</span>; } <span class="comment">//+------------------------------------------------------------------+</span>
| <span class="comment">// | 创建按钮 | </span> |
|---|
<span class="comment">//+------------------------------------------------------------------+</span> <span class="keyword">bool</span> ButtonCreate(<span class="keyword">const</span> <span class="keyword">string</span> name,<span class="keyword">const</span> <span class="keyword">int</span> x,<span class="keyword">const</span> <span class="keyword">int</span> y,<span class="keyword">const</span>
◍ 面板按钮的状态读写与事件映射
在 MT5 自定义面板里,按钮不是画上去就完事,得实时知道它被没被按、也能主动改它的亮灭状态。下面这两个重载函数就是干这个的:一个读 OBJPROP_STATE 返回布尔,一个写 OBJPROP_STATE 设置状态,图表 ID 固定填 0 代表当前图。 读按钮状态直接用 ObjectGetInteger 拿整数再强转布尔,开销极小,放在 OnChartEvent 里轮询也不会卡。写状态用 ObjectSetInteger 同理,常用于下单后把‘Buy’键置灰防止重复触发。 枚举名转按钮文字这段值得抄:StringSubstr 从第 5 个字符截掉前缀,再小写下划线替换成空格与首字母大写,比如 BUTT_BUY_LIMIT 会变成‘Buy Limit’。这样加按钮标签不用手敲字符串,改枚举自动同步。 PressButtonEvents 里先截掉全局 prefix 拿到裸按钮名,再比对其是否等于 EnumToString(BUTT_BUY)。若按下且命中,就调 CorrectStopLoss / CorrectTakeProfit 算好挂单价位——外汇与贵金属杠杆高,止损碰止损位无效单是常态,这类校正必须做。
class="type">bool ButtonState(const class="type">class="kw">string name) { class="kw">return (class="type">bool)ObjectGetInteger(class="num">0,name,OBJPROP_STATE); } class="type">void ButtonState(const class="type">class="kw">string name,const class="type">bool state) { ObjectSetInteger(class="num">0,name,OBJPROP_STATE,state); } class="type">class="kw">string EnumToButtText(const ENUM_BUTTONS member) { class="type">class="kw">string txt=StringSubstr(EnumToString(member),class="num">5); StringToLower(txt); StringReplace(txt,"buy","Buy"); StringReplace(txt,"sell","Sell"); StringReplace(txt,"_limit"," Limit"); StringReplace(txt,"_stop"," Stop"); StringReplace(txt,"close_","Close "); StringReplace(txt,"class="num">2"," class="num">1/class="num">2"); StringReplace(txt,"_by_"," by "); StringReplace(txt,"profit_","Profit "); StringReplace(txt,"delete_","Delete "); class="kw">return txt; } class="type">void PressButtonEvents(const class="type">class="kw">string button_name) { class="type">class="kw">string button=StringSubstr(button_name,StringLen(prefix)); if(ButtonState(button_name)) { if(button==EnumToString(BUTT_BUY)) { class="type">class="kw">double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY,class="num">0,stoploss); class="type">class="kw">double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY,class="num">0,takeprofit); } } }
「挂单与市价单的按钮分发逻辑」
在 MT5 面板里,不同交易按钮被按下后,EA 会根据按钮枚举名走不同的下单分支。BuyLimit、BuyStop、BuyStopLimit 与市价 Buy/Sell 的核心差异,在于挂单价格必须先用 CorrectPricePending 按 StopLevel 约束算出来,而不是随手填个整数点差。 以 BuyStopLimit 为例,它要先算触发价 price_set_stop(按 BuyStop 规则偏移 distance_pending),再以此为基础算挂单价 price_set_limit(按 BuyLimit 规则再偏移 distance_stoplimit)。这两层偏移若忽略经纪商最小止损距离,订单会直接被拒。 下面这段是分支代码里 Buy 与三类 Buy 挂单的派发骨架,注意 trade.Buy 市价单传 0 为当前价,而挂单全部走 CorrectPricePending 后的价格变量。外汇与贵金属杠杆高,StopLevel 随品种波动,实盘前务必在策略测试器用对应品种验证距离参数。
trade.Buy(NormalizeLot(Symbol(),lot),Symbol(),class="num">0,sl,tp); } class=class="str">"cmt">//--- 如果 BUTT_BUY_LIMIT 按钮被按下:设置 BuyLimit else if(button==EnumToString(BUTT_BUY_LIMIT)) { class=class="str">"cmt">//--- 获取相对于 StopLevel 的正确下单价格 class="type">class="kw">double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_LIMIT,distance_pending); class=class="str">"cmt">//--- 参考 StopLevel,获取相对于下单价位的正确止损和止盈价位 class="type">class="kw">double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY_LIMIT,price_set,stoploss); class="type">class="kw">double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY_LIMIT,price_set,takeprofit); class=class="str">"cmt">//--- 设置 BuyLimit 订单 trade.BuyLimit(lot,price_set,Symbol(),sl,tp); } class=class="str">"cmt">//--- 如果 BUTT_BUY_STOP 按钮被按下:设置 BuyStop else if(button==EnumToString(BUTT_BUY_STOP)) { class=class="str">"cmt">//--- 获取相对于 StopLevel 的正确下单价格 class="type">class="kw">double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_STOP,distance_pending); class=class="str">"cmt">//--- 参考 StopLevel,获取相对于下单价位的正确止损和止盈价位 class="type">class="kw">double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY_STOP,price_set,stoploss); class="type">class="kw">double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY_STOP,price_set,takeprofit); class=class="str">"cmt">//--- 设置 BuyStop 订单 trade.BuyStop(lot,price_set,Symbol(),sl,tp); } class=class="str">"cmt">//--- 如果 BUTT_BUY_STOP_LIMIT 按钮被按下:设置 BuyStopLimit else if(button==EnumToString(BUTT_BUY_STOP_LIMIT)) { class=class="str">"cmt">//--- 获取相对于 StopLevel 的正确 BuyStop 价格 class="type">class="kw">double price_set_stop=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_STOP,distance_pending); class=class="str">"cmt">//--- 参考 StopLevel,计算相对于 BuyStop 下单位置的 BuyLimit 订单价格 class="type">class="kw">double price_set_limit=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_LIMIT,distance_stoplimit,price_set_stop); class=class="str">"cmt">//--- 参考 StopLevel,获取相对于下单价位的正确止损和止盈价位 class="type">class="kw">double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY_STOP,price_set_limit,stoploss); class="type">class="kw">double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY_STOP,price_set_limit,takeprofit); class=class="str">"cmt">//--- 设置 BuyStopLimit 订单 trade.OrderOpen(Symbol(),ORDER_TYPE_BUY_STOP_LIMIT,lot,price_set_limit,price_set_stop,sl,tp); } class=class="str">"cmt">//--- 如果 BUTT_SELL 按钮被按下,开空头仓位 else if(button==EnumToString(BUTT_SELL)) { class=class="str">"cmt">//--- 获取相对于 StopLevel 的正确止损和止盈价位 class="type">class="kw">double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL,class="num">0,stoploss);
空头与挂单的分支执行逻辑
面板里每按一个卖向按钮,程序都要先按 StopLevel 约束把价格和止损止盈算对,再交给 CTrade 去下单。SellLimit、SellStop 各自只调一次 CorrectPricePending 拿挂单位,而 SellStopLimit 要算两个价:先取 Stop 触价,再基于它算 Limit 挂价。 注意 SellStopLimit 的 OrderOpen 传参顺序——price_set_limit 在前、price_set_stop 在后,和市价 Sell 只传 sl/tp 完全不同。若把两个价格写反,订单可能直接被经纪商拒单。 平仓分支不再算新的止损止盈,而是先抓 GetListMarketPosition 的持仓对象数组,后续再遍历清掉多头。外汇与贵金属杠杆高,挂单未成交前也可能因跳空越过 StopLevel 而失效,实盘前务必在 MT5 策略测试器用 2023 年 XAUUSD 数据跑一遍分支覆盖。
class="type">class="kw">double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL,class="num">0,takeprofit); class=class="str">"cmt">//--- 开空头仓位 trade.Sell(lot,Symbol(),class="num">0,sl,tp); } class=class="str">"cmt">//--- 如果 BUTT_SELL_LIMIT 按钮被按下:设置 SellLimit else if(button==EnumToString(BUTT_SELL_LIMIT)) { class=class="str">"cmt">//--- 获取相对于 StopLevel 的正确下单价格 class="type">class="kw">double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_LIMIT,distance_pending); class=class="str">"cmt">//--- 参考 StopLevel,获取相对于下单价位的正确止损和止盈价位 class="type">class="kw">double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL_LIMIT,price_set,stoploss); class="type">class="kw">double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL_LIMIT,price_set,takeprofit); class=class="str">"cmt">//--- 设置 SellLimit 订单 trade.SellLimit(lot,price_set,Symbol(),sl,tp); } class=class="str">"cmt">//--- 如果 BUTT_SELL_STOP 按钮被按下:设置 SellStop else if(button==EnumToString(BUTT_SELL_STOP)) { class=class="str">"cmt">//--- 获取相对于 StopLevel 的正确下单价格 class="type">class="kw">double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_STOP,distance_pending); class=class="str">"cmt">//--- 参考 StopLevel,获取相对于下单价位的正确止损和止盈价位 class="type">class="kw">double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL_STOP,price_set,stoploss); class="type">class="kw">double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL_STOP,price_set,takeprofit); class=class="str">"cmt">//--- 设置 SellStop 订单 trade.SellStop(lot,price_set,Symbol(),sl,tp); } class=class="str">"cmt">//--- 如果 BUTT_SELL_STOP_LIMIT 按钮被按下:设置 SellStopLimit else if(button==EnumToString(BUTT_SELL_STOP_LIMIT)) { class=class="str">"cmt">//--- 获取相对于 StopLevel 的正确 SellStop 订单价格 class="type">class="kw">double price_set_stop=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_STOP,distance_pending); class=class="str">"cmt">//--- 参考 StopLevel,计算相对于 SellStop 下单位置的 SellLimit 订单价格 class="type">class="kw">double price_set_limit=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_LIMIT,distance_stoplimit,price_set_stop); class=class="str">"cmt">//--- 参考 StopLevel,获取相对于下单价位的正确止损和止盈价位 class="type">class="kw">double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL_STOP,price_set_limit,stoploss); class="type">class="kw">double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL_STOP,price_set_limit,takeprofit); class=class="str">"cmt">//--- 设置 SellStopLimit 订单 trade.OrderOpen(Symbol(),ORDER_TYPE_SELL_STOP_LIMIT,lot,price_set_limit,price_set_stop,sl,tp); } class=class="str">"cmt">//--- 如果 BUTT_CLOSE_BUY 按钮被按下:多头平仓 else if(button==EnumToString(BUTT_CLOSE_BUY)) { class=class="str">"cmt">//--- 获取所有持仓的清单 CArrayObj* list=engine.GetListMarketPosition();
◍ 按利润锁定的多头退场写法
在面板按钮触发的平仓逻辑里,先抓全部持仓再用 CSelect::ByOrderProperty 按 ORDER_PROP_TYPE 过滤出 POSITION_TYPE_BUY,这一步保证只动多头。接着 list.Sort(SORT_BY_ORDER_PROFIT_FULL) 把含佣金与掉期的全利润纳入排序,FindOrderMax 取利润最大那一单的索引,避免随手平掉亏损单。 整平与减半的差别只在最后一句:PositionClose 直接清掉整仓,PositionClosePartial 用 Volume()/2.0 再经 NormalizeLot 修约后平一半。外汇与贵金属杠杆高,部分平仓后残留仓位仍可能随跳空扩大浮亏,参数改动前建议在 MT5 策略测试器跑一遍。 第三种按钮 BUTT_CLOSE_BUY_BY_SELL 的思路是双向对冲退场:多头列表与空头列表各自按全利润排序后,分别取最大利润多单与最大利润空单索引,为后续用反向单抵消头寸做准备。下面这段是多头整平与减半的核心分支。
class=class="str">"cmt">//--- 仅从列表中选择多头仓位 list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL); class=class="str">"cmt">//--- 参考佣金和隔夜利率,按利润对列表进行排序 list.Sort(SORT_BY_ORDER_PROFIT_FULL); class=class="str">"cmt">//--- 获取具有最大利润的多头仓位索引 class="type">int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL); if(index>WRONG_VALUE) { COrder* position=list.At(index); if(position!=NULL) { class=class="str">"cmt">//--- 获取多头仓位票据,并按票据平仓 trade.PositionClose(position.Ticket()); } } } class=class="str">"cmt">//--- 如果 BUTT_CLOSE_BUY2 按钮被按下:多头平仓一半 else if(button==EnumToString(BUTT_CLOSE_BUY2)) { class=class="str">"cmt">//--- 获取所有持仓的清单 CArrayObj* list=engine.GetListMarketPosition(); class=class="str">"cmt">//--- 仅从列表中选择多头仓位 list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL); class=class="str">"cmt">//--- 参考佣金和隔夜利率,按利润对列表进行排序 list.Sort(SORT_BY_ORDER_PROFIT_FULL); class=class="str">"cmt">//--- 获取具有最大利润的多头仓位索引 class="type">int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL); if(index>WRONG_VALUE) { COrder* position=list.At(index); if(position!=NULL) { class=class="str">"cmt">//--- 按票据计算平仓交易量,并将多头持仓平仓一半 trade.PositionClosePartial(position.Ticket(),NormalizeLot(position.Symbol(),position.Volume()/class="num">2.0)); } } }
「按利润挑持仓做反向平仓与半平」
这段逻辑展示了在 MT5 面板里点不同按钮时,如何挑出‘最赚’的仓位来处理。核心思路是先拿到全部持仓,按类型过滤,再按含佣金与隔夜利息的净利排序,最后对排第一的仓位执行动作。 先说双向互平:当多空列表里各自最大利润仓位索引都大于 WRONG_VALUE 时,取两个 COrder 指针,若都不为空,就用买入仓的 ticket 对卖出仓做 PositionCloseBy 反向平仓。这在锁仓管理里能直接抵消两边浮盈最大的单。 点 BUTT_CLOSE_SELL 时走另一条路:只筛空头,排序后取最大利润那个,直接按票据全平。点 BUTT_CLOSE_SELL2 则同样筛空头取最赚的,但交易量用 position.Volume()/2.0 算一半,再 NormalizeLot 后走 PositionClosePartial 半平。 在 MT5 里跑这套,重点看 Volume()/2.0 后 NormalizeLot 的舍入——若券商最小手数步长是 0.01,0.05 手一半会变成 0.02 或 0.03,实际平掉的比例可能偏离精确的 50%。外汇与贵金属杠杆高,部分平仓后残留风险敞口仍可能快速扩大。
class="type">int index_sell=CSelect::FindOrderMax(list_sell,ORDER_PROP_PROFIT_FULL); if(index_buy>WRONG_VALUE && index_sell>WRONG_VALUE) { class=class="str">"cmt">//--- 获取具有最大利润的多头仓位索引 COrder* position_buy=list_buy.At(index_buy); class=class="str">"cmt">//--- 获取具有最大利润的空头仓位索引 COrder* position_sell=list_sell.At(index_sell); if(position_buy!=NULL && position_sell!=NULL) { class=class="str">"cmt">//--- 以逆向空头仓位将多头平仓 trade.PositionCloseBy(position_buy.Ticket(),position_sell.Ticket()); } } class=class="str">"cmt">//--- 如果 BUTT_CLOSE_SELL 按钮被按下:空头平仓 else if(button==EnumToString(BUTT_CLOSE_SELL)) { class=class="str">"cmt">//--- 获取所有持仓的清单 CArrayObj* list=engine.GetListMarketPosition(); class=class="str">"cmt">//--- 仅从列表中选择空头仓位 list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL); class=class="str">"cmt">//--- 参考佣金和隔夜利率,按利润对列表进行排序 list.Sort(SORT_BY_ORDER_PROFIT_FULL); class=class="str">"cmt">//--- 获取具有最大利润的空头仓位索引 class="type">int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL); if(index>WRONG_VALUE) { COrder* position=list.At(index); if(position!=NULL) { class=class="str">"cmt">//--- 获取空头仓位票据,并按票据平仓 trade.PositionClose(position.Ticket()); } } } class=class="str">"cmt">//--- 如果 BUTT_CLOSE_SELL2 按钮被按下:空头平仓一半 else if(button==EnumToString(BUTT_CLOSE_SELL2)) { class=class="str">"cmt">//--- 获取所有持仓的清单 CArrayObj* list=engine.GetListMarketPosition(); class=class="str">"cmt">//--- 仅从列表中选择空头仓位 list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL); class=class="str">"cmt">//--- 参考佣金和隔夜利率,按利润对列表进行排序 list.Sort(SORT_BY_ORDER_PROFIT_FULL); class=class="str">"cmt">//--- 获取具有最大利润的空头仓位索引 class="type">int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL); if(index>WRONG_VALUE) { COrder* position=list.At(index); if(position!=NULL) { class=class="str">"cmt">//--- 按票据计算平仓交易量,并将空头持仓平仓一半 trade.PositionClosePartial(position.Ticket(),NormalizeLot(position.Symbol(),position.Volume()/class="num">2.0)); } } }
用反向单对冲平仓的按钮逻辑
在 MT5 面板里点 BUTT_CLOSE_SELL_BY_BUY,程序不会直接市价平掉空头,而是挑出当前浮利最大的空单和最大的多单,用多头票据去对冲关闭空单。这种做法在锁仓账户里能省一次点差,但外汇和贵金属杠杆高,对冲不等于风险消失,净值仍随价差波动。 代码先分别拉取持仓列表,用 CSelect::ByOrderProperty 按 POSITION_TYPE_SELL / BUY 过滤,再 Sort(SORT_BY_ORDER_PROFIT_FULL) 把佣金与隔夜费算进真实利润排序。FindOrderMax 拿到两端最赚的那张单,都有效才调 trade.PositionCloseBy(sell_ticket, buy_ticket)。 另一个 BUTT_CLOSE_ALL 则是一次性清仓:同一份列表排序后从利润最少的那张开始 for 循环,逐票 PositionClose。实盘里若持仓超过 20 张,这种顺序可能让最亏的单多扛几毫秒,遇到跳空可能扩大滑点。
else if(button==EnumToString(BUTT_CLOSE_SELL_BY_BUY)) { CArrayObj* list_sell=engine.GetListMarketPosition(); list_sell=CSelect::ByOrderProperty(list_sell,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL); list_sell.Sort(SORT_BY_ORDER_PROFIT_FULL); class="type">int index_sell=CSelect::FindOrderMax(list_sell,ORDER_PROP_PROFIT_FULL); CArrayObj* list_buy=engine.GetListMarketPosition(); list_buy=CSelect::ByOrderProperty(list_buy,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL); list_buy.Sort(SORT_BY_ORDER_PROFIT_FULL); class="type">int index_buy=CSelect::FindOrderMax(list_buy,ORDER_PROP_PROFIT_FULL); if(index_sell>WRONG_VALUE && index_buy>WRONG_VALUE) { COrder* position_sell=list_sell.At(index_sell); COrder* position_buy=list_buy.At(index_buy); if(position_sell!=NULL && position_buy!=NULL) { trade.PositionCloseBy(position_sell.Ticket(),position_buy.Ticket()); } } } else if(button==EnumToString(BUTT_CLOSE_ALL)) { CArrayObj* list=engine.GetListMarketPosition(); if(list!=NULL) { list.Sort(SORT_BY_ORDER_PROFIT_FULL); class="type">int total=list.Total(); for(class="type">int i=class="num">0;i<total;i++) { COrder* position=list.At(i); if(position==NULL) class="kw">continue; trade.PositionClose(position.Ticket()); } } }
◍ 按钮事件里的挂单清理与模拟出金
这段代码承接上一节的图表按钮交互,处理两类关键操作:按下删除挂单按钮时,拉取当前所有挂单并按开单时间排序后倒序循环删除;按下出金按钮时,仅在策略测试器内调用 TesterWithdrawal 模拟利润提取。 删除逻辑先通过 engine.GetListMarketPendings() 拿到挂单对象数组,用 Sort(SORT_BY_ORDER_TIME_OPEN_MSC) 排好序,再从列表末尾(时间最新)往前用 trade.OrderDelete(order.Ticket()) 逐笔撤单。这种倒序写法保证遍历期间数组索引不会因前端删除而错位。 出金分支用 MQLInfoInteger(MQL_TESTER) 判断运行环境,实盘不触发任何资金动作,只在回测里跑模拟出金。下方日志显示 2019.04.05 23:19:55.248 的 EURUSD 0.10 Sell Limit #375419507 挂单价 1.14562,从下达到移除几乎同毫秒完成,对应按钮删除路径。 最后 Sleep(100) 让出 UI 响应节奏,ButtonState 复位、ChartRedraw 重绘。外汇与贵金属挂单操作杠杆高,误删或错序可能直接改变敞口,建议先在 MT5 策略测试器单步跑这套事件。
}
}
}
class=class="str">"cmt">//--- 如果 BUTT_DELETE_PENDING 按钮被按下: 删除首笔挂单
else if(button==EnumToString(BUTT_DELETE_PENDING))
{
class=class="str">"cmt">//--- 获取所有订单的列表
CArrayObj* list=engine.GetListMarketPendings();
if(list!=NULL)
{
class=class="str">"cmt">//--- 按下单时间排序列表
list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC);
class="type">int total=list.Total();
class=class="str">"cmt">//--- 从时间最多的持仓开始循环
for(class="type">int i=total-class="num">1;i>=class="num">0;i--)
{
COrder* order=list.At(i);
if(order==NULL)
class="kw">continue;
class=class="str">"cmt">//--- 按票据删除订单
trade.OrderDelete(order.Ticket());
}
}
}
class=class="str">"cmt">//--- 如果 BUTT_PROFIT_WITHDRAWAL 按钮被按下: 从账户出金
if(button==EnumToString(BUTT_PROFIT_WITHDRAWAL))
{
class=class="str">"cmt">//--- 如果程序在测试器里启动
if(MQLInfoInteger(MQL_TESTER))
{
class=class="str">"cmt">//--- 模拟出金
TesterWithdrawal(withdrawal);
}
}
class=class="str">"cmt">//--- 等待 class="num">1/class="num">10 秒
Sleep(class="num">100);
class=class="str">"cmt">//--- “取消”按钮并重绘图表
ButtonState(button_name,false);
ChartRedraw();
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
- Pending order placed: class="num">2019.04.class="num">05 class="num">23:class="num">19:class="num">55.248 -
EURUSD class="num">0.10 Sell Limit #class="num">375419507 at price class="num">1.14562
OnChartEvent: id=class="num">1001, event=TRADE_EVENT_PENDING_ORDER_PLASED, lparam=class="num">375419507, dparam=class="num">1.14562, sparam=EURUSD
- Pending order removed: class="num">2019.04.class="num">05 class="num">23:class="num">19:class="num">55.248 -
EURUSD class="num">0.10 Sell Limit #class="num">375419507 at price class="num">1.14562
OnChartEvent: id=class="num">1002, event=TRADE_EVENT_PENDING_ORDER_REMOVED, lparam=class="num">375419507, dparam=class="num">1.14562, sparam=EURUSD「历史订单检索里的断点取舍」
上一节把历史订单和成交集合搭完之后,社区里有人盯上了 CHistoryCollection::OrderSearch 的循环效率。原实现从 start-1 倒序扫到 0,即便已经拿到丢失订单也不提前退出,社区用户 Alvaro Arioni 在 2020 年 8 月的讨论里指出:在订单历史量过大时,找到目标后加 break 可能更划算。 作者 Artyom Trishkin 的回应也很直接——丢失的订单可能不止一个,而该函数当前只返回最接近零的那一张,所以单纯从 0 到 start-1 正序迭代配合中断,逻辑上和原倒序写法不等价。他答应在后续版本里检查并测试是否引入 break。 下面这段是社区讨论后补出的两种写法对照,左边是倒序带 break 的修订版,右边是正序扫描版,都没有额外编造收益数据,纯属接口内部的遍历路径差异。外汇与贵金属自动化检索本就带平台稳定性风险,改循环前务必在策略测试器里跑一遍大历史样本。 让小布替你跑这套 把两段代码分别丢进 MT5 的 EA 里,用 AccountHistory 拉满十万级成交记录,对比两者的毫秒耗时,比脑补谁快更靠谱。
class="type">ulong CHistoryCollection::OrderSearch(const class="type">int start,ENUM_ORDER_TYPE &order_type) { class="type">ulong order_ticket=class="num">0; for(class="type">int i=start-class="num">1;i>=class="num">0;i--) { class="type">ulong ticket=::HistoryOrderGetTicket(i); if(ticket==class="num">0) class="kw">continue; ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)::HistoryOrderGetInteger(ticket,ORDER_TYPE); if(this.IsPresentOrderInList(ticket,type)) class="kw">continue; order_ticket=ticket; order_type=type; break; } class="kw">return order_ticket; } class="type">ulong CHistoryCollection::OrderSearch(const class="type">int start,ENUM_ORDER_TYPE &order_type) { class="type">ulong order_ticket=class="num">0; for(class="type">int i=class="num">0; i < start ;i++) { class="type">ulong ticket=::HistoryOrderGetTicket(i); if(ticket==class="num">0) class="kw">continue; ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)::HistoryOrderGetInteger(ticket,ORDER_TYPE); if(this.IsPresentOrderInList(ticket,type)) class="kw">continue; order_ticket=ticket; order_type=type;
把这条线请下神坛
上面那段 return order_ticket 的收口,意味着脚本在循环里一旦命中挂单就跳出,ticket 拿不到就返回 0。它只是把‘找指定挂单’这件事自动化了,不代表任何方向判断。 真正用的时候,记得在 MT5 里把魔术码和订单注释对齐,否则循环跑完大概率返回 0,你还以为脚本坏了。外汇和贵金属杠杆高,挂单逻辑再顺也挡不住滑点和跳空,这根线只是工具,别当成信号本身。
break; } class="kw">return order_ticket; }