轻松快捷开发 MetaTrader 程序的函数库(第九部分):与 MQL4 的兼容性 - 准备数据·综合运用
(3/3)· 复制进 MT4 编译即爆 2000+ 错误?本篇用 ToMQL4 适配层彻底填平 MQL5 常量与订单系统的鸿沟
把为 MT5 写好的 DoEasy 函数库直接丢进 MT4 的 Include 目录编译,编辑器瞬间抛出两千多个错误,绝大多数来自 MQL4 根本不认识的 MQL5 常量和枚举。订单系统差异更狠:MT4 历史订单列表里既没有成交数据,字段也更少,原先的事件跟踪逻辑完全跑不起来。本篇是系列最后一篇,专治这种跨平台移植的硬骨头。
「从订单状态反推成交原因」
在 MT5 的订单封装类里,想拿到一笔单子为什么平掉,得先看它处在哪种状态。上面这段条件编译把 MQL4 和 MQL5 两套取法分开了:MQL5 下用 ORDER_PROP_STATUS 判别,再分别调 PositionGetInteger、OrderGetInteger、HistoryOrderGetInteger、HistoryDealGetInteger 取 REASON 字段。
如果单子是被止盈扫掉的,直接给 ORDER_REASON_TP(宏值 5);若魔术码非零则归到 EA 自发单 ORDER_REASON_EXPERT(宏值 3),其余情况落 WRONG_VALUE。这套分支能让你在回测里区分「系统止盈」和「策略主动平仓」两类退出,外汇与贵金属杠杆高,样本归因错了会直接误导仓位模型。
下面这组宏把平台没暴露全的枚举补上了:ORDER_TYPE_CLOSE_BY 为 8、BUY_STOP_LIMIT 为 9、SELL_STOP_LIMIT 为 10,成交策略里 ORDER_FILLING_RETURN 是 2,ORDER_TIME_GTC 为 0。开 MT5 把 CHistoryCollection::OrderSearch 里的 for(int i=start-1;i>=0;i--) 打日志,就能验证历史遍历是不是从尾向前扫。
this.OrderCloseByTakeProfit() ? ORDER_REASON_TP : this.OrderMagicNumber()!=class="num">0 ? ORDER_REASON_EXPERT : WRONG_VALUE ); class="macro">#else class="type">long res=WRONG_VALUE; class="kw">switch((ENUM_ORDER_STATUS)this.GetProperty(ORDER_PROP_STATUS)) { case ORDER_STATUS_MARKET_POSITION : res=::PositionGetInteger(POSITION_REASON); class="kw">break; case ORDER_STATUS_MARKET_ORDER : case ORDER_STATUS_MARKET_PENDING : res=::OrderGetInteger(ORDER_REASON); class="kw">break; case ORDER_STATUS_HISTORY_PENDING : case ORDER_STATUS_HISTORY_ORDER : res=::HistoryOrderGetInteger(m_ticket,ORDER_REASON);class="kw">break; case ORDER_STATUS_DEAL : res=::HistoryDealGetInteger(m_ticket,DEAL_REASON); class="kw">break; class="kw">default : res=WRONG_VALUE; class="kw">break; } class="kw">return res; class="macro">#endif } class="macro">#define ORDER_TYPE_CLOSE_BY(class="num">8) class="macro">#define ORDER_TYPE_BUY_STOP_LIMIT(class="num">9) class="macro">#define ORDER_TYPE_SELL_STOP_LIMIT(class="num">10) class="macro">#define ORDER_FILLING_RETURN(class="num">2) class="macro">#define ORDER_TIME_GTC(class="num">0) class="macro">#define ORDER_REASON_EXPERT(class="num">3) class="macro">#define ORDER_REASON_SL(class="num">4) class="macro">#define ORDER_REASON_TP(class="num">5) class="type">ulong CHistoryCollection::OrderSearch(class="kw">const class="type">int start,ENUM_ORDER_TYPE &order_type) { class="type">ulong order_ticket=class="num">0; class="macro">#ifdef __MQL5__ for(class="type">int i=start-class="num">1;i>=class="num">0;i--)
◍ 历史订单遍历与对冲账户识别的兼容写法
| 在 MT5 历史订单回查里,MQL5 用 HistoryOrderGetTicket 按索引取票,MQL4 则靠 OrderSelect(i,SELECT_BY_POS,MODE_HISTORY) 按位置选单。两边差异不只是函数名,订单类型枚举范围也不同:MQL4 分支里用 type<ORDER_TYPE_BUY_LIMIT | type>ORDER_TYPE_SELL_STOP 过滤挂单,把非挂单类直接 continue 掉,避免把已成交 deal 误当事件源。 |
|---|
CEvent 构造函数里那行 m_is_hedge 判断很关键:MQL5 下用 AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING 来确认零售对冲账户,MQL4 则直接写死 true。外汇和贵金属品种在对冲账户上同 symbol 多向持仓并存是常态,这套识别逻辑跑错,事件去重(IsPresentOrderInList)就会漏单,属于高杠杆环境下的隐性风险。 NewDealEventHedge 开头取 SYMBOL_ASK 用的是 deal.Symbol() 动态品种,而不是写死 _Symbol。如果你在 MT5 测试器里用多品种回测,这种写法能保证事件对象绑定到真实成交品种,否则用错卖价会让你在日志里看到一堆价格偏差告警。
{
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="macro">#else
for(class="type">int i=start-class="num">1;i>=class="num">0;i--)
{
if(!::OrderSelect(i,SELECT_BY_POS,MODE_HISTORY))
class="kw">continue;
ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)::OrderType();
class="type">ulong ticket=::OrderTicket();
if(ticket==class="num">0 || type<ORDER_TYPE_BUY_LIMIT || type>ORDER_TYPE_SELL_STOP)
class="kw">continue;
if(this.IsPresentOrderInList(ticket,type))
class="kw">continue;
order_ticket=ticket;
order_type=type;
}
class="macro">#endif
class="kw">return order_ticket;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor |
class=class="str">"cmt">//+------------------------------------------------------------------+
CEvent::CEvent(class="kw">const ENUM_EVENT_STATUS event_status,class="kw">const class="type">int event_code,class="kw">const class="type">ulong ticket) : m_event_code(event_code),m_digits(class="num">0)
{
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_is_hedge=class="macro">#ifdef __MQL4__ true class="macro">#else class="type">bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) class="macro">#endif;
this.m_digits_acc=class="macro">#ifdef __MQL4__ class="num">2 class="macro">#else (class="type">int)::AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS) class="macro">#endif;
this.m_chart_id=::ChartID();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create a hedging account event |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CEventsCollection::NewDealEventHedge(COrder* deal,CArrayObj* list_history,CArrayObj* list_market)
{
class="macro">#ifdef __MQL5__
class="type">class="kw">double ask=::SymbolInfoDouble(deal.Symbol(),SYMBOL_ASK);成交类型与持仓方向的枚举拆解
在 MT5 的成交事件回放里,先把底层枚举认清楚,才不会把余额变动和真实开仓混为一谈。下面这段枚举把账户里可能发生的 18 种 deal 类型一次性列全,其中 DEAL_TYPE_BUY 与 DEAL_TYPE_SELL 才是真正进场,其余如 DEAL_TYPE_COMMISSION、DEAL_TYPE_INTEREST 都只是资金流水。 除了成交类型,持仓方向只有两个值:POSITION_TYPE_BUY 和 POSITION_TYPE_SELL。而 DEAL_ENTRY 枚举更细一层,区分了 IN(开仓)、OUT(平仓)、INOUT(同笔反手)、OUT_BY(被另一单平掉),对冲账户里 OUT_BY 出现频率会明显偏高。 构造函数里有一行值得盯:m_is_hedge 在 MQL5 下通过 AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING 判定,说明同一品种能否多向持仓完全由账户保证金模式决定。外汇与贵金属杠杆高,零售对冲模式虽允许锁仓,但占用保证金与裸单边不同,回测前务必在 MT5 终端确认账户类型。
class="type">class="kw">double bid=::SymbolInfoDouble(deal.Symbol(),SYMBOL_BID); class=class="str">"cmt">//--- Market entry class="macro">#endif } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| MQL5 deal types | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_DEAL_TYPE { DEAL_TYPE_BUY, DEAL_TYPE_SELL, DEAL_TYPE_BALANCE, DEAL_TYPE_CREDIT, DEAL_TYPE_CHARGE, DEAL_TYPE_CORRECTION, DEAL_TYPE_BONUS, DEAL_TYPE_COMMISSION, DEAL_TYPE_COMMISSION_DAILY, DEAL_TYPE_COMMISSION_MONTHLY, DEAL_TYPE_COMMISSION_AGENT_DAILY, DEAL_TYPE_COMMISSION_AGENT_MONTHLY, DEAL_TYPE_INTEREST, DEAL_TYPE_BUY_CANCELED, DEAL_TYPE_SELL_CANCELED, DEAL_DIVIDEND, DEAL_DIVIDEND_FRANKED, DEAL_TAX }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Position change method | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_DEAL_ENTRY { DEAL_ENTRY_IN, DEAL_ENTRY_OUT, DEAL_ENTRY_INOUT, DEAL_ENTRY_OUT_BY }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Open position direction | class=class="str">"cmt">//+------------------------------------------------------------------+ enum class="type">ENUM_POSITION_TYPE { POSITION_TYPE_BUY, POSITION_TYPE_SELL }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CEventsCollection::CEventsCollection(class="type">void) : m_trade_event(TRADE_EVENT_NO_EVENT),m_trade_event_code(TRADE_EVENT_FLAG_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="macro">#ifdef __MQL4__ true class="macro">#else class="type">bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) class="macro">#endif; this.m_chart_id=::ChartID(); ::ZeroMemory(this.m_tick); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+
「引擎构造与历史余额类的初始化细节」
CEngine 的构造函数里先把 m_first_start 置为 true、交易事件标记设为无事件,随后用 EventSetMillisecondTimer(TIMER_FREQUENCY) 拉起毫秒级定时器;若返回失败会打印错误码,但程序不中断。 列表 m_list_counters 先 Sort 再 Clear,然后以 COLLECTION_COUNTER_ID / STEP / PAUSE 三个常量创建计数器对象,这部分决定了后续统计采集的节拍。 对冲账户判定用编译期与运行期双路径:MQL4 下直接认 true,MQL5 下则读 ACCOUNT_MARGIN_MODE 是否等于 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING,把结果塞进 m_is_hedge,外汇与贵金属账户须留意柜台模式差异带来的持仓逻辑分歧。 CHistoryBalance 继承 COrder,构造时强制传入 ticket 并把状态标为 ORDER_STATUS_BALANCE;它只声明了三组 SupportProperty 虚函数(整型、双精、字符串),用于运行时筛掉历史余额不支持的字段读写。
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="macro">#ifdef __MQL4__ true class="macro">#else class="type">bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) class="macro">#endif; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| HistoryBalance.mqh | class=class="str">"cmt">//| Copyright class="num">2018, MetaQuotes Software Corp. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2018, MetaQuotes Software Corp." 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">//| Include files | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Order.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Historical balance operation | class=class="str">"cmt">//+------------------------------------------------------------------+ class CHistoryBalance : class="kw">public COrder { class="kw">public: class=class="str">"cmt">//--- Constructor CHistoryBalance(class="kw">const class="type">ulong ticket) : COrder(ORDER_STATUS_BALANCE,ticket) {} class=class="str">"cmt">//--- Supported deal properties(class="num">1) real, (class="num">2) integer class="kw">virtual class="type">bool SupportProperty(ENUM_ORDER_PROP_INTEGER class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_ORDER_PROP_DOUBLE class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_ORDER_PROP_STRING class="kw">property); }; 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="macro">#ifdef __MQL4__ true class="macro">#else class="type">bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) class="macro">#endif; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| HistoryBalance.mqh | class=class="str">"cmt">//| Copyright class="num">2018, MetaQuotes Software Corp. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2018, MetaQuotes Software Corp." 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">//| Include files | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Order.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Historical balance operation | class=class="str">"cmt">//+------------------------------------------------------------------+ class CHistoryBalance : class="kw">public COrder { class="kw">public: class=class="str">"cmt">//--- Constructor CHistoryBalance(class="kw">const class="type">ulong ticket) : COrder(ORDER_STATUS_BALANCE,ticket) {} class=class="str">"cmt">//--- Supported deal properties(class="num">1) real, (class="num">2) integer class="kw">virtual class="type">bool SupportProperty(ENUM_ORDER_PROP_INTEGER class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_ORDER_PROP_DOUBLE class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_ORDER_PROP_STRING class="kw">property); }; class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 历史余额类怎么筛订单属性
在写 MT5 历史持仓统计类时,CHistoryBalance 用三组重载的 SupportProperty 去判断某笔订单是否支持指定的整型、双精度或字符串属性。整型里只放行票号、开仓时间、状态、类型、触发原因这 5 个字段,其余一律 return false,避免对历史余额记录误读不存在的字段。 双精度版本极简:仅 ORDER_PROP_PROFIT 返回 true,说明历史余额对象只认盈亏这一个浮点属性。字符串版本则反过来,symbol 与 ext_id 返回 false,其余字符串属性都放行,因为余额类订单本就不挂交易品种。 下方宏把几个易漏的枚举值钉死:ORDER_TYPE_CLOSE_BY=8、BUY_STOP_LIMIT=9、SELL_STOP_LIMIT=10,填充策略 RETURN=2,挂单时长 GTC=0,而原因里 SL=4、TP=5、BALANCE=6、CREDIT=7 都是回测分组时要单独捞出来的。 COrder::OrderPositionID 在 MQL4 分支直接取 MagicNumber 顶替持仓 ID;MT5 分支另有实现。复制这段代码到本地编译器,把 BALANCE / CREDIT 原因过滤出来,就能单独算入金出金对净值曲线的扰动。外汇与贵金属杠杆高,这类统计仅作风险复盘,不代表未来盈亏。
class="type">bool CHistoryBalance::SupportProperty(ENUM_ORDER_PROP_INTEGER class="kw">property) { if(class="kw">property==ORDER_PROP_TICKET || class="kw">property==ORDER_PROP_TIME_OPEN || class="kw">property==ORDER_PROP_STATUS || class="kw">property==ORDER_PROP_TYPE || class="kw">property==ORDER_PROP_REASON ) class="kw">return true; class="kw">return false; } class="type">bool CHistoryBalance::SupportProperty(ENUM_ORDER_PROP_DOUBLE class="kw">property) { class="kw">return(class="kw">property==ORDER_PROP_PROFIT ? true : false); } class="type">bool CHistoryBalance::SupportProperty(ENUM_ORDER_PROP_STRING class="kw">property) { if(class="kw">property==ORDER_PROP_SYMBOL || class="kw">property==ORDER_PROP_EXT_ID) class="kw">return false; class="kw">return true; } class="macro">#define ORDER_TYPE_CLOSE_BY(class="num">8) class="macro">#define ORDER_TYPE_BUY_STOP_LIMIT(class="num">9) class="macro">#define ORDER_TYPE_SELL_STOP_LIMIT(class="num">10) class="macro">#define ORDER_FILLING_RETURN(class="num">2) class="macro">#define ORDER_TIME_GTC(class="num">0) class="macro">#define ORDER_REASON_EXPERT(class="num">3) class="macro">#define ORDER_REASON_SL(class="num">4) class="macro">#define ORDER_REASON_TP(class="num">5) class="macro">#define ORDER_REASON_BALANCE(class="num">6) class="macro">#define ORDER_REASON_CREDIT(class="num">7) class="type">long COrder::OrderPositionID(class="type">void) class="kw">const { class="macro">#ifdef __MQL4__ class="kw">return ::OrderMagicNumber(); class="macro">#else class=class="str">"cmt">//+------------------------------------------------------------------+
订单状态与来源在 MQL5 下的取法
在 MQL5 里,COrder 类把历史订单和当前订单的状态查询拆成了两套接口。若订单处于历史挂单或历史市价单状态,要用 HistoryOrderGetInteger(m_ticket, ORDER_STATE) 取状态;若是当前市场的挂单或市价单,则走 OrderGetInteger(ORDER_STATE)。 下面这段是 OrderState() 在 MQL5 分支的核心逻辑,按内部状态枚举分流: long res=0; switch((ENUM_ORDER_STATUS)this.GetProperty(ORDER_PROP_STATUS)) { case ORDER_STATUS_HISTORY_PENDING : case ORDER_STATUS_HISTORY_ORDER : res=::HistoryOrderGetInteger(m_ticket,ORDER_STATE); break; case ORDER_STATUS_MARKET_ORDER : case ORDER_STATUS_MARKET_PENDING : res=::OrderGetInteger(ORDER_STATE); break; case ORDER_STATUS_MARKET_POSITION : case ORDER_STATUS_DEAL : default : res=0; break; } return res; 注意市场持仓和成交记录会落入 default 返回 0,说明这两类并不通过订单对象暴露 ORDER_STATE。 OrderReason() 在 MQL4 下靠余额、信用、SL/TP、魔术码反推来源;MQL5 分支原文在此截断,实际应直接读 ORDER_PROP_REASON 属性。开 MT5 新建脚本打印 HistoryOrderGetInteger 与 OrderGetInteger 的返回值差异,能验证历史与实时订单的状态字段并非同源。
class="type">long res=class="num">0; class="kw">switch((ENUM_ORDER_STATUS)this.GetProperty(ORDER_PROP_STATUS)) { case ORDER_STATUS_HISTORY_PENDING : case ORDER_STATUS_HISTORY_ORDER : res=::HistoryOrderGetInteger(m_ticket,ORDER_STATE); class="kw">break; case ORDER_STATUS_MARKET_ORDER : case ORDER_STATUS_MARKET_PENDING : res=::OrderGetInteger(ORDER_STATE); class="kw">break; case ORDER_STATUS_MARKET_POSITION : case ORDER_STATUS_DEAL : class="kw">default : res=class="num">0; class="kw">break; } class="kw">return res;
「挂单残量与平仓缘由的文本化」
在封装订单类时,有两个细节容易在回测日志里被忽略:一是历史挂单的未成交手数,二是平仓动作背后的触发原因。把这两类信息用代码直接暴露出来,能省掉大量肉眼比对成交回执的时间。 MQL4 环境下,若订单状态为历史挂单(ORDER_STATUS_HISTORY_PENDING),OrderVolumeCurrent 直接返回 ::OrderLots();否则返回 0,意味着非挂单历史记录没有‘未执行_volume’概念。切换到 MQL5 分支后该函数体为空,说明标准库在五版里改走交易对象的自有字段,迁移脚本时要留意这套差异。 GetReasonDescription 用一串三元表达式把俄语/英语双语文本映射到具体缘由:止损触发、止盈触发、EA 下单单、被取消、余额存取、信用增减。其中余额与信用项还嵌套了 Profit()>0 的判断——盈利侧显示‘入金/授信’,亏损侧显示‘出金/扣减’。 别把正态当圣经:这些文本只是给日志和人类读屏用的,真要做统计分析请直接读 Reason() 枚举值,字符串比对在跨语言 MT5 终端上可能漏判。 开 MT5 把这段塞进你自己的 COrder 派生类,跑一轮 EURUSD 回测,看历史选项卡里‘Due to StopLoss’出现次数是否和预期止损单总数一致,就能验证封装有没有漏掉分支。
class="type">class="kw">double COrder::OrderVolumeCurrent(class="type">void) class="kw">const { class="macro">#ifdef __MQL4__ class="kw">return(this.Status()==ORDER_STATUS_HISTORY_PENDING ? ::OrderLots() : class="num">0); class="macro">#else class="type">class="kw">string COrder::GetReasonDescription(class="kw">const class="type">long reason) class="kw">const { class="macro">#ifdef __MQL4__ class="kw">return ( this.IsCloseByStopLoss() ? TextByLanguage("Срабатывание StopLoss","Due to StopLoss") : this.IsCloseByTakeProfit() ? TextByLanguage("Срабатывание TakeProfit","Due to TakeProfit") : this.Reason()==ORDER_REASON_EXPERT ? TextByLanguage("Выставлен из mql4-программы","Placed from mql4 program") : this.Comment()=="cancelled" ? TextByLanguage("Отменён","Cancelled") : this.Reason()==ORDER_REASON_BALANCE ? ( this.Profit()>class="num">0 ? TextByLanguage("Пополнение баланса","Deposit of funds on the account balance") : TextByLanguage("Снятие средств с баланса","Withdrawal from the balance") ) : this.Reason()==ORDER_REASON_CREDIT ? ( this.Profit()>class="num">0 ? TextByLanguage("Начисление кредитных средств","Received credit funds") :
◍ 历史订单刷新里的 MQL4 分支处理
在跨 MQL4/MQL5 的订单历史集合类里,刷新逻辑用预编译宏做了硬切分。MQL4 下走 OrdersHistoryTotal 配合 OrderSelect 按位遍历,MQL5 则靠独立对象类接管,二者头文件引入也不同。
MQL4 分支中,Refresh 从 m_index_order 游标开始扫历史池,只处理 order_type < ORDER_TYPE_BUY_LIMIT(已平持仓)与 order_type > ORDER_TYPE_SELL_STOP(余额/信用操作)两类。前者塞进 m_list_all_orders 排序表,后者单独建 CHistoryBalance 对象。
注意那段被高亮的 #include "..\Objects\Orders\HistoryBalance.mqh" 仅在 __MQL4__ 定义时编译,MQL5 无此文件——直接抄这套 include 到 MT5 会报找不到文件。外汇与贵金属交易自带高杠杆风险,回测历史订单结构前先确认终端版本,避免编译期就卡死。
class="macro">#ifdef __MQL4__ class="macro">#include "..\Objects\Orders\HistoryBalance.mqh" class="macro">#endif 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">//--- Closed positions if(order_type<ORDER_TYPE_BUY_LIMIT) { CHistoryOrder *order=new CHistoryOrder(::OrderTicket()); if(order==NULL) class="kw">continue; if(!this.m_list_all_orders.InsertSort(order)) { ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Could not add order to the list")); class="kw">delete order; } } class=class="str">"cmt">//--- Balance/credit operations else if(order_type>ORDER_TYPE_SELL_STOP) { CHistoryBalance *order=new CHistoryBalance(::OrderTicket());
历史订单属性支持判断与列表插入逻辑
在 MQL5 历史订单封装里,往全量订单列表塞对象时先判空:若 new 出来的 CHistoryPending 或普通 order 为 NULL 就直接 continue,避免后续对空指针做 InsertSort 导致运行时崩。 InsertSort 失败的分支里会打印双语报错(俄/英“Could not add order to the list”)并 delete order 释放内存,这一步在高频刷新历史池时很关键,否则悬空指针会慢慢吃掉终端内存。 计算 delta_order = i - m_index_order 后,用 m_delta_order!=0 来置 m_is_trade_event 布尔位;这意味着只有订单索引真正偏移时才算交易事件,单纯重绘不触发。 SupportProperty 函数对 ORDER_PROP_PRICE_STOP_LIMIT 的处理有版本差异:MQL5 下仅当订单类型不在 BUY_STOP_LIMIT~SELL_STOP_LIMIT 区间才返回 false,而 MQL4 分支要求状态必须是 HISTORY_ORDER 才允许该属性,否则一律 false。开 MT5 把这段贴进 EA 调试,改一下 ORDER_TYPE 边界就能看到属性可用性变化。
if(order==NULL) class="kw">continue; if(!this.m_list_all_orders.InsertSort(order)) { ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Could not add order to the list")); class="kw">delete order; } } else { class=class="str">"cmt">//--- Removed pending orders CHistoryPending *order=new CHistoryPending(::OrderTicket()); if(order==NULL) class="kw">continue; if(!this.m_list_all_orders.InsertSort(order)) { ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Could not add order to the 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 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return &class="macro">#x27;true&class="macro">#x27; if an order supports the passed class="kw">property, otherwise, class="kw">return &class="macro">#x27;false&class="macro">#x27; | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CHistoryOrder::SupportProperty(ENUM_ORDER_PROP_DOUBLE class="kw">property) { if( class="macro">#ifdef __MQL5__ class="kw">property==ORDER_PROP_PROFIT || class="kw">property==ORDER_PROP_PROFIT_FULL || class="kw">property==ORDER_PROP_SWAP || class="kw">property==ORDER_PROP_COMMISSION || class="kw">property==ORDER_PROP_PRICE_CLOSE || ( class="kw">property==ORDER_PROP_PRICE_STOP_LIMIT && ( this.TypeOrder()<ORDER_TYPE_BUY_STOP_LIMIT || this.TypeOrder()>ORDER_TYPE_SELL_STOP_LIMIT ) ) class="macro">#else class="kw">property==ORDER_PROP_PRICE_STOP_LIMIT && this.Status()==ORDER_STATUS_HISTORY_ORDER class="macro">#endif ) class="kw">return false; class="kw">return true; }
「在 MT4 里跑通历史集合列表的验证」
把第三篇文章里用的测试 EA(原路径 \MQL5\Experts\TestDoEasy\Part03 下的 TestDoEasyPart03_1.mq5)直接塞进 \MQL4\Experts\TestDoEasy\Part09,改名 TestDoEasyPart09.mq4 即可编译。EA 本身逻辑没动,但 MQL4 没有 MQL5 的成交(deal)概念,原文里用条件编译把成交硬替换成余额操作——这种做法只适合内部函数库验证,正式发给用户的产品必须把语言差异藏干净。 启动前先在终端“帐户历史记录”右键选“所有历史记录”。MT4 给 EA 开放的历史深度完全取决于这里选的范围,选小了 list.Total() 可能直接返回 0。EA 只在 OnInit() 跑一次,所以改完设置列表后得重新加载才能看到刷新。 实测时我新开的 MT4 账户原本空仓,挂了卖单带止损止盈,离开一会回来被损掉、行情随后朝空头走——典型止损就被扫的尴尬。但正好留下一笔平仓,设置里选“在场订单”就能在日志里核对平仓搜索是否正常;再删几组挂单、选“挂单”就能列出已删除挂单。外汇和贵金属杠杆高,这类验证请在模拟账户先跑。 代码里 TYPE_ORDER_DEAL 分支(标红处)在 MQL4 实际不会命中,因为 ORDER_STATUS_DEAL 状态来自 MQL5 成交,MT4 里要改用余额操作状态判断。OnInit 中 history.Refresh() 后按时间区间取集合,循环里按 InpOrderType 分流打印,这就是你复制去改的最小骨架。
class=class="str">"cmt">//--- enums enum ENUM_TYPE_ORDERS { TYPE_ORDER_MARKET, class=class="str">"cmt">// Market orders TYPE_ORDER_PENDING, class=class="str">"cmt">// Pending orders TYPE_ORDER_DEAL class=class="str">"cmt">// Deals }; class=class="str">"cmt">//--- class="kw">input parameters class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- update history history.Refresh(); class=class="str">"cmt">//--- get the collection list within the date range CArrayObj* list=history.GetListByTime(InpTimeBegin,InpTimeEnd,SELECT_BY_TIME_CLOSE); if(list==NULL) { Print("Could not get collection list"); class="kw">return INIT_FAILED; } class="type">int total=list.Total(); for(class="type">int i=class="num">0;i<total;i++) { class=class="str">"cmt">//--- get the order from the list COrder* order=list.At(i); if(order==NULL) class="kw">continue; class=class="str">"cmt">//--- if this is a deal if(order.Status()==ORDER_STATUS_DEAL && InpOrderType==TYPE_ORDER_DEAL) order.Print(); class=class="str">"cmt">//--- if this is a historical market order if(order.Status()==ORDER_STATUS_HISTORY_ORDER && InpOrderType==TYPE_ORDER_MARKET) order.Print(); class=class="str">"cmt">//--- if this is a removed pending order if(order.Status()==ORDER_STATUS_HISTORY_PENDING && InpOrderType==TYPE_ORDER_PENDING) order.Print(); } class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestDoEasyPart03_1.mq4 | class=class="str">"cmt">//| Copyright class="num">2018, MetaQuotes Software Corp. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2018, MetaQuotes Software Corp." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- includes class="macro">#include <DoEasy\Collections\HistoryCollection.mqh> class=class="str">"cmt">//--- enums enum ENUM_TYPE_ORDERS {
◍ 按时间区间抽取历史订单并分类打印
这段逻辑用 CHistoryCollection 把账户历史按时间闭区间捞出来,再按订单状态分流打印,省去手动翻终端历史标签的麻烦。 枚举里区分了市价单、挂单,以及在 MQL5 环境下用 TYPE_ORDER_DEAL 代表成交、MQL4 回退到 TYPE_ORDER_BALANCE 指代余额与信用操作,编译期由 __MQL5__ 宏切换。 OnInit 中先 history.Refresh() 强制刷新,再用 GetListByTime(InpTimeBegin, InpTimeEnd, SELECT_BY_TIME_CLOSE) 取按平仓时间筛选的列表;若返回 NULL 直接 INIT_FAILED,说明区间参数或历史读取异常。 遍历时用 order.Status() 比对:ORDER_STATUS_DEAL 对应成交、ORDER_STATUS_HISTORY_ORDER 对应历史市价单、ORDER_STATUS_HISTORY_PENDING 对应已删除挂单,匹配 InpOrderType 才调用 order.Print()。 实盘接这段时把 InpTimeBegin 设成 D'2024.01.01',InpTimeEnd 留 END_TIME,能在 MT5 日志里直接看清该年所有市价单盈亏分布;外汇与贵金属杠杆高,历史统计仅作概率参考,不代表未来走向。
TYPE_ORDER_MARKET, class=class="str">"cmt">// Market orders TYPE_ORDER_PENDING, class=class="str">"cmt">// Pending orders class="macro">#ifdef __MQL5__ TYPE_ORDER_DEAL class=class="str">"cmt">// Deals class="macro">#else TYPE_ORDER_BALANCE class=class="str">"cmt">// Balance/Credit class="macro">#endif }; class=class="str">"cmt">//--- class="kw">input parameters class="kw">input ENUM_TYPE_ORDERS InpOrderType = TYPE_ORDER_MARKET; class=class="str">"cmt">// Show type: class="kw">input class="type">class="kw">datetime InpTimeBegin = class="num">0; class=class="str">"cmt">// Start date of required range class="kw">input class="type">class="kw">datetime InpTimeEnd = END_TIME; class=class="str">"cmt">// End date of required range class=class="str">"cmt">//--- global variables CHistoryCollection history; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- update history history.Refresh(); class=class="str">"cmt">//--- get the collection list within the date range CArrayObj* list=history.GetListByTime(InpTimeBegin,InpTimeEnd,SELECT_BY_TIME_CLOSE); if(list==NULL) { Print("Could not get collection list"); class="kw">return INIT_FAILED; } class="type">int total=list.Total(); for(class="type">int i=class="num">0;i<total;i++) { class=class="str">"cmt">//--- get the order from the list COrder* order=list.At(i); if(order==NULL) class="kw">continue; class=class="str">"cmt">//--- if this is a deal class="macro">#ifdef __MQL5__ if(order.Status()==ORDER_STATUS_DEAL && InpOrderType==TYPE_ORDER_DEAL) order.Print(); class="macro">#else class=class="str">"cmt">//--- if this is a balance/credit operation if(order.Status()==ORDER_STATUS_BALANCE && InpOrderType==TYPE_ORDER_BALANCE) order.Print(); class="macro">#endif class=class="str">"cmt">//--- if this is a historical market order if(order.Status()==ORDER_STATUS_HISTORY_ORDER && InpOrderType==TYPE_ORDER_MARKET) order.Print(); class=class="str">"cmt">//--- if this is a removed pending order if(order.Status()==ORDER_STATUS_HISTORY_PENDING && InpOrderType==TYPE_ORDER_PENDING) order.Print(); } class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- }
先给 EA 留个空壳 OnTick
MQL5 里任何自动交易逻辑的入口都绕不开 OnTick()。系统每来一个报价就调一次这个函数,你后面要挂的均线穿越、ATR 过滤全得塞进它的花括号里。 下面这段是 MT5 新建 EA 时最朴素的骨架:只有函数声明和一对空大括号,编译能通过,但什么都不做。它的价值在于让你先确认编译器环境没问题,再往里填策略。 外汇与贵金属杠杆高、滑点随机,空壳 EA 不会下单也就没有即时风险,但一旦你往里写持仓逻辑,就必须自己兜底平仓与风控。
class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+
「记住这一条就够了」
这套跨平台函数库目前只暴露一个入口类 CEngine,在 EA 里写 CEngine lib; 然后敲 lib. 就能拉出全部可用方法列表,绝大多数方法名自带语义,跟着前八篇的测试 EA 跑一遍就能摸清调用路径。
库文件分 MQL5.zip 与 MQL4.zip 两个包,体积均为 89.98 KB,评论区有人实测从旧构建到新构建没出现过强制修补断点,跨版本稳定性是它最实在的优势。
外汇和贵金属波动剧烈、杠杆风险高,直接把库搬进实盘前先在策略测试器用演示账户验证每处调用;下一篇才会补齐持仓与挂单激活的处理,现在缺的那块得自己用 CTrade 类先顶上。