为莫斯科交易所开发一个交易机器人从哪里开始呢?(基础篇)
在 MOEX 上跑自动策略的起步点
想把莫斯科交易所(MOEX)的手动交易逻辑变成机器人,第一道坎不是写指标,而是搞清楚 MQL5 在 MOEX 接入上提供了哪些现成能力。 MQL5 本身封装了较宽范围的交易函数,并额外给出易用的基础类,让算法交易起步成本明显下降。对 MOEX 用户来说,这些本地化封装意味着不必从零处理交易所协议层。
- 年 7 月 8 日那篇官方示例放出后,仅公开计数就有 6607 次查看、16 条讨论,说明俄语区 MOEX 自动化需求一直真实存在。外汇与贵金属品种波动剧烈、杠杆风险高,即便用现成类库也先在小账户验证。
◍ MOEX 上市价与限价订单的底层差异
在莫斯科交易所跑策略,先得弄清两类原生订单:市价和限价。市价单通过 MQL5 的 ORDER_TYPE_BUY / ORDER_TYPE_SELL 发出,进交易所后按当时最优对手价撮合;限价单对应 ORDER_TYPE_BUY_LIMIT / ORDER_TYPE_SELL_LIMIT,挂在交易所服务器上等反向价出现才成交。 市价单保执行不保价——只要有流动性就能成,但滑点可能很大;限价单保价不保执行,挂在那儿可能一直不吃单。外汇和贵金属本身高波动高杠杆,这种滑点风险在 MOEX 的证券衍生品上同样不能忽视。 其余看似丰富的订单类型(止损、止损限价、挂单的止盈止损)都不是交易所原生,而是 MetaTrader 5 服务器端算好后再拆成市价或限价单送进去。比如 ORDER_TYPE_BUY_STOP 触发后,证券类走市价买入,期货类却以最差通道阈值价走限价买入;止盈触发一律发限价单,止损在期货上也是限价单而非市价。 有个实操细节:仓位和挂单的止损获利水平存在 MT5 服务器,断线也能自动触发。开 MT5 连 MOEX 仿真账号,把一张 BUY_STOP 和一张 BUY_STOP_LIMIT 都挂上,观察触发后成交流向是市价还是限价,比看文档直观得多。
「MT5 里下单到底走了哪几步」
MT5 把交易动作拆得很干净:市价买卖、挂单、改单删单、平仓加仓反转,全走同一个出口——OrderSend()。函数把单子递给莫斯科交易所后立刻把控制权交还,此时订单状态只是 ORDER_STATE_PLACED,不等于成交;真填了还是拒了,得看市场,状态会变成 ORDER_STATE_FILLED、ORDER_STATE_PARTIAL 或 ORDER_STATE_REJECTED。 想抢速度就用 OrderSendAsync(),它不等单子进交易所系统,终端过完基础校验发到 MT5 服务器就返回。剩下排队的耗时、成交或拒绝的耗时,全看交易所忙不忙和你网络延迟,这块没法在代码里控。 真正干活前得先填好 MqlTradeRequest 结构,它描述这一次请求的所有字段。市价单直接 BUY/SELL;挂单分四类:BUY STOP / SELL STOP 是突破更差价位触发,留服务器上、触发后转市价(外汇证券)或限价(期货)发往交易所;BUY LIMIT / SELL LIMIT 是到更好价位立刻以限价单进交易所,还能指定内部点差掐滑点;BUY STOP LIMIT / SELL STOP LIMIT 则是触发后再挂限价,挂单水平可高于也可低于触发价。 外汇和贵金属波动大、杠杆高,挂单触发后能否按预期价位成交仍有滑点风险,实盘前建议在策略测试器用历史数据跑一遍不同网络模拟。
交易所品种下单与事务监听的同一套写法
MT5 上的 MQL5 把交易功能做成了统一接口:期货、证券、期权、债券都用同一组交易函数处理订单、仓位和请求,资产类别差异被收敛到合约规格里,不用为不同交易所重写逻辑。 发单走 OrderSend() 或 OrderSendAsync(),前者同步等结果、后者丢给服务器异步跑;执行结果、历史、交易时段都能在代码里直接取。想在莫斯科交易所这类环境看清一笔单子从发单到成交在服务器端经历了什么,挂个 TradeTransaction 监听器最直观。 下面这段 EA 只做一件事:初始化时打印最后一次终端 PING 的毫秒数(TerminalInfoInteger(TERMINAL_PING_LAST)/1000 得到秒级值),随后在 OnTradeTransaction 里用 GetTickCount() 判 1 秒窗口,把同一交易操作触发的多次回调归到一个计数器里,避免把一次成交拆成几笔误读。 开 MT5 新建 EA 把代码贴进去,跑在真实账户上做一次手动开仓,就能在日志里看到 PING 数值和事务回调计数——外汇贵金属杠杆高、滑点跳空频繁,这类监听对排查“单子发了没成交”很有用,但任何监听都不改变订单在服务器端被拒或延迟的概率。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TradeTransactionListener.mq5 | class=class="str">"cmt">//| Copyright class="num">2016, MetaQuotes Software Corp. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2016, 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">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- PrintFormat("最后一次 PING=%.f ms", TerminalInfoInteger(TERMINAL_PING_LAST)/class="num">1000.); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TradeTransaction function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTradeTransaction(class="kw">const MqlTradeTransaction &trans, class="kw">const class="type">MqlTradeRequest &request, class="kw">const class="type">MqlTradeResult &result) { class=class="str">"cmt">//--- class="kw">static class="type">int counter=class="num">0; class=class="str">"cmt">// OnTradeTransaction() 调用计数器 class="kw">static class="type">uint lasttime=class="num">0; class=class="str">"cmt">// 最后调用 OnTradeTransaction() 的时间 class=class="str">"cmt">//--- class="type">uint time=GetTickCount(); class=class="str">"cmt">//--- 如果最后一次事务是在1秒钟之前进行的, if(time-lasttime>class="num">1000) { counter=class="num">0; class=class="str">"cmt">// 那么这就是一个新的交易操作,计数器要被重置 if(IS_DEBUG_MODE) Print(" 新的交易操作"); } lasttime=time; counter++;
◍ 用 OnTradeTransaction 抓取每笔成交细节
在 MT5 的 EA 里挂上 OnTradeTransaction 回调,就能实时拿到 MqlTradeTransaction 结构里的字段,不必轮询历史。下面这段 switch 把仓位修改、请求发送、成交新增、历史订单落地四类事件分开打印,省得日志糊成一团。 注意 TRADE_TRANSACTION_DEAL_ADD 分支里调了 HistoryDealSelect(lastDealID) 再去取 DEAL_EXTERNAL_ID,这是莫斯科交易所回传的内部编号;若跑在普通外汇symbol上该字段通常为空串,Exchange_ticket 就不会附加。 外汇和贵金属杠杆高、滑点随机,这类事件日志只帮你复盘执行流,不预示任何盈亏方向。把下面代码直接塞进 EA 的 trade transaction 处理函数,开模拟盘跑几单,看终端里打印出来的 deal ID 与 volume 是否和交易面板对得上。
Print(counter,". ",__FUNCTION__); class=class="str">"cmt">//--- 交易请求的执行结果 class="type">class="kw">ulong lastOrderID =trans.order; ENUM_ORDER_TYPE lastOrderType =trans.order_type; ENUM_ORDER_STATE lastOrderState=trans.order_state; class=class="str">"cmt">//--- 进行交易的交易品种名称 class="type">class="kw">string trans_symbol=trans.symbol; class=class="str">"cmt">//--- 交易的类型 ENUM_TRADE_TRANSACTION_TYPE trans_type=trans.type; class="kw">switch(trans.type) { case TRADE_TRANSACTION_POSITION: class=class="str">"cmt">// 修改仓位 { class="type">class="kw">ulong pos_ID=trans.position; PrintFormat("MqlTradeTransaction: 修改了仓位 #%I64u %s: SL=%.5f TP=%.5f", pos_ID,trans_symbol,trans.price_sl,trans.price_tp); } class="kw">break; case TRADE_TRANSACTION_REQUEST: class=class="str">"cmt">// 发送交易请求 PrintFormat("MqlTradeTransaction: TRADE_TRANSACTION_REQUEST"); class="kw">break; case TRADE_TRANSACTION_DEAL_ADD: class=class="str">"cmt">// 增加一项交易 { class="type">class="kw">ulong lastDealID =trans.deal; ENUM_DEAL_TYPE lastDealType =trans.deal_type; class="type">class="kw">double lastDealVolume=trans.volume; class=class="str">"cmt">//--- 内部系统的交易 ID - 莫斯科交易所赋予的编号 class="type">class="kw">string Exchange_ticket=""; if(HistoryDealSelect(lastDealID)) Exchange_ticket=HistoryDealGetString(lastDealID,DEAL_EXTERNAL_ID); if(Exchange_ticket!="") Exchange_ticket=StringFormat("(MOEX 交易=%s)",Exchange_ticket); PrintFormat("MqlTradeTransaction: %s 交易 #%I64u %s %s %.2f lot %s",EnumToString(trans_type), lastDealID,EnumToString(lastDealType),trans_symbol,lastDealVolume,Exchange_ticket); } class="kw">break; case TRADE_TRANSACTION_HISTORY_ADD: class=class="str">"cmt">// 向历史中增加一个订单 { class=class="str">"cmt">//--- 内部系统的订单 ID - 莫斯科交易所赋予的一个编号 class="type">class="kw">string Exchange_ticket=""; if(lastOrderState==ORDER_STATE_FILLED) { if(HistoryOrderSelect(lastOrderID)) Exchange_ticket=HistoryOrderGetString(lastOrderID,ORDER_EXTERNAL_ID); if(Exchange_ticket!="") Exchange_ticket=StringFormat("(MOEX 订单编号=%s)",Exchange_ticket); }
「把成交回执和交易所编号打进日志」
在 OnTradeTransaction 的 default 分支里,除了打印交易事务类型,还要把莫斯科交易所赋予的外部订单号捞出来。当 lastOrderState 等于 ORDER_STATE_PLACED 且 OrderSelect 成功时,用 OrderGetString(ORDER_EXTERNAL_ID) 取到 Exchange_ticket,再拼成「MOEX 订单编号=xxx」写进日志,方便后续对账。 交易发单后别只盯 deal,result.order 和 result.retcode 才是 MT5 给你的落地凭证。下面这段把 orderID_result 和 retcode_result 分别取出,若订单号非 0 就 PrintFormat 输出,肉眼一眼能看出哪单卡在 requote、哪单走了 done。 返回码本身是数字,直接看 10009 不如看字符串。GetRetcodeID 用 switch 把 10004 映射到 TRADE_RETCODE_REQUOTE、10009 映射到 TRADE_RETCODE_DONE,覆盖 reject、cancel、placed、done_partial、error 等至少 8 种状态。拷到 MT5 的 include 里就能直接调用,外汇与贵金属杠杆高,回执异常要即时排查。
PrintFormat("MqlTradeTransaction: %s 订单 #%I64u %s %s %s",EnumToString(trans_type), lastOrderID,EnumToString(lastOrderType),trans_symbol,EnumToString(lastOrderState),Exchange_ticket); } class="kw">break; class="kw">default: class=class="str">"cmt">// 其它事务 { class=class="str">"cmt">//--- 内部系统的订单 ID - 莫斯科交易所赋予的一个编号 class="type">class="kw">string Exchange_ticket=""; if(lastOrderState==ORDER_STATE_PLACED) { if(OrderSelect(lastOrderID)) Exchange_ticket=OrderGetString(ORDER_EXTERNAL_ID); if(Exchange_ticket!="") Exchange_ticket=StringFormat("MOEX 订单编号=%s",Exchange_ticket); } PrintFormat("MqlTradeTransaction: %s 订单 #%I64u %s %s %s",EnumToString(trans_type), lastOrderID,EnumToString(lastOrderType),EnumToString(lastOrderState),Exchange_ticket); } class="kw">break; } class=class="str">"cmt">//--- 订单编号 class="type">class="kw">ulong orderID_result=result.order; class="type">class="kw">string retcode_result=GetRetcodeID(result.retcode); if(orderID_result!=class="num">0) PrintFormat("class="type">MqlTradeResult: 订单 #%d 返回代码=%s ",orderID_result,retcode_result); class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 把返回的数字代码转为字符串型的描述 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string GetRetcodeID(class="type">int retcode) { class="kw">switch(retcode) { case class="num">10004: class="kw">return("TRADE_RETCODE_REQUOTE"); class="kw">break; case class="num">10006: class="kw">return("TRADE_RETCODE_REJECT"); class="kw">break; case class="num">10007: class="kw">return("TRADE_RETCODE_CANCEL"); class="kw">break; case class="num">10008: class="kw">return("TRADE_RETCODE_PLACED"); class="kw">break; case class="num">10009: class="kw">return("TRADE_RETCODE_DONE"); class="kw">break; case class="num">10010: class="kw">return("TRADE_RETCODE_DONE_PARTIAL"); class="kw">break; case class="num">10011: class="kw">return("TRADE_RETCODE_ERROR"); class="kw">break;