为莫斯科交易所开发一个交易机器人从哪里开始呢?·进阶篇
(2/3)· 市价与限价订单在莫斯科交易所的真实执行差异,多数人写第一行下单代码前都没理清
「交易回码 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 的触发路径。
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 的回码打印函数,跑一单就能对照出自己经纪商的边界值。
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 里单独计数告警。
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 显示的是哪种保证金算法。不同券商的保证金模式差异,会直接影响你仓位计算的假设。
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的下单函数匹配。
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__," 完毕。"); }