交易事务. 请求和响应结构、描述和记录(基础篇)
「交易请求的收发结构怎么看」
在 MT5 里做自动化交易,绕不开「请求—响应」这一对结构。每次下单、改单、撤单,EA 都要先组装一个请求包发给交易服务器,服务器回一个响应包,里面带着执行结果和ticket。
请求结构主要包含动作类型、品种、手数、价格、偏差、订单类型、止损止盈等字段;响应结构则回吐重填价格、成交量和错误代码。理解这两层,是写稳交易类 EA 的前提。
以 2024 年 2 月 22 日一篇社区示例为例,该文获 1409 次查看、82 条讨论,说明基础事务结构仍是高频痛点。外汇与贵金属杠杆高,请求字段填错可能瞬间错单,实盘前务必在策略测试器跑通。
打开 MT5 按 F4 进 MetaEditor,搜 MqlTradeRequest 和 MqlTradeResult 两个结构体,对照官方字段逐条读一遍,比看十篇泛文都管用。
◍ CTrade 类封装了哪些交易接口
MQL5 标准库里的 CTrade 类把下单、挂单、平仓等动作收敛成一组直白方法,省去手工填 MqlTradeRequest 的繁琐。打开仓位只需调 OpenBuy / OpenSell,平仓有 ClosePosition 与 ClosePositionsAll,挂单则细分到 SetBuyStop、SetBuyLimit、SetSellStopLimit 等七种。 每笔请求底层都依赖 MqlTradeRequest 结构,字段覆盖交易量、价格、止损获利位、订单类型、有效期与备注等 17 项。发送前可用 MqlTradeCheckResult 做预检,返回响应代码、余额、净值、保证金水平等数据,能在实盘前暴露多数参数错误。 成交后由 MqlTradeResult 回写运行代码、订单号、成交价、买卖报价及经纪商备注;异步事务则走 MqlTradeTransaction,携带事务类型、订单状态与仓位编号。外汇与贵金属杠杆高,任何接口调用失败都可能漏单,建议在策略初始化时先跑一次 OpenBuy 微型手数验证通道可用性。
从填结构到抓回执:MT5 下单链路怎么跑通
MT5 里所有下单、改单、平仓动作都收敛到 OrderSend() 一个入口,第一个参数永远是 MqlTradeRequest 结构。这个结构的 action 字段决定你要干的事:市价成交、挂单、改 SLTP、改挂单参数、删挂单、反向仓平仓,分别对应 TRADE_ACTION_DEAL / PENDING / SLTP / MODIFY / REMOVE / CLOSE_BY 六种枚举。填错 action 类型,后面字段填了也白填——每种操作只认自己那几个必填项。 动手前先用 OrderCheck() 探一遍:把填好的结构和 MqlTradeCheckResult 变量传进去,资金不足或参数非法它会返回 false;返回 true 只代表结构基础校验过,不保证服务器真给你成交。真要发单再调 OrderSend(),第二个参数接 MqlTradeResult,回执写进这个结构里,但函数返回成功仅意味『服务器收了请求』,不是『单子已成交』。 异步场景更值得盯。OrderSendAsync() 发出去后,终端给每次请求打一个 request_id,服务器回的消息进 OnTradeTransaction() 处理函数,里面三个参数里只有后两个(请求描述、执行结果)在 TRADE_TRANSACTION_REQUEST 类型时才有效。靠 request_id 把『我发的单』和『回来的果』对上号,就能在回调里完整追踪挂单进场、成交、拒绝的全过程。 一套标准下单流程就五步:填 MqlTradeRequest → OrderCheck() 验证 → OrderSend() 发单 → 必要时读 MqlTradeResult → OnTradeTransaction() 里落日志分析。下面这段是 MqlTradeRequest 的原生结构定义,逐行看清楚每个字段的语义,后面写通知型 EA 打印全链路事件就靠它打底。
class="kw">struct class="type">MqlTradeRequest { ENUM_TRADE_REQUEST_ACTIONS action; class=class="str">"cmt">// Type of a performed action class="type">ulong magic; class=class="str">"cmt">// EA stamp(magic number ID) class="type">ulong order; class=class="str">"cmt">// Order ticket class="type">class="kw">string symbol; class=class="str">"cmt">// Symbol name class="type">class="kw">double volume; class=class="str">"cmt">// Requested volume of a deal in lots class="type">class="kw">double price; class=class="str">"cmt">// Price class="type">class="kw">double stoplimit; class=class="str">"cmt">// StopLimit order level class="type">class="kw">double sl; class=class="str">"cmt">// Stop Loss order level class="type">class="kw">double tp; class=class="str">"cmt">// Take Profit order level class="type">ulong deviation; class=class="str">"cmt">// Maximum acceptable deviation from the requested price ENUM_ORDER_TYPE type; class=class="str">"cmt">// Order type ENUM_ORDER_TYPE_FILLING type_filling; class=class="str">"cmt">// Order filling type ENUM_ORDER_TYPE_TIME type_time; class=class="str">"cmt">// Order lifetime type
「挂单结构与成交前校验的字段映射」
在 MT5 的订单发送流程里,先填 MqlTradeRequest 再拿 MqlTradeCheckResult 做预检,是绕不开的两步。前者描述“我想下什么单”,后者回吐“经纪商觉得这单下去账户会变成什么样”。 MqlTradeRequest 末尾几个字段很容易被忽略:expiration 只在 ORDER_TIME_SPECIFIED 类型挂单时生效,comment 是随单备注,position 与 position_by 分别挂的是持仓单号和对冲反向单号。改错这几个值,订单可能直接被服务器拒掉。 MqlTradeCheckResult 的价值在于“先算后下”。retcode 非 0 就别发单;balance、equity 是成交后的账面与净值,profit 是浮动盈亏,margin 到 margin_level 四项是保证金占用与水位。外汇与贵金属杠杆高,预检里 margin_level 掉到 100% 附近时,再下新单可能触发强平,务必在脚本里拦一道。 把下面结构直接贴进 MT5 的 MQ5 文件,就能在策略里调用校验,不用等报错才发现问题。
class="type">class="kw">datetime expiration; class=class="str">"cmt">// Order expiration time(for ORDER_TIME_SPECIFIED type orders) class="type">class="kw">string comment; class=class="str">"cmt">// Order comment class="type">ulong position; class=class="str">"cmt">// Position ticket class="type">ulong position_by; class=class="str">"cmt">// Opposite position ticket }; class="kw">struct MqlTradeCheckResult { class="type">uint retcode; class=class="str">"cmt">// Response code class="type">class="kw">double balance; class=class="str">"cmt">// Balance after performing a deal class="type">class="kw">double equity; class=class="str">"cmt">// Equity after performing a deal class="type">class="kw">double profit; class=class="str">"cmt">// Floating profit class="type">class="kw">double margin; class=class="str">"cmt">// Margin requirements class="type">class="kw">double margin_free; class=class="str">"cmt">// Free margin class="type">class="kw">double margin_level; class=class="str">"cmt">// Margin level class="type">class="kw">string comment; class=class="str">"cmt">// Comment on the response code(error description) };
◍ 键盘状态与服务器回报的底层读取函数
用快捷键控EA时,先得知道Ctrl和Shift是否被按住。MQL5里不用自己挂钩子,直接问终端:TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL)返回值小于0就代表Ctrl处于按下态,Shift同理换TERMINAL_KEYSTATE_SHIFT。这两个布尔函数返回快、无副作用,适合在OnChartEvent里每次按键都跑一遍。 交易发单后,服务器会给一个数字回报码。RetcodeDescription()把常用码翻成可读串:比如0是"OK (0)",10004是REQUOTE,10006是REJECT,10007是CANCEL。第二个参数ext_descr设为true时追加括号里的英文长描述,方便在日志里排查拒单原因。 订单类型、成交填充模式、订单有效期这些枚举,最好也各写一个小函数做描述映射。特别是SYMBOL_FILLING_MODE,用SymbolInfoInteger(SYMBOL_FILLING_MODE)拿到的是标志组合,ORDER_FILLING_RETURN除市场执行模式外常开,发单前不核对就可能报参数错。外汇和贵金属杠杆高,回报码解读错一笔就可能放大亏损,验证函数逻辑后再上实盘。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Return the state of the Ctrl key |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">bool</span> IsCtrlKeyPressed(<span class="keyword">class="type">void</span>) { <span class="keyword">class="kw">return</span>(::<span class="functions">TerminalInfoInteger</span>(<span class="macro">TERMINAL_KEYSTATE_CONTROL</span>)<<span class="number">class="num">0</span>); } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Return the state of the Shift key |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">bool</span> IsShiftKeyPressed(<span class="keyword">class="type">void</span>) { <span class="keyword">class="kw">return</span>(::<span class="functions">TerminalInfoInteger</span>(<span class="macro">TERMINAL_KEYSTATE_SHIFT</span>)<<span class="number">class="num">0</span>); } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Return a description of the trade server class="kw">return code |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">class="kw">string</span> RetcodeDescription(<span class="keyword">const</span> <span class="keyword">class="type">uint</span> retcode,<span class="keyword">class="type">bool</span> ext_descr=<span class="macro">false</span>) { <span class="keyword">class="kw">switch</span>(retcode) { <span class="comment">class=class="str">"cmt">//--- Done</span> <span class="keyword">case</span> <span class="number">class="num">0</span> : <span class="keyword">class="kw">return</span> <span class="class="type">class="kw">string">"OK(class="num">0)"</span>; <span class="comment">class=class="str">"cmt">//--- Requote</span> <span class="keyword">case</span> <span class="macro">TRADE_RETCODE_REQUOTE</span> : <span class="keyword">class="kw">return</span> <span class="class="type">class="kw">string">"class="num">10004 REQUOTE"</span>+(ext_descr ? <span class="class="type">class="kw">string">" (Requote)"</span> : <span class="class="type">class="kw">string">""</span>); <span class="comment">class=class="str">"cmt">//--- Request rejected</span> <span class="keyword">case</span> <span class="macro">TRADE_RETCODE_REJECT</span> : <span class="keyword">class="kw">return</span> <span class="class="type">class="kw">string">"class="num">10006 REJECT"</span>+(ext_descr ? <span class="class="type">class="kw">string">" (Request rejected)"</span> : <span class="class="type">class="kw">string">""</span>); <span class="comment">class=class="str">"cmt">//--- Request canceled by trader</span> <span class="keyword">case</span> <span class="macro">TRADE_RETCODE_CANCEL</span> : <span class="keyword">class="kw">return</span> <span class="class="type">class="kw">string">"class="num">10007 CANCEL"</span>+(ext_descr ? <span class="class="type">class="kw">string">" (Request canceled by trader)"</span> : <span class="class="type">class="kw">string">""</span>); <span class="comment">class=class="str">"cmt">//--- Order placed</span>
成交回执码的后半段映射
上面这段 switch 分支接着把 10008 到 10019 这一批交易回执码翻译成可读字符串,做 EA 日志或报警时直接用人话而不是冷冰冰的数字。10009 DONE 代表整单请求完全成交,10010 DONE_PARTIAL 则提示只成交了一部分,黄金剥头皮策略里若碰上流动性薄的时段,部分成交概率会明显抬升。
- 到 10013 属于典型失败类:ERROR 是服务端处理出错,TIMEOUT 是超时撤单,INVALID 则是请求本身结构有问题;这三类在 MT5 真实账户回测和实盘里都可能出现,需要分别计数。
10014~10017 基本是参数层面被拒:手数、价格、止损止盈位置不合规,或者品种处于禁交易状态。10018 MARKET_CLOSED 在非农间隙或平台维护时常见,10019 NO_MONEY 则说明保证金不够——外汇和贵金属杠杆高,这条最容易在重仓时触发。 把 ext_descr 参数设为 true,返回串会带上括号里的英文说明,调试阶段建议开着;上线后关掉能省一点日志体积。
case TRADE_RETCODE_PLACED : class="kw">return "class="num">10008 PLACED"+(ext_descr ? " (Order placed)" : ""); class=class="str">"cmt">//--- Request completed case TRADE_RETCODE_DONE : class="kw">return "class="num">10009 DONE"+(ext_descr ? " (Request completed)" : ""); class=class="str">"cmt">//--- Request completed partially case TRADE_RETCODE_DONE_PARTIAL : class="kw">return "class="num">10010 DONE_PARTIAL"+(ext_descr ? " (Only part of the request was completed)" : ""); class=class="str">"cmt">//--- Request processing error case TRADE_RETCODE_ERROR : class="kw">return "class="num">10011 ERROR"+(ext_descr ? " (Request processing error)" : ""); class=class="str">"cmt">//--- Request canceled by timeout case TRADE_RETCODE_TIMEOUT : class="kw">return "class="num">10012 TIMEOUT"+(ext_descr ? " (Request canceled by timeout)" : ""); class=class="str">"cmt">//--- Invalid request case TRADE_RETCODE_INVALID : class="kw">return "class="num">10013 INVALID"+(ext_descr ? " (Invalid request)" : ""); class=class="str">"cmt">//--- Invalid volume in the request case TRADE_RETCODE_INVALID_VOLUME : class="kw">return "class="num">10014 INVALID_VOLUME"+(ext_descr ? " (Invalid volume in the request)" : ""); class=class="str">"cmt">//--- Invalid price in the request case TRADE_RETCODE_INVALID_PRICE : class="kw">return "class="num">10015 INVALID_PRICE"+(ext_descr ? " (Invalid price in the request)" : ""); class=class="str">"cmt">//--- Invalid stops in the request case TRADE_RETCODE_INVALID_STOPS : class="kw">return "class="num">10016 INVALID_STOPS"+(ext_descr ? " (Invalid stops in the request)" : ""); class=class="str">"cmt">//--- Trading disabled case TRADE_RETCODE_TRADE_DISABLED : class="kw">return "class="num">10017 TRADE_DISABLED"+(ext_descr ? " (Trade is disabled)" : ""); class=class="str">"cmt">//--- Market is closed case TRADE_RETCODE_MARKET_CLOSED : class="kw">return "class="num">10018 MARKET_CLOSED"+(ext_descr ? " (Market is closed)" : ""); class=class="str">"cmt">//--- There is not enough money to complete the request case TRADE_RETCODE_NO_MONEY : class="kw">return "class="num">10019 NO_MONEY"+(ext_descr ? " (There is not enough money to complete the request)" : ""); class=class="str">"cmt">//--- Prices changed