轻松快捷开发 MetaTrader 程序的函数库(第 二十一部分):交易类 - 基准跨平台交易对象(基础篇)
从零搭一个跨平台交易基类
这一节拉开整个交易类系列的序幕:先在 MQL5 里写一个不依赖具体订单类型的基准交易对象,把下单、撤单、改仓这些动作先抽象成统一接口。后面所有品种(外汇、贵金属、差价合约)的具体交易类都会继承它,避免在每套 EA 里重复造轮子。 原文给出的背景数据是 2019 年 12 月 23 日发布,截至统计时帖文浏览 4890、评论 26,说明这类底层封装对社区是有真实需求的。外汇与贵金属杠杆交易风险极高,封装层只解决代码结构问题,不替代仓位与风控判断。 当前这一步只做概念与基类骨架,具体买卖触发逻辑放到后续小节。你可以现在打开 MT5 的 MetaEditor,新建一个空白类文件,准备承接下一节的接口定义。
◍ 统一交易管道先过验证关
想把 MT4 和 MT5 的下单逻辑揉成一个口子,第一步不是发单,而是先挡掉错误请求。无效请求一旦丢给服务器,只会白白加重负载,所以基准交易对象本身不带参数校验,真正的验证要留到后面开发的基准交易类里做。 EA 跟服务器之间本质是“请求-响应”会话。我们要分配好这条通信管道,让对象发请求前参数已经校正过,同时能正确处理服务器返回码。 有些场景要求“不惜代价开仓”:遇拒单可改参数重发、原样等待时机再发,或明知价格更差也照发。但必须考虑价位,避免劣价重复下单。 整个交易绑定具体品种,所以基准交易对象会挂进第14篇讲过的交易品种对象里;本文临时在 CEngine 基类(第3篇)里放品种对象访问,它聚齐了账户和品种属性,供交易类取数。
「把交易动作收拢进一个基准类」
做一套能直接调的交易封装,第一步是在函数库里立一个基准交易对象 CTradeObj。它不负责策略,只把 MQL5 里那些绕来绕去的请求结构、结果结构和默认参数先固化下来,后面开仓平仓挂单都从它派生。 核心是两个结构变量:MqlTradeRequest 类型的 m_request 用来填所有请求字段再丢给 OrderSend();MqlTradeResult 类型的 m_result 由服务器回写。只要请求返回不对,直接读 m_result.retcode 就能知道哪一步炸了,不用猜。 构造函数里给了一组默认值,实盘里大概率要动:魔幻数字 0、滑点 5 点、StopLimit 价格 0、过期时间 0(无限期)、异步发送关闭、填单规则 Fill or Kill、注释写程序名 + " by DoEasy"、记录级别仅出错。MQL5 下保证金计算方式用 AccountInfoInteger(ACCOUNT_MARGIN_MODE) 取,MQL4 直接写死对冲模式。 别把正态当圣经 默认参数只是省事用的。Init() 方法能重设交易量最小值(SymbolInfoDouble(SYMBOL_VOLUME_MIN))和过期模式标志(SymbolInfoInteger(SYMBOL_EXPIRATION_MODE)),真要发请求时传进方法的参数只一次性覆盖,不污染默认值。 开仓方法只接持仓类型、交易量、止损止盈、魔术码、滑点、注释,且假定参数已校验过——这版是为了快速跑通测试,不是生产级。MQL5 平仓本质是开反向单,所以 DELib 里写了 OrderTypeByPositionType() 把持仓方向翻成订单类型再填进请求。 基准对象直接挂进 CSymbol 里,品种对象一建就顺带 Init 好交易对象。填单规则和过期类型用两个方法做兼容:品种不支持就退回它允许的模式,永远不传非法值。手数常规化方法拿最小最大手数卡一道,超界就夹回边界,否则按 DigitsLot() 小数位规整。 外汇和贵金属杠杆高、点值跳动快,滑点 5 点这种硬编码在跳空行情可能直接拒单,上 MT5 前先按自己品种重设。临时测试入口塞在 CEngine 里,按品种名取 Symbol 再取 TradeObj 就能发请求,等完整交易类落地再拆掉。
挂单与交易对象的跨平台封装逻辑
在 MT5 环境里,放置 BuyStopLimit 这类复合挂单前,程序先按品名取品种对象,失败就报对应消息并返 false;再从品种对象取交易对象,同样失败即退出。随后把交易量、品种、BuyStop 触发价、触发后的 BuyLimit 价,以及可选的 SL/TP、魔幻数(默认 0)、注释(默认程序名 +“by DoEasy”)、期限与生存周期(默认直到明确取消)一并传给下单方法。 MQL4 下这套 BuyStopLimit 入口不做实际操作,直接返 true;对应的 SellStop、SellLimit、SellStopLimit 方法参数结构与此一致,只是方向反转。修改挂单方法靠订单票据调用 GetTradeObjByOrder() 拿交易对象,新价、SL、TP、StopLimit 价、期限与生存模式均默认“无更改”,删挂单也走同一票据取对象路径,取不到就返 false。 按持仓票据与按挂单票据取品种交易对象的两个函数逻辑几乎重合,差异仅在于前者拉持仓列表、后者拉挂单列表。填单规则(默认“填单或取消”)可批量设到品种集合,也可只针对单一品种。CEngine 里已备好临时辅助方法,令测试 EA 在 MQL4/MQL5 都不必写条件编译,调用形态保持一致——外汇与贵金属杠杆高,这类封装仅降低开发耦合,不预示任何胜率。 下面这段枚举与消息常量定义了交易日志级别和典型失败原因,开 MT5 把高亮项对照编译器报错,能快速定位是品种不在列表还是票据无对应持仓。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Data for working with trading classes | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Logging level | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_LOG_LEVEL { LOG_LEVEL_NO_MSG, class=class="str">"cmt">// Trading logging disabled LOG_LEVEL_ERROR_MSG, class=class="str">"cmt">// Only trading errors LOG_LEVEL_ALL_MSG class=class="str">"cmt">// Full logging }; class=class="str">"cmt">//+------------------------------------------------------------------+ MSG_LIB_SYS_NOT_SYMBOL_ON_SERVER, class=class="str">"cmt">// Error. No such symbol on server MSG_LIB_SYS_NOT_SYMBOL_ON_LIST, class=class="str">"cmt">// Error. No such symbol in the list of used symbols: MSG_LIB_SYS_FAILED_PUT_SYMBOL, class=class="str">"cmt">// Failed to place to market watch. Error: MSG_LIB_SYS_ERROR_NOT_POSITION, class=class="str">"cmt">// Error. Not a position: MSG_LIB_SYS_ERROR_NO_OPEN_POSITION_WITH_TICKET, class=class="str">"cmt">// Error. No open position with ticket # MSG_LIB_SYS_ERROR_NO_PLACED_ORDER_WITH_TICKET, class=class="str">"cmt">// Error. No placed order with ticket # MSG_LIB_SYS_ERROR_FAILED_CLOSE_POS, class=class="str">"cmt">// Failed to closed position. Error MSG_LIB_SYS_ERROR_FAILED_MODIFY_ORD, class=class="str">"cmt">// Failed to modify order. Error MSG_LIB_SYS_ERROR_UNABLE_PLACE_WITHOUT_TIME_SPEC, class=class="str">"cmt">// Error: Cannot place order without explicitly specified expiration time MSG_LIB_SYS_ERROR_FAILED_GET_TRADE_OBJ class=class="str">"cmt">// Error. Failed to get trading object
◍ 标准库报错枚举与多语消息映射
在 MT5 标准交易库里,系统级错误和引擎状态不是靠零散字符串硬写,而是先收进一组枚举常量,再和俄/英双语消息表做键值对应。上面这段截取的就是 CEngine 相关片段,能看到从对象获取到持仓列表读取全流程的失败分支。 像 MSG_LIB_SYS_ERROR_FAILED_GET_POS_OBJ 对应「Failed to get position object」,MSG_ENG_FAILED_GET_MARKET_POS_LIST 对应「Failed to get the list of open positions」,这类常量在库内部统一调度,避免调用方各自拼错提示语。 消息表后半段出现了带 #ticket 的动态报错,例如「Error. No open position with ticket #」和「Error. No placed order with ticket #」,说明底层在按订单号回查时若落空会精确抛出。复制下面代码到 MT5 的 include 段,编译后可在专家日志里直接比对实际报错与枚举名,省去猜字符串的功夫。 外汇与贵金属杠杆高,任何持仓/订单对象获取失败都可能让 EA 在极端波动中漏单,验证时建议在模拟盘先跑通。
MSG_LIB_SYS_ERROR_FAILED_GET_POS_OBJ, class=class="str">"cmt">// Error. Failed to get position object MSG_LIB_SYS_ERROR_FAILED_GET_ORD_OBJ, class=class="str">"cmt">// Error. Failed to get order object MSG_LIB_SYS_ERROR_FAILED_GET_SYM_OBJ, class=class="str">"cmt">// Error. Failed to get symbol object MSG_LIB_SYS_ERROR_CODE_OUT_OF_RANGE, class=class="str">"cmt">// Return code out of range of error codes MSG_LIB_TEXT_FAILED_ADD_TO_LIST, class=class="str">"cmt">// failed to add to list MSG_LIB_TEXT_TIME_UNTIL_THE_END_DAY, class=class="str">"cmt">// Order lifetime till the end of the current day to be used MSG_LIB_TEXT_SUNDAY, class=class="str">"cmt">// Sunday MSG_ACC_MARGIN_MODE_RETAIL_EXCHANGE, class=class="str">"cmt">// Exchange markets mode MSG_ACC_UNABLE_CLOSE_BY, class=class="str">"cmt">// Close by is available only on hedging accounts MSG_ACC_SAME_TYPE_CLOSE_BY, class=class="str">"cmt">// Error. Positions for close by are of the same type class=class="str">"cmt">//--- CEngine MSG_ENG_NO_TRADE_EVENTS, class=class="str">"cmt">// There have been no trade events since the last launch of EA MSG_ENG_FAILED_GET_LAST_TRADE_EVENT_DESCR, class=class="str">"cmt">// Failed to get description of the last trading event MSG_ENG_FAILED_GET_MARKET_POS_LIST, class=class="str">"cmt">// Failed to get the list of open positions MSG_ENG_FAILED_GET_PENDING_ORD_LIST, class=class="str">"cmt">// Failed to get the list of placed orders MSG_ENG_NO_OPEN_POSITIONS, class=class="str">"cmt">// No open positions MSG_ENG_NO_PLACED_ORDERS, class=class="str">"cmt">// No placed orders }; {"Ошибка. Такого символа нет на сервере","Error. No such symbol on server"}, {"Ошибка. Такого символа нет в списке используемых символов: ","Error. This symbol is not in the list of symbols used: "}, {"Не удалось поместить в обзор рынка. Ошибка: ","Failed to put in market watch. Error: "}, {"Ошибка. Не позиция: ","Error. Not position: "}, {"Ошибка. Нет открытой позиции с тикетом #","Error. No open position with ticket #"}, {"Ошибка. Нет установленного ордера с тикетом #","Error. No placed order with ticket #"}, {"Не удалось закрыть позицию. Ошибка ","Could not close position. Error "}, {"Не удалось модифицировать ордер. Ошибка ","Failed to modify order. Error "}, {"Ошибка: невозможно разместить ордер без явно заданного его времени истечения","Error: Unable to place order without explicitly specified expiration time"},
「头寸类型到订单类型的映射函数」
在标准交易类库里,把持仓方向翻译成下单指令是两个高频动作。库里用两个极短函数直接做三元映射,避免在主逻辑里反复写 if-else。 OrderTypeByPositionType 接收 ENUM_POSITION_TYPE,买仓返回 ORDER_TYPE_BUY,否则返回 ORDER_TYPE_SELL,即「同方向开仓」用的订单类型。 OrderTypeOppositeByPositionType 做反向映射:买仓返回 ORDER_TYPE_SELL,卖仓返回 ORDER_TYPE_BUY,这是做对冲平仓或反手时取反向单类型的入口。 顺带注意,前面那批多语言报错文本里明确写了:反向平仓(close by opposite)仅在 hedging 账户类型可用,且两端持仓不能同类型,否则返回 Error. Positions of the same type in counterclosure request。在实盘外接 EA 前,先确认账户类型再调这两个函数,能少踩一类运行时坑。外汇与贵金属杠杆交易风险高,这类账户限制可能导致策略在真实环境直接失效。
ENUM_ORDER_TYPE OrderTypeByPositionType(class="type">ENUM_POSITION_TYPE type_position) { class="kw">return(type_position==POSITION_TYPE_BUY ? ORDER_TYPE_BUY : ORDER_TYPE_SELL); } ENUM_ORDER_TYPE OrderTypeOppositeByPositionType(class="type">ENUM_POSITION_TYPE type_position) { class="kw">return(type_position==POSITION_TYPE_BUY ? ORDER_TYPE_SELL : ORDER_TYPE_BUY); }