轻松快捷开发 MetaTrader 程序的函数库 (第 二十七部分) :操控交易请求 - 下挂单·进阶篇
(2/3)· 从品种规格冲突到挂单错误自愈,手把手把延后请求写进函数库
「交易诊断字段与集合 ID 的硬编码约定」
在自建 EA 的调试面板里,先把俄语/英语双语标签写死成常量数组,是定位撤单与重发逻辑最快的办法。上面这段就列出了总交易尝试次数、请求 ID、服务端返回码、两种挂单请求类型等 7 个诊断项,MT5 终端里直接打印就能看到每次发单的来龙去脉。 集合列表用 16 进制宏隔离数据域:历史 0x777A、实时 0x777B、事件 0x777C、账户 0x777D、品种 0x777E。改这些值时注意别和平台内部对象 ID 撞车,否则 Collection 操作会静默失败。 挂单请求分两类——错误触发型(宏值 1)和主动请求型(宏值 2),对应枚举 ENUM_PENDING_REQUEST_TYPE 里的 PENDING_REQUEST_TYPE_ERROR 与 PENDING_REQUEST_TYPE_REQUEST。外汇与贵金属杠杆高,重发机制若不分清来源,可能在滑点行情里叠出双倍仓位,概率上偏向放大回撤。 SYMBOLS_COMMON_TOTAL 写死为 1000,意味着默认遍历池上限千个品种;做跨市场扫描时若券商符号数超这个值,要改宏重新编译,否则漏掉的品种不会报警。
class="macro">#define COLLECTION_HISTORY_ID(0x777A) class=class="str">"cmt">// Historical collection list ID class="macro">#define COLLECTION_MARKET_ID(0x777B) class=class="str">"cmt">// Market collection list ID class="macro">#define COLLECTION_EVENTS_ID(0x777C) class=class="str">"cmt">// Event collection list ID class="macro">#define COLLECTION_ACCOUNT_ID(0x777D) class=class="str">"cmt">// Account collection list ID class="macro">#define COLLECTION_SYMBOLS_ID(0x777E) class=class="str">"cmt">// Symbol collection list ID class=class="str">"cmt">//--- Data parameters for file operations class=class="str">"cmt">//--- Symbol parameters class="macro">#define CLR_DEFAULT(0xFF000000) class=class="str">"cmt">// Default class="type">class="kw">color class="macro">#define SYMBOLS_COMMON_TOTAL(class="num">1000) class=class="str">"cmt">// Total number of working symbols class=class="str">"cmt">//--- Pending request type IDs class="macro">#define PENDING_REQUEST_ID_TYPE_ERR(class="num">1) class=class="str">"cmt">// Type of a pending request created based on the server class="kw">return code class="macro">#define PENDING_REQUEST_ID_TYPE_REQ(class="num">2) class=class="str">"cmt">// Type of a pending request created by request class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Pending request type | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_PENDING_REQUEST_TYPE { PENDING_REQUEST_TYPE_ERROR=PENDING_REQUEST_ID_TYPE_ERR, class=class="str">"cmt">// Pending request created based on the class="kw">return code or error PENDING_REQUEST_TYPE_REQUEST=PENDING_REQUEST_ID_TYPE_REQ, class=class="str">"cmt">// Pending request created by request };
◍ 把成交与有效期类型翻成可读日志
在 MT5 的 EA 调试里,交易请求结构体 MqlTradeRequest 里的填充方式(ENUM_ORDER_TYPE_FILLING)和挂单有效期(ENUM_ORDER_TYPE_TIME)都是以枚举值存在的。直接 Print 出来只是 ORDER_FILLING_IOC 这种机器字串,肉眼排查报错时很费劲。 下面两段函数就是把枚举映射成文字:OrderTypeFillingDescription 覆盖 FOK / IOC / RETURN 以及 WRONG_VALUE 四种情况,OrderTypeTimeDescription 覆盖 GTC / DAY / SPECIFIED / SPECIFIED_DAY 四种情况,未知值则退回 EnumToString 原样输出。 真正把信息聚合到日志的是 PrintRequestDescription。它把动作、魔法码、订单类型、品种、手数、挂单价、SL/TP、偏差、填充与有效期等 14 个字段用换行拼成一段 datas 文本。你在策略测试器里跑一遍,日志里就能看到每笔下单的完整中文(或终端语言)描述,而不是对着枚举猜。 外汇与贵金属杠杆高,这类日志只用于定位请求构造错误,不预示任何成交优劣。
class="type">class="kw">string OrderTypeFillingDescription(class="kw">const ENUM_ORDER_TYPE_FILLING type) { class="kw">return ( type==ORDER_FILLING_FOK ? CMessage::Text(MSG_LIB_TEXT_REQUEST_ORDER_FILLING_FOK) : type==ORDER_FILLING_IOC ? CMessage::Text(MSG_LIB_TEXT_REQUEST_ORDER_FILLING_IOK) : type==ORDER_FILLING_RETURN ? CMessage::Text(MSG_LIB_TEXT_REQUEST_ORDER_FILLING_RETURN): type==WRONG_VALUE ? "WRONG_VALUE" : EnumToString(type) ); } class="type">class="kw">string OrderTypeTimeDescription(class="kw">const ENUM_ORDER_TYPE_TIME type) { class="kw">return ( type==ORDER_TIME_GTC ? CMessage::Text(MSG_LIB_TEXT_REQUEST_ORDER_TIME_GTC) : type==ORDER_TIME_DAY ? CMessage::Text(MSG_LIB_TEXT_REQUEST_ORDER_TIME_DAY) : type==ORDER_TIME_SPECIFIED ? CMessage::Text(MSG_LIB_TEXT_REQUEST_ORDER_TIME_SPECIFIED) : type==ORDER_TIME_SPECIFIED_DAY ? CMessage::Text(MSG_LIB_TEXT_REQUEST_ORDER_TIME_SPECIFIED_DAY): type==WRONG_VALUE ? "WRONG_VALUE" : EnumToString(type) ); } class="type">void PrintRequestDescription(class="kw">const class="type">MqlTradeRequest &request) { class="type">class="kw">string datas= ( " - "+RequestActionDescription(request)+"\n"+ " - "+RequestMagicDescription(request)+"\n"+ " - "+RequestOrderDescription(request)+"\n"+ " - "+RequestSymbolDescription(request)+"\n"+ " - "+RequestVolumeDescription(request)+"\n"+ " - "+RequestPriceDescription(request)+"\n"+ " - "+RequestStopLimitDescription(request)+"\n"+ " - "+RequestStopLossDescription(request)+"\n"+ " - "+RequestTakeProfitDescription(request)+"\n"+ " - "+RequestDeviationDescription(request)+"\n"+ " - "+RequestTypeDescription(request)+"\n"+ " - "+RequestTypeFillingDescription(request)+"\n"+ " - "+RequestTypeTimeDescription(request)+"\n"+
把交易请求拆成可读字符串
在 MT5 的 EA 调试里,直接 Print 一个 MqlTradeRequest 结构只会得到内存地址,看不出到底发了什么单。上面这组函数把请求对象逐个字段转成中文(或本地化)描述串,拼进 datas 变量后一次性打印,省去你手动对照结构体成员。 RequestActionDescription 用三元运算符把 action 枚举映射成文本:TRADE_ACTION_DEAL 对应市价成交,TRADE_ACTION_PENDING 对应挂单,SLTP 改止损止盈,MODIFY 改单,REMOVE 删挂单,CLOSE_BY 对冲平仓,其余归为未知动作。这样日志里一眼能看出这笔请求想干什么。 RequestOrderDescription 对 order 字段做了防护:只有 request.order>0 才输出具体 ticket,否则返回「未设置」。外汇与贵金属杠杆高,误把未初始化订单号当有效参数发回去,可能触发异常成交,这类边界判断在实盘代码里不能省。 RequestVolumeDescription 里 DigitsLots(request.symbol) 取该品种手数精度,dg 为 0 时强制 dgl=1,避免 0 位小数导致体积显示为空。复制这段代码到你的 CTrade 封装类,开 MT5 用 EURUSD 跑一笔 0.01 手市价单,日志会输出带「动作 / 魔术码 / 订单号 / 品种 / 手数」的分段信息,验证字段解析是否对齐。
" - "+RequestExpirationDescription(request)+"\n"+ " - "+RequestCommentDescription(request)+"\n"+ " - "+RequestPositionDescription(request)+"\n"+ " - "+RequestPositionByDescription(request) ); Print("================== ",CMessage::Text(MSG_LIB_TEXT_REQUEST_DATAS)," ==================\n",datas,"\n"); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the executed action type description | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string RequestActionDescription(class="kw">const class="type">MqlTradeRequest &request) { class="type">int code_descr= ( request.action==TRADE_ACTION_DEAL ? MSG_LIB_TEXT_REQUEST_ACTION_DEAL : request.action==TRADE_ACTION_PENDING ? MSG_LIB_TEXT_REQUEST_ACTION_PENDING : request.action==TRADE_ACTION_SLTP ? MSG_LIB_TEXT_REQUEST_ACTION_SLTP : request.action==TRADE_ACTION_MODIFY ? MSG_LIB_TEXT_REQUEST_ACTION_MODIFY : request.action==TRADE_ACTION_REMOVE ? MSG_LIB_TEXT_REQUEST_ACTION_REMOVE : request.action==TRADE_ACTION_CLOSE_BY ? MSG_LIB_TEXT_REQUEST_ACTION_CLOSE_BY : MSG_LIB_TEXT_REQUEST_ACTION_UNCNOWN ); class="kw">return CMessage::Text(MSG_LIB_TEXT_REQUEST_ACTION)+": "+CMessage::Text(code_descr); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the magic number value description | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string RequestMagicDescription(class="kw">const class="type">MqlTradeRequest &request) { class="kw">return CMessage::Text(MSG_ORD_MAGIC)+": "+(class="type">class="kw">string)request.magic; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the order ticket value description | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string RequestOrderDescription(class="kw">const class="type">MqlTradeRequest &request) { class="kw">return CMessage::Text(MSG_LIB_TEXT_REQUEST_ORDER)+": "+(request.order>class="num">0 ? (class="type">class="kw">string)request.order : CMessage::Text(MSG_LIB_PROP_NOT_SET)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the trading instrument name description | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string RequestSymbolDescription(class="kw">const class="type">MqlTradeRequest &request) { class="kw">return CMessage::Text(MSG_LIB_TEXT_REQUEST_SYMBOL)+": "+request.symbol; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the request volume description | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string RequestVolumeDescription(class="kw">const class="type">MqlTradeRequest &request) { class="type">int dg=(class="type">int)DigitsLots(request.symbol); class="type">int dgl=(dg==class="num">0 ? class="num">1 : dg);
「交易请求里各价格字段的回显写法」
在 MT5 的 CTrade 封装逻辑里,把 MqlTradeRequest 结构体转成可读字符串时,价格类字段不能硬编码小数位,必须按品种动态取精度。上面这组函数就演示了 volume、price、stoplimit、sl、tp 五类数值怎么回显。 核心区别在于:手数 volume 用的是独立变量 dgl(可能是账户小数位设置),而 price / stoplimit / sl / tp 全部走 SymbolInfoInteger(request.symbol, SYMBOL_DIGITS) 强转 int 后交给 DoubleToString。欧元兑美元这类 5 位报价品种,SL 写 1.08523 就不会被截成 1.09。 deviation 字段直接强转 string,不调精度函数——它本身是整型点数偏差,硬套 DoubleToString 反而多此一举。type 则委托给 OrderTypeDescription 做枚举翻译,不在本层拼字符串。 开 MT5 把这段塞进你的调试面板,挂一单 XAUUSD 观察 sl 回显位数:若 SYMBOL_DIGITS 返回 2,1.23456 的止损会显示成 1.23,贵金属报价跳变快时这种截断可能让你误判实际风险。
class="type">class="kw">string RequestPriceDescription(class="kw">const class="type">MqlTradeRequest &request) { class="kw">return CMessage::Text(MSG_LIB_TEXT_REQUEST_PRICE)+": "+(request.price>class="num">0 ? DoubleToString(request.price,(class="type">int)SymbolInfoInteger(request.symbol,SYMBOL_DIGITS)) : CMessage::Text(MSG_LIB_PROP_NOT_SET)); } class="type">class="kw">string RequestStopLimitDescription(class="kw">const class="type">MqlTradeRequest &request) { class="kw">return CMessage::Text(MSG_LIB_TEXT_REQUEST_STOPLIMIT)+": "+(request.stoplimit>class="num">0 ? DoubleToString(request.stoplimit,(class="type">int)SymbolInfoInteger(request.symbol,SYMBOL_DIGITS)) : CMessage::Text(MSG_LIB_PROP_NOT_SET)); } class="type">class="kw">string RequestStopLossDescription(class="kw">const class="type">MqlTradeRequest &request) { class="kw">return CMessage::Text(MSG_LIB_TEXT_REQUEST_SL)+": "+(request.sl>class="num">0 ? DoubleToString(request.sl,(class="type">int)SymbolInfoInteger(request.symbol,SYMBOL_DIGITS)) : CMessage::Text(MSG_LIB_PROP_NOT_SET)); } class="type">class="kw">string RequestTakeProfitDescription(class="kw">const class="type">MqlTradeRequest &request) { class="kw">return CMessage::Text(MSG_LIB_TEXT_REQUEST_TP)+": "+(request.tp>class="num">0 ? DoubleToString(request.tp,(class="type">int)SymbolInfoInteger(request.symbol,SYMBOL_DIGITS)) : CMessage::Text(MSG_LIB_PROP_NOT_SET)); } class="type">class="kw">string RequestDeviationDescription(class="kw">const class="type">MqlTradeRequest &request) { class="kw">return CMessage::Text(MSG_LIB_TEXT_REQUEST_DEVIATION)+": "+(class="type">class="kw">string)request.deviation; } class="type">class="kw">string RequestTypeDescription(class="kw">const class="type">MqlTradeRequest &request) { class="kw">return CMessage::Text(MSG_LIB_TEXT_REQUEST_TYPE)+": "+OrderTypeDescription(request.type); }
◍ 把交易请求字段翻成可读文本
在 MT5 的 EA 调试里,直接打印 MqlTradeRequest 结构往往是一串看不懂的数字和枚举。下面这组函数把常用字段接上本地化文案,返回「字段名: 值」格式,方便写日志或弹窗。 RequestTypeFillingDescription 取 type_filling 的填充方式说明;RequestTypeTimeDescription 取 type_time 的挂单生命周期说明;RequestExpirationDescription 判断 expiration 大于 0 才格式化为时间字符串,否则标「未设置」。 RequestCommentDescription 对 comment 做了空串和 NULL 双判断,避免老版本终端把未赋值注释当成乱码输出;RequestPositionDescription 与 RequestPositionByDescription 则分别处理持仓单号和对冲反向单号,ticket 为 0 时统一回退到「未设置」文案。 复制进你的 include 工具类,调用时传入 request 引用即可在专家日志里看到结构化输出,省去手动解析枚举的功夫。外汇与贵金属杠杆高,日志仅辅助排查,不构成任何方向判断。
class="type">class="kw">string RequestTypeFillingDescription(class="kw">const class="type">MqlTradeRequest &request) { class="kw">return CMessage::Text(MSG_LIB_TEXT_REQUEST_TYPE_FILLING)+": "+OrderTypeFillingDescription(request.type_filling); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the request order lifetime type description | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string RequestTypeTimeDescription(class="kw">const class="type">MqlTradeRequest &request) { class="kw">return CMessage::Text(MSG_LIB_TEXT_REQUEST_TYPE_TIME)+": "+OrderTypeTimeDescription(request.type_time); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the request order expiration time description | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string RequestExpirationDescription(class="kw">const class="type">MqlTradeRequest &request) { class="kw">return CMessage::Text(MSG_LIB_TEXT_REQUEST_EXPIRATION)+": "+(request.expiration>class="num">0 ? TimeToString(request.expiration) : CMessage::Text(MSG_LIB_PROP_NOT_SET)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the request order comment description | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string RequestCommentDescription(class="kw">const class="type">MqlTradeRequest &request) { class="kw">return CMessage::Text(MSG_LIB_TEXT_REQUEST_COMMENT)+": "+(request.comment!="" && request.comment!=NULL ? "\""+request.comment+"\"" : CMessage::Text(MSG_LIB_PROP_NOT_SET)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the request position ticket description | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string RequestPositionDescription(class="kw">const class="type">MqlTradeRequest &request) { class="kw">return CMessage::Text(MSG_LIB_TEXT_REQUEST_POSITION)+": "+(request.position>class="num">0 ? (class="type">class="kw">string)request.position : CMessage::Text(MSG_LIB_PROP_NOT_SET)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the request opposite position ticket description | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string RequestPositionByDescription(class="kw">const class="type">MqlTradeRequest &request) { class="kw">return CMessage::Text(MSG_LIB_TEXT_REQUEST_POSITION_BY)+": "+(request.position_by>class="num">0 ? (class="type">class="kw">string)request.position_by : CMessage::Text(MSG_LIB_PROP_NOT_SET)); } class=class="str">"cmt">//+------------------------------------------------------------------+
处理无买卖价品种与延后请求骨架
部分品种图表以 Last 价构建,盘中可能缺失 Ask 或 Bid 其一。CSymbol 抽象类里需先判定图表构建类型:基于 Last 时再探测 Ask/Bid 是否存在,为零则回退 Last 价,否则取真实报价。这一改动让函数库在贵金属某些交叉盘上不再因取价失败而中断。 时间方法返回类型由 datetime 改为 ulong,以毫秒精度传递;同时新增三个探测方法。原出价三方法补上“是否为零”的判断,逻辑变成:出价图表直接返回,Last 图表下出价为零取 Last,否则取 Bid。 魔幻数字中若嵌了延后请求 ID,原先在日志里不显示。在 EventModify / EventOrderPlaced / EventOrderRemoved / EventPositionClose / EventPositionOpen 五个文件里,把单字符串替换成双字符串,就把延后 ID 描述带进日志了。 TradeObj.mqh 公开段挂了几个返回 MqlTradeRequest 字段说明的方法,直接调服务函数;开仓方法补了订单填充类型参数,声明和实现都要同步改。CPendingReq 私密段加 m_type 存延后请求类型,公开段加返回服务器代码、属性说明、Type() 虚函数和整对象日志输出。 Compare() 原来只按 request ID 比,现在引入 mode:0 比 ID,非 0 比类型。构造函数里按服务器返回码定类型——大于 0 是服务器错误生成,等于 0 是程序请求生成。 CTrading 私密和公开开仓都加了填充类型参数;RequestErrorsCorrecting() 补了到期类型纠正应对“无效订单到期日期”。全局把 Ask()/Bid() 搜替换成 AskLast()/BidLast(),下挂单私密方法里接了创建延后请求模块,新对象属性直接打印日志便于调试。 CEngine 发交易请求的方法定义里也加了填充类型参数,默认 -1 时从品种对象取实际值。这些就是本轮剔除交易类缺陷、铺延后请求底子的全部改动,外汇与贵金属杠杆高,改完请在 MT5 用真实点差环境跑一轮挂单回测验证取价回退是否生效。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Methods of a simplified access to the order object properties |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//--- Integer properties</span> <span class="keyword">class="type">long</span> Status(<span class="keyword">class="type">void</span>) <span class="keyword">class="kw">const</span> { <span class="keyword">class="kw">return</span> <span class="keyword">this</span>.GetProperty(SYMBOL_PROP_STATUS); } <span class="keyword">class="type">int</span> IndexInMarketWatch(<span class="keyword">class="type">void</span>) <span class="keyword">class="kw">const</span> { <span class="keyword">class="kw">return</span> (<span class="keyword">class="type">int</span>)<span class="keyword">this</span>.GetProperty(SYMBOL_PROP_INDEX_MW); } <span class="keyword">class="type">bool</span> IsCustom(<span class="keyword">class="type">void</span>) <span class="keyword">class="kw">const</span> { <span class="keyword">class="kw">return</span> (<span class="keyword">class="type">bool</span>)<span class="keyword">this</span>.GetProperty(SYMBOL_PROP_CUSTOM); }