内容目录
- 概论
- 约定
- MetaTrader 4
- MetaTrader 5, 净持
- MetaTrader 5, 对冲
- 交易标识符 (单号)
- 状态
- 交易量
- 订单容器
- 例程
- 扩展
- 结论
概论
MetaTrader 4 和 MetaTrader 5 在处理交易请求时使用不同的约定。本文讨论使用类对象来表达由服务器处理的交易的可能性, 目的是让跨平台智能交易程序可以无视交易平台版本和使用模式均可工作。
约定
MetaTrader 4 和 MetaTrader 5 的终端对于如何处理交易请求存在诸多不同。精简了交易服务器处理交易请求的细节, 我们只需要考虑两类交易平台的三种不同版本/模式: (1) MetaTrader 4, (2) MetaTrader 5, 净持模式, 和 (3) MetaTrader 5, 对冲模式。
MetaTrader 4
在 MetaTrader 4 中, 当智能交易程序成功发送一笔订单, 它会收到一个单号, 即那笔交易的数字识别符。当修改订单或平单时, 正常地使用同一单号直至离场。
当部分平仓时, 情形稍有点复杂。操作也是使用 OrderClose 函数来完成, 在请求里指定的手数要少于订单的总手数。当这笔交易请求发送后, 按照函数调用中指示的数量, 订单 (或单号) 以确定的手数平仓。剩余的手数将会作为与部分平仓同类型的一笔新订单继续持有。由于 OrderClose 函数仅返回布尔值, 没有比重新检查账户中当前活跃订单列表更快速的方式来获得新单号。注意, 不可能单独使用 OrderClose 函数来获取单号。函数返回一个布尔值, 而像 MQL4 的 OrderSend, 当业务成功后会返回一个有效的单号。
MetaTrader 5 (净持)
在 MetaTrader 5 中交易操作处理乍一看将会感觉十分复杂, 但管理比 MetaTrader 4 中更简单。至少在交易者 (非程序员) 一侧, 这应该是真实的。
在 MetaTrader 5 中省缺是净持模式。在此模式下, 由服务器处理过的订单结果会合并为单独一笔持仓。这笔特殊持仓的类型以及成交量, 会基于入场订单的类型和交易量随时间而变化。在程序员一侧, 这有一点复杂。不像在 MQL4 里只有一个订单的概念, 程序员不得不与交易中使用的三种不同类型的令牌打交道。下表展示出一些 MQL5 净持模式与 MQL4 大致等效部分之间的比较:
构件 | MQL5 (净持) | MQL4 (大致等效) |
---|---|---|
订单 | 交易请求 (挂单或市价) | 交易请求 (挂单或市价) |
成交 | 成交基于单笔订单完成 (市价单, 或执行的挂单) | 反映在终端里的单笔市价单 |
仓位 | 交易 (合并) | 在交易终端上的所有市价单之合 (适用订单类型) |
在 MQL5 中, 订单一旦被执行, 在客户端不可改变, 而在 MQL4 里, 当一笔订单未平仓时一些属性依然可以改变。这就是, 在前者中, 一笔订单只是一个简单的发送到服务器的交易请求。在后者上, 它可以用来表示交易请求, 以及这个请求的结果。在这个方面, MQL5 使得整个处理过程更复杂, 以便降低歧义, 可在交易请求和交易结果之间进行明显区分。在 MQL4 中一笔订单可依据不同配置入场和离场, 而在 MQL5 中所有交易可向后追踪到订单, 或触发它们的交易请求。
当发送一个交易请求, 只有两个反馈: 已处理或未处理。如果交易未被处理, 则意味着没有交易, 因为交易服务器出于某种原因 (通常是由于错误) 无法处理它。现在, 如果交易在 MQL5 中已被处理, 客户端和服务器之间达成一笔成交。在此情况下, 订单可被完全或部分执行。
MetaTrader 4 没有这个选项, 因为订单只能完全执行或不执行 (填充或放弃)。
在 MetaTrader 5 中此模式的一个显著缺点是不允许对冲。给定品种的仓位类型可以改变。例如, 给定品种有一笔 0.1 手的多头仓位, 一笔 1.0 手交易量的卖单入场将把此仓位转换为该品种的空头仓位, 交易量为 0.9 手。
MetaTrader 5 (对冲)
MetaTrader 5 中的对冲模式类似于 MetaTrader 4 中使用的约定。对冲模式允许一个品种有一个以上的持仓, 而非把所有已处理交易合并为一笔单一持仓。当交易服务器触发挂单, 或处理市价交易请求后生成一笔持仓。
构件 | MQL5 (净持) | MQL4 (大致等效) |
---|---|---|
订单 | 交易请求 (挂单或市价) | 交易请求 (挂单或市价) |
成交 | 成交基于单笔订单完成 | 市价单反映在终端里 |
仓位 | 由单个交易请求合并而来的交易 | 订单反映在终端里 |
为了让跨平台智能交易系统能够适应这些区别, 一个可能的解决方案是令智能交易系统保存每笔成交的细节。每次交易成功完成, 一份订单的详情拷贝将被保存在 COrder 类的对象里。以下代码显示其基类的声明:
class COrderBase : public CObject { protected: bool m_closed; bool m_suspend; long m_order_flags; int m_magic; double m_price; ulong m_ticket; ENUM_ORDER_TYPE m_type; double m_volume; double m_volume_initial; string m_symbol; public: COrderBase(void); ~COrderBase(void); //--- 取值与赋值 void IsClosed(const bool); bool IsClosed(void) const; void IsSuspended(const bool); bool IsSuspended(void) const; void Magic(const int); int Magic(void) const; void Price(const double); double Price(void) const; void OrderType(const ENUM_ORDER_TYPE); ENUM_ORDER_TYPE OrderType(void) const; void Symbol(const string); string Symbol(void) const; void Ticket(const ulong); ulong Ticket(void) const; void Volume(const double); double Volume(void) const; void VolumeInitial(const double); double VolumeInitial(void) const; //--- 输出 virtual string OrderTypeToString(void) const; //--- 静态方法 static bool IsOrderTypeLong(const ENUM_ORDER_TYPE); static bool IsOrderTypeShort(const ENUM_ORDER_TYPE); };
由于策略能够记住它自己的交易, 它可在运行各种约定的交易平台上更加独立地工作。它的缺点就是要依赖一个事实, 类的实例只能在智能交易系统操作期间维持。如果智能交易系统或交易平台需要重启动, 所有保存的数据将会丢失, 除非有一个中间值来保存和加载信息。
交易标识符 (单号)
使用 COrder 的实例创建跨平台兼容智能交易系统的可能障碍是如何保存订单 (或仓位) 的单号。区别总结在下表:
操作 | MQL4 | MQL5 (净持) | MQL5 (对冲) |
---|---|---|---|
发送订单 | 新订单号 | 新仓位号 (不存在仓位) 或已存在的仓位号 (当存在仓位时) | 新仓位号 |
部分平仓 | 新订单号 | 相同仓位号 (如果有剩余), 否则为空 | 相同仓位号 |
当发送一笔订单, 所有三个版本均有不同方式来表示已入场交易。在 MQL4 里, 当一个交易请求成功时, 将会开新单。此新单将由一个标识符表示 (订单号)。在 MQL5 净持模式里, 每个入场交易的请求标识为一个订单号。然而, 订单号也许并非代表已入场交易的最佳方式, 但结果是仓位本身。其原因是, 与在 MQL4 里不同, 已入场交易产生的结果订单号不能进一步直接用于操作 (但试图通过确定订单得到其结果仓位时订单号是很有用的)。此外, 当存在相同类型的持仓时, 仓位号将会保持相同 (与 MQL4 不同)。另一方面, 在 MQL5 对冲模式里, 每笔新成交生成一个新仓位 (大致等价于 MQL4 的订单号)。然而, 它与 MQL4 的区别在于, 一个单一交易请求总是得到一笔单一订单, 而在 MQL5 (对冲模式) 里, 有可能是一笔订单有一笔以上的成交 (当填充规则被设定为 SYMBOL_FILLING_FOK 以外的情况)。
当部分平单 (MQL4) 或平仓 (MQL5) 时, 还会有其它问题。如早前所言, 在 MQL4 中, 当特定单号以少于总交易量 (OrderLots) 平单时, 单号已平仓交易, 剩余交易量将会被分配一个新单号, 类型与部分平单的那一笔相同。在 MQL5 中略有不同。在净持模式里, 它请求一笔反向交易 (买则卖, 或卖则买), 以便平仓 (部分或全部)。在对冲模式里, 处理更类似于 MQL4 (OrderClose 对决 CTrade 的 PositionClose), 但不像在 MQL4 里, 部分平仓不会触发其所代表的标识符的任何变化。
解决这个问题的一种途径是, 在两个平台上, 分开实现交易标识符的表达逻辑。由于订单号在 MetaTrader 5 中不会被改变, 我们可以简单地为其分配一个典型的数字变量。另一方面, 对于 the MetaTrader 4 版本, 我们将使用一个 CArrayInt 的实例来保存单号。对于 COrderBase (以及 MQL5 COrder 版本的后续), Ticket 方法将使用以下代码:
COrderBase::Ticket(const ulong value) { m_ticket=value; }
在 MQL4 版本上此方法将用以下代码覆盖:
COrder::Ticket(const ulong ticket) { m_ticket_current.InsertSort((int)ticket); }
状态
跨平台智能交易程序中, 订单至少有两种可能的内部状态:
已平仓
待定
这两种状态非常相似, 但有一个关键的区别。一笔订单的已平仓状态表示此订单已平仓, 智能交易程序应在其内部数据里将此订单存档。在 MQL4 中, 这大致等价于将一笔订单移到历史数据。待定状态, 发生于智能交易程序平单失败, 或订单链接一个停止位时。在此情况下, 智能交易程序将会尝试再次平单 (及其停止项), 直到它完全平仓。
交易量
在 MQL4 中, 交易量的计算是直接的。当智能交易程序发送交易请求时, 交易量也包含在请求里, 它要么被拒绝要么被接受。这等价于 MQL5 中的完全或遗弃保证金规则, 这也是交易对象 (CTrade 和 CExpertTrade) 的默认设置。共同的功能为我们带来完全或遗弃保证金规则。为了在 MQL4 和 MQL5 之间保持交易量处理的一致性, 一种方法是基于交易请求的交易量派生出 COrder 类实例的交易量。然而, 这意味着对于 MQL5 版本, 我们应需要坚持完全或遗弃保证金规则。仍然可以使用其它保证金规则, 但结果会稍有不同 (即相同 EA 的测试, 在 MQL5 上 COrder 实例的计数可能会更大)。
订单容器
如果智能交易程序处理的 COrder 实例多于一个, 则需要一些组织方法。为此提供助力的是订单容器, 或 COrders。这个类扩展自 CArrayObj 并保存 COrder 实例。这将允许智能交易程序轻松保存并恢复已入场交易。用于所述类的基础模板如下所示:
#include <Arrays\ArrayObj.mqh> #include "OrderBase.mqh" class CExpertAdvisor; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class COrdersBase : public CArrayObj { public: COrdersBase(void); ~COrdersBase(void); virtual bool NewOrder(const ulong,const string,const int,const ENUM_ORDER_TYPE,const double,const double); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ COrdersBase::COrdersBase(void) { if(!IsSorted()) Sort(); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ COrdersBase::~COrdersBase(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool COrdersBase::NewOrder(const ulong ticket,const string symbol,const int magic,const ENUM_ORDER_TYPE type,const double volume,const double price) { COrder *order=new COrder(ticket,symbol,type,volume,price); if(CheckPointer(order)==POINTER_DYNAMIC) if(InsertSort(GetPointer(order))) order.Magic(magic); return false; } //+------------------------------------------------------------------+ #ifdef __MQL5__ #include "..\..\MQL5\Order\Orders.mqh" #else #include "..\..\MQL4\Order\Orders.mqh" #endif //+------------------------------------------------------------------+
因为其主要功能是保存 COrder 的实例, 它必须要有添加所述类的方法。CArrayObj 的省缺 Add 方法不见得就是理想的, 因为 COrder 的实例需要实例化。为此, 我们应有 NewOrder 方法, 它将已创建的 COrder 实例自动作为数组成员添加:
bool COrdersBase::NewOrder(const ulong ticket,const string symbol,const int magic,const ENUM_ORDER_TYPE type,const double volume,const double price) { COrder *order=new COrder(ticket,symbol,type,volume,price); if(CheckPointer(order)==POINTER_DYNAMIC) if(InsertSort(GetPointer(order))) order.Magic(magic); return false; }
现在基础模板已经完成, 其它方法也已添加到这个类中。一个 OnTick 方法的例程。在此方法中, 一个容器类应简单地迭代保存在其内的元素 (COrder)。另一种可能是 COrder 也有一个 OnTick 方法。如此, 即可编码, 以便每次瞬时报价时可以调用 COrders 中的这个方法。
例程
我们的例程代码尝试开一笔多头仓位。在入场开仓后, 交易的细节保存在 COrder 的实例中。这是通过调用 COrders 的 NewOrder 方法 (它将参与创建 COrder 的实例) 来达成的。
两个版本均使用交易对象的实例 (CExpertTradeX), 一笔订单对象 (COrders), 和品种对象 (CSymbolInfo)。在智能交易程序的 OnTick 处理器中, 交易对象将尝试使用它的 Buy 方法来开多头仓位。两个版本 (MQL4 和 MQL5) 之间仅有的区别是如何恢复交易的细节。对于 MQL5 版本, 细节通过使用 HistoryOrderSelect 和其它相关函数进行恢复。订单号则使用交易对象的 ResultOrder 方法进行恢复。此版本的实现如下所示:
ulong retcode=trade.ResultRetcode(); ulong order = trade.ResultOrder(); if(retcode==TRADE_RETCODE_DONE) { if(HistoryOrderSelect(order)) { ulong ticket=HistoryOrderGetInteger(order,ORDER_TICKET); ulong magic=HistoryOrderGetInteger(order,ORDER_MAGIC); string symbol = HistoryOrderGetString(order,ORDER_SYMBOL); double volume = HistoryOrderGetDouble(order,ORDER_VOLUME_INITIAL); double price=HistoryOrderGetDouble(order,ORDER_PRICE_OPEN); ENUM_ORDER_TYPE order_type=(ENUM_ORDER_TYPE)HistoryOrderGetInteger(order,ORDER_TYPE); orders.NewOrder((int)ticket,symbol,(int)magic,order_type,volume,price); } }
在 MQL4 中的交易对象比之 MQL5 版本功能较少。针对此版本可以扩展交易对象, 或是简单地迭代所欲账户里的活跃订单, 以便得到刚入场的交易:
for(int i=0;i<OrdersTotal();i++) { if(!OrderSelect(i,SELECT_BY_POS)) continue; if(OrderMagicNumber()==12345) orders.NewOrder(OrderTicket(),OrderSymbol(),OrderMagicNumber(),(ENUM_ORDER_TYPE)OrderType(),OrderLots(),OrderOpenPrice()); }
用于主要头文件的完整代码如下所示:
(test_orders.mqh)
#include <MQLx-Orders\Base\Trade\ExpertTradeXBase.mqh> #include <MQLx-Orders\Base\Order\OrdersBase.mqh> CExpertTradeX trade; COrders orders; CSymbolInfo symbolinfo; //+------------------------------------------------------------------+ //| 智能程序初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- if(!symbolinfo.Name(Symbol())) { Print("品种初始化失败"); return INIT_FAILED; } trade.SetSymbol(GetPointer(symbolinfo)); trade.SetExpertMagicNumber(12345); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| 智能程序逆初函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| 智能程序瞬时报价函数 | //+------------------------------------------------------------------+ void OnTick() { //--- if(!symbolinfo.RefreshRates()) { Print("品种不能刷新"); return; } if(trade.Buy(1.0,symbolinfo.Ask(),0,0)) { #ifdef __MQL5__ int retcode=trade.ResultRetCode(); ulong order = trade.ResultOrder(); if(retcode==TRADE_RETCODE_DONE) { if(HistoryOrderSelect(order)) { ulong ticket=HistoryOrderGetInteger(order,ORDER_TICKET);; ulong magic=HistoryOrderGetInteger(order,ORDER_MAGIC); string symbol = HistoryOrderGetString(order,ORDER_SYMBOL); double volume = HistoryOrderGetDouble(order,ORDER_VOLUME_INITIAL); double price=HistoryOrderGetDouble(order,ORDER_PRICE_OPEN); ENUM_ORDER_TYPE order_type=order_type; m_orders.NewOrder((int)ticket,symbol,(int)magic,order_type,volume,price); } } #else for(int i=0;i<OrdersTotal();i++) { if(!OrderSelect(i,SELECT_BY_POS)) continue; if(OrderMagicNumber()==12345) orders.NewOrder(OrderTicket(),OrderSymbol(),OrderMagicNumber(),(ENUM_ORDER_TYPE)OrderType(),OrderLots(),OrderOpenPrice()); } #endif } Sleep(5000); ExpertRemove(); } //+------------------------------------------------------------------+
头文件包含所有需要的代码。所以, 主要的源文件仅需要在 test_orders.mqh 里包含最少的预处理指令:
(test_orders.mq4 and test_orders.mq5)
#include "test_orders.mqh"
在平台上运行智能交易程序可给出以下日志条目:
在 MetaTrader 4 中, 将生成以下日志文件:
Expert test_orders EURUSD,H1: loaded successfully
test_orders EURUSD,H1: initialized
test_orders EURUSD,H1: open #358063536 buy 1.00 EURUSD at 1.12470 ok
test_orders EURUSD,H1: ExpertRemove function called
test_orders EURUSD,H1: uninit reason 0
Expert test_orders EURUSD,H1: removed
以下显示平台上执行的 EA 屏幕截图。注意, 由于 EA 调用 ExpertRemove 函数, 它会在执行其代码之后立即从图表上移除 (仅执行一次 OnTick 处理器)。
在 MetaTrader 5 中, 生成的日志文件几乎相同:
Experts expert test_orders (EURUSD,M1) loaded successfully
Trades '3681006': instant buy 1.00 EURUSD at 1.10669 (deviation: 10)
Trades '3681006': accepted instant buy 1.00 EURUSD at 1.10669 (deviation: 10)
Trades '3681006': deal #75334196 buy 1.00 EURUSD at 1.10669 done (based on order #90114599)
Trades '3681006': order #90114599 buy 1.00 / 1.00 EURUSD at 1.10669 done in 275 ms
Experts expert test_orders (EURUSD,M1) removed
不像在 MetaTrader 4 中, 如上显示的日志消息可在终端窗口的日志栏找到 (而非智能程序栏):
该 EA 还在智能程序栏中打印一条消息。然而, 消息并非关于交易的执行, 只是显示已调用 ExpertRemove 函数, 与 MetaTrader4 比较, 显示的是智能程序栏里的消息:
扩展
我们目前的实现缺乏某些现实世界中的智能交易程序会经常使用的功能, 诸如以下:
1. 交易入场时初始化止损和止盈值
2. 修改止损和止盈位 (即盈亏平衡, 尾随停止, 或任意自定方法)
3. 数据持久化 - 在两个平台上如何保存交易和其停止位是有区别的。我们的类对象在两个版本里保持一致, 但只能保存在内存中。因此, 我们应需要一种数据持久化的方法。也就是说, 我们的智能交易程序需要一种遇到某些事件时保存和加载订单信息的方法, 如终端重启, 或在 MetaTrader 中切换跨平台智能交易程序所加载的图表。
这些将在今后的文章中介绍。
结论
在本文中, 我们讨论了一种方法, 可令跨平台智能交易程序将交易服务器成功处理的交易请求细节保存为一个类对象的实例。这个对象实例随后可由智能交易程序基于特定策略, 针对其所代表的交易进行其它操作。已经为这个类对象提供了一个基础模板, 它可以进一步开发, 以便用于更复杂的交易策略。