为莫斯科交易所开发一个交易机器人从哪里开始呢?·进阶篇
🤖

为莫斯科交易所开发一个交易机器人从哪里开始呢?·进阶篇

(2/3)· 市价与限价订单在莫斯科交易所的真实执行差异,多数人写第一行下单代码前都没理清

含代码示例 第 2/3 篇
不少人在 MOEX 上跑 EA,把止损挂单当成市价单直接发,结果期货品种触发后价格滑到最差通道阈值才成交。交易所只认市价和限价两类原生单,其余都是终端先算再转发的,搞错保存位置就会误判成交逻辑。

「交易回码 10012 到 10032 的硬含义」

在 MT5 的 OrderSend / OrderCheck 返回体系中,10012–10032 这一段属于「请求被拒或异常中断」的细分回码,和前段的成功码完全不同逻辑。下面这段 switch-case 把每个数字映射成可读字符串,直接贴进 EA 的报错解析函数就能用。 这些回码里,10020(PRICE_CHANGED)和 10021(PRICE_OFF)在剥头皮策略中最常撞到——报价在发送与成交间跳了点,服务器拒单概率偏高。10019(NO_MONEY)和 10024(TOO_MANY_REQUESTS)则多在重仓或高频场景下触发,外汇与贵金属杠杆高,前者直接关联爆仓风险。 逐行看这段映射:case 10012 对应超时,常见于网络抖动;10014 是手数非法,比如小于最小合约单位;10016 代表止损止盈位置不合规,例如止损太贴近市价被经纪商拒。10018 说明市场休市,黄金在周末就稳定走这个码。 把回码转成中文描述后,你能在日志里一眼定位拒单原因,而不是对着数字翻文档。建议开 MT5 用脚本循环发一笔非法 order 验证下 10014 的触发路径。

MQL5 / C++
case class="num">10012: class="kw">return("TRADE_RETCODE_TIMEOUT"); class="kw">break;
case class="num">10013: class="kw">return("TRADE_RETCODE_INVALID"); class="kw">break;
case class="num">10014: class="kw">return("TRADE_RETCODE_INVALID_VOLUME"); class="kw">break;
case class="num">10015: class="kw">return("TRADE_RETCODE_INVALID_PRICE"); class="kw">break;
case class="num">10016: class="kw">return("TRADE_RETCODE_INVALID_STOPS"); class="kw">break;
case class="num">10017: class="kw">return("TRADE_RETCODE_TRADE_DISABLED"); class="kw">break;
case class="num">10018: class="kw">return("TRADE_RETCODE_MARKET_CLOSED"); class="kw">break;
case class="num">10019: class="kw">return("TRADE_RETCODE_NO_MONEY"); class="kw">break;
case class="num">10020: class="kw">return("TRADE_RETCODE_PRICE_CHANGED"); class="kw">break;
case class="num">10021: class="kw">return("TRADE_RETCODE_PRICE_OFF"); class="kw">break;
case class="num">10022: class="kw">return("TRADE_RETCODE_INVALID_EXPIRATION"); class="kw">break;
case class="num">10023: class="kw">return("TRADE_RETCODE_ORDER_CHANGED"); class="kw">break;
case class="num">10024: class="kw">return("TRADE_RETCODE_TOO_MANY_REQUESTS"); class="kw">break;
case class="num">10025: class="kw">return("TRADE_RETCODE_NO_CHANGES"); class="kw">break;
case class="num">10026: class="kw">return("TRADE_RETCODE_SERVER_DISABLES_AT"); class="kw">break;
case class="num">10027: class="kw">return("TRADE_RETCODE_CLIENT_DISABLES_AT"); class="kw">break;
case class="num">10028: class="kw">return("TRADE_RETCODE_LOCKED"); class="kw">break;
case class="num">10029: class="kw">return("TRADE_RETCODE_FROZEN"); class="kw">break;
case class="num">10030: class="kw">return("TRADE_RETCODE_INVALID_FILL"); class="kw">break;
case class="num">10031: class="kw">return("TRADE_RETCODE_CONNECTION"); class="kw">break;
case class="num">10032: class="kw">return("TRADE_RETCODE_ONLY_REAL"); class="kw">break;

◍ 从回码到成交:一次买单调用的五段事务日志

在 MT5 的 OnTradeTransaction 回调里,订单生命周期不是一步到位,而是被拆成多次事务推送。下面这段 switch 把交易返回码做了可读化映射,方便在日志里直接看出失败原因而不是一堆数字。 case 10033: return("TRADE_RETCODE_LIMIT_ORDERS"); break; case 10034: return("TRADE_RETCODE_LIMIT_VOLUME"); break; case 10035: return("TRADE_RETCODE_INVALID_ORDER"); break; case 10036: return("TRADE_RETCODE_POSITION_CLOSED"); break; default: return("TRADE_RETCODE_UNKNOWN="+IntegerToString(retcode)); break; 逐行看:10033 是挂单总数超限,经纪商对账户活跃挂单量有硬上限;10034 是单笔或总成交量超出限制;10035 是订单结构本身非法,比如价格偏离市价过远;10036 表示该持仓已被平掉,无法再操作;default 分支把未枚举的码转成字符串,避免静默吞错。 实盘日志里能看清时间线:2016.06.09 14:51:19.763 最后一次 PING=14 ms,5 秒后买入触发。14:51:24.856 起连续 5 次 OnTradeTransaction 被调用——先是 ORDER_ADD 状态 STARTED,随后 REQUEST 返回 TRADE_RETCODE_PLACED,接着两次 ORDER_UPDATE 从 REQUEST_ADD 走到 PLACED。 外汇与贵金属杠杆高,这类事务回码映射能帮你秒级定位拒单类型;开 MT5 把这段代码塞进 EA 的回码打印函数,跑一单就能对照出自己经纪商的边界值。

MQL5 / C++
case class="num">10033: class="kw">return("TRADE_RETCODE_LIMIT_ORDERS"); class="kw">break;
case class="num">10034: class="kw">return("TRADE_RETCODE_LIMIT_VOLUME"); class="kw">break;
case class="num">10035: class="kw">return("TRADE_RETCODE_INVALID_ORDER"); class="kw">break;
case class="num">10036: class="kw">return("TRADE_RETCODE_POSITION_CLOSED"); class="kw">break;
class="kw">default:
  class="kw">return("TRADE_RETCODE_UNKNOWN="+IntegerToString(retcode));
  class="kw">break;

从日志看清一笔买单的成交与平仓链路

下面这段真实回放来自 Si-6.16 合约的 M15 图表,时间停在 2016.06.09 14:51:24。先看到订单 #49118594 以 ORDER_TYPE_BUY 挂出后被删除(STATE_PLACED),紧接着同一毫秒内以 ORDER_STATE_FILLED 写入历史,MOEX 订单编号 3377179723,说明撤单与重发在同一次事务里完成。 真正成交的 Deal 是 #6945344,DEAL_TYPE_BUY,手数 1.00,MOEX 交易号 185290434。注意从订单填充到 DEAL_ADD 只隔了 1 毫秒,这种时序在 MOEX 品种上常见,但在外汇 Symbol 上往往会被 broker 延迟放大到几十毫秒。

  • 秒后(14:51:50)出现 TRADE_TRANSACTION_REQUEST,仓位 #0 的 SL 被改成 62000.00000、TP 改成 67000.00000。也就是开仓后程序立刻补了止损止盈,没有裸奔。

又过 33 秒发起平仓:ORDER_TYPE_SELL 订单 #49118750 从 STATE_STARTED 走到 TRADE_RETCODE_PLACED,回码确认已挂出。外汇与贵金属的高杠杆意味着这类毫秒级链路一旦在实盘跑,滑点可能让 SL/TP 实际成交价偏离设置值,务必在策略测试器里用真实点差复验。 开 MT5 把这段日志贴进 Experts 标签对照看,重点核对 ORDER_STATE_FILLED 与 DEAL_ADD 的时间差,就能判断你的 broker 成交通道是不是也这么利落。

「从日志看清卖单的成交链路」

在 MT5 里挂一个 OnTradeTransaction 监听,能把每笔订单从发单到成交的完整状态流打印出来。上面这段日志来自 2016.06.09 14:52:24 的 Si-6.16(莫斯科交易所白银合约)M15 图表,卖单 #49118750 在 4 毫秒内走完了请求、挂单、成交、删除、入史五个状态。 具体看时间戮:24.067 先收到 ORDER_STATE_REQUEST_ADD,24.071 变成 ORDER_STATE_PLACED,24.073 冒出 DEAL_TYPE_SELL 成交 1.00 lot(MOEX deal=185290646),24.075 订单被 DELETE,24.077 进入 HISTORY_ADD 且标记 ORDER_STATE_FILLED(MOEX ticket=3377182821)。这种微秒级序列说明券商撮合回执和终端事件并非同一帧到达。 做外汇或贵金属时,同样机制可用于捕捉滑点:若 PLACED 到 DEAL_ADD 的间隔明显大于这几毫秒,可能暗示流动性薄层拒单或重新询价。高风险品种在数据行情时该间隔偶尔拉长到几十毫秒,值得在 EA 里单独计数告警。

MQL5 / C++
class="num">2016.06.class="num">09 class="num">14:class="num">52:class="num">24.067 TradeTransactionListener(Si-class="num">6.16,M15)  class="num">3. OnTradeTransaction
class="num">2016.06.class="num">09 class="num">14:class="num">52:class="num">24.067 TradeTransactionListener(Si-class="num">6.16,M15)  MqlTradeTransaction: TRADE_TRANSACTION_ORDER_UPDATE 订单 #class="num">49118750 ORDER_TYPE_SELL ORDER_STATE_REQUEST_ADD
class="num">2016.06.class="num">09 class="num">14:class="num">52:class="num">24.071 TradeTransactionListener(Si-class="num">6.16,M15)  class="num">4. OnTradeTransaction
class="num">2016.06.class="num">09 class="num">14:class="num">52:class="num">24.071 TradeTransactionListener(Si-class="num">6.16,M15)  MqlTradeTransaction: TRADE_TRANSACTION_ORDER_UPDATE 订单 #class="num">49118750 ORDER_TYPE_SELL ORDER_STATE_PLACED
class="num">2016.06.class="num">09 class="num">14:class="num">52:class="num">24.073 TradeTransactionListener(Si-class="num">6.16,M15)  class="num">5. OnTradeTransaction
class="num">2016.06.class="num">09 class="num">14:class="num">52:class="num">24.073 TradeTransactionListener(Si-class="num">6.16,M15)  MqlTradeTransaction: TRADE_TRANSACTION_DEAL_ADD 交易 #class="num">6945378 DEAL_TYPE_SELL Si-class="num">6.16 class="num">1.00 lot(MOEX deal=class="num">185290646)
class="num">2016.06.class="num">09 class="num">14:class="num">52:class="num">24.075 TradeTransactionListener(Si-class="num">6.16,M15)  class="num">6. OnTradeTransaction
class="num">2016.06.class="num">09 class="num">14:class="num">52:class="num">24.075 TradeTransactionListener(Si-class="num">6.16,M15)  MqlTradeTransaction: TRADE_TRANSACTION_ORDER_DELETE 订单 #class="num">49118750 ORDER_TYPE_SELL ORDER_STATE_PLACED
class="num">2016.06.class="num">09 class="num">14:class="num">52:class="num">24.077 TradeTransactionListener(Si-class="num">6.16,M15)  class="num">7. OnTradeTransaction
class="num">2016.06.class="num">09 class="num">14:class="num">52:class="num">24.077 TradeTransactionListener(Si-class="num">6.16,M15)  MqlTradeTransaction: TRADE_TRANSACTION_HISTORY_ADD 订单 #class="num">49118750 ORDER_TYPE_SELL Si-class="num">6.16 ORDER_STATE_FILLED(MOEX ticket=class="num">3377182821)

◍ 用 CAccountInfo 把账户底牌读出来

想在 EA 启动阶段就掌握当前账户的运行环境,直接引用 Trade\AccountInfo.mqh 里的 CAccountInfo 类最省事。声明一个 account 对象后,登录号、币别、余额、净值、保证金模式这些字段都能在 OnInit 或 OnStart 里一行取出来,省得后面调试时还要手动去终端看。 下面这段脚本在 MT5 里建个脚本跑一次,就能把账户关键信息打印到 Experts 日志。外汇与贵金属杠杆高,账户若用投资者密码连入,TradeAllowed() 会返回 false,EA 根本下不了单,这种坑靠日志一眼就能排掉。 把输出结果和终端『账户历史』对照,你能确认 LimitOrders 是否为 0(0 表示券商未限制挂单数量),以及 MarginModeDescription 显示的是哪种保证金算法。不同券商的保证金模式差异,会直接影响你仓位计算的假设。

MQL5 / C++
class="macro">#include <Trade\AccountInfo.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- 用于操作账户的对象
CAccountInfo account;
class=class="str">"cmt">//--- 取得EA所运行的账户的编号
   class="type">long login=account.Login();
   Print("登录=",login);
class=class="str">"cmt">//--- 打印账户币别   
   Print("账户币别: ",account.Currency());  
class=class="str">"cmt">//--- 打印账户的余额和当前的利润
   Print("余额=",account.Balance(),"  利润=",account.Profit(),"  净值=",account.Equity());
class=class="str">"cmt">//--- 打印账户类型   
   Print("账户类型: ",account.TradeModeDescription());
class=class="str">"cmt">//--- 看帐户是否允许交易
   if(account.TradeAllowed())
      Print("允许在此账户交易。");
   else
      Print("不允许在此账户上交易: 也许是使用投资者密码连接中。");
class=class="str">"cmt">//--- 保证金计算模式
   Print("保证金计算模式: ",account.MarginModeDescription());
class=class="str">"cmt">//--- 检查是否允许在此账户上运行EA交易
   if(account.TradeExpert())
      Print("允许在此账户上自动交易。");
   else
      Print("不允许在此账户上使用EA交易或者脚本程序。");
class=class="str">"cmt">//--- 是否指定了允许的最大订单数量
   class="type">int orders_limit=account.LimitOrders();
   if(orders_limit!=class="num">0)Print("允许的最大挂单数量: ",orders_limit);
class=class="str">"cmt">//--- 打印公司名称和服务器的名称
   Print(account.Company(),": 服务器 ",account.Server());
   Print(__FUNCTION__,"  完毕。"); class=class="str">"cmt">//---  
  }

用 CSymbolInfo 抓取品种交易属性

账户信息到手后,真正要下单还得先摸清交易标的本身的规则。MQL5 里直接挂一个 CSymbolInfo 对象就能把当前品种的报价、点值、合约参数和交易限制一次性拉出来,不用自己逐个调终端接口。 上面这段脚本在莫斯科 FORTS 的 Si-6.16 期货品种上跑过,能打印出买卖价、小数位数、点数、交易模式、执行模式、合约大小、初始保证金以及最小/最大/步进手数。外汇和贵金属市场里这些字段差异极大,比如 XAUUSD 的 Point 通常是 0.01,而某些交叉盘 Digits 可能是 3 或 5,不先读准就写手数逻辑很容易报无效交易量错误,这类品种杠杆高、波动猛,参数误用会放大穿仓风险。 实盘前建议把脚本里的 _Symbol 换成你要盯的标的,在 MT5 策略测试器或脚本环境跑一遍,重点看 TradeMode 返回的是否为 SYMBOL_TRADE_MODE_FULL,以及 LotsStep 是否和你EA的下单函数匹配。

MQL5 / C++
class="macro">#include<Trade\SymbolInfo.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- 用于接收交易品种属性的对象
CSymbolInfo symbol_info;
class=class="str">"cmt">//--- 设置我们需要取得信息的交易品种名称
   symbol_info.Name(_Symbol);
class=class="str">"cmt">//--- 取得当前报价并打印
   symbol_info.RefreshRates();
   Print(symbol_info.Name()," (",symbol_info.Description(),")",
         "  卖出价=",symbol_info.Bid(),"  买入价=",symbol_info.Ask());
class=class="str">"cmt">//--- 取得小数点位数以及点数大小
   Print("小数位数=",symbol_info.Digits(),
         ", 1点=",DoubleToString(symbol_info.Point(),symbol_info.Digits()));
class=class="str">"cmt">//--- 请求订单执行类型,检查限制
   Print("交易操作的限制: ",EnumToString(symbol_info.TradeMode()),
         " (",symbol_info.TradeModeDescription(),")");
class=class="str">"cmt">//--- 检查交易模式
   Print("交易执行模式: ",EnumToString(symbol_info.TradeExecution()),
         " (",symbol_info.TradeExecutionDescription(),")");
class=class="str">"cmt">//--- 检查合约计算方式
   Print("计算合约值: ",EnumToString(symbol_info.TradeCalcMode()),
         " (",symbol_info.TradeCalcModeDescription(),")");
class=class="str">"cmt">//--- 合约大小
   Print("标准合约大小: ",symbol_info.ContractSize());
class=class="str">"cmt">//--- 每一手合约的初始保证金
   Print("1标准合约的初始保证金: ",symbol_info.MarginInitial()," ",symbol_info.CurrencyBase());
class=class="str">"cmt">//--- 交易操作中的最小和最大交易量
   Print("交易量信息: 最小手数=",symbol_info.LotsMin(),"  最大手数=",symbol_info.LotsMax(),
         "  手数步长=",symbol_info.LotsStep());
class=class="str">"cmt">//--- 
   Print(__FUNCTION__,"  完毕。");   
  }
把订单路由差异交给小布盯盘
MOEX 各品种在市价/限价与 STOP 转发规则上的差异,小布盯盘的 AIGC 已内置到对应品种页,打开就能看到该类资产的下单约束,你只管把策略逻辑写对。

常见问题

证券触发后转市价买入订单,期货则以限价买入订单按最差通道阈值价格发送,这是终端本地处理后再发往交易所的差异,写代码时需按品种分支判断。
限价单存于交易所等待反向对手价到位才撮合,若行情未触指定价则一直挂起,流动性不足时可能终身不执行,属于价格优先、成交不保的天然属性。
可以,小布盯盘对应品种页已内置该交易所资产的下单类型与转发约束提示,你粘贴策略片段即可对照检查路由是否写错。
市价单只保执行不保价格,调用 OrderSend 前可先取 bid/ask 与深度,对滑点敏感品种建议改发 LIMIT 并预留通道,外汇贵金属类高波动资产尤需谨慎。
多为品种名拼写错或该属性在当前 MOEX 连接方式下不支持,进阶篇第 6 节会列具体字段与回退取值方式,跨篇详见《为莫斯科交易所开发一个交易机器人从哪里开始呢?·基础篇》。