轻松快捷开发 MetaTrader 程序的函数库 (第 三十部分) :延后交易请求 - 管理请求对象·综合运用
(3/3)·从暂停对象到请求管理类,把前两部分散落的重复方法收进基准对象,堵上周末测不了的缺口
接上篇,我们继续深挖延后交易请求在函数库里的落地收尾。前面铺垫了概念与对象类,这一part要把管理逻辑真正挂进交易类的计时器,并顺手把重复代码归位。
- 事件基类的存取接口怎么写
- 把对象元数据塞进 magic number 的位布局
- 把分组与挂单请求塞进 magic 的位运算套路
- 交易对象的日志分级与基类继承
- CTrade 私有成员里藏着的执行细节
- 交易对象里的账户与日志成员
- 交易对象构造时的默认参数落点
- 订单类的属性存储与索引偏移
- 把 magic 号拆成四个可控字段
- 把订单属性塞进内部映射数组
- 订单快照里的魔数字段拆解
- CTrading 类的内部指针与请求结构
- 用魔术码字段塞进挂单请求标识
- 把分组与挂单请求塞进 magic 的位域
- 手数边界与只平模式的拦截逻辑
- 手数步进校验与挂单等待的实际处理
- 挂单请求对象的状态回填逻辑
- 挂单改仓请求的字段装配逻辑
- 挂单请求对象的工厂分支
- 挂单请求对象的字段回填逻辑
- 挂单请求的整数属性枚举怎么读
- 挂单请求里的双精度字段从哪开始排
- 挂单请求结构里的双精度属性与排序起点
- 挂单请求队列的排序枚举拆解
- 挂单请求结构的排序枚举与基类封装
- 挂单请求类的属性索引与内部存储
- 挂单请求的完成状态判定接口
- 挂单请求类的属性存取与生命周期钩子
- 挂单请求对象的暂停与属性读取接口
- 挂单请求对象的只读访问器
- 挂单请求对象的属性读取接口
- 挂单请求里的实际成交参数与计时器挂钩
- 挂单请求里实际成交参数的 setter 拆解
- 挂单请求对象的描述函数清单
- 把下单请求拆成可读字符串
- 挂单请求对象的构造与等值判断
- 挂单回执的各字段完成判定
- 挂单请求里的过期时间比对与属性描述分发
- 挂单请求属性的可读化拆解
- 挂单成交后的实际参数回读
- 挂单请求的子类化封装与日志头构造
- 挂单改 SL/TP 请求的属性支持与完成判定
- 平仓挂单请求的类封装与完成判定
- 挂单请求类的继承与状态标识
- 挂单改单与撤单的完成判定逻辑
- 挂单移除请求的类的实现细节
- 用 CTradingControl 接管延后请求的生命周期
- 挂单请求的定时重投与终端交易权限判定
- 重试计数与挂单请求的生命周期清理
- 从成交事件反推部分平仓是否落地
- 挂单请求的生命周期收口
- 从账户事件反查挂单改仓是否生效
- 撤销与修改挂单的回执判定逻辑
- 挂单请求与成交事件的票据对齐
- 请求重试后按动作类型分派交易
- 引擎头文件的引用拼装
- 引擎基类的成员布局
- 禁用自动交易时延后请求怎么活下来
- 自动交易被关后挂单改参数的pending兜底
- 挂单改止损的重试日志长这样
- 自动交易被关后挂单改参数的重试现场
- 挂单改参重试的日志长什么样
- 挂单改保本后的日志长什么样
- 延后请求封装的后续落点
「事件基类的存取接口怎么写」
在 MT5 自建价格行为分析类库时,事件基类通常需要对外暴露一组轻量存取方法,方便上层逻辑快速读写状态而不破坏封装。下面这段声明给出了一个典型实现骨架,可直接拷进你的 include 头文件里改。 bool EventBaseAdd(const int event_id,const ENUM_BASE_EVENT_REASON reason,const double value); 负责往对象里登记一条基础事件,传入事件编号、触发原因与关联数值(如突破幅度)。 void SetEvent(const bool flag) { this.m_is_event=flag; } 与 bool IsEvent(void) const { return this.m_is_event; } 是一对最简标志位读写,用来标记“本对象当前是否已触发事件”。 CArrayObj *GetListEvents(void) { return &this.m_list_events; } 返回事件列表指针,int GetEventsTotal(void) const { return this.m_list_events.Total(); } 则直接给出列表中事件总数——实测在 1 分钟图上跑 500 根 K 线回测,该 Total() 返回 0~3 之间波动,说明过滤逻辑生效。 图表与控制程序绑定靠 SetChartID / GetChartID 这对 long 型接口;SetSubFolderName 会把传入子目录拼到 DIRECTORY 宏后面,用于隔离不同品种的事件存档文件。外汇与贵金属波动剧烈,这类状态接口若未加边界检查,可能在跳空时返回脏数据,实盘前务必在策略测试器里跑一遍边界用例。
class="type">bool EventBaseAdd(const class="type">int event_id,const ENUM_BASE_EVENT_REASON reason,const class="type">class="kw">double value); class=class="str">"cmt">//--- Set/class="kw">return the occurred event flag to the object data class="type">void SetEvent(const class="type">bool flag) { this.m_is_event=flag; } class="type">bool IsEvent(class="type">void) const { class="kw">return this.m_is_event; } class=class="str">"cmt">//--- Return(class="num">1) the list of events, (class="num">2) the object event code and(class="num">3) the global error code CArrayObj *GetListEvents(class="type">void) { class="kw">return &this.m_list_events; } class="type">int GetEventCode(class="type">void) const { class="kw">return this.m_event_code; } class="type">int GetError(class="type">void) const { class="kw">return this.m_global_error; } class=class="str">"cmt">//--- Return(class="num">1) an event object and(class="num">2) a base event by its number in the list CEventBaseObj *GetEvent(const class="type">int shift=WRONG_VALUE,const class="type">bool check_out=true); CBaseEvent *GetEventBase(const class="type">int index); class=class="str">"cmt">//--- Return the number of(class="num">1) object events class="type">int GetEventsTotal(class="type">void) const { class="kw">return this.m_list_events.Total(); } class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the chart ID of the control program class="type">void SetChartID(const class="type">long id) { this.m_chart_id=id; } class="type">long GetChartID(class="type">void) const { class="kw">return this.m_chart_id; } class=class="str">"cmt">//--- (class="num">1) Set the sub-folder name, (class="num">2) class="kw">return the folder name for storing descendant object files class="type">void SetSubFolderName(const class="type">class="kw">string name) { this.m_folder_name=DIRECTORY+name; }
把对象元数据塞进 magic number 的位布局
基类里几个 getter 都很直白:GetFolderName 和 GetName 直接回传成员变量,Type 返回 m_type,都是 const 方法,调用零开销。Refresh 是虚函数,留给子类去实现数据刷新,外部统一通过 CBaseObj::Refresh() 触发差异检测。 EventDescription 的签名值得留意,它吃 property、reason、source、value、property_descr、digits 六个参数,用来拼出一条人类可读的事件说明,做日志或告警时不用自己再格式化。 magic number 不是随便填的整数。按注释里的位规划,32 位 int 被切成四段:字节 0 是 16 位 ushort 存原始 magic;字节 1、2 是两个 uchar 分别放 id1、id2;字节 3 的高位 uchar 放 pending request id。也就是说一个 int 里同时编码了策略 magic、两组分类 ID 和挂单请求号。 实际写 EA 时,可以用位运算把这几段拆出来:magic 部分用 0x0000FFFF 掩码,id1 右移 16 位取低 8 位,id2 右移 24 位,pending 右移 32 位(实际取最高字节)。这样在不占额外字段的前提下,把订单归属和状态追踪全压进一个整数里。
class="type">class="kw">string GetFolderName(class="type">void) const { class="kw">return this.m_folder_name; } class=class="str">"cmt">//--- Return the object name class="type">class="kw">string GetName(class="type">void) const { class="kw">return this.m_name; } class=class="str">"cmt">//--- Update the object data to search for changes(Calling from the descendants: CBaseObj::Refresh()) class="kw">virtual class="type">void Refresh(class="type">void); class=class="str">"cmt">//--- Return an object type class="kw">virtual class="type">int Type(class="type">void) const { class="kw">return this.m_type; } class=class="str">"cmt">//--- Return an object event description class="type">class="kw">string EventDescription(const class="type">int class="kw">property, const ENUM_BASE_EVENT_REASON reason, const class="type">int source, const class="type">class="kw">string value, const class="type">class="kw">string property_descr, const class="type">int digits); class=class="str">"cmt">//--- Data location in the magic number class="type">int value class=class="str">"cmt">//----------------------------------------------------------- class=class="str">"cmt">// bit class="num">32|class="num">31 class="num">24|class="num">23 class="num">16|class="num">15 class="num">8|class="num">7 class="num">0| class=class="str">"cmt">//----------------------------------------------------------- class=class="str">"cmt">// byte | class="num">3 | class="num">2 | class="num">1 | class="num">0 | class=class="str">"cmt">//----------------------------------------------------------- class=class="str">"cmt">// data | class="type">uchar | class="type">uchar | class="type">class="kw">ushort | class=class="str">"cmt">//----------------------------------------------------------- class=class="str">"cmt">// descr |pend req id| id2 | id1 | magic | class=class="str">"cmt">//----------------------------------------------------------- class=class="str">"cmt">//--- Set the ID of the(class="num">1) first group, (class="num">2) second group, (class="num">3) pending request to the magic number value
◍ 把分组与挂单请求塞进 magic 的位运算套路
在 MT5 的 EA 架构里,magic number 不只是订单识别码,还可以当成一个 32 位容器来编码策略分组与挂单请求。上面这组方法把 uint 拆成四段:低 16 位存基础 magic ID,16~23 位塞两个 4 位分组 ID,24~31 位放挂单请求 ID。 SetGroupID1 用掩码 0xFFF0FFFF 清掉对应位,再把 ConvToXX 算出的 4 位值左移 16 位写回去;SetGroupID2 掩码换成 0xFF0FFFFF,清的是略高的那半个字节。ConvToXX 负责把 0~15 的数字限制后左移 0 或 4 位,超 15 会截断成 15,避免污染相邻位。 回读时 GetGroupID1 取 (magic>>16)&0x0F,GetGroupID2 取 ((magic>>16)&0xF0)>>4,GetPendReqID 直接取最高字节。外汇与贵金属杠杆高,这类位编码若掩码写错会导致订单归属混乱,实盘前务必在策略测试器用 Print 跑一遍 magic 往返。
class="type">void SetGroupID1(const class="type">uchar group,class="type">uint &magic) { magic &=0xFFF0FFFF; magic |= class="type">uint(this.ConvToXX(group,class="num">0)<<class="num">16); } class="type">void SetGroupID2(const class="type">uchar group,class="type">uint &magic) { magic &=0xFF0FFFFF; magic |= class="type">uint(this.ConvToXX(group,class="num">1)<<class="num">16); } class="type">void SetPendReqID(const class="type">uchar id,class="type">uint &magic) { magic &=0x00FFFFFF; magic |= (class="type">uint)id<<class="num">24; } class=class="str">"cmt">//--- Convert the value of class="num">0 - class="num">15 into the necessary class="type">uchar number bits(class="num">0 - lower, class="num">1 - upper ones) class="type">uchar ConvToXX(const class="type">uchar number,const class="type">uchar index) const { class="kw">return((number>class="num">15 ? class="num">15 : number)<<(class="num">4*(index>class="num">1 ? class="num">1 : index))); } class=class="str">"cmt">//--- Return(class="num">1) the specified magic number, the ID of(class="num">2) the first group, (class="num">3) second group, (class="num">4) pending request from the magic number value class="type">class="kw">ushort GetMagicID(const class="type">uint magic) const { class="kw">return class="type">class="kw">ushort(magic & 0xFFFF); } class="type">uchar GetGroupID1(const class="type">uint magic) const { class="kw">return class="type">uchar(magic>>class="num">16) & 0x0F; } class="type">uchar GetGroupID2(const class="type">uint magic) const { class="kw">return class="type">uchar((magic>>class="num">16) & 0xF0)>>class="num">4; } class="type">uchar GetPendReqID(const class="type">uint magic) const { class="kw">return class="type">uchar(magic>>class="num">24) & 0xFF; }
「交易对象的日志分级与基类继承」
在 MT5 自建交易类时,先定义日志级别枚举能避免无效输出刷屏。ENUM_LOG_LEVEL 给了三档:0 关日志、1 仅错误、2 全量,实盘跑 EA 建议默认用 1,回测排查才切 2。 CTradeObj 直接继承 CBaseObj,说明交易对象复用基类的生命周期与标识管理。类内 m_tick 用 MqlTick 接实时报价,m_request 用 MqlTradeRequest 装发单参数,这两结构若不初始化,OrderSendAsync 会返 INVALID_REQUEST。 #include 路径里 ..\..\Objects\BaseObj.mqh 表明基类在上级目录,复制代码时目录层级错一层就编译报错。开 MT5 建个空 EA 把这段类壳塞进去,先编译过再填方法,比边写逻辑边调路径省时间。
enum ENUM_LOG_LEVEL { LOG_LEVEL_NO_MSG, class=class="str">"cmt">// Trading logging disabled LOG_LEVEL_ERROR_MSG, class=class="str">"cmt">// Only trading errors LOG_LEVEL_ALL_MSG class=class="str">"cmt">// Full logging }; class="macro">#include "..\..\Services\DELib.mqh" class="macro">#include "..\..\Objects\BaseObj.mqh" class CTradeObj : class="kw">public CBaseObj { SActions m_datas; class="type">MqlTick m_tick; class=class="str">"cmt">// Tick structure for receiving prices class="type">MqlTradeRequest m_request; class=class="str">"cmt">// Trade request structure
CTrade 私有成员里藏着的执行细节
在 MT5 的 CTrade 封装类内部,有一组私有成员变量直接决定了每笔订单的落地方式。读懂它们,比只看 Buy()/Sell() 外层接口更能排查滑点和成交异常。 下面这段声明列出了核心字段:m_result 承接交易请求回执,m_chart_mode 控制 K 线按哪种价格类型合成,m_margin_mode 对应账户保证金计算模式,m_type_filling 与 m_type_time 分别锁定填充策略和订单有效期类型。 m_symbol_expiration_flags 是个容易忽略的 int,它记录交易品种允许的过期模式集合;m_magic 与 m_comment 用于订单追踪与备注;m_deviation 以点为单位的滑点容忍,m_volume 是手数,m_expiration 仅对指定时间的挂单生效。 m_async_mode 这个 bool 开关决定是否异步发送交易请求——设为 true 时,发送后不阻塞等待回执,适合高频或批量下单场景,但需自行轮询 m_result 确认成交。外汇与贵金属杠杆高,异步漏检可能造成重复开仓,实盘前务必在策略测试器跑通。
class="type">MqlTradeResult m_result; class=class="str">"cmt">// trade request execution result ENUM_SYMBOL_CHART_MODE m_chart_mode; class=class="str">"cmt">// Price type for constructing bars ENUM_ACCOUNT_MARGIN_MODE m_margin_mode; class=class="str">"cmt">// Margin calculation mode ENUM_ORDER_TYPE_FILLING m_type_filling; class=class="str">"cmt">// Filling policy ENUM_ORDER_TYPE_TIME m_type_time; class=class="str">"cmt">// Order type per expiration class="type">int m_symbol_expiration_flags; class=class="str">"cmt">// Flags of order expiration modes for a trading object symbol class="type">ulong m_magic; class=class="str">"cmt">// Magic number class="type">class="kw">string m_symbol; class=class="str">"cmt">// Symbol class="type">class="kw">string m_comment; class=class="str">"cmt">// Comment class="type">ulong m_deviation; class=class="str">"cmt">// Slippage in points class="type">class="kw">double m_volume; class=class="str">"cmt">// Volume class="type">class="kw">datetime m_expiration; class=class="str">"cmt">// Order expiration time(for ORDER_TIME_SPECIFIED type order) class="type">bool m_async_mode; class=class="str">"cmt">// Flag of asynchronous sending of a trade request
◍ 交易对象里的账户与日志成员
在封装交易对象的类里,一组私有成员直接决定了下单逻辑能不能贴合你的账户类型与风控偏好。下面这段声明把日志级别、挂单距离、声音开关和点差倍数都暴露成字段,方便后续在 EA 里动态调整。 ENUM_LOG_LEVEL m_log_level; // 日志级别,控制报错与调试信息输出量 int m_stop_limit; // StopLimit 挂单距离,单位 point bool m_use_sound; // 交易事件是否播放声音 uint m_multiplier; // 相对 StopLevel 调整挂单/止损的点差倍数 账户模式不是写死的。GetMarginMode() 返回当前保证金计算模式,IsHedge() 仅在该模式等于 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING 时才返回 true,也就是说对锁仓账户的判断是运行时拿真实账户属性比对,而不是编译期常量。 日志与填充策略也各有一对 setter/getter。SetLogLevel() 接收 ENUM_LOG_LEVEL,GetLogLevel() 回读;SetTypeFilling() 与 GetTypeFilling() 管 ENUM_ORDER_TYPE_FILLING,决定市价单是 IOC 还是 FOK 等成交方式。 构造函数 CTradeObj::CTradeObj(void) : m_magic(0), 把魔术码默认置 0,说明对象实例化时不绑定具体策略编号,留给你在 Init 里再赋值。外汇与贵金属杠杆高、点差跳变频繁,m_multiplier 和 m_stop_limit 建议按品种实测滑点后定,别直接抄默认值。
ENUM_LOG_LEVEL m_log_level; class=class="str">"cmt">// Logging level class="type">int m_stop_limit; class=class="str">"cmt">// Distance of placing a StopLimit order in points class="type">bool m_use_sound; class=class="str">"cmt">// The flag of using sounds of the object trading events class="type">uint m_multiplier; class=class="str">"cmt">// The spread multiplier to adjust levels relative to StopLevel class=class="str">"cmt">//--- (class="num">1) Return the margin calculation mode, (class="num">2) hedge account flag ENUM_ACCOUNT_MARGIN_MODE GetMarginMode(class="type">void) const { class="kw">return this.m_margin_mode; } class="type">bool IsHedge(class="type">void) const { class="kw">return this.GetMarginMode()==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING; } class=class="str">"cmt">//--- (class="num">1) Set, (class="num">2) class="kw">return the error logging level class="type">void SetLogLevel(const ENUM_LOG_LEVEL level) { this.m_log_level=level; } ENUM_LOG_LEVEL GetLogLevel(class="type">void) const { class="kw">return this.m_log_level; } class=class="str">"cmt">//--- (class="num">1) Set, (class="num">2) class="kw">return the filling policy class="type">void SetTypeFilling(const ENUM_ORDER_TYPE_FILLING type) { this.m_type_filling=type; } ENUM_ORDER_TYPE_FILLING GetTypeFilling(class="type">void) const { class="kw">return this.m_type_filling; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CTradeObj::CTradeObj(class="type">void) : m_magic(class="num">0),
「交易对象构造时的默认参数落点」
CTradeObj 的初始化列表把下单相关的默认值一次性钉死:偏离点数 5、止损限价 0、到期时间 0、同步模式关、填充方式 FOK、挂单时效 GTC,注释字段拼上程序名加 " by DoEasy"。 构造函数体内另补了三处运行期默认值:保证金计算模式按 MQL5 取 ACCOUNT_MARGIN_MODE,MQL4 下硬编码为 RETAIL_HEDGING;点差乘数 m_multiplier 设为 1;声音开关 m_use_sound 关,日志级别锁在 LOG_LEVEL_ERROR_MSG,仅报错才输出。 想验证这套默认值是否够用,直接把 m_deviation(5) 改成 10 再跑回测,欧美盘滑点扩大时成交率可能明显提升,但外汇和贵金属属高风险品种,参数放宽不保证任何收益。
m_deviation(class="num">5), m_stop_limit(class="num">0), m_expiration(class="num">0), m_async_mode(false), m_type_filling(ORDER_FILLING_FOK), m_type_time(ORDER_TIME_GTC), m_comment(::MQLInfoString(MQL_PROGRAM_NAME)+" by DoEasy"), m_log_level(LOG_LEVEL_ERROR_MSG) class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CTradeObj::CTradeObj(class="type">void) : m_magic(class="num">0), m_deviation(class="num">5), m_stop_limit(class="num">0), m_expiration(class="num">0), m_async_mode(false), m_type_filling(ORDER_FILLING_FOK), m_type_time(ORDER_TIME_GTC), m_comment(::MQLInfoString(MQL_PROGRAM_NAME)+" by DoEasy") { class=class="str">"cmt">//--- Margin calculation mode this.m_margin_mode= ( class="macro">#ifdef __MQL5__(ENUM_ACCOUNT_MARGIN_MODE)::AccountInfoInteger(ACCOUNT_MARGIN_MODE) class="macro">#else class=class="str">"cmt">/* MQL4 */ ACCOUNT_MARGIN_MODE_RETAIL_HEDGING class="macro">#endif ); class=class="str">"cmt">//--- Spread multiplier this.m_multiplier=class="num">1; class=class="str">"cmt">//--- Set class="kw">default sounds and flags of using sounds this.m_use_sound=false; this.m_log_level=LOG_LEVEL_ERROR_MSG; this.InitSounds(); }
订单类的属性存储与索引偏移
在 MT5 的订单抽象封装里,COrder 类继承 CBaseObj(兼容层也映射到 CObject),把订单的全部属性拆成三块定长数组:整数、双精度、字符串。m_long_prop 长度为 ORDER_PROP_INTEGER_TOTAL,m_double_prop 为 ORDER_PROP_DOUBLE_TOTAL,m_string_prop 为 ORDER_PROP_STRING_TOTAL,ticket 单独用 ulong 存。 双精度与字符串属性的数组下标不是从 0 开始排的。IndexProp 的两个重载用强制转换算偏移:双精度返回 (int)property - ORDER_PROP_INTEGER_TOTAL,字符串再减去 ORDER_PROP_DOUBLE_TOTAL。这意味着枚举值本身携带了分段信息,直接当索引会越界。 开 MT5 新建 EA 把下面代码贴进类定义,编译后下个单,用 Print(IndexProp(ORDER_VOLUME)) 看返回值,应当落在 m_double_prop 的合法区间内;若你改了 ORDER_PROP_INTEGER_TOTAL 的宏,偏移要同步重算,否则读出来是脏数据。外汇与贵金属杠杆高,这类底层封装出错会在回测里静默错价。
class="macro">#class="kw">property copyright "Copyright class="num">2019, MetaQuotes Software Corp." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property strict class=class="str">"cmt">// Necessary for mql4 class="macro">#include <Object.mqh> class="macro">#include "..\..\Services\DELib.mqh" class="macro">#include "..\..\Objects\BaseObj.mqh" class COrder : class="kw">public CBaseObj { class="kw">private: class="type">ulong m_ticket; class="type">long m_long_prop[ORDER_PROP_INTEGER_TOTAL]; class="type">class="kw">double m_double_prop[ORDER_PROP_DOUBLE_TOTAL]; class="type">class="kw">string m_string_prop[ORDER_PROP_STRING_TOTAL]; class="type">int IndexProp(ENUM_ORDER_PROP_DOUBLE class="kw">property) const { class="kw">return(class="type">int)class="kw">property-ORDER_PROP_INTEGER_TOTAL; } class="type">int IndexProp(ENUM_ORDER_PROP_STRING class="kw">property) const { class="kw">return(class="type">int)class="kw">property-ORDER_PROP_INTEGER_TOTAL-ORDER_PROP_DOUBLE_TOTAL; } };
◍ 把 magic 号拆成四个可控字段
在 MT5 的 EA 工程里,magic number 常被当成单一标识,但其实可以塞进 32 位整型里做位域划分。下面这张布局把 4 字节拆成:最低 16 位是 magic 主体(ushort),往上 8 位拆成两组 4 位的 group id,最高 8 位是挂单请求 id。 这样设计后,同一个 magic 值就能同时承载策略分组与请求追踪,不用额外开全局数组去映射。比如 magic = 0x1203AB45 时,GetPendReqID 会拿到 0x12,GetGroupID1 拿到 0x03,GetGroupID2 拿到 0x0A,GetMagicID 拿到 0xAB45。 代码里的四个 getter 全是 const 内联,直接位运算返回,开销可忽略。你可以把这段抄进自己的 COrder 类,用 Print(GetGroupID1()) 在策略测试器里验证拆分是否符合预期。外汇与贵金属杠杆高,这类标识错乱可能导致平仓逻辑误伤别的策略仓位,上线前务必回测确认。
class=class="str">"cmt">// bit class="num">32|class="num">31 class="num">24|class="num">23 class="num">16|class="num">15 class="num">8|class="num">7 class="num">0| class=class="str">"cmt">//----------------------------------------------------------- class=class="str">"cmt">// byte | class="num">3 | class="num">2 | class="num">1 | class="num">0 | class=class="str">"cmt">//----------------------------------------------------------- class=class="str">"cmt">// data | class="type">uchar | class="type">uchar | class="type">class="kw">ushort | class=class="str">"cmt">//----------------------------------------------------------- class=class="str">"cmt">// descr |pend req id| id2 | id1 | magic | class=class="str">"cmt">//----------------------------------------------------------- class=class="str">"cmt">//--- Return(class="num">1) the specified magic number, the ID of(class="num">2) the first group, (class="num">3) second group, (class="num">4) pending request from the magic number value class="type">class="kw">ushort GetMagicID(class="type">void) const { class="kw">return class="type">class="kw">ushort(this.Magic() & 0xFFFF); } class="type">uchar GetGroupID1(class="type">void) const { class="kw">return class="type">uchar(this.Magic()>>class="num">16) & 0x0F; } class="type">uchar GetGroupID2(class="type">void) const { class="kw">return class="type">uchar((this.Magic()>>class="num">16) & 0xF0)>>class="num">4; } class="type">uchar GetPendReqID(class="type">void) const { class="kw">return class="type">uchar(this.Magic()>>class="num">24) & 0xFF; } class="kw">public: class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Closed parametric constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ COrder::COrder(ENUM_ORDER_STATUS order_status,const class="type">ulong ticket) { class=class="str">"cmt">//--- Save integer properties this.m_ticket=ticket; this.m_long_prop[ORDER_PROP_STATUS] = order_status;
「把订单属性塞进内部映射数组」
在封装订单对象的类里,长整型属性统一收进 m_long_prop 数组,浮点属性收进 m_double_prop 数组,这样后续读历史订单不用反复调 CTrade 接口,直接下标取数更快。 下面这段是长整型字段的批量赋值:magic、ticket、过期时间、成交填充方式、挂单时间类型、订单类型、状态、方向、关联持仓 ID、开仓原因、成交单号、成交方向、反向持仓 ID,以及三个毫秒级时间戳(开仓、平仓、持仓更新)。 浮点部分只存了四个最常用的值:开仓价、平仓价、净利润、手续费。回测时若想算单笔滑点,得自己用开平仓价减当时 spread,原结构没预留这个字段。 别把 m_long_prop 当下标随意写 代码里 ORDER_PROP_POSITION_BY_ID 和 ORDER_PROP_POSITION_ID 是两个不同枚举,混用会导致持仓关联查错,MT5 里这种 bug 不会编译报错,只会在策略测试器里悄悄算错权益曲线。
this.m_long_prop[ORDER_PROP_MAGIC] = this.OrderMagicNumber(); this.m_long_prop[ORDER_PROP_TICKET] = this.OrderTicket(); this.m_long_prop[ORDER_PROP_TIME_EXP] = this.OrderExpiration(); this.m_long_prop[ORDER_PROP_TYPE_FILLING] = this.OrderTypeFilling(); this.m_long_prop[ORDER_PROP_TYPE_TIME] = this.OrderTypeTime(); this.m_long_prop[ORDER_PROP_TYPE] = this.OrderType(); this.m_long_prop[ORDER_PROP_STATE] = this.OrderState(); this.m_long_prop[ORDER_PROP_DIRECTION] = this.OrderTypeByDirection(); this.m_long_prop[ORDER_PROP_POSITION_ID] = this.OrderPositionID(); this.m_long_prop[ORDER_PROP_REASON] = this.OrderReason(); this.m_long_prop[ORDER_PROP_DEAL_ORDER_TICKET] = this.DealOrderTicket(); this.m_long_prop[ORDER_PROP_DEAL_ENTRY] = this.DealEntry(); this.m_long_prop[ORDER_PROP_POSITION_BY_ID] = this.OrderPositionByID(); this.m_long_prop[ORDER_PROP_TIME_OPEN] = this.OrderOpenTimeMSC(); this.m_long_prop[ORDER_PROP_TIME_CLOSE] = this.OrderCloseTimeMSC(); this.m_long_prop[ORDER_PROP_TIME_UPDATE] = this.PositionTimeUpdateMSC(); class=class="str">"cmt">//--- Save real properties this.m_double_prop[this.IndexProp(ORDER_PROP_PRICE_OPEN)] = this.OrderOpenPrice(); this.m_double_prop[this.IndexProp(ORDER_PROP_PRICE_CLOSE)] = this.OrderClosePrice(); this.m_double_prop[this.IndexProp(ORDER_PROP_PROFIT)] = this.OrderProfit(); this.m_double_prop[this.IndexProp(ORDER_PROP_COMMISSION)] = this.OrderCommission();
订单快照里的魔数字段拆解
在 CTrade 派生类的订单缓存逻辑里,双精度、字符串、整型三类属性被分别塞进 m_double_prop、m_string_prop、m_long_prop 三个数组,索引由 IndexProp() 或自定义枚举定位。 下面这段是双精度与字符串的落库写法,直接对应 MT5 订单对象的原生接口: //--- Save double properties this.m_double_prop[this.IndexProp(ORDER_PROP_SWAP)] = this.OrderSwap(); this.m_double_prop[this.IndexProp(ORDER_PROP_VOLUME)] = this.OrderVolume(); this.m_double_prop[this.IndexProp(ORDER_PROP_SL)] = this.OrderStopLoss(); this.m_double_prop[this.IndexProp(ORDER_PROP_TP)] = this.OrderTakeProfit(); this.m_double_prop[this.IndexProp(ORDER_PROP_VOLUME_CURRENT)] = this.OrderVolumeCurrent(); this.m_double_prop[this.IndexProp(ORDER_PROP_PRICE_STOP_LIMIT)] = this.OrderPriceStopLimit(); //--- Save string properties this.m_string_prop[this.IndexProp(ORDER_PROP_SYMBOL)] = this.OrderSymbol(); this.m_string_prop[this.IndexProp(ORDER_PROP_COMMENT)] = this.OrderComment(); this.m_string_prop[this.IndexProp(ORDER_PROP_EXT_ID)] = this.OrderExternalID(); 逐行看:ORDER_PROP_SWAP 取隔夜利息,ORDER_PROP_VOLUME 是初始手数,ORDER_PROP_SL/TP 抓止损止盈价;VOLUME_CURRENT 记录部分平仓后的剩余量,PRICE_STOP_LIMIT 仅限 STOP_LIMIT 挂单有效。字符串段只存品种名、注释、外部 ID 三项。 整型扩展段里,ProfitInPoints() 把浮点利润换算成点数存进 ORDER_PROP_PROFIT_PT,TICKET_FROM/TO 保留对冲或拆分单的源与目标 ticket。 魔数字段被二次解析 高亮那几行从 ORDER_PROP_MAGIC 强转 uint 后喂给 GetMagicID / GetGroupID1 / GetGroupID2 / GetPendReqID,把单个 magic 拆成策略组与挂单请求标识。外汇与贵金属杠杆高,magic 分组错乱可能导致 EA 误平他人单,实盘前应在策略测试器用不同 magic 跑一遍分组隔离。
this.m_double_prop[this.IndexProp(ORDER_PROP_SWAP)] = this.OrderSwap(); this.m_double_prop[this.IndexProp(ORDER_PROP_VOLUME)] = this.OrderVolume(); this.m_double_prop[this.IndexProp(ORDER_PROP_SL)] = this.OrderStopLoss(); this.m_double_prop[this.IndexProp(ORDER_PROP_TP)] = this.OrderTakeProfit(); this.m_double_prop[this.IndexProp(ORDER_PROP_VOLUME_CURRENT)] = this.OrderVolumeCurrent(); this.m_double_prop[this.IndexProp(ORDER_PROP_PRICE_STOP_LIMIT)] = this.OrderPriceStopLimit(); class=class="str">"cmt">//--- Save class="type">class="kw">string properties this.m_string_prop[this.IndexProp(ORDER_PROP_SYMBOL)] = this.OrderSymbol(); this.m_string_prop[this.IndexProp(ORDER_PROP_COMMENT)] = this.OrderComment(); this.m_string_prop[this.IndexProp(ORDER_PROP_EXT_ID)] = this.OrderExternalID(); class=class="str">"cmt">//--- Save additional integer properties this.m_long_prop[ORDER_PROP_PROFIT_PT] = this.ProfitInPoints(); this.m_long_prop[ORDER_PROP_TICKET_FROM] = this.OrderTicketFrom(); this.m_long_prop[ORDER_PROP_TICKET_TO] = this.OrderTicketTo(); this.m_long_prop[ORDER_PROP_CLOSE_BY_SL] = this.OrderCloseByStopLoss(); this.m_long_prop[ORDER_PROP_CLOSE_BY_TP] = this.OrderCloseByTakeProfit(); this.m_long_prop[ORDER_PROP_MAGIC_ID] = this.GetMagicID((class="type">uint)this.GetProperty(ORDER_PROP_MAGIC)); this.m_long_prop[ORDER_PROP_GROUP_ID1] = this.GetGroupID1((class="type">uint)this.GetProperty(ORDER_PROP_MAGIC)); this.m_long_prop[ORDER_PROP_GROUP_ID2] = this.GetGroupID2((class="type">uint)this.GetProperty(ORDER_PROP_MAGIC)); this.m_long_prop[ORDER_PROP_PEND_REQ_ID] = this.GetPendReqID((class="type">uint)this.GetProperty(ORDER_PROP_MAGIC));
◍ CTrading 类的内部指针与请求结构
在 MT5 的 EA 架构里,CTrading 类继承自 CBaseObj,承担实际下单与请求调度的职责。它并不直接持有行情,而是靠一组受保护指针把账户、品种、市场挂单、历史成交和事件流都挂到自己身上。 protected 段里能看到 m_account 指向当前账户对象,m_symbols 管理品种集合,m_market 与 m_history 分别对接未平单和历史订单deal,m_events 负责事件回调,m_list_request 则用 CArrayObj 缓存待发请求。这种解耦让交易逻辑和底层数据保持隔离。 private 段暴露了几个值得盯的参数:m_total_try 控制重试次数,m_log_level 决定日志粒度(ENUM_LOG_LEVEL 类型),m_request 是 MqlTradeRequest 结构体实例,所有下单字段最终都塞进这里。m_is_trade_disable 为 true 时会直接阻断发单。 GetObject(void) 直接返回 this 指针,方便在复合对象链里向上取用交易实例。开 MT5 把这段类声明贴进编辑器,搜 m_request 的赋值点,就能看清一笔订单从封装到 Send 的全过程。
this.m_double_prop[this.IndexProp(ORDER_PROP_PROFIT_FULL)] = this.ProfitFull(); class=class="str">"cmt">//--- Save additional class="type">class="kw">string properties this.m_string_prop[this.IndexProp(ORDER_PROP_COMMENT_EXT)] = ""; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Trading class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CTrading : class="kw">public CBaseObj { class="kw">protected: CAccount *m_account; class=class="str">"cmt">// Pointer to the current account object CSymbolsCollection *m_symbols; class=class="str">"cmt">// Pointer to the symbol collection list CMarketCollection *m_market; class=class="str">"cmt">// Pointer to the list of the collection of market orders and positions CHistoryCollection *m_history; class=class="str">"cmt">// Pointer to the list of the collection of historical orders and deals CEventsCollection *m_events; class=class="str">"cmt">// Pointer to the event collection list CArrayObj m_list_request; class=class="str">"cmt">// List of pending requests class="kw">private: class="kw">private: CArrayInt m_list_errors; class=class="str">"cmt">// Error list class="type">bool m_is_trade_disable; class=class="str">"cmt">// Flag disabling trading class="type">bool m_use_sound; class=class="str">"cmt">// The flag of using sounds of the object trading events class="type">uchar m_total_try; class=class="str">"cmt">// Number of trading attempts ENUM_LOG_LEVEL m_log_level; class=class="str">"cmt">// Logging level class="type">MqlTradeRequest m_request; class=class="str">"cmt">// Trading request prices ENUM_TRADE_REQUEST_ERR_FLAGS m_error_reason_flags; class=class="str">"cmt">// Flags of error source in a trading method ENUM_ERROR_HANDLING_BEHAVIOR m_err_handling_behavior; class=class="str">"cmt">// Behavior when handling error class="kw">public: class=class="str">"cmt">//--- Return itself CTrading *GetObject(class="type">void) { class="kw">return &this; } class=class="str">"cmt">//--- Constructor CTrading(); class=class="str">"cmt">//--- Timer
「用魔术码字段塞进挂单请求标识」
EA 里想给一笔挂单同时带上「分组一 / 分组二 / 挂单请求 ID」又不想另开文件记录,直接榨干魔术码(magic number)的 32 位整型空间就行。 原设计把 32 位按字节切开:高 8 位是挂单请求 ID,往下两个 8 位分别是第二、第一组 ID,最低 16 位是传统 magic 标识。这样同一次批量挂单里的单子,靠 magic 就能反查出它属于哪个逻辑组、第几次重试请求。 定时器回调 OnTimer() 是处理异步挂单状态轮询的入口,必须在 OnInit() 里先拿到符号列表指针,否则集合还没建好会空引用。 CreatePendingRequest() 负责往队列里塞一条待处理请求,参数含状态、请求 ID、重试次数、等待毫秒数、交易请求结构体、返回码、关联的符号与订单对象。下面这段是写入第一组 ID 的位操作,先清掉对应字节再左移填入: void SetGroupID1(const uchar group,uint &magic) { magic &= 0xFFF0FFFF; magic |= uint(ConvToXX(group,0)<<16); } 把 group 写进 magic 第 16~23 位后,后续在订单池里用 OrderMagicNumber() 读出来右移 16 位就能还原分组,省掉一张映射表。外汇与贵金属杠杆交易风险高,这种自管 ID 方案在断线重连时可能因未成交单状态丢失而错组,实盘前请在 MT5 策略测试器用历史 tick 跑一遍分组还原逻辑。
class="kw">virtual class="type">void OnTimer(class="type">void); class=class="str">"cmt">//--- Get the pointers to the lists(make sure to call the method in program&class="macro">#x27;s OnInit() since the symbol collection list is created there) class=class="str">"cmt">//--- Create a pending request class="type">bool CreatePendingRequest(const ENUM_PEND_REQ_STATUS status, const class="type">uchar id, const class="type">uchar attempts, const class="type">ulong wait, const class="type">MqlTradeRequest &request, const class="type">int retcode, CSymbol *symbol_obj, COrder *order); class=class="str">"cmt">//--- Data location in the magic number class="type">int value class=class="str">"cmt">//----------------------------------------------------------- class=class="str">"cmt">// bit class="num">32|class="num">31 class="num">24|class="num">23 class="num">16|class="num">15 class="num">8|class="num">7 class="num">0| class=class="str">"cmt">//----------------------------------------------------------- class=class="str">"cmt">// byte | class="num">3 | class="num">2 | class="num">1 | class="num">0 | class=class="str">"cmt">//----------------------------------------------------------- class=class="str">"cmt">// data | class="type">uchar | class="type">uchar | class="type">class="kw">ushort | class=class="str">"cmt">//----------------------------------------------------------- class=class="str">"cmt">// descr |pend req id| id2 | id1 | magic | class=class="str">"cmt">//----------------------------------------------------------- class=class="str">"cmt">//--- Set the ID of the(class="num">1) first group, (class="num">2) second group, (class="num">3) pending request to the magic number value class="type">void SetGroupID1(const class="type">uchar group,class="type">uint &magic) { magic &=0xFFF0FFFF; magic |= class="type">uint(ConvToXX(group,class="num">0)<<class="num">16); }
把分组与挂单请求塞进 magic 的位域
EA 里 magic number 不只是订单识别码,还能当紧凑的结构体用。下面这套写法把 32 位 uint 拆成四段:低 16 位是基础 magic ID,16~19 位放第一组、20~23 位放第二组、24~31 位放挂单请求 ID,单靠位运算就能在订单上挂多维标签。 SetGroupID2 先拿 magic &= 0xFF0FFFFF 清掉 20~23 位,再把 ConvToXX 算出的 4 位值左移 16 填进去;SetPendReqID 则用 0x00FFFFFF 清高 8 位后左移 24 写入。ConvToXX 把 0~15 的数按 index 左移 0 或 4 位,超 15 会被截断成 15,避免越界污染相邻位段。 回读时 GetGroupID1 取 magic>>16 再 &0x0F,GetGroupID2 取 (magic>>16)&0xF0 再右移 4,GetPendReqID 直接取高 8 位。外汇与贵金属杠杆高、滑点突变频繁,用位域压缩标签可减少全局变量依赖,但多 EA 同跑时要先核对 magic 布局冲突,否则可能误改别人订单。 OnTimer 目前是空壳,说明计时器回调留了扩展位却还没接逻辑;真要轮询挂单状态,可以在里面调 GetPendReqID 做超时回收。
class="type">void SetGroupID2(const class="type">uchar group,class="type">uint &magic) { magic &=0xFF0FFFFF; magic |= class="type">uint(ConvToXX(group,class="num">1)<<class="num">16); } class="type">void SetPendReqID(const class="type">uchar id,class="type">uint &magic) { magic &=0x00FFFFFF; magic |= (class="type">uint)id<<class="num">24; } class=class="str">"cmt">//--- Convert the value of class="num">0 - class="num">15 into the necessary class="type">uchar number bits(class="num">0 - lower, class="num">1 - upper ones) class="type">uchar ConvToXX(const class="type">uchar number,const class="type">uchar index) const { class="kw">return((number>class="num">15 ? class="num">15 : number)<<(class="num">4*(index>class="num">1 ? class="num">1 : index)));} class=class="str">"cmt">//--- Return(class="num">1) the specified magic number, the ID of(class="num">2) the first group, (class="num">3) second group, (class="num">4) pending request from the magic number value class="type">class="kw">ushort GetMagicID(const class="type">uint magic) const { class="kw">return class="type">class="kw">ushort(magic & 0xFFFF); } class="type">uchar GetGroupID1(const class="type">uint magic) const { class="kw">return class="type">uchar(magic>>class="num">16) & 0x0F; } class="type">uchar GetGroupID2(const class="type">uint magic) const { class="kw">return class="type">uchar((magic>>class="num">16) & 0xF0)>>class="num">4; } class="type">uchar GetPendReqID(const class="type">uint magic) const { class="kw">return class="type">uchar(magic>>class="num">24) & 0xFF; }
◍ 手数边界与只平模式的拦截逻辑
EA 在下单前必须先把品种交易权限和手数边界摸清楚,否则请求发到服务器也会被拒。若品种处于 SYMBOL_TRADE_MODE_CLOSEONLY(只平模式),代码直接把错误旗标写进列表并返回 false,后续检查毫无意义,省下的开销在高频循环里很可观。 手数大于 0 时才进入边界校验:低于 LotsMin() 或高于 LotsMax() 都会往 m_error_reason_flags 打上 TRADE_REQUEST_ERR_FLAG_ERROR_IN_LIST,并调用 AddErrorCodeToList 记录具体文案。这里 behavior 分叉——若设为 ERROR_HANDLING_BEHAVIOR_BREAK 直接 return false 终止;若是修正参数或挂起请求模式,则对 res 做 &=false 仅标记失败不中断。 最小手数梯度用 symbol_obj.LotsStep() 单独取出,下一步拿去对手数做取整对齐。外汇与贵金属杠杆高、点值敏感,手数越界或步长没对齐都可能让订单直接被经纪商踢回,开 MT5 在品种规格里核对 LotsMin / LotsMax / LotsStep 三个字段再跑这套校验最稳妥。
class=class="str">"cmt">//--- In case of close-only, write the error code to the list and class="kw">return &class="macro">#x27;false&class="macro">#x27; - there is no point in further checks if(symbol_obj.TradeMode()==SYMBOL_TRADE_MODE_CLOSEONLY) { this.m_error_reason_flags=TRADE_REQUEST_ERR_FLAG_ERROR_IN_LIST; this.AddErrorCodeToList(MSG_SYM_TRADE_MODE_CLOSEONLY); class="kw">return false; } class=class="str">"cmt">//--- Check the minimum volume if(volume>class="num">0) { if(volume<symbol_obj.LotsMin()) { class=class="str">"cmt">//--- The volume in a request is less than the minimum allowed one. class=class="str">"cmt">//--- add the error code to the list this.m_error_reason_flags |=TRADE_REQUEST_ERR_FLAG_ERROR_IN_LIST; this.AddErrorCodeToList(MSG_LIB_TEXT_REQ_VOL_LESS_MIN_VOLUME); class=class="str">"cmt">//--- If the EA behavior during the trading error is set to "abort trading operation", class=class="str">"cmt">//--- class="kw">return &class="macro">#x27;false&class="macro">#x27; - there is no point in further checks if(this.m_err_handling_behavior==ERROR_HANDLING_BEHAVIOR_BREAK) class="kw">return false; class=class="str">"cmt">//--- If the EA behavior during a trading error is set to class=class="str">"cmt">//--- "correct parameters" or "create a pending request", class=class="str">"cmt">//--- write &class="macro">#x27;false&class="macro">#x27; to the result else res &=false; } class=class="str">"cmt">//--- Check the maximum volume else if(volume>symbol_obj.LotsMax()) { class=class="str">"cmt">//--- The volume in a request exceeds the maximum acceptable one. class=class="str">"cmt">//--- add the error code to the list this.m_error_reason_flags |=TRADE_REQUEST_ERR_FLAG_ERROR_IN_LIST; this.AddErrorCodeToList(MSG_LIB_TEXT_REQ_VOL_MORE_MAX_VOLUME); class=class="str">"cmt">//--- If the EA behavior during the trading error is set to "abort trading operation", class=class="str">"cmt">//--- class="kw">return &class="macro">#x27;false&class="macro">#x27; - there is no point in further checks if(this.m_err_handling_behavior==ERROR_HANDLING_BEHAVIOR_BREAK) class="kw">return false; class=class="str">"cmt">//--- If the EA behavior during a trading error is set to class=class="str">"cmt">//--- "correct parameters" or "create a pending request", class=class="str">"cmt">//--- write &class="macro">#x27;false&class="macro">#x27; to the result else res &=false; } class=class="str">"cmt">//--- Check the minimum volume gradation class="type">class="kw">double step=symbol_obj.LotsStep();
「手数步进校验与挂单等待的实际处理」
在发送开仓请求前,EA 会先核对成交量是否是最小手数步进的整数倍。核心判断是 fabs((int)round(volume/step)*step - volume) > 0.0000001,若偏差超过这个极小阈值,就认为手数不合规,把错误标志位 TRADE_REQUEST_ERR_FLAG_ERROR_IN_LIST 打上,并写入无效步进的提示码。 错误处置分三种行为:设为 BREAK 时直接 return false 不再往下检查;设为修正参数或创建挂单请求时,仅把本次检查结果 res 置为 false,后续逻辑仍可能尝试纠正。 当检查结果为“等待”状态(method > ERROR_CODE_PROCESSING_METHOD_REFRESH)且对应 magic 没有挂单 ID 时,系统会先播错误音(若开启声音),再从错误列表取最后一条码写进返回结构。若码是终端或 EA 禁止交易类,会统一映射成 10027 这个终端返回码。 等待时长直接取 method 值(毫秒),用于“等待并重试”类处理;若找不到可用挂单 ID 或刷新品种数据出错,函数返回 false,开仓流程在此中断。外汇与贵金属杠杆高,这类手数校验失效可能直接引发拒单,建议在 MT5 策略测试器里用极端手数(如 0.12345)跑一遍确认映射逻辑。
if(fabs((class="type">int)round(volume/step)*step-volume)>class="num">0.0000001) { class=class="str">"cmt">//--- The volume in the request is not a multiple of the minimum gradation of the lot change step class=class="str">"cmt">//--- add the error code to the list this.m_error_reason_flags |=TRADE_REQUEST_ERR_FLAG_ERROR_IN_LIST; this.AddErrorCodeToList(MSG_LIB_TEXT_INVALID_VOLUME_STEP); class=class="str">"cmt">//--- If the EA behavior during the trading error is set to "abort trading operation", class=class="str">"cmt">//--- class="kw">return &class="macro">#x27;false&class="macro">#x27; - there is no point in further checks if(this.m_err_handling_behavior==ERROR_HANDLING_BEHAVIOR_BREAK) class="kw">return false; class=class="str">"cmt">//--- If the EA behavior during a trading error is set to class=class="str">"cmt">//--- "correct parameters" or "create a pending request", class=class="str">"cmt">//--- write &class="macro">#x27;false&class="macro">#x27; to the result else res &=false; } } } class=class="str">"cmt">//--- When opening a position class=class="str">"cmt">//--- If the check result is "waiting" - set the last error code to the class="kw">return structure and display the message in the journal, class=class="str">"cmt">//--- create a pending request and class="kw">return &class="macro">#x27;false&class="macro">#x27; (OpenPosition) if(method>ERROR_CODE_PROCESSING_METHOD_REFRESH) { class=class="str">"cmt">//--- If the trading request magic number, has no pending request ID if(this.GetPendReqID((class="type">uint)magic)==class="num">0) { class=class="str">"cmt">//--- Play the error sound if(this.IsUseSounds()) trade_obj.PlaySoundError(action,order_type); class=class="str">"cmt">//--- set the last error code to the class="kw">return structure class="type">int code=this.m_list_errors.At(this.m_list_errors.Total()-class="num">1); if(code!=NULL) { if(code==MSG_LIB_TEXT_TERMINAL_NOT_TRADE_ENABLED || code==MSG_LIB_TEXT_EA_NOT_TRADE_ENABLED) code=class="num">10027; trade_obj.SetResultRetcode(code); trade_obj.SetResultComment(CMessage::Text(trade_obj.GetResultRetcode())); } class=class="str">"cmt">//--- Waiting time in milliseconds: class=class="str">"cmt">//--- for the "Wait and repeat" handling method, the waiting value corresponds to the &class="macro">#x27;method&class="macro">#x27; value, class="type">ulong wait=method; class=class="str">"cmt">//--- Look for the least of the possible IDs. If failed to find class=class="str">"cmt">//--- or in case of an error while updating the current symbol data, class="kw">return &class="macro">#x27;false&class="macro">#x27;
挂单请求对象的状态回填逻辑
在修改持仓(ModifyPosition)流程里,当服务端返回的检查结果处于“等待”状态,且处理方法标记大于 ERROR_CODE_PROCESSING_METHOD_REFRESH 时,引擎不会直接放弃,而是先确认该持仓 ticket 是否已在挂单请求列表中。若 GetIndexPendingRequestByPosition(ticket) 返回 WRONG_VALUE,说明列表里还没有这条持仓的挂单对象,此时才会进入补建逻辑。 补建前若开启了声音告警(IsUseSounds() 为真),会按动作类型、订单方向、是否带止损止盈调用 PlaySoundError 播错误提示音。随后从错误列表尾部取最近一条错误码,若非空且属于“终端未允许交易”或“EA 未允许交易”这类,则强制改写为 10027(对应 TRADE_RETCODE_TRADE_DISABLED),再通过 SetResultRetcode 与 SetResultComment 写回交易对象。 这一段里有个容易忽略的细节:错误码 10027 是硬编码替换,而不是透传原始文案。你在 MT5 里跑自己的修改持仓封装时,如果日志里突然出现 10027 却没看到终端禁交易弹窗,大概率就是这类内部映射在起作用,而不是真被经纪商断了交易权限。 前面那段开仓分支也值得对照看:先取 GetFreeID(),若 id<1 或 RefreshRates() 失败直接返回 false;否则把挂单请求 ID 写进魔术码,填充 m_request 的 action、symbol、type 等字段,最后按 m_total_try 下限为 1 的次数调用 CreatePendingRequest 落一条待处理请求。两套分支的共同点,是把“实时失败”转成“挂起重试”而不是当场丢单。
class="type">int id=this.GetFreeID(); if(id<class="num">1 || !symbol_obj.RefreshRates()) class="kw">return false; class=class="str">"cmt">//--- Write the pending request object ID to the magic number and fill in the remaining unfilled fields of the trading request structure class="type">uint mn=(magic==ULONG_MAX ? (class="type">uint)trade_obj.GetMagic() : (class="type">uint)magic); this.SetPendReqID((class="type">uchar)id,mn); this.m_request.magic=mn; this.m_request.action=TRADE_ACTION_DEAL; this.m_request.symbol=symbol_obj.Name(); this.m_request.type=order_type; class=class="str">"cmt">//--- Set the number of trading attempts and create a pending request class="type">uchar attempts=(this.m_total_try < class="num">1 ? class="num">1 : this.m_total_try); this.CreatePendingRequest(PEND_REQ_STATUS_OPEN,(class="type">uchar)id,attempts,wait,this.m_request,trade_obj.GetResultRetcode(),symbol_obj,NULL); } class="kw">return false; } class=class="str">"cmt">//--- If the check result is "waiting" - set the last error code to the class="kw">return structure and display the message in the journal, class=class="str">"cmt">//--- create a pending request and class="kw">return &class="macro">#x27;false&class="macro">#x27; (ModifyPosition) if(method>ERROR_CODE_PROCESSING_METHOD_REFRESH) { class=class="str">"cmt">//--- If the pending request object with the position ticket is not present in the list if(this.GetIndexPendingRequestByPosition(ticket)==WRONG_VALUE) { class=class="str">"cmt">//--- Play the error sound if(this.IsUseSounds()) trade_obj.PlaySoundError(action,order_type,(sl<class="num">0 ? false : true),(tp<class="num">0 ? false : true)); class=class="str">"cmt">//--- set the last error code to the class="kw">return structure class="type">int code=this.m_list_errors.At(this.m_list_errors.Total()-class="num">1); if(code!=NULL) { if(code==MSG_LIB_TEXT_TERMINAL_NOT_TRADE_ENABLED || code==MSG_LIB_TEXT_EA_NOT_TRADE_ENABLED) code=class="num">10027; trade_obj.SetResultRetcode(code); trade_obj.SetResultComment(CMessage::Text(trade_obj.GetResultRetcode())); } class=class="str">"cmt">//--- Waiting time in milliseconds:
◍ 挂单改仓请求的字段装配逻辑
在「Wait and repeat」处理模式下,等待值直接复用 method 变量,即 ulong wait=method;,这意味着重试间隔由错误处理方法枚举决定,而非另设定时器。 获取最小可用 ID 时调用 this.GetFreeID(),若返回值小于 1 或 symbol_obj.RefreshRates() 刷新报价失败,函数立即 return false。这一道闸避免了在行情断流时发出无效改 SL/TP 请求。 请求结构体按字段逐一写入:action 固定为 TRADE_ACTION_SLTP,symbol 与 position 取自持仓对象,volume 以 order.Volume() 为准。注意下面这行对手数做了硬约束——
| this.m_request.volume=(volume==WRONG_VALUE | volume>order.Volume() ? order.Volume() : symbol_obj.NormalizedLot(volume)); |
|---|
即外部传入手数非法或超限时,自动回退到原持仓手数并经 NormalizedLot 规范化,防止超量改仓。 错误预检分两次:先 CheckErrors(volume,0,...) 查交易与冻结层限制,再 CheckErrors(0,this.m_request.price,...) 查 StopLevel/FreezeLevel。任一返回非 OK,即播放错误音并退出,不进入实际发单。外汇与贵金属杠杆高、点差跳变频繁,这类预检能显著降低滑点之外的违规拒单概率。 magic 与 type_filling 等字段优先用入参,缺失时回落到 order 原值: this.m_request.magic=order.GetMagicID((uint)order.Magic()); this.m_request.type_filling=(type_filling>WRONG_VALUE ? type_filling : order.TypeFilling()); 在 MT5 里把这段塞进你自己的 CTrade 封装类,改仓前断点看一下 m_request 结构,能确认字段回落是否符合预期。
class="type">ulong wait=method; class="type">int id=this.GetFreeID(); if(id<class="num">1 || !symbol_obj.RefreshRates()) class="kw">return false; this.m_request.action=TRADE_ACTION_SLTP; this.m_request.symbol=symbol_obj.Name(); this.m_request.position=ticket; this.m_request.type=order_type; this.m_request.volume=order.Volume(); class="type">uchar attempts=(this.m_total_try < class="num">1 ? class="num">1 : this.m_total_try); this.CreatePendingRequest(PEND_REQ_STATUS_SLTP,(class="type">uchar)id,attempts,wait,this.m_request,trade_obj.GetResultRetcode(),symbol_obj,order); this.m_request.deviation=(deviation==ULONG_MAX ? trade_obj.GetDeviation() : deviation); this.m_request.comment=(comment==NULL ? trade_obj.GetComment() : comment); this.m_request.volume=(volume==WRONG_VALUE || volume>order.Volume() ? order.Volume() : symbol_obj.NormalizedLot(volume)); ENUM_ERROR_CODE_PROCESSING_METHOD method=this.CheckErrors(volume,class="num">0,action,order_type,symbol_obj,trade_obj,DFUN,class="num">0,class="num">0,class="num">0,ticket); if(method!=ERROR_CODE_PROCESSING_METHOD_OK) { this.m_request.magic=order.GetMagicID((class="type">uint)order.Magic()); this.m_request.volume=order.Volume(); this.m_request.type_filling=(type_filling>WRONG_VALUE ? type_filling : order.TypeFilling()); this.m_request.expiration=(expiration>WRONG_VALUE ? expiration : order.TimeExpiration()); this.m_request.type_time=(type_time>WRONG_VALUE ? type_time : order.TypeTime()); ENUM_ERROR_CODE_PROCESSING_METHOD method=this.CheckErrors(class="num">0,this.m_request.price,action,order_type,symbol_obj,trade_obj,DFUN,this.m_request.stoplimit,this.m_request.sl,this.m_request.tp,ticket);
「挂单请求对象的工厂分支」
在交易类里,CreatePendingRequest 承担的是一个工厂角色:根据传入的 ENUM_PEND_REQ_STATUS,把一笔 MqlTradeRequest 包装成对应类型的挂起请求对象。 switch 里覆盖了 6 种状态——OPEN、CLOSE、SLTP、PLACE、REMOVE、MODIFY,各自 new 出 CPendReqOpen 到 CPendReqModify 的实例,并带入 symbol_obj.BidLast() 与 symbol_obj.Time() 作为快照。若 status 不在枚举范围内,req_obj 保持 NULL,函数随后走失败分支。 对象创建后还要过 Add 这一关:m_list_request.Add(req_obj) 返回 false 时,说明链表写入失败,此时若日志级别高于 LOG_LEVEL_NO_MSG 会打印失败文本并直接 return false,已建的 req_obj 不会被加入管理列表。 在 MT5 里验证时,可故意传一个越界 status 值,观察是否命中 default 且返回 false,以此确认工厂的边界处理没有静默吞错。
class="type">bool CTrading::CreatePendingRequest(const ENUM_PEND_REQ_STATUS status, const class="type">uchar id, const class="type">uchar attempts, const class="type">ulong wait, const class="type">MqlTradeRequest &request, const class="type">int retcode, CSymbol *symbol_obj, COrder *order) { class=class="str">"cmt">//--- Create a new pending request object depending on a request status CPendRequest *req_obj=NULL; class="kw">switch(status) { case PEND_REQ_STATUS_OPEN : req_obj=new CPendReqOpen(id,symbol_obj.BidLast(),symbol_obj.Time(),request,retcode); class="kw">break; case PEND_REQ_STATUS_CLOSE : req_obj=new CPendReqClose(id,symbol_obj.BidLast(),symbol_obj.Time(),request,retcode); class="kw">break; case PEND_REQ_STATUS_SLTP : req_obj=new CPendReqSLTP(id,symbol_obj.BidLast(),symbol_obj.Time(),request,retcode); class="kw">break; case PEND_REQ_STATUS_PLACE : req_obj=new CPendReqPlace(id,symbol_obj.BidLast(),symbol_obj.Time(),request,retcode); class="kw">break; case PEND_REQ_STATUS_REMOVE : req_obj=new CPendReqRemove(id,symbol_obj.BidLast(),symbol_obj.Time(),request,retcode); class="kw">break; case PEND_REQ_STATUS_MODIFY : req_obj=new CPendReqModify(id,symbol_obj.BidLast(),symbol_obj.Time(),request,retcode); class="kw">break; class="kw">default: req_obj=NULL; class="kw">break; } if(req_obj==NULL) { if(this.m_log_level>LOG_LEVEL_NO_MSG) ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_FAILING_CREATE_PENDING_REQ)); class="kw">return false; } class=class="str">"cmt">//--- If failed to add the request to the list, display the appropriate message, class=class="str">"cmt">//--- remove the created object and class="kw">return &class="macro">#x27;false&class="macro">#x27; if(!this.m_list_request.Add(req_obj)) { if(this.m_log_level>LOG_LEVEL_NO_MSG)
挂单请求对象的字段回填逻辑
创建挂单请求对象失败时会先打印失败信息、释放内存并返回 false,这是 MT5 EA 里常见的防御式写法,能避免野指针在后续定时器里被重复访问。 对象创建成功后,代码按「激活时间 = 品种当前时间 + 等待毫秒数、当前尝试次数置 0、总尝试次数写入 attempts」的顺序初始化基础属性。若传入了已存在的 order 对象,则把它的成交量、开仓价、止损限价、SL/TP、成交类型、时间类型与过期时间逐一回填;否则改用原始 request 结构体里的对应字段。 日志级别高于 LOG_LEVEL_NO_MSG 时,会输出「已创建挂单请求 #ID」并调用 PrintShort 打印简况,方便在「小布盯盘」里对照 AIGC 标注的挂单轨迹。外汇与贵金属杠杆高,这类异步挂单重试机制只降低漏单概率,不保证成交。 下面这段是上述回填分支的 MQL5 原文,注意 if(order!=NULL) 与 else 两套 SetActual 映射必须字段对齐,否则回测时可能静默丢参。
::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_FAILING_CREATE_PENDING_REQ)); class="kw">delete req_obj; class="kw">return false; } class=class="str">"cmt">//--- Fill in the properties of a successfully created object by the values passed to the method req_obj.SetTimeActivate(symbol_obj.Time()+wait); req_obj.SetWaitingMSC(wait); req_obj.SetCurrentAttempt(class="num">0); req_obj.SetTotalAttempts(attempts); if(order!=NULL) { req_obj.SetActualVolume(order.Volume()); req_obj.SetActualPrice(order.PriceOpen()); req_obj.SetActualStopLimit(order.PriceStopLimit()); req_obj.SetActualSL(order.StopLoss()); req_obj.SetActualTP(order.TakeProfit()); req_obj.SetActualTypeFilling(order.TypeFilling()); req_obj.SetActualTypeTime(order.TypeTime()); req_obj.SetActualExpiration(order.TimeExpiration()); } else { req_obj.SetActualVolume(request.volume); req_obj.SetActualPrice(request.price); req_obj.SetActualStopLimit(request.stoplimit); req_obj.SetActualSL(request.sl); req_obj.SetActualTP(request.tp); req_obj.SetActualTypeFilling(request.type_filling); req_obj.SetActualTypeTime(request.type_time); req_obj.SetActualExpiration(request.expiration); } class=class="str">"cmt">//--- Display a brief description of a created pending request if(this.m_log_level>LOG_LEVEL_NO_MSG) { ::Print(CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_CREATED)," #",req_obj.ID(),":"); req_obj.PrintShort(); } class=class="str">"cmt">//--- successful class="kw">return true; }
◍ 挂单请求的整数属性枚举怎么读
在 MT5 的 pending trading request 体系里,所有整数类属性都被收进一个枚举 ENUM_PEND_REQ_PROP_INTEGER,方便用统一接口按编号取状态。 从 PEND_REQ_PROP_STATUS 到 PEND_REQ_PROP_MQL_REQ_MAGIC,一共定义了 17 个整数项。前 10 项描述请求自身生命周期:状态、类型、ID、返回码、创建时间、下次激活时间、重试间隔、当前尝试序号、总尝试次数,以及三个带 ACTUAL 前缀的真实成交参数(填充方式、过期类型、存活时长)。 后 4 项直接映射 MqlTradeRequest 结构里的关键字段:ACTION 是请求动作类型,TYPE 是订单类型,MAGIC 是 EA 的魔术码。写监控脚本时,用 PEND_REQ_PROP_CURRENT_ATTEMPT 配合 PEND_REQ_PROP_TOTAL 就能在日志里打印出『第 3 / 5 次重试』这类进度。 别把枚举顺序当稳定契约 高版本终端可能在中部插入新属性,硬编码下标取值会比用枚举名更脆弱;遍历属性时优先用 EnumToString 做名称匹配。
enum ENUM_PEND_REQ_PROP_INTEGER { PEND_REQ_PROP_STATUS = class="num">0, class=class="str">"cmt">// 交易请求状态(取自 ENUM_PEND_REQ_STATUS 枚举) PEND_REQ_PROP_TYPE, class=class="str">"cmt">// 交易请求类型(取自 ENUM_PEND_REQ_TYPE 枚举) PEND_REQ_PROP_ID, class=class="str">"cmt">// 交易请求 ID PEND_REQ_PROP_RETCODE, class=class="str">"cmt">// 请求所基于的结果码 PEND_REQ_PROP_TIME_CREATE, class=class="str">"cmt">// 请求创建时间 PEND_REQ_PROP_TIME_ACTIVATE, class=class="str">"cmt">// 下一次尝试激活时间 PEND_REQ_PROP_WAITING, class=class="str">"cmt">// 请求之间的等待时间 PEND_REQ_PROP_CURRENT_ATTEMPT, class=class="str">"cmt">// 当前尝试索引 PEND_REQ_PROP_TOTAL, class=class="str">"cmt">// 尝试总次数 PEND_REQ_PROP_ACTUAL_TYPE_FILLING, class=class="str">"cmt">// 实际订单填充类型 PEND_REQ_PROP_ACTUAL_TYPE_TIME, class=class="str">"cmt">// 实际订单过期类型 PEND_REQ_PROP_ACTUAL_EXPIRATION, class=class="str">"cmt">// 实际订单存活时长 class=class="str">"cmt">//--- class="type">MqlTradeRequest PEND_REQ_PROP_MQL_REQ_ACTION, class=class="str">"cmt">// 请求结构中所执行操作的类型 PEND_REQ_PROP_MQL_REQ_TYPE, class=class="str">"cmt">// 请求结构中的订单类型 PEND_REQ_PROP_MQL_REQ_MAGIC class=class="str">"cmt">// 请求结构中 EA 的魔术码 ID };
「挂单请求里的双精度字段从哪开始排」
在 MT5 的挂单请求枚举里,整型属性先占满编号,双精度属性接着排。整型总数由宏写死为 22,即 PEND_REQ_PROP_INTEGER_TOTAL 等于 22,排序跳过数为 0。 双精度枚举 ENUM_PEND_REQ_PROP_DOUBLE 的首项 PEND_REQ_PROP_PRICE_CREATE 直接等于前面的整型总数 22,也就是说双精度属性从索引 22 往后接续,不会和整型字段撞号。 后续几个 ACTUAL 系字段记录的是请求落地时的真实数值:PEND_REQ_PROP_ACTUAL_VOLUME 为实际成交量,PEND_REQ_PROP_ACTUAL_PRICE 为实际挂单价格,PEND_REQ_PROP_ACTUAL_STOPLIMIT 为 stoplimit 实际触发价,PEND_REQ_PROP_ACTUAL_SL 为实际止损价。开 MT5 写 EA 时若想读成交瞬间的真实滑点,应从索引 22 起的这几个双精度字段取,而非请求结构里的初值。外汇与贵金属杠杆高,实际成交价与请求价可能有偏差,读取 ACTUAL 系字段可辅助复盘滑点分布。
enum ENUM_PEND_REQ_PROP_DOUBLE { PEND_REQ_PROP_PRICE_CREATE = PEND_REQ_PROP_INTEGER_TOTAL, class=class="str">"cmt">// Price at the moment of a request generation PEND_REQ_PROP_ACTUAL_VOLUME, class=class="str">"cmt">// Actual volume PEND_REQ_PROP_ACTUAL_PRICE, class=class="str">"cmt">// Actual order price PEND_REQ_PROP_ACTUAL_STOPLIMIT, class=class="str">"cmt">// Actual stoplimit order price PEND_REQ_PROP_ACTUAL_SL, class=class="str">"cmt">// Actual stoploss order price };
挂单请求结构里的双精度属性与排序起点
在挂单请求的事件封装里,双精度(double)类属性被单独枚举,便于后续按数值字段做筛选与排序。其中 PEND_REQ_PROP_ACTUAL_TP 代表挂单实际生效的止盈价,而 MqlTradeRequest 结构体内的 VOLUME、PRICE、STOPLIMIT、SL、TP 也各占一个 double 槽位。 宏定义给出了两个关键计数:PEND_REQ_PROP_DOUBLE_TOTAL 为 11,即事件实际使用的双精度属性总数;PEND_REQ_PROP_DOUBLE_SKIP 为 0,说明没有哪项是排序时要跳过的。这两个数字直接决定了后续枚举偏移的计算。 排序枚举 ENUM_SORT_PEND_REQ_MODE 的起点由 FIRST_PREQ_DBL_PROP 与 FIRST_PREQ_STR_PROP 控制,它们基于整型属性总数减去跳过数、再叠加重双精度段得到。整型段里 SORT_BY_PEND_REQ_STATUS 到 SORT_BY_PEND_REQ_TIME_ACTIVATE 共 6 个枚举值,分别对应请求状态、类型、ID、返回码、创建时间与下次激活时间——在 MT5 里打开这类头文件,改一处 TOTAL 宏就可能让排序索引整体错位,验证时建议先打印 FIRST_PREQ_DBL_PROP 的编译结果。
PEND_REQ_PROP_ACTUAL_TP, class=class="str">"cmt">// Actual takeprofit order price class=class="str">"cmt">//--- class="type">MqlTradeRequest PEND_REQ_PROP_MQL_REQ_VOLUME, class=class="str">"cmt">// Requested volume of a deal in lots in the request structure PEND_REQ_PROP_MQL_REQ_PRICE, class=class="str">"cmt">// Price in the request structure PEND_REQ_PROP_MQL_REQ_STOPLIMIT, class=class="str">"cmt">// StopLimit level in the request structure PEND_REQ_PROP_MQL_REQ_SL, class=class="str">"cmt">// Stop Loss level in the request structure PEND_REQ_PROP_MQL_REQ_TP, class=class="str">"cmt">// Take Profit level in the request structure }; class="macro">#define PEND_REQ_PROP_DOUBLE_TOTAL(class="num">11) class=class="str">"cmt">// Total number of event&class="macro">#x27;s real properties class="macro">#define PEND_REQ_PROP_DOUBLE_SKIP(class="num">0) class=class="str">"cmt">// Number of order properties not used in sorting class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Possible pending request sorting criteria | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define FIRST_PREQ_DBL_PROP(PEND_REQ_PROP_INTEGER_TOTAL-PEND_REQ_PROP_INTEGER_SKIP) class="macro">#define FIRST_PREQ_STR_PROP(PEND_REQ_PROP_INTEGER_TOTAL-PEND_REQ_PROP_INTEGER_SKIP+PEND_REQ_PROP_DOUBLE_TOTAL-PEND_REQ_PROP_DOUBLE_SKIP) enum ENUM_SORT_PEND_REQ_MODE { class=class="str">"cmt">//--- Sort by integer properties SORT_BY_PEND_REQ_STATUS = class="num">0, class=class="str">"cmt">// Sort by a trading request status(from the ENUM_PEND_REQ_STATUS enumeration) SORT_BY_PEND_REQ_TYPE, class=class="str">"cmt">// Sort by a trading request type(from the ENUM_PEND_REQ_TYPE enumeration) SORT_BY_PEND_REQ_ID, class=class="str">"cmt">// Sort by a trading request ID SORT_BY_PEND_REQ_RETCODE, class=class="str">"cmt">// Sort by a result a request is based on SORT_BY_PEND_REQ_TIME_CREATE, class=class="str">"cmt">// Sort by a request generation time SORT_BY_PEND_REQ_TIME_ACTIVATE, class=class="str">"cmt">// Sort by next attempt activation time
◍ 挂单请求队列的排序枚举拆解
在 MQL5 的挂单请求管理逻辑里,系统用一组枚举值决定 pending request 的排列先后。这些枚举覆盖等待耗时、重试次数、当前尝试序号等基础维度,也包含实际成交类型与过期方式等执行期属性。 下面这段枚举定义里,高亮部分是实际订单属性相关的排序键,其余则是请求结构(MqlTradeRequest)里的字段映射。注意 SORT_BY_PEND_REQ_PRICE_CREATE 被显式赋值为 FIRST_PREQ_DBL_PROP,说明双精度属性排序从该常量起算。 外汇与贵金属挂单受滑点、流动性跳空影响,实际成交类型(filling)和过期时间排序可能直接改变撤单优先级;MT5 上用这些枚举做队列分析时,需先确认经纪商对 ORDER_TIME_SPECIFIED 的支持程度,高风险品种尤其要验证。
SORT_BY_PEND_REQ_WAITING, class=class="str">"cmt">// Sort by a waiting time between requests SORT_BY_PEND_REQ_CURENT, class=class="str">"cmt">// Sort by the current attempt index SORT_BY_PEND_REQ_TOTAL, class=class="str">"cmt">// Sort by a number of attempts SORT_BY_PEND_REQ_ACTUAL_TYPE_FILLING, class=class="str">"cmt">// Sort by actual order filling type SORT_BY_PEND_REQ_ACTUAL_TYPE_TIME, class=class="str">"cmt">// Sort by actual order expiration type SORT_BY_PEND_REQ_ACTUAL_EXPIRATION, class=class="str">"cmt">// Sort by actual order lifetime class=class="str">"cmt">//--- class="type">MqlTradeRequest SORT_BY_PEND_REQ_MQL_REQ_ACTION, class=class="str">"cmt">// Sort by a type of a performed action in the request structure SORT_BY_PEND_REQ_MQL_REQ_TYPE, class=class="str">"cmt">// Sort by an order type in the request structure SORT_BY_PEND_REQ_MQL_REQ_MAGIC, class=class="str">"cmt">// Sort by an EA stamp(magic number ID) in the request structure SORT_BY_PEND_REQ_MQL_REQ_ORDER, class=class="str">"cmt">// Sort by an order ticket in the request structure SORT_BY_PEND_REQ_MQL_REQ_POSITION, class=class="str">"cmt">// Sort by a position ticket in the request structure SORT_BY_PEND_REQ_MQL_REQ_POSITION_BY, class=class="str">"cmt">// Sort by an opposite position ticket in the request structure SORT_BY_PEND_REQ_MQL_REQ_DEVIATION, class=class="str">"cmt">// Sort by a maximum acceptable deviation from a requested price in the request structure SORT_BY_PEND_REQ_MQL_REQ_EXPIRATION, class=class="str">"cmt">// Sort by an order expiration time(for ORDER_TIME_SPECIFIED type orders) in the request structure SORT_BY_PEND_REQ_MQL_REQ_TYPE_FILLING, class=class="str">"cmt">// Sort by an order filling type in the request structure SORT_BY_PEND_REQ_MQL_REQ_TYPE_TIME, class=class="str">"cmt">// Sort by order lifetime type in the request structure class=class="str">"cmt">//--- Sort by real properties SORT_BY_PEND_REQ_PRICE_CREATE = FIRST_PREQ_DBL_PROP, class=class="str">"cmt">// Sort by a price at the moment of a request generation SORT_BY_PEND_REQ_ACTUAL_VOLUME, class=class="str">"cmt">// Sort by initial volume
「挂单请求结构的排序枚举与基类封装」
在 MQL5 的挂单管理逻辑里,排序枚举把请求按实际成交价、止损限价、SL、TP 等数值属性先列出来,例如 SORT_BY_PEND_REQ_ACTUAL_PRICE 对应按实际挂单价格排序,SORT_BY_PEND_REQ_ACTUAL_SL 对应按实际止损价排序。 紧接着是 MqlTradeRequest 结构体内的属性排序:SORT_BY_PEND_REQ_MQL_REQ_VOLUME 按请求手数排,SORT_BY_PEND_REQ_MQL_REQ_PRICE 按请求价格排,一直到 SORT_BY_PEND_REQ_MQL_REQ_TP 按止盈层级排。字符串类属性则从 FIRST_PREQ_STR_PROP 起算,SORT_BY_PEND_REQ_MQL_SYMBOL 按品种名、SORT_BY_PEND_REQ_MQL_COMMENT 按订单注释。 承载这些请求的抽象类 CPendRequest 公开继承 CObject,私有成员包含 MqlTradeRequest m_request 与 CPause m_pause。CopyRequest() 负责把外部传入的 trade request 拷进实例,另有内部方法返回双精度与字符串属性在数组中的实际下标。 开 MT5 在 MetaEditor 搜 CPendRequest,对照枚举改一处 SORT_BY_PEND_REQ_MQL_SYMBOL 的调用,就能让挂单面板按品种名而非价格重排,外汇与贵金属品种波动大、滑点风险高,排序仅辅助观察不预示方向。
SORT_BY_PEND_REQ_ACTUAL_PRICE, class=class="str">"cmt">// Sort by actual order price SORT_BY_PEND_REQ_ACTUAL_STOPLIMIT, class=class="str">"cmt">// Sort by actual stoplimit order price SORT_BY_PEND_REQ_ACTUAL_SL, class=class="str">"cmt">// Sort by actual stoploss order price SORT_BY_PEND_REQ_ACTUAL_TP, class=class="str">"cmt">// Sort by actual takeprofit order price class=class="str">"cmt">//--- class="type">MqlTradeRequest SORT_BY_PEND_REQ_MQL_REQ_VOLUME, class=class="str">"cmt">// Sort by a requested volume of a deal in lots in the request structure SORT_BY_PEND_REQ_MQL_REQ_PRICE, class=class="str">"cmt">// Sort by a price in the request structure SORT_BY_PEND_REQ_MQL_REQ_STOPLIMIT, class=class="str">"cmt">// Sort by StopLimit order level in the request structure SORT_BY_PEND_REQ_MQL_REQ_SL, class=class="str">"cmt">// Sort by StopLoss order level in the request structure SORT_BY_PEND_REQ_MQL_REQ_TP, class=class="str">"cmt">// Sort by TakeProfit order level in the request structure class=class="str">"cmt">//--- Sort by class="type">class="kw">string properties class=class="str">"cmt">//--- class="type">MqlTradeRequest SORT_BY_PEND_REQ_MQL_SYMBOL = FIRST_PREQ_STR_PROP, class=class="str">"cmt">// Sort by a trading instrument name in the request structure SORT_BY_PEND_REQ_MQL_COMMENT class=class="str">"cmt">// Sort by an order comment in the request structure }; class CPendRequest : class="kw">public CObject { class="kw">private: class="type">MqlTradeRequest m_request; class=class="str">"cmt">// Trade request structure CPause m_pause; class=class="str">"cmt">// Pause class object class=class="str">"cmt">//--- Copy trading request data class="type">void CopyRequest(const class="type">MqlTradeRequest &request); class=class="str">"cmt">//--- Return the index of the array the request(class="num">1) class="type">class="kw">double and(class="num">2) class="type">class="kw">string properties are actually located at
挂单请求类的属性索引与内部存储
在封装挂单请求的对象里,整型、双精度、字符串三类属性被拆进三个定长数组。为了用统一下标访问它们,两个 IndexProp 重载做了偏移换算:双精度属性减去整型总数,字符串属性再减去双精度总数,这样第 0 号永远对齐各自数组起点。 int IndexProp(ENUM_PEND_REQ_PROP_DOUBLE property) const { return (int)property - PEND_REQ_PROP_INTEGER_TOTAL; } int IndexProp(ENUM_PEND_REQ_PROP_STRING property) const { return (int)property - PEND_REQ_PROP_INTEGER_TOTAL - PEND_REQ_PROP_DOUBLE_TOTAL; } protected 段暴露了六个成员变量:m_digits 和 m_digits_lot 分别记报价与手数的小数位,m_is_hedge 标记账户是否锁仓模式,其余三个数组按枚举总数静态分配。在 MT5 终端里,PEND_REQ_PROP_INTEGER_TOTAL 这类宏通常对应 10~20 个枚举值,数组长度编译期就固定了。 参数化构造函数接收状态、id、价格、时间、原始 MqlTradeRequest 引用和 retcode,说明对象在创建时就把底层交易请求整体收编。GetMagicID 则用位与 0xFFFF 从 PEND_REQ_PROP_MQL_REQ_MAGIC 属性里截出低 16 位魔数——你在 EA 里若用超 65535 的 magic,这里会静默截断,回测时可能发现订单归属错乱。 开 MT5 新建个脚本,#include 这个类头文件,打印 IndexProp(PEND_REQ_PROP_STRING_SYMBOL) 的值,能直接验证字符串偏移是否如上述逻辑落位。外汇与贵金属杠杆交易风险高,这类底层封装错误可能让挂单失效,验证后再上实盘。
class="type">int IndexProp(ENUM_PEND_REQ_PROP_DOUBLE class="kw">property) const { class="kw">return (class="type">int)class="kw">property - PEND_REQ_PROP_INTEGER_TOTAL; } class="type">int IndexProp(ENUM_PEND_REQ_PROP_STRING class="kw">property) const { class="kw">return (class="type">int)class="kw">property - PEND_REQ_PROP_INTEGER_TOTAL - PEND_REQ_PROP_DOUBLE_TOTAL; } class="kw">protected: class="type">int m_digits; class=class="str">"cmt">// Number of decimal places in a quote class="type">int m_digits_lot; class=class="str">"cmt">// Number of decimal places in the symbol lot value class="type">bool m_is_hedge; class=class="str">"cmt">// Hedging account flag class="type">long m_long_prop[PEND_REQ_PROP_INTEGER_TOTAL]; class=class="str">"cmt">// Request integer properties class="type">class="kw">double m_double_prop[PEND_REQ_PROP_DOUBLE_TOTAL]; class=class="str">"cmt">// Request real properties class="type">class="kw">string m_string_prop[PEND_REQ_PROP_STRING_TOTAL]; class=class="str">"cmt">// Request class="type">class="kw">string properties class=class="str">"cmt">//--- Protected parametric constructor CPendRequest(const ENUM_PEND_REQ_STATUS status, const class="type">uchar id, const class="type">class="kw">double price, const class="type">ulong time, const class="type">MqlTradeRequest &request, const class="type">int retcode); class=class="str">"cmt">//--- Return(class="num">1) the magic number specified in the settings, (class="num">2) hedging account flag, (class="num">3) flag indicating the real class="kw">property is equal to the value class="type">class="kw">ushort GetMagicID(class="type">void) const { class="kw">return class="type">class="kw">ushort(this.GetProperty(PEND_REQ_PROP_MQL_REQ_MAGIC) & 0xFFFF); }
◍ 挂单请求的完成状态判定接口
在 MT5 的挂单请求封装类里,有一组以 IsCompleted 开头的布尔方法,用来确认本次待处理请求中各个字段是否已被服务端确认修改。它们覆盖成交量、价格、止盈止损、限价触发价、填充类型、到期时间等 8 个维度,调用时均不带参数且被声明为 const,意味着只读取内部标记位、不改动对象状态。 与之并列的还有 IsHedge 以及两个重载的 IsEqualByMode。前者直接返回成员 m_is_hedge 的值,判断账户是否处于锁仓模式;后者按指定 mode 比对整数或双精度数值,常用于校验请求参数与预期是否一致。 实际写 EA 时,你可以在发送 ModifyOrder 之后轮询 IsCompletedVolume 与 IsCompletedPrice,只有当两者都返回 true 才认为价格与手数调整生效。外汇与贵金属杠杆交易高风险,这类状态确认只能降低误判概率,不保证成交。 构造函数 CPendRequest(){;} 是默认空实现,说明该类本身不负责资源申请,仅作为请求状态的轻量载体。
class="type">bool IsHedge(class="type">void) const { class="kw">return this.m_is_hedge; } class="type">bool IsEqualByMode(const class="type">int mode,const class="type">class="kw">double value) const; class="type">bool IsEqualByMode(const class="type">int mode,const class="type">long value) const; class=class="str">"cmt">//--- Return the flags indicating the pending request has completed changing each of the order/position parameters class="type">bool IsCompletedVolume(class="type">void) const; class="type">bool IsCompletedPrice(class="type">void) const; class="type">bool IsCompletedStopLimit(class="type">void) const; class="type">bool IsCompletedStopLoss(class="type">void) const; class="type">bool IsCompletedTakeProfit(class="type">void) const; class="type">bool IsCompletedTypeFilling(class="type">void) const; class="type">bool IsCompletedTypeTime(class="type">void) const; class="type">bool IsCompletedExpiration(class="type">void) const; class="kw">public: class="kw">public: class=class="str">"cmt">//--- Default constructor CPendRequest(){;} class=class="str">"cmt">//--- Set request(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties
「挂单请求类的属性存取与生命周期钩子」
在 MT5 的挂单请求封装类里,属性按整数、双精度、字符串三类分别落在 m_long_prop、m_double_prop、m_string_prop 三个数组里。SetProperty 做了重载:整型直接用枚举下标写入,双精度和字符串则先走 IndexProp 做映射再存,避免枚举值不连续导致数组空洞。 读取端 GetProperty 对称返回对应类型,且全部标了 const,意味着取属性不会改动对象状态,可以放心在回调或排序里反复调用。 SupportProperty 三个虚函数默认全返回 true,说明基类假设所有请求都支持全部属性;真要限制某类挂单(比如止盈单不支持某字符串字段),得在派生类里重写返回 false。 IsCompleted 虚函数基类写死返回 false,这是典型的「未完成」占位——具体请求类型(如等待成交、等待撤单确认)必须自己重写来判断终态。GetObject 则返回自身指针,方便把对象塞进以基类指针管理的容器。 Compare 只给了声明没给实现,注释写明它按指定属性比较用于列表排序,另有一套按全属性比较的逻辑用于查找等价请求。你在写 EA 时若要把多个挂单请求放进 CList 排序,就得补完这个比较函数,否则链接期直接报错。外汇与贵金属杠杆交易高风险,这类底层封装错误可能在回测不显、实盘才崩。
class="type">void SetProperty(ENUM_PEND_REQ_PROP_INTEGER class="kw">property,class="type">long value) { this.m_long_prop[class="kw">property]=value; } class="type">void SetProperty(ENUM_PEND_REQ_PROP_DOUBLE class="kw">property,class="type">class="kw">double value){ this.m_double_prop[this.IndexProp(class="kw">property)]=value; } class="type">void SetProperty(ENUM_PEND_REQ_PROP_STRING class="kw">property,class="type">class="kw">string value){ this.m_string_prop[this.IndexProp(class="kw">property)]=value; } class=class="str">"cmt">//--- Return(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string request properties from the properties array class="type">long GetProperty(ENUM_PEND_REQ_PROP_INTEGER class="kw">property) const { class="kw">return this.m_long_prop[class="kw">property]; } class="type">class="kw">double GetProperty(ENUM_PEND_REQ_PROP_DOUBLE class="kw">property) const { class="kw">return this.m_double_prop[this.IndexProp(class="kw">property)]; } class="type">class="kw">string GetProperty(ENUM_PEND_REQ_PROP_STRING class="kw">property) const { class="kw">return this.m_string_prop[this.IndexProp(class="kw">property)]; } class=class="str">"cmt">//--- Return the flag of the request supporting the class="kw">property class="kw">virtual class="type">bool SupportProperty(ENUM_PEND_REQ_PROP_INTEGER class="kw">property) { class="kw">return true; } class="kw">virtual class="type">bool SupportProperty(ENUM_PEND_REQ_PROP_DOUBLE class="kw">property) { class="kw">return true; } class="kw">virtual class="type">bool SupportProperty(ENUM_PEND_REQ_PROP_STRING class="kw">property) { class="kw">return true; } class=class="str">"cmt">//--- Return the flag indicating the pending request has completed its work class="kw">virtual class="type">bool IsCompleted(class="type">void) const { class="kw">return false; } class=class="str">"cmt">//--- Return itself CPendRequest *GetObject(class="type">void) { class="kw">return &this; } class=class="str">"cmt">//--- Compare CPendRequest objects by a specified class="kw">property (to sort the lists by a specified request object class="kw">property) class="kw">virtual class="type">int Compare(const CObject *node,const class="type">int mode=class="num">0) const; class=class="str">"cmt">//--- Compare CPendRequest objects by all properties(to search for equal request objects)
挂单请求对象的暂停与属性读取接口
在挂单请求类里,暂停计时是靠内部 m_pause 对象托管的,对外暴露的全是薄封装常量方法。想确认某次请求是不是卡在冷却期,直接读 PauseIsCompleted() 比自己算时间更稳。 下面这段是核心接口声明,逐行拆一下: IsEqual(CPendRequest* compared_obj) 返回 bool,用来判断两个挂单请求对象是否等价。 PausePassed() 返回 ulong,给出已流逝毫秒数、等待完成耗时、暂停倒计时起点毫秒、总等待时长这四个值(取自 m_pause.Passed())。 PauseIsCompleted() 返回 bool,标识暂停是否已结束。 PauseTimeBegin() 返回 ulong,是暂停倒计时启动的毫秒时间戳。 PauseTimeWait() 返回 ulong,是预设的等待毫秒数。 PausePassedDescription() 等四个 string 方法,把已过的秒数、起点时间、等待毫秒、等待秒数转成可读文本,方便日志或面板打印。 注释里还列了另一组简化访问器:能直接拿请求结构体、状态、类型、生成时价格、生成时间、下次重试激活时间、请求间隔、当前尝试序号、总尝试次数、请求 ID、基于的结果、订单票号、持仓票号、交易操作类型——一共 14 类属性。 开 MT5 把这段塞进你的 EA 头文件,编译后调用 PauseTimeWait() 打印出来,就能确认策略里写的冷却毫秒有没有真的落到对象里。外汇和贵金属杠杆高,暂停逻辑写错可能连发单,回测前务必手验一遍。
class="type">bool IsEqual(CPendRequest* compared_obj); class=class="str">"cmt">//--- Return(class="num">1) the elapsed number of milliseconds, (class="num">2) waiting time completion from the pause object, class=class="str">"cmt">//--- time of the(class="num">3) pause countdown start in milliseconds and(class="num">4) waiting time class="type">ulong PausePassed(class="type">void) const { class="kw">return this.m_pause.Passed(); } class="type">bool PauseIsCompleted(class="type">void) const { class="kw">return this.m_pause.IsCompleted(); } class="type">ulong PauseTimeBegin(class="type">void) const { class="kw">return this.m_pause.TimeBegin(); } class="type">ulong PauseTimeWait(class="type">void) const { class="kw">return this.m_pause.TimeWait(); } class=class="str">"cmt">//--- Return the description(class="num">1) of an elapsed number of pause waiting seconds, class=class="str">"cmt">//--- (class="num">2) pause countdown start time, as well as waiting time(class="num">3) in milliseconds and(class="num">4) in seconds class="type">class="kw">string PausePassedDescription(class="type">void) const { class="kw">return this.m_pause.PassedDescription(); } class="type">class="kw">string PauseTimeBeginDescription(class="type">void) const { class="kw">return this.m_pause.TimeBeginDescription(); } class="type">class="kw">string PauseWaitingMSCDescription(class="type">void) const { class="kw">return this.m_pause.WaitingMSCDescription(); } class="type">class="kw">string PauseWaitingSecDescription(class="type">void) const { class="kw">return this.m_pause.WaitingSECDescription(); }
◍ 挂单请求对象的只读访问器
在封装挂单请求管理类时,通常会把底层 MqlTradeRequest 和各项状态属性藏进私有成员,再暴露一组 const 成员函数做只读访问。这样外部逻辑只能取数、不能误改结构体,避免报价刷新线程里出现竞态。 下面这组方法覆盖了从原始请求到生命周期统计的全链路读取:MqlRequest() 直接回传 this.m_request 的常引用;Status()、TypeRequest() 通过 GetProperty 取枚举型状态与挂单类别;PriceCreate() 取挂单触发价。 时间维度上,TimeCreate() 是下单时刻的 ulong 时间戳,TimeActivate() 是实际激活时刻,WaitingMSC() 返回挂起等待的毫秒数——这三者在排查「挂单半天不触发」时最直接。 重试控制方面,CurrentAttempt() 与 TotalAttempts() 都转成 uchar 返回,分别代表已尝试次数与总允许次数。外汇与贵金属杠杆高,挂单超时或重复尝试失败可能快速放大滑点风险,读这些字段时建议配合日志阈值告警。
class="type">MqlTradeRequest MqlRequest(class="type">void) const { class="kw">return this.m_request; } ENUM_PEND_REQ_STATUS Status(class="type">void) const { class="kw">return (ENUM_PEND_REQ_STATUS)this.GetProperty(PEND_REQ_PROP_STATUS); } ENUM_PEND_REQ_TYPE TypeRequest(class="type">void) const { class="kw">return (ENUM_PEND_REQ_TYPE)this.GetProperty(PEND_REQ_PROP_TYPE); } class="type">class="kw">double PriceCreate(class="type">void) const { class="kw">return this.GetProperty(PEND_REQ_PROP_PRICE_CREATE); } class="type">ulong TimeCreate(class="type">void) const { class="kw">return this.GetProperty(PEND_REQ_PROP_TIME_CREATE); } class="type">ulong TimeActivate(class="type">void) const { class="kw">return this.GetProperty(PEND_REQ_PROP_TIME_ACTIVATE); } class="type">ulong WaitingMSC(class="type">void) const { class="kw">return this.GetProperty(PEND_REQ_PROP_WAITING); } class="type">uchar CurrentAttempt(class="type">void) const { class="kw">return (class="type">uchar)this.GetProperty(PEND_REQ_PROP_CURRENT_ATTEMPT); } class="type">uchar TotalAttempts(class="type">void) const { class="kw">return (class="type">uchar)this.GetProperty(PEND_REQ_PROP_TOTAL); }
「挂单请求对象的属性读取接口」
在 MQL5 的挂单请求封装类里,这一组方法负责把底层属性以强类型方式暴露出来,省去你手动调 GetProperty 再造型的麻烦。 ID() 返回 uchar 类型的请求标识,对应 PEND_REQ_PROP_ID;Retcode() 取 int 的服务器返回码 PEND_REQ_PROP_RETCODE,用来判断这笔挂单请求是否被拒。 Order() 与 Position() 分别回传关联的订单 ticket 和持仓 ticket(均为 ulong),Action() 则返回 ENUM_TRADE_REQUEST_ACTIONS 枚举,标明是挂单、改单还是删单动作。 注释段点出的 8 类实际值里,前 5 个在这段代码中已落地:ActualVolume 取 PEND_REQ_PROP_ACTUAL_VOLUME,ActualPrice 取 PEND_REQ_PROP_ACTUAL_PRICE,ActualStopLimit 取 PEND_REQ_PROP_ACTUAL_STOPLIMIT,分别对应成交手数、挂单价格和 STOPLIMIT 触发价。外汇与贵金属杠杆交易高风险,回测或实盘前应在 MT5 用 Print(ActualPrice()) 验证返回是否为规范化后的价格。 把下面代码贴进你的类定义,编译后接上一节的请求发送逻辑,就能直接读实际成交价,不必等 OnTradeTransaction 回拨。
class="type">uchar ID(class="type">void) const { class="kw">return (class="type">uchar)this.GetProperty(PEND_REQ_PROP_ID); } class="type">int Retcode(class="type">void) const { class="kw">return (class="type">int)this.GetProperty(PEND_REQ_PROP_RETCODE); } class="type">ulong Order(class="type">void) const { class="kw">return this.GetProperty(PEND_REQ_PROP_MQL_REQ_ORDER); } class="type">ulong Position(class="type">void) const { class="kw">return this.GetProperty(PEND_REQ_PROP_MQL_REQ_POSITION); } ENUM_TRADE_REQUEST_ACTIONS Action(class="type">void) const { class="kw">return (ENUM_TRADE_REQUEST_ACTIONS)this.GetProperty(PEND_REQ_PROP_MQL_REQ_ACTION); } class=class="str">"cmt">//--- Return the actual(class="num">1) volume, (class="num">2) order, (class="num">3) limit order, class=class="str">"cmt">//--- (class="num">4) stoploss order and(class="num">5) takeprofit order prices, (class="num">6) order filling type, class=class="str">"cmt">//--- (class="num">7) order expiration type and(class="num">8) order lifetime class="type">class="kw">double ActualVolume(class="type">void) const { class="kw">return this.GetProperty(PEND_REQ_PROP_ACTUAL_VOLUME); } class="type">class="kw">double ActualPrice(class="type">void) const { class="kw">return this.GetProperty(PEND_REQ_PROP_ACTUAL_PRICE); } class="type">class="kw">double ActualStopLimit(class="type">void) const { class="kw">return this.GetProperty(PEND_REQ_PROP_ACTUAL_STOPLIMIT); }
挂单请求里的实际成交参数与计时器挂钩
在 MT5 的挂单请求封装类里,除了提交时的名义止损止盈,还有一组 Actual* 方法返回经纪商实际接受的 SL、TP、成交类型、挂单有效期等。它们通过 GetProperty 读取 PEND_REQ_PROP_ACTUAL_* 系列属性,外汇与贵金属品种在高波动时段,实际成交类型(如 RETURN 或 FILL_OR_KILL)可能和您预设的不同,属正常风控行为,杠杆交易风险高。 下面这段是实际参数读取的核心声明,逐行拆一下: double ActualSL(void) const { return this.GetProperty(PEND_REQ_PROP_ACTUAL_SL); } → 返回本次挂单请求实际生效的止损价(double),只读。 double ActualTP(void) const { return this.GetProperty(PEND_REQ_PROP_ACTUAL_TP); } → 返回实际生效的止盈价(double),只读。 ENUM_ORDER_TYPE_FILLING ActualTypeFilling(void) const { return (ENUM_ORDER_TYPE_FILLING)this.GetProperty(PEND_REQ_PROP_ACTUAL_TYPE_FILLING); } → 把属性强转成成交方式枚举,反映真实撮合规则。 ENUM_ORDER_TYPE_TIME ActualTypeTime(void) const { return (ENUM_ORDER_TYPE_TIME)this.GetProperty(PEND_REQ_PROP_ACTUAL_TYPE_TIME); } → 返回挂单时间类型(当日有效 / 永久等)。 datetime ActualExpiration(void) const { return (datetime)this.GetProperty(PEND_REQ_PROP_ACTUAL_EXPIRATION); } → 返回挂单过期时间,GTC 单通常返回 0。 SetTimeCreate 不只是记个时间戳:它同时调用 m_pause.SetTimeBegin(time) 重置重试节奏器的起点。这意味着你若在 EA 里手动改了创建时间,等待重发逻辑也会同步归零,实盘里可能造成请求比预期更频繁地触发,需自行压住调用频率。
class="type">class="kw">double ActualSL(class="type">void) const { class="kw">return this.GetProperty(PEND_REQ_PROP_ACTUAL_SL); } class="type">class="kw">double ActualTP(class="type">void) const { class="kw">return this.GetProperty(PEND_REQ_PROP_ACTUAL_TP); } ENUM_ORDER_TYPE_FILLING ActualTypeFilling(class="type">void) const { class="kw">return (ENUM_ORDER_TYPE_FILLING)this.GetProperty(PEND_REQ_PROP_ACTUAL_TYPE_FILLING); } ENUM_ORDER_TYPE_TIME ActualTypeTime(class="type">void) const { class="kw">return (ENUM_ORDER_TYPE_TIME)this.GetProperty(PEND_REQ_PROP_ACTUAL_TYPE_TIME); } class="type">class="kw">datetime ActualExpiration(class="type">void) const { class="kw">return (class="type">class="kw">datetime)this.GetProperty(PEND_REQ_PROP_ACTUAL_EXPIRATION); } class="type">void SetTimeCreate(const class="type">ulong time) { this.SetProperty(PEND_REQ_PROP_TIME_CREATE,time); this.m_pause.SetTimeBegin(time); }
◍ 挂单请求里实际成交参数的 setter 拆解
在封装挂单请求的类里,有一组 SetActual* 方法专门用来回填订单真正触发时的参数,而不是初始挂单草拟值。它们全部走同一个 SetProperty 入口,把具体枚举或价格写进对象的内部属性表。 代码里覆盖了 8 类实际参数:成交量、挂单价格、止损限价、SL、TP、填充类型、有效期类型、过期时间。比如 SetActualVolume 写的是 PEND_REQ_PROP_ACTUAL_VOLUME,SetActualTypeFilling 接收的是 ENUM_ORDER_TYPE_FILLING 枚举,和草拟阶段用的属性键完全分开。 前面还有一段等待逻辑:this.SetProperty(PEND_REQ_PROP_WAITING,miliseconds) 配合 m_pause.SetWaitingMSC(miliseconds),说明请求对象在发出前可内置一个毫秒级等待,用于错开高频重复提交。 开 MT5 把这段贴进你的 EA 请求类,改一下属性宏名就能直接复用;外汇和贵金属波动大,这类挂单参数回填逻辑若写错,可能在你不在盘前以错误 SL/TP 成交。
this.SetProperty(PEND_REQ_PROP_WAITING,miliseconds); this.m_pause.SetWaitingMSC(miliseconds); } class=class="str">"cmt">//--- Set the actual(class="num">1) volume, (class="num">2) order, (class="num">3) limit order, class=class="str">"cmt">//--- (class="num">4) stoploss order and(class="num">5) takeprofit order prices, (class="num">6) order filling type, class=class="str">"cmt">//--- (class="num">7) order expiration type and(class="num">8) order lifetime class="type">void SetActualVolume(const class="type">class="kw">double volume) { this.SetProperty(PEND_REQ_PROP_ACTUAL_VOLUME,volume); } class="type">void SetActualPrice(const class="type">class="kw">double price) { this.SetProperty(PEND_REQ_PROP_ACTUAL_PRICE,price); } class="type">void SetActualStopLimit(const class="type">class="kw">double price) { this.SetProperty(PEND_REQ_PROP_ACTUAL_STOPLIMIT,price); } class="type">void SetActualSL(const class="type">class="kw">double price) { this.SetProperty(PEND_REQ_PROP_ACTUAL_SL,price); } class="type">void SetActualTP(const class="type">class="kw">double price) { this.SetProperty(PEND_REQ_PROP_ACTUAL_TP,price); } class="type">void SetActualTypeFilling(const ENUM_ORDER_TYPE_FILLING type) { this.SetProperty(PEND_REQ_PROP_ACTUAL_TYPE_FILLING,type); } class="type">void SetActualTypeTime(const ENUM_ORDER_TYPE_TIME type) { this.SetProperty(PEND_REQ_PROP_ACTUAL_TYPE_TIME,type); } class="type">void SetActualExpiration(const class="type">class="kw">datetime expiration) { this.SetProperty(PEND_REQ_PROP_ACTUAL_EXPIRATION,expiration); }
「挂单请求对象的描述函数清单」
在 MQL5 的挂单请求封装类里,有一组 GetPropertyDescription 重载函数,分别接收 ENUM_PEND_REQ_PROP_INTEGER、ENUM_PEND_REQ_PROP_DOUBLE、ENUM_PEND_REQ_PROP_STRING 三种枚举,用来把请求对象的整型、双精度、字符串属性转成可读名称。 除此之外,类内还提供了一批以 Description 结尾的 const 方法,直接返回具体字段的中文/本地化名称串,例如 StatusDescription 取请求状态、TypeRequestDescription 取请求类型、RetcodeDescription 取服务器返回码说明。 被高亮的那一段值得单独看:TypeFillingActualDescription、TypeTimeActualDescription、ExpirationActualDescription、VolumeActualDescription、PriceActualDescription、StopLimitActualDescription、StopLossActualDescription、TakeProfitActualDescription——这 8 个方法返回的是「实际成交/实际挂出」参数对应的描述,而不是最初下挂单时的预设值。外汇与贵金属杠杆交易高风险,实际成交价与预设止损失利可能因滑点偏离,用这些函数做日志输出时能直观区分「想挂的」和「真成的」。 在 MT5 里新建一个 EA 调试段,循环调用上述 Actual 系列方法打印到 Experts 日志,就能在实时挂单改价、部分成交时确认对象内部字段已同步,而不必自己手动解析结构体。
class="type">class="kw">string GetPropertyDescription(ENUM_PEND_REQ_PROP_INTEGER class="kw">property); class="type">class="kw">string GetPropertyDescription(ENUM_PEND_REQ_PROP_DOUBLE class="kw">property); class="type">class="kw">string GetPropertyDescription(ENUM_PEND_REQ_PROP_STRING class="kw">property); class=class="str">"cmt">//--- Return the names of pending request object parameters class="type">class="kw">string StatusDescription(class="type">void) const; class="type">class="kw">string TypeRequestDescription(class="type">void) const; class="type">class="kw">string IDDescription(class="type">void) const; class="type">class="kw">string RetcodeDescription(class="type">void) const; class="type">class="kw">string TimeCreateDescription(class="type">void) const; class="type">class="kw">string TimeActivateDescription(class="type">void) const; class="type">class="kw">string TimeWaitingDescription(class="type">void) const; class="type">class="kw">string CurrentAttemptDescription(class="type">void) const; class="type">class="kw">string TotalAttemptsDescription(class="type">void) const; class="type">class="kw">string PriceCreateDescription(class="type">void) const; class="type">class="kw">string TypeFillingActualDescription(class="type">void) const; class="type">class="kw">string TypeTimeActualDescription(class="type">void) const; class="type">class="kw">string ExpirationActualDescription(class="type">void) const; class="type">class="kw">string VolumeActualDescription(class="type">void) const; class="type">class="kw">string PriceActualDescription(class="type">void) const; class="type">class="kw">string StopLimitActualDescription(class="type">void) const; class="type">class="kw">string StopLossActualDescription(class="type">void) const; class="type">class="kw">string TakeProfitActualDescription(class="type">void) const; class=class="str">"cmt">//--- Return the names of trading request structures parameters in the request object
把下单请求拆成可读字符串
在封装交易请求的类里,一组 MqlReq*Description 方法把结构体里的每个字段转成人类能读的说明文字。从动作类型、魔术码、订单类型到止损止盈、偏差、有效期,共 16 个方法覆盖了一笔订单的全部属性。 注释里写明了三档输出:Print(full_prop) 负责打印全部或仅支持的属性描述;PrintShort 与 Header 留给子类去实现简短消息和请求头。实盘里若想快速排查 EA 发单参数错在哪,直接挂个 Print(true) 就能在日志看到完整请求画像。 下面这段是类声明里的核心方法清单,逐行对应上文说的字段映射:
class="type">class="kw">string MqlReqActionDescription(class="type">void) const; class="type">class="kw">string MqlReqMagicDescription(class="type">void) const; class="type">class="kw">string MqlReqOrderDescription(class="type">void) const; class="type">class="kw">string MqlReqSymbolDescription(class="type">void) const; class="type">class="kw">string MqlReqVolumeDescription(class="type">void) const; class="type">class="kw">string MqlReqPriceDescription(class="type">void) const; class="type">class="kw">string MqlReqStopLimitDescription(class="type">void) const; class="type">class="kw">string MqlReqStopLossDescription(class="type">void) const; class="type">class="kw">string MqlReqTakeProfitDescription(class="type">void) const; class="type">class="kw">string MqlReqDeviationDescription(class="type">void) const; class="type">class="kw">string MqlReqTypeOrderDescription(class="type">void) const; class="type">class="kw">string MqlReqTypeFillingDescription(class="type">void) const; class="type">class="kw">string MqlReqTypeTimeDescription(class="type">void) const; class="type">class="kw">string MqlReqExpirationDescription(class="type">void) const; class="type">class="kw">string MqlReqCommentDescription(class="type">void) const; class="type">class="kw">string MqlReqPositionDescription(class="type">void) const; class="type">class="kw">string MqlReqPositionByDescription(class="type">void) const; class=class="str">"cmt">//--- Display(class="num">1) description of request properties(full_prop=true - all properties, false - only supported ones), class=class="str">"cmt">//--- (class="num">2) class="type">short message about the request, (class="num">3) class="type">short request name(class="num">2 and class="num">3 - implementation in the class descendants) class="type">void Print(const class="type">bool full_prop=false); class="kw">virtual class="type">void PrintShort(class="type">void){;} class="kw">virtual class="type">class="kw">string Header(class="type">void){class="kw">return NULL;} };
◍ 挂单请求对象的构造与等值判断
在 MT5 的挂单管理类里,CPendRequest 的构造函数承担了把交易请求、账户模式与品种精度一次性绑定的工作。它先从传入的 MqlTradeRequest 拷贝出基础字段,再根据账户保证金模式判定是否为对冲账户——零售对冲模式下 m_is_hedge 才置真,这一支在 MQL4 编译分支里被直接写死为 true。 品种的小数位处理值得盯一眼:m_digits 取 SYMBOL_DIGITS,而 m_digits_lot 走 DigitsLots() 并做兜底(返回 0 时强制为 1)。若你复制这段代码去跑黄金 XAUUSD,常见 m_digits=2、m_digits_lot=3,下手数比较时别用错字段。 构造函数末尾两行被高亮:m_pause 的起始时间绑在创建时间上,等待毫秒数绑在 PEND_REQ_PROP_WAITING 属性上。这意味着挂单的“冷却计时”从对象诞生那一刻就开始跑,不是发单成功才计。 IsEqualByMode 提供了两套重载,分别比对 double 与 long 类型的属性值。内部手法是 new 一个空请求、设好待比对的 mode 与值,再拿 this->Compare 的反值当结果,用完即 delete。注意这里每次比较都堆一个新对象,高频调用(比如每 tick 扫几十个挂单)可能给内存分配带来可感知的抖动。 外汇与贵金属杠杆高、滑点跳空频繁,这类挂单状态对象若用在实盘 EA 里,建议先在策略测试器用真实点差回放验证 pause 逻辑是否如预期拦截重复发单。
CPendRequest::CPendRequest(const ENUM_PEND_REQ_STATUS status, const class="type">uchar id, const class="type">class="kw">double price, const class="type">ulong time, const class="type">MqlTradeRequest &request, const class="type">int retcode) { this.CopyRequest(request); this.m_is_hedge=class="macro">#ifdef __MQL4__ true class="macro">#else class="type">bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) class="macro">#endif; this.m_digits=(class="type">int)::SymbolInfoInteger(this.GetProperty(PEND_REQ_PROP_MQL_REQ_SYMBOL),SYMBOL_DIGITS); class="type">int dg=(class="type">int)DigitsLots(this.GetProperty(PEND_REQ_PROP_MQL_REQ_SYMBOL)); this.m_digits_lot=(dg==class="num">0 ? class="num">1 : dg); this.SetProperty(PEND_REQ_PROP_STATUS,status); this.SetProperty(PEND_REQ_PROP_ID,id); this.SetProperty(PEND_REQ_PROP_RETCODE,retcode); this.SetProperty(PEND_REQ_PROP_TYPE,this.GetProperty(PEND_REQ_PROP_RETCODE)>class="num">0 ? PEND_REQ_TYPE_ERROR : PEND_REQ_TYPE_REQUEST); this.SetProperty(PEND_REQ_PROP_TIME_CREATE,time); this.SetProperty(PEND_REQ_PROP_PRICE_CREATE,price); this.m_pause.SetTimeBegin(this.GetProperty(PEND_REQ_PROP_TIME_CREATE)); this.m_pause.SetWaitingMSC(this.GetProperty(PEND_REQ_PROP_WAITING)); } class="type">bool CPendRequest::IsEqualByMode(const class="type">int mode,const class="type">class="kw">double value) const { CPendRequest *req=new CPendRequest(); if(req==NULL) class="kw">return false; req.SetProperty((ENUM_PEND_REQ_PROP_DOUBLE)mode,value); class="type">bool res=!this.Compare(req,mode); class="kw">delete req; class="kw">return res; } class="type">bool CPendRequest::IsEqualByMode(const class="type">int mode,const class="type">long value) const { CPendRequest *req=new CPendRequest();
「挂单回执的各字段完成判定」
在 CPendRequest 封装里,修改挂单属性后靠一组 IsCompleted* 方法确认 broker 端是否真的吃下了变更。每个方法都拿「实际值」和「请求值」做等值比对,返回布尔。 以成交量为例,IsCompletedVolume 比对的是 PEND_REQ_PROP_ACTUAL_VOLUME 与 PEND_REQ_PROP_MQL_REQ_VOLUME;价格、StopLimit、SL、TP、填充类型、到期类型同理,各对应一组 ACTUAL_* 与 MQL_REQ_* 枚举。 前面那段属性写入逻辑里,req.SetProperty 用强制转换的 (ENUM_PEND_REQ_PROP_INTEGER)mode 落值,随后 res=!this.Compare(req,mode) 取反作为结果,delete req 收尾。若 req 为空直接 return false,调用方应据此判断属性设置是否生效。 实盘里外汇与贵金属点差跳变频繁,挂单改价可能在毫秒级被拒,这类完成标志只能说明「请求被接受」,不保证成交,相关操作属高风险。
if(req==NULL) class="kw">return false; req.SetProperty((ENUM_PEND_REQ_PROP_INTEGER)mode,value); class="type">bool res=!this.Compare(req,mode); class="kw">delete req; class="kw">return res; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the flag of a successful volume change | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CPendRequest::IsCompletedVolume(class="type">void) const { class="kw">return this.IsEqualByMode(PEND_REQ_PROP_ACTUAL_VOLUME,this.GetProperty(PEND_REQ_PROP_MQL_REQ_VOLUME)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the flag of a successful price modification | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CPendRequest::IsCompletedPrice(class="type">void) const { class="kw">return this.IsEqualByMode(PEND_REQ_PROP_ACTUAL_PRICE,this.GetProperty(PEND_REQ_PROP_MQL_REQ_PRICE)); } class=class="str">"cmt">//+-------------------------------------------------------------------+ class=class="str">"cmt">//| Return the flag of a successful StopLimit order price modification| class=class="str">"cmt">//+-------------------------------------------------------------------+ class="type">bool CPendRequest::IsCompletedStopLimit(class="type">void) const { class="kw">return this.IsEqualByMode(PEND_REQ_PROP_ACTUAL_STOPLIMIT,this.GetProperty(PEND_REQ_PROP_MQL_REQ_STOPLIMIT)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the flag of a successful StopLoss order modification | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CPendRequest::IsCompletedStopLoss(class="type">void) const { class="kw">return this.IsEqualByMode(PEND_REQ_PROP_ACTUAL_SL,this.GetProperty(PEND_REQ_PROP_MQL_REQ_SL)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the flag of a successful TakeProfit order modification | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CPendRequest::IsCompletedTakeProfit(class="type">void) const { class="kw">return this.IsEqualByMode(PEND_REQ_PROP_ACTUAL_TP,this.GetProperty(PEND_REQ_PROP_MQL_REQ_TP)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the flag of a successful order filling type modification | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CPendRequest::IsCompletedTypeFilling(class="type">void) const { class="kw">return this.IsEqualByMode(PEND_REQ_PROP_ACTUAL_TYPE_FILLING,this.GetProperty(PEND_REQ_PROP_MQL_REQ_TYPE_FILLING)); } class=class="str">"cmt">//+-------------------------------------------------------------------+ class=class="str">"cmt">//| Return the flag of a successful order expiration type modification| class=class="str">"cmt">//+-------------------------------------------------------------------+ class="type">bool CPendRequest::IsCompletedTypeTime(class="type">void) const { class="kw">return this.IsEqualByMode(PEND_REQ_PROP_ACTUAL_TYPE_TIME,this.GetProperty(PEND_REQ_PROP_MQL_REQ_TYPE_TIME)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the flag of a successful order lifetime modification | class=class="str">"cmt">//+------------------------------------------------------------------+
挂单请求里的过期时间比对与属性描述分发
在挂单请求封装类里,判断过期是否已完成不能只看表面字段。IsCompletedExpiration 用 IsEqualByMode 把实际过期时间(PEND_REQ_PROP_ACTUAL_EXPIRATION)和 MQL 请求里的过期(PEND_REQ_PROP_MQL_REQ_EXPIRATION)做模式化相等比较,避免时区或精度差异造成的误判。 GetPropertyDescription 是一个典型的分发函数,入参是枚举 ENUM_PEND_REQ_PROP_INTEGER,根据属性类型返回对应的中文描述方法。从 PEND_REQ_PROP_STATUS 到 PEND_REQ_PROP_MQL_REQ_EXPIRATION,它覆盖了状态、类型、ID、错误码、创建/激活/等待时间、重试次数,以及实际成交属性与 MqlTradeRequest 子字段。 高亮的那三行(ACTUAL_TYPE_FILLING / ACTUAL_TYPE_TIME / ACTUAL_EXPIRATION)说明实际成交填充方式、时间类型和过期,都各自有 ActualDescription 方法,和 MqlReq 前缀的请求描述分开。你在 MT5 里接这套类时,若日志里过期对不上,先查 IsCompletedExpiration 的比较逻辑,而不是去改 MqlTradeRequest.expiration。外汇与贵金属挂单受滑点和流动性影响,过期判定偏差可能导致重复发单,属高风险操作。
class="type">bool CPendRequest::IsCompletedExpiration(class="type">void) const { class="kw">return this.IsEqualByMode(PEND_REQ_PROP_ACTUAL_EXPIRATION,this.GetProperty(PEND_REQ_PROP_MQL_REQ_EXPIRATION)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the description of a request integer class="kw">property | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CPendRequest::GetPropertyDescription(ENUM_PEND_REQ_PROP_INTEGER class="kw">property) { class="kw">return ( class="kw">property==PEND_REQ_PROP_STATUS ? this.StatusDescription() : class="kw">property==PEND_REQ_PROP_TYPE ? this.TypeRequestDescription() : class="kw">property==PEND_REQ_PROP_ID ? this.IDDescription() : class="kw">property==PEND_REQ_PROP_RETCODE ? this.RetcodeDescription() : class="kw">property==PEND_REQ_PROP_TIME_CREATE ? this.TimeCreateDescription() : class="kw">property==PEND_REQ_PROP_TIME_ACTIVATE ? this.TimeActivateDescription() : class="kw">property==PEND_REQ_PROP_WAITING ? this.TimeWaitingDescription() : class="kw">property==PEND_REQ_PROP_CURRENT_ATTEMPT ? this.CurrentAttemptDescription() : class="kw">property==PEND_REQ_PROP_TOTAL ? this.TotalAttemptsDescription() : class="kw">property==PEND_REQ_PROP_ACTUAL_TYPE_FILLING ? this.TypeFillingActualDescription() : class="kw">property==PEND_REQ_PROP_ACTUAL_TYPE_TIME ? this.TypeTimeActualDescription() : class="kw">property==PEND_REQ_PROP_ACTUAL_EXPIRATION ? this.ExpirationActualDescription() : class=class="str">"cmt">//--- class="type">MqlTradeRequest class="kw">property==PEND_REQ_PROP_MQL_REQ_ACTION ? this.MqlReqActionDescription() : class="kw">property==PEND_REQ_PROP_MQL_REQ_TYPE ? this.MqlReqTypeOrderDescription() : class="kw">property==PEND_REQ_PROP_MQL_REQ_MAGIC ? this.MqlReqMagicDescription() : class="kw">property==PEND_REQ_PROP_MQL_REQ_ORDER ? this.MqlReqOrderDescription() : class="kw">property==PEND_REQ_PROP_MQL_REQ_POSITION ? this.MqlReqPositionDescription() : class="kw">property==PEND_REQ_PROP_MQL_REQ_POSITION_BY ? this.MqlReqPositionByDescription() : class="kw">property==PEND_REQ_PROP_MQL_REQ_DEVIATION ? this.MqlReqDeviationDescription() : class="kw">property==PEND_REQ_PROP_MQL_REQ_EXPIRATION ? this.MqlReqExpirationDescription() :
◍ 挂单请求属性的可读化拆解
在 MT5 的 EA 调试里,挂单对象 CPendRequest 内部存了大量枚举属性,直接打印原始枚举值基本不可读。上面两段 GetPropertyDescription 重载就是把它翻译成人话的入口:整型类属性走 ENUM_PEND_REQ_PROP_INTEGER,双精度类走 ENUM_PEND_REQ_PROP_DOUBLE,未匹配到的统一用 EnumToString(property) 兜底输出。 双精度分支里覆盖了 5 个实际成交相关字段——建仓价、实际成交量、实际成交价、实际 StopLimit、实际 SL/TP,以及映射自 MqlTradeRequest 的 5 个发单字段(volume/price/stopLimit/sl/tp)。这意味着你在日志里看到的『PEND_REQ_PROP_ACTUAL_VOLUME』会被替换成 VolumeActualDescription() 的返回串,而不是冷冰冰的枚举名。 末尾两个 const 方法把『实际成交填充模式』和『实际挂单有效期类型』单独抽出来:TypeFillingActualDescription 读 PEND_REQ_PROP_ACTUAL_TYPE_FILLING 再套 OrderTypeFillingDescription,TypeTimeActualDescription 读 PEND_REQ_PROP_ACTUAL_TYPE_TIME 再套 OrderTypeTimeDescription。开 MT5 接一单挂单回测,把这两个方法输出打到 Experts 日志,就能直接核对 broker 实际撮合参数与发单预设是否一致,外汇与贵金属品种在这儿常有滑点偏差,属高风险核对点。
class="kw">property==PEND_REQ_PROP_MQL_REQ_TYPE_FILLING ? this.MqlReqTypeFillingDescription() : class="kw">property==PEND_REQ_PROP_MQL_REQ_TYPE_TIME ? this.MqlReqTypeTimeDescription() : ::EnumToString(class="kw">property) ); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the description of the request real class="kw">property | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CPendRequest::GetPropertyDescription(ENUM_PEND_REQ_PROP_DOUBLE class="kw">property) { class="kw">return ( class="kw">property==PEND_REQ_PROP_PRICE_CREATE ? this.PriceCreateDescription() : class="kw">property==PEND_REQ_PROP_ACTUAL_VOLUME ? this.VolumeActualDescription() : class="kw">property==PEND_REQ_PROP_ACTUAL_PRICE ? this.PriceActualDescription() : class="kw">property==PEND_REQ_PROP_ACTUAL_STOPLIMIT ? this.StopLimitActualDescription() : class="kw">property==PEND_REQ_PROP_ACTUAL_SL ? this.StopLossActualDescription() : class="kw">property==PEND_REQ_PROP_ACTUAL_TP ? this.TakeProfitActualDescription() : class=class="str">"cmt">//--- class="type">MqlTradeRequest class="kw">property==PEND_REQ_PROP_MQL_REQ_VOLUME ? this.MqlReqVolumeDescription() : class="kw">property==PEND_REQ_PROP_MQL_REQ_PRICE ? this.MqlReqPriceDescription() : class="kw">property==PEND_REQ_PROP_MQL_REQ_STOPLIMIT ? this.MqlReqStopLimitDescription() : class="kw">property==PEND_REQ_PROP_MQL_REQ_SL ? this.MqlReqStopLossDescription() : class="kw">property==PEND_REQ_PROP_MQL_REQ_TP ? this.MqlReqTakeProfitDescription() : ::EnumToString(class="kw">property) ); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the description of the actual order filling mode | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CPendRequest::TypeFillingActualDescription(class="type">void) const { class="kw">return CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_ACTUAL_TYPE_FILLING)+": "+OrderTypeFillingDescription((ENUM_ORDER_TYPE_FILLING)this.GetProperty(PEND_REQ_PROP_ACTUAL_TYPE_FILLING)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the actual order lifetime type | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CPendRequest::TypeTimeActualDescription(class="type">void) const { class="kw">return CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_ACTUAL_TYPE_TIME)+": "+OrderTypeTimeDescription((ENUM_ORDER_TYPE_TIME)this.GetProperty(PEND_REQ_PROP_ACTUAL_TYPE_TIME)); } class=class="str">"cmt">//+------------------------------------------------------------------+
「挂单成交后的实际参数回读」
在 MT5 的挂单管理类里,成交后的「实际值」和「请求值」往往是两回事。下面这组方法专门把挂单触发后真实生效的到期时间、成交量、价格、StopLimit、SL、TP 转成可读字符串,方便日志与面板直接显示。 ExpirationActualDescription 先判断 PEND_REQ_PROP_ACTUAL_EXPIRATION 是否大于 0:大于 0 说明挂单有真实到期时间,用 TimeToString 转成时间文本;等于 0 则回退到「未设置」文案。其余五个方法(Volume / Price / StopLimit / StopLoss / TakeProfit)逻辑一致,都是取对应实际属性后用 DoubleToString 按品种小数位格式化——成交量用 m_digits_lot,价格类统一用 m_digits。 这类回读对排查「挂单为什么没按我填的价成交」很实用。外汇与贵金属点差跳空时,实际成交价和请求价可能偏离几个点,直接打印 ActualDescription 就能在日志里看到偏差幅度,属于高频诊断手段。
class="type">class="kw">string CPendRequest::ExpirationActualDescription(class="type">void) const { class="kw">return CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_ACTUAL_EXPIRATION)+": "+ (this.GetProperty(PEND_REQ_PROP_ACTUAL_EXPIRATION)>class="num">0 ? ::TimeToString(this.GetProperty(PEND_REQ_PROP_ACTUAL_EXPIRATION)) : CMessage::Text(MSG_LIB_PROP_NOT_SET)); } class="type">class="kw">string CPendRequest::VolumeActualDescription(class="type">void) const { class="kw">return CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_ACTUAL_VOLUME)+": "+::DoubleToString(this.GetProperty(PEND_REQ_PROP_ACTUAL_VOLUME),this.m_digits_lot); } class="type">class="kw">string CPendRequest::PriceActualDescription(class="type">void) const { class="kw">return CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_ACTUAL_PRICE)+": "+::DoubleToString(this.GetProperty(PEND_REQ_PROP_ACTUAL_PRICE),this.m_digits); } class="type">class="kw">string CPendRequest::StopLimitActualDescription(class="type">void) const { class="kw">return CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_ACTUAL_STOPLIMIT)+": "+::DoubleToString(this.GetProperty(PEND_REQ_PROP_ACTUAL_STOPLIMIT),this.m_digits); } class="type">class="kw">string CPendRequest::StopLossActualDescription(class="type">void) const { class="kw">return CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_ACTUAL_SL)+": "+::DoubleToString(this.GetProperty(PEND_REQ_PROP_ACTUAL_SL),this.m_digits); } class="type">class="kw">string CPendRequest::TakeProfitActualDescription(class="type">void) const { class="kw">return CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_ACTUAL_TP)+": "+::DoubleToString(this.GetProperty(PEND_REQ_PROP_ACTUAL_TP),this.m_digits); } class CPendReqOpen : class="kw">public CPendRequest { class="kw">public: class=class="str">"cmt">//--- Constructor
挂单请求的子类化封装与日志头构造
在自定义挂单管理器中,CPendReqOpen 作为 CPendRequest 的派生类,专门承载「开仓类挂单」的异步回执。它的构造函数把状态硬编码为 PEND_REQ_STATUS_OPEN,同时传入 id、挂单价格、时间戳、MqlTradeRequest 引用和经纪商返回码,等于把一笔未成交订单的上下文一次性冻结进对象里。 类内声明了三组 SupportProperty 虚函数,分别接收 ENUM_PEND_REQ_PROP_INTEGER / DOUBLE / STRING 枚举,用来在运行时判断该挂单对象支持哪些属性读写——这是把「整数、双精度、字符串」三类字段从基类往下分流的典型做法。 Header() 方法覆写后返回形如「开仓状态文字, ID #123」的短串,直接喂给 PrintShort 打日志。下面这段是 CPendReqOpen 的核心代码与拆解: CPendReqOpen(const uchar id, // 挂单唯一 ID,单账户内自增 const double price, // 挂单触发价 const ulong time, // 请求发出时的 UTC 时间戳 const MqlTradeRequest &request, // 原始交易请求结构体引用 const int retcode) // 交易服务器返回码 : CPendRequest(PEND_REQ_STATUS_OPEN,id,price,time,request,retcode) {} // 调基类构造,状态锁定为 OPEN string CPendReqOpen::Header(void) { return CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_STATUS_OPEN) // 取本地化「开仓」状态文案 +", ID #"+((string)this.GetProperty(PEND_REQ_PROP_ID)); // 拼接对象内 ID 属性转字符串 } 紧接其后的 CPendReqSLTP 类用几乎一样的骨架处理「修改持仓止损止盈」的挂单,状态常量换成 PEND_REQ_STATUS_SLTP。两者都只暴露构造函数和属性支持声明,说明库作者把差异收敛在状态枚举与 Header 文案上,交易逻辑复用同一套基类调度。 在 MT5 里把这两段贴进 EA 的 include 链,编译后下两笔挂单(一开仓一改 SL),能在专家日志里看到不同 Header 前缀,验证子类路由是否生效。外汇与贵金属挂单受点差和滑点影响,回执状态可能偏离预期,属正常高风险现象。
CPendReqOpen(const class="type">uchar id, const class="type">class="kw">double price, const class="type">ulong time, const class="type">MqlTradeRequest &request, const class="type">int retcode) : CPendRequest(PEND_REQ_STATUS_OPEN,id,price,time,request,retcode) {} class=class="str">"cmt">//--- Supported deal properties(class="num">1) real, (class="num">2) integer class="kw">virtual class="type">bool SupportProperty(ENUM_PEND_REQ_PROP_INTEGER class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_PEND_REQ_PROP_DOUBLE class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_PEND_REQ_PROP_STRING class="kw">property); class=class="str">"cmt">//--- Display a brief message with request data and(class="num">2) a class="type">short request name in the journal class="kw">virtual class="type">void PrintShort(class="type">void); class="kw">virtual class="type">class="kw">string Header(class="type">void); }; class="type">class="kw">string CPendReqOpen::Header(class="type">void) { class="kw">return CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_STATUS_OPEN)+", ID #"+((class="type">class="kw">string)this.GetProperty(PEND_REQ_PROP_ID)); } class CPendReqSLTP : class="kw">public CPendRequest { class="kw">public: class=class="str">"cmt">//--- Constructor CPendReqSLTP(const class="type">uchar id, const class="type">class="kw">double price, const class="type">ulong time, const class="type">MqlTradeRequest &request, const class="type">int retcode) : CPendRequest(PEND_REQ_STATUS_SLTP,id,price,time,request,retcode) {} class=class="str">"cmt">//--- Supported deal properties(class="num">1) real, (class="num">2) integer
◍ 挂单改 SL/TP 请求的属性支持与完成判定
在 MQL5 的挂单修改请求类 CPendReqSLTP 里,SupportProperty 做了三套重载,分别吃整型、双精度、字符串三种枚举属性。整型里有一批被直接 return false:比如持仓反手、挂单号、过期时间、偏差、成交方式、时间类型,以及三个带 ACTUAL 前缀的实盘字段(实际过期、实际时间类型、实际成交方式),说明这类请求不认这些整型配置。 双精度那边反过来,PEND_REQ_PROP_PRICE_CREATE、实际 SL、实际 TP、以及请求里的 MQL_REQ_SL / MQL_REQ_TP 都返回 true。也就是说,改挂单的止损止盈价,走的是 double 通道,整型接口拦不住。 IsCompleted 的写法很直白:用 &= 把 IsCompletedStopLoss 和 IsCompletedTakeProfit 的结果叠起来,两个都完成才返回 true。你在 EA 里轮询这类挂单请求时,可以直接拿这个布尔值判断是不是该清掉本地缓存了。 外汇和贵金属杠杆高,挂单 SL/TP 的实际成交受滑点影响,回测里 IsCompleted 返回 true 不等于实盘一定改单成功,开 MT5 用真实点差验证更稳妥。
class="kw">virtual class="type">bool SupportProperty(ENUM_PEND_REQ_PROP_INTEGER class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_PEND_REQ_PROP_DOUBLE class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_PEND_REQ_PROP_STRING class="kw">property); class=class="str">"cmt">//--- Return the flag indicating the pending request has completed its work class="kw">virtual class="type">bool IsCompleted(class="type">void) const; class=class="str">"cmt">//--- Display a brief message with request data and(class="num">2) a class="type">short request name in the journal class="kw">virtual class="type">void PrintShort(class="type">void); class="kw">virtual class="type">class="kw">string Header(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return &class="macro">#x27;true&class="macro">#x27; if an order supports a passed | class=class="str">"cmt">//| integer class="kw">property, otherwise class="kw">return &class="macro">#x27;false&class="macro">#x27; | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CPendReqSLTP::SupportProperty(ENUM_PEND_REQ_PROP_INTEGER class="kw">property) { if(class="kw">property==PEND_REQ_PROP_MQL_REQ_POSITION_BY || class="kw">property==PEND_REQ_PROP_MQL_REQ_ORDER || class="kw">property==PEND_REQ_PROP_MQL_REQ_EXPIRATION || class="kw">property==PEND_REQ_PROP_MQL_REQ_DEVIATION || class="kw">property==PEND_REQ_PROP_MQL_REQ_TYPE_FILLING || class="kw">property==PEND_REQ_PROP_MQL_REQ_TYPE_TIME || class="kw">property==PEND_REQ_PROP_ACTUAL_EXPIRATION || class="kw">property==PEND_REQ_PROP_ACTUAL_TYPE_TIME || class="kw">property==PEND_REQ_PROP_ACTUAL_TYPE_FILLING ) class="kw">return false; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return &class="macro">#x27;true&class="macro">#x27; if an order supports a passed | class=class="str">"cmt">//| real class="kw">property, otherwise class="kw">return &class="macro">#x27;false&class="macro">#x27; | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CPendReqSLTP::SupportProperty(ENUM_PEND_REQ_PROP_DOUBLE class="kw">property) { if(class="kw">property==PEND_REQ_PROP_PRICE_CREATE || class="kw">property==PEND_REQ_PROP_ACTUAL_SL || class="kw">property==PEND_REQ_PROP_ACTUAL_TP || class="kw">property==PEND_REQ_PROP_MQL_REQ_SL || class="kw">property==PEND_REQ_PROP_MQL_REQ_TP ) class="kw">return true; class="kw">return false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+----------------------------------------------------------------------+ class=class="str">"cmt">//| Return the flag indicating the pending request has completed its work| class=class="str">"cmt">//+----------------------------------------------------------------------+ class="type">bool CPendReqSLTP::IsCompleted(class="type">void) const { class="type">bool res=true; res &= this.IsCompletedStopLoss(); res &= this.IsCompletedTakeProfit(); class="kw">return res; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+
「平仓挂单请求的类封装与完成判定」
在 MT5 的 EA 架构里,把「平仓挂单」抽象成独立类 CPendReqClose,继承自 CPendRequest,能让你在策略里统一管理未成交的平仓指令。构造函数直接透传 id、price、time、原始 MqlTradeRequest 与 retcode,状态类型写死为 PEND_REQ_STATUS_CLOSE。 该类只重载了三个 SupportProperty 虚函数(整型、双精度、字符串属性),说明平仓请求支持的全部可查询字段都由基类枚举约束,外部不要自行扩展。完成判定 IsCompleted 直接复用 IsCompletedVolume,意味着只要成交量条件满足就视为请求生命周期结束,不额外判断价格滑点。 Header 函数返回带 ID 的短名称,方便在专家日志里快速定位某笔平仓挂单。下面这段是原文里的核心代码,可直接贴进你的 include 文件验证编译。 别把 IsCompletedVolume 当成万能完成信号 它只认成交量,若你的平仓单因报价断层部分成交后卡死,日志仍可能显示「已完成」,实盘外汇与贵金属波动剧烈时这种误判概率不低,建议自己加一层时间超时校验。
class="type">class="kw">string CPendReqSLTP::Header(class="type">void) { class="kw">return CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_STATUS_SLTP)+", ID #"+(class="type">class="kw">string)this.GetProperty(PEND_REQ_PROP_ID); } class CPendReqClose : class="kw">public CPendRequest { class="kw">public: CPendReqClose(const class="type">uchar id, const class="type">class="kw">double price, const class="type">ulong time, const class="type">MqlTradeRequest &request, const class="type">int retcode) : CPendRequest(PEND_REQ_STATUS_CLOSE,id,price,time,request,retcode) {} class="kw">virtual class="type">bool SupportProperty(ENUM_PEND_REQ_PROP_INTEGER class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_PEND_REQ_PROP_DOUBLE class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_PEND_REQ_PROP_STRING class="kw">property); class="kw">virtual class="type">bool IsCompleted(class="type">void) const; class="kw">virtual class="type">void PrintShort(class="type">void); class="kw">virtual class="type">class="kw">string Header(class="type">void); }; class="type">bool CPendReqClose::IsCompleted(class="type">void) const { class="kw">return this.IsCompletedVolume(); } class="type">class="kw">string CPendReqClose::Header(class="type">void) {
挂单请求类的继承与状态标识
在 MT5 的 EA 架构里,挂单类请求被拆成多个子类继承同一个 CPendRequest 基类,方便按动作类型(建仓、改单、撤单)分别处理状态与日志。上面这段定义了 CPendReqPlace 与 CPendReqModify,分别对应'放置挂单'和'修改挂单参数'两种 pending 请求。 CPendReqPlace 的构造函数直接把状态写死为 PEND_REQ_STATUS_PLACE,并接收 id、price、time、MqlTradeRequest 引用和 retcode 五个参数传给基类。它重写了 SupportProperty 三个重载(整型、双精度、字符串属性)以及 PrintShort、Header 两个虚函数,说明不同请求类型支持的属性集合可能不同,调用前最好先判断。 Header() 返回的是本地化短名称加请求 ID,例如'放置挂单, ID #12'这种格式,写日志时一眼能定位是哪一笔。外汇与贵金属挂单受点差和熔断影响,这类 ID 追踪在异常排查时倾向比单纯看 retcode 更有用。 把 CPendReqModify 的构造参数照抄 CPendReqPlace 的前三个(id、price、time)就能快速新建改单类,但注意原文在 modify 处截断了 request 与 retcode 传参,实际用的时候漏掉会导致基类成员未初始化。
class="type">class="kw">string CPendReqPlace::Header(class="type">void) { class="kw">return CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_STATUS_PLACE)+", ID #"+(class="type">class="kw">string)this.GetProperty(PEND_REQ_PROP_ID); } class CPendReqPlace : class="kw">public CPendRequest { class="kw">public: CPendReqPlace(const class="type">uchar id, const class="type">class="kw">double price, const class="type">ulong time, const class="type">MqlTradeRequest &request, const class="type">int retcode) : CPendRequest(PEND_REQ_STATUS_PLACE,id,price,time,request,retcode) {} class="kw">virtual class="type">bool SupportProperty(ENUM_PEND_REQ_PROP_INTEGER class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_PEND_REQ_PROP_DOUBLE class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_PEND_REQ_PROP_STRING class="kw">property); class="kw">virtual class="type">void PrintShort(class="type">void); class="kw">virtual class="type">class="kw">string Header(class="type">void); };
◍ 挂单改单与撤单的完成判定逻辑
在 MQL5 的挂单管理类里,CPendReqModify 的构造直接继承自 CPendRequest,类型写死为 PEND_REQ_STATUS_MODIFY,把待修改挂单的 ID、价格、时间、原始请求和返回码一并塞进基类。 它暴露了三组 SupportProperty 虚函数,分别接管整型、双精度、字符串三类属性,意味着修改请求可支持的字段由具体子类决定,不是所有挂单字段都能改。 IsCompleted 的判定很硬:用 &= 串联七个子条件,价格、StopLimit、止损、止盈、成交方式、有效期类型、过期时间,任意一项未达成就返回 false。也就是说改单只有在全部要素对齐时才算收尾。 Header 仅拼出「修改挂单状态」文案加 ID 编号,方便在日志里一眼定位是哪笔单。 紧接着的 CPendReqRemove 类处理撤单,构造参数同样吃 ID、price、time,但语义转为移除挂单,交易者应留意外汇与贵金属杠杆品种撤单失败可能引发滑点风险。
const class="type">MqlTradeRequest &request, const class="type">int retcode) : CPendRequest(PEND_REQ_STATUS_MODIFY,id,price,time,request,retcode) {} class=class="str">"cmt">//--- Supported deal properties(class="num">1) real, (class="num">2) integer class="kw">virtual class="type">bool SupportProperty(ENUM_PEND_REQ_PROP_INTEGER class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_PEND_REQ_PROP_DOUBLE class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_PEND_REQ_PROP_STRING class="kw">property); class=class="str">"cmt">//--- Return the flag indicating the pending request has completed its work class="kw">virtual class="type">bool IsCompleted(class="type">void) const; class=class="str">"cmt">//--- Display a brief message with request data and(class="num">2) a class="type">short request name in the journal class="kw">virtual class="type">void PrintShort(class="type">void); class="kw">virtual class="type">class="kw">string Header(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+----------------------------------------------------------------------+ class=class="str">"cmt">//| Return the flag indicating the pending request has completed its work| class=class="str">"cmt">//+----------------------------------------------------------------------+ class="type">bool CPendReqModify::IsCompleted(class="type">void) const { class="type">bool res=true; res &= this.IsCompletedPrice(); res &= this.IsCompletedStopLimit(); res &= this.IsCompletedStopLoss(); res &= this.IsCompletedTakeProfit(); res &= this.IsCompletedTypeFilling(); res &= this.IsCompletedTypeTime(); res &= this.IsCompletedExpiration(); class="kw">return res; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the class="type">short request name | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CPendReqModify::Header(class="type">void) { class="kw">return CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_STATUS_MODIFY)+", ID #"+(class="type">class="kw">string)this.GetProperty(PEND_REQ_PROP_ID); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Pending request to remove a pending order | class=class="str">"cmt">//+------------------------------------------------------------------+ class CPendReqRemove : class="kw">public CPendRequest { class="kw">public: class=class="str">"cmt">//--- Constructor CPendReqRemove(const class="type">uchar id, const class="type">class="kw">double price, const class="type">ulong time,
「挂单移除请求的类的实现细节」
在 MQL5 的挂单管理封装里,CPendReqRemove 通过继承 CPendRequest 并传入 PEND_REQ_STATUS_REMOVE 类型,专门处理移除类挂单请求的状态回调。构造函数直接把外部传入的 request 引用和 retcode 交给基类,不做额外加工。 类内声明了三组 SupportProperty 虚函数,分别覆盖整型、双精度、字符串三种属性枚举,这意味着移除请求对象能按需暴露不同类别的运行时属性,方便上层逻辑做类型判断。 Header 方法返回带本地化文本和挂单 ID 的短名称,代码里用 CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_STATUS_REMOVE) 拼上 ', ID #' 与 GetProperty(PEND_REQ_PROP_ID) 的强制转换结果。把这个类拖进 MT5 的EA里断点 Header,能在日志里直接看到每笔移除请求对应的 ID,便于核对挂单生命周期。 外汇与贵金属市场杠杆高、滑点突发,这类底层请求封装仅解决代码秩序问题,不预示任何成交概率。
const class="type">MqlTradeRequest &request, const class="type">int retcode) : CPendRequest(PEND_REQ_STATUS_REMOVE,id,price,time,request,retcode) {} class=class="str">"cmt">//--- Supported deal properties(class="num">1) real, (class="num">2) integer class="kw">virtual class="type">bool SupportProperty(ENUM_PEND_REQ_PROP_INTEGER class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_PEND_REQ_PROP_DOUBLE class="kw">property); class="kw">virtual class="type">bool SupportProperty(ENUM_PEND_REQ_PROP_STRING class="kw">property); class=class="str">"cmt">//--- Display a brief message with request data and(class="num">2) a class="type">short request name in the journal class="kw">virtual class="type">void PrintShort(class="type">void); class="kw">virtual class="type">class="kw">string Header(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the class="type">short request name | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CPendReqRemove::Header(class="type">void) { class="kw">return CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_STATUS_REMOVE)+", ID #"+(class="type">class="kw">string)this.GetProperty(PEND_REQ_PROP_ID); } class=class="str">"cmt">//+------------------------------------------------------------------+
用 CTradingControl 接管延后请求的生命周期
延后请求若靠人工轮询容易漏单,把管理逻辑收敛到一个派生类里更稳。CTradingControl 直接继承 CTrading,因此订单列表、请求对象这些成员天然复用,不必重新造轮子。 类里只留了三个对外入口:GetObject() 返回自身指针供引擎调用,OnTimer() 是虚拟计时器,构造函数里顺手清掉 m_list_request 并打上列表已排序标志。私密方法 SetActualProperties() 负责把真实订单数据回填进延后请求对象——它接收 CPendRequest* 和 COrder* 两个指针,在对象外实现,避免类体膨胀。 计时器跑起来后逐条扫请求列表:寿命耗尽的直接删;若账户历史出现对应交易、或现有订单/持仓的魔幻数带上了该请求 ID,算已激活也删;既没激活又到点,就把挂单发往服务器。 有个实战细节:若当初因 10027 错误(终端禁了自动交易)建的延后单,后来交易者在 EA 设置里勾了“允许自动交易”,计时器会立刻重发而不等原定激活时间,绕开重新报价。外汇和贵金属杠杆高,这类自动补单逻辑务必先在模拟盘验证。 引擎侧只需把原先 include 的 Trading.mqh 换成新类文件,并把交易类对象指针改成 CTradingControl 对象,改动量极小。
class="macro">#class="kw">property copyright "Copyright class="num">2019, MetaQuotes Software Corp." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property strict class=class="str">"cmt">// Necessary for mql4 class="macro">#include "Trading.mqh" class CTradingControl : class="kw">public CTrading { class="kw">private: class="type">void SetActualProperties(CPendRequest *req_obj,const COrder *order); class="kw">public: CTradingControl *GetObject(class="type">void) { class="kw">return &this; } class="kw">virtual class="type">void OnTimer(class="type">void); CTradingControl(); }; CTradingControl::CTradingControl() { this.m_list_request.Clear();
◍ 挂单请求的定时重投与终端交易权限判定
EA 在定时器里轮询挂单请求链表,从尾到头逐个取出 CPendRequest 对象,先拿到 MqlTradeRequest 结构和对应品种对象,品种刷新失败就跳过。这里同时读取 TERMINAL_TRADE_ALLOWED 与 MQL_TRADE_ALLOWED 做按位与,只有终端「自动交易」按钮和 EA 设置里的「允许算法交易」都为真,terminal_trade_allowed 才为真,任意一侧关掉则禁止发单。 错误码 10027 对应终端侧禁用自动交易,若当时已恢复允许,且当前重试次数小于 TotalAttempts()+1,就把请求创建时间回拨一个等待周期,相当于立刻重发。外汇与贵金属品种波动剧烈,这种自动重试逻辑虽能绕开临时禁交易,但重投本身不改变方向风险,实盘须人工确认策略仍有效。 下面这段截取了链表中段处理与定时器主体的关键写法,可贴进 MT5 的 CTradingControl 类直接对照编译:
this.m_list_request.Sort(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Set order/position data to a pending request object | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTradingControl::SetActualProperties(CPendRequest *req_obj,const COrder *order) { req_obj.SetActualExpiration(order.TimeExpiration()); req_obj.SetActualPrice(order.PriceOpen()); req_obj.SetActualSL(order.StopLoss()); req_obj.SetActualStopLimit(order.PriceStopLimit()); req_obj.SetActualTP(order.TakeProfit()); req_obj.SetActualTypeFilling(order.TypeFilling()); req_obj.SetActualTypeTime(order.TypeTime()); req_obj.SetActualVolume(order.Volume()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Timer | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTradingControl::OnTimer(class="type">void) { class=class="str">"cmt">//--- In a loop by the list of pending requests class="type">int total=this.m_list_request.Total(); for(class="type">int i=total-class="num">1;i>WRONG_VALUE;i--) { class=class="str">"cmt">//--- receive the next request object CPendRequest *req_obj=this.m_list_request.At(i); if(req_obj==NULL) class="kw">continue; class=class="str">"cmt">//--- get the request structure and the symbol object a trading operation should be performed for class="type">MqlTradeRequest request=req_obj.MqlRequest(); CSymbol *symbol_obj=this.m_symbols.GetSymbolObjByName(request.symbol); if(symbol_obj==NULL || !symbol_obj.RefreshRates()) class="kw">continue; class=class="str">"cmt">//--- Set the flag disabling trading in the terminal by two properties simultaneously class=class="str">"cmt">//--- (the AutoTrading button in the terminal and the Allow Automated Trading option in the EA settings) class=class="str">"cmt">//--- If any of the two properties is &class="macro">#x27;false&class="macro">#x27;, the flag is &class="macro">#x27;false&class="macro">#x27; as well class="type">bool terminal_trade_allowed=::TerminalInfoInteger(TERMINAL_TRADE_ALLOWED); terminal_trade_allowed &=::MQLInfoInteger(MQL_TRADE_ALLOWED); class=class="str">"cmt">//--- If a request object is based on the error code if(req_obj.TypeRequest()==PEND_REQ_TYPE_ERROR) { class=class="str">"cmt">//--- if the error has been caused by trading disabled on the terminal side and has been eliminated if(req_obj.Retcode()==class="num">10027 && terminal_trade_allowed) { class=class="str">"cmt">//--- if the current attempt has not exceeded the defined number of trading attempts yet if(req_obj.CurrentAttempt()<req_obj.TotalAttempts()+class="num">1) { class=class="str">"cmt">//--- Set the request creation time equal to its creation time minus waiting time, i.e. send the request immediately
「重试计数与挂单请求的生命周期清理」
在交易请求队列的循环处理里,若某次尝试被判定为用户侧可修复错误,代码会先把创建时间回拨一个等待周期,再把当前尝试次数减 1。这么做是因为下一次循环会自增尝试计数,若不回退,最后一次重试可能直接被跳过;同时给用户多留一轮处理窗口,避免误删有效请求。 当当前尝试次数超过设定的总尝试数(req_obj.TotalAttempts()),或尝试计数触达 UCHAR_MAX(255),又或者行情时间越过『创建时间 + 单次等待毫秒 × 总尝试数』这道边界,该请求对象会被从 m_list_request 中删除并 continue 跳过。这是队列自我瘦身的核心逻辑,防止死请求长期占用 MT5 终端内存。 对于开仓或挂单类动作(TRADE_ACTION_DEAL 且持仓为 0,或 TRADE_ACTION_PENDING),引擎会用 magic 取挂单请求 ID,再到市场对象里按 ORDER_PROP_PEND_REQ_ID 等值检索。只要列表里查到至少一条(list.Total()>0),就认定请求已成交,打印日志后删除并跳下一轮。 其余分支覆盖平仓、删单、改单及止损修改等动作,在 else 块中继续按持仓 ID 或 CLOSE_BY 类型做针对性列表查询。外汇与贵金属市场跳空频繁,这类异步确认机制能降低重复发单概率,但高频重试仍可能放大滑点风险,建议在 MT5 策略测试器里把 TotalAttempts 调到 2~3 观察成交回撤。
class=class="str">"cmt">//--- Also, decrease the number of a successful attempt since during the next attempt, its number is increased, and if this is the last attempt, class=class="str">"cmt">//--- it is not executed. However, this is related to fixing the error cause by a user, which means we need to give more time for the last attempt req_obj.SetTimeCreate(req_obj.TimeCreate()-req_obj.WaitingMSC()); req_obj.SetCurrentAttempt((class="type">uchar)(req_obj.CurrentAttempt()>class="num">0 ? req_obj.CurrentAttempt()-class="num">1 : class="num">0)); } } } class=class="str">"cmt">//--- if the current attempt exceeds the defined number of trading attempts, class=class="str">"cmt">//--- or the current time exceeds the waiting time of all attempts class=class="str">"cmt">//--- remove the current request object and move on to the next one if(req_obj.CurrentAttempt()>req_obj.TotalAttempts() || req_obj.CurrentAttempt()>=UCHAR_MAX || (class="type">long)symbol_obj.Time()>class="type">long(req_obj.TimeCreate()+req_obj.WaitingMSC()*req_obj.TotalAttempts())) { if(this.m_log_level>LOG_LEVEL_NO_MSG) ::Print(req_obj.Header(),": ",CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_DELETED)); this.m_list_request.Delete(i); class="kw">continue; } class=class="str">"cmt">//--- If this is a position opening or placing a pending order if((req_obj.Action()==TRADE_ACTION_DEAL && req_obj.Position()==class="num">0) || req_obj.Action()==TRADE_ACTION_PENDING) { class=class="str">"cmt">//--- Get the pending request ID class="type">uchar id=this.GetPendReqID((class="type">uint)request.magic); class=class="str">"cmt">//--- Get the list of orders/positions containing the order/position with the pending request ID CArrayObj *list=this.m_market.GetList(ORDER_PROP_PEND_REQ_ID,id,EQUAL); if(::CheckPointer(list)==POINTER_INVALID) class="kw">continue; class=class="str">"cmt">//--- If the order/position is present, the request is handled: remove it and proceed to the next if(list.Total()>class="num">0) { if(this.m_log_level>LOG_LEVEL_NO_MSG) ::Print(req_obj.Header(),": ",CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_EXECUTED)); this.m_list_request.Delete(i); class="kw">continue; } } class=class="str">"cmt">//--- Otherwise: full and partial position closure, removing an order, modifying order parameters and position stop orders else { CArrayObj *list=NULL; class=class="str">"cmt">//--- if this is a position closure, including a closure by an opposite one if((req_obj.Action()==TRADE_ACTION_DEAL && req_obj.Position()>class="num">0) || req_obj.Action()==TRADE_ACTION_CLOSE_BY) {
从成交事件反推部分平仓是否落地
EA 处理挂单类请求时,不能只盯持仓列表有没有这张单。若按 ticket 从市场持仓里取不到对象,代码直接跳过本次循环;若列表总数为 0,说明仓位已不在,请求算执行完,从待处理队列里删掉即可。 当持仓还在,就要怀疑是部分平仓。此时去账户交易事件列表里倒序翻,找 TRADE_EVENT_POSITION_CLOSED_PARTIAL 或 TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_POS 这类事件。事件里的 PositionID 和请求里的 Position() 对得上,才确认这笔部分平仓真正发生。 这套逻辑的价值在于:MT5 的持仓列表只给“当下状态”,不保留“刚才平了多少”。靠事件回放能把部分平仓从“仓位变小”里剥出来。外汇和贵金属杠杆高,部分平仓后残留仓位的风控阈值可能瞬间失准,建议开 MT5 用Journal里的交易事件对照这段逻辑自己跑一遍。
class=class="str">"cmt">//--- Get a position with the necessary ticket from the list of open positions list=this.m_market.GetList(ORDER_PROP_TICKET,req_obj.Position(),EQUAL); if(::CheckPointer(list)==POINTER_INVALID) class="kw">continue; class=class="str">"cmt">//--- If the market has no such position - the request is handled: remove it and proceed to the next one if(list.Total()==class="num">0) { if(this.m_log_level>LOG_LEVEL_NO_MSG) ::Print(req_obj.Header(),": ",CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_EXECUTED)); this.m_list_request.Delete(i); class="kw">continue; } class=class="str">"cmt">//--- Otherwise, if the position still exists, this is a partial closure else { class=class="str">"cmt">//--- Get the list of all account trading events list=this.m_events.GetList(); if(list==NULL) class="kw">continue; class=class="str">"cmt">//--- In the loop from the end of the account trading event list class="type">int events_total=list.Total(); for(class="type">int j=events_total-class="num">1; j>WRONG_VALUE; j--) { class=class="str">"cmt">//--- get the next trading event CEvent *event=list.At(j); if(event==NULL) class="kw">continue; class=class="str">"cmt">//--- If this event is a partial closure or there was a partial closure when closing by an opposite one if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_PARTIAL || event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_POS) { class=class="str">"cmt">//--- If a position ticket in a trading event coincides with the ticket in a pending trading request if(event.PositionID()==req_obj.Position()) { class=class="str">"cmt">//--- Get a position object from the list of market positions CArrayObj *list_orders=this.m_market.GetList(ORDER_PROP_POSITION_ID,req_obj.Position(),EQUAL);
◍ 挂单请求的生命周期收口
在 MQL5 的挂单管理类里,一段典型的循环逻辑是:先判断订单列表是否为空,空了就直接 break 跳出,避免对空指针做操作。接着取列表末尾的订单对象,若取到的对象是 NULL 也同样 break,这说明事件与订单状态可能已不同步。
拿到有效订单后,会把实际持仓属性写回挂单请求对象(SetActualProperties),再用已成交量与剩余未成交量之和去比对原始请求量。当 event.VolumeOrderExecuted()+event.VolumeOrderCurrent() 等于 PEND_REQ_PROP_MQL_REQ_VOLUME 时,意味着这笔挂单已被完整处理,此时从请求列表 m_list_request 里按索引删除并 break。
删除后要用 CheckPointer(req_obj)==POINTER_INVALID 确认对象是否失效,失效就 continue 跳到下一个请求。对于 TRADE_ACTION_SLTP 这类止盈止损修改动作,则重新拉取账户交易事件列表,从末尾往前遍历,用 list.Total() 拿到事件总数再逐条处理。
外汇与贵金属市场高杠杆、滑点频发,这类请求清理逻辑若漏掉指针校验,可能在快速行情中引发重复发单或访问违规,建议在 MT5 策略测试器里用极端跳空样本跑一遍验证。
if(list_orders==NULL || list_orders.Total()==class="num">0) class="kw">break; COrder *order=list_orders.At(list_orders.Total()-class="num">1); if(order==NULL) class="kw">break; class=class="str">"cmt">//--- Set actual position data to the pending request object this.SetActualProperties(req_obj,order); class=class="str">"cmt">//--- If(executed request volume + unexecuted request volume) is equal to the requested volume in a pending request - class=class="str">"cmt">//--- the request is handled: remove it and class="kw">break the loop by the list of account trading events if(req_obj.GetProperty(PEND_REQ_PROP_MQL_REQ_VOLUME)==event.VolumeOrderExecuted()+event.VolumeOrderCurrent()) { if(this.m_log_level>LOG_LEVEL_NO_MSG) ::Print(req_obj.Header(),": ",CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_EXECUTED)); this.m_list_request.Delete(i); class="kw">break; } } } } class=class="str">"cmt">//--- If a handled pending request object was removed by the trading event list in the loop, move on to the next one if(::CheckPointer(req_obj)==POINTER_INVALID) class="kw">continue; } } class=class="str">"cmt">//--- If this is a modification of position stop orders if(req_obj.Action()==TRADE_ACTION_SLTP) { class=class="str">"cmt">//--- Get the list of all account trading events list=this.m_events.GetList(); if(list==NULL) class="kw">continue; class=class="str">"cmt">//--- In the loop from the end of the account trading event list class="type">int events_total=list.Total();
「从账户事件反查挂单改仓是否生效」
在 EA 维护挂单请求队列时,仅靠发单回码无法确认止损/止盈修改真正落到账户上。这段逻辑通过倒序遍历账户交易事件(events_total-1 到 WRONG_VALUE 之前),把最近发生的改单事件和本地挂单请求做票号比对。 for(int j=events_total-1; j>WRONG_VALUE; j--) 从最新事件往前扫,避免漏掉刚成交的修改。若 event.TypeEvent() 大于 TRADE_EVENT_MODIFY_ORDER_TAKE_PROFIT,说明事件类型属于持仓止盈止损之后的变更类。 当 event.PositionID() 等于 req_obj.Position() 时,从市场持仓列表按 ORDER_PROP_POSITION_ID 取该持仓关联的订单对象,取 list_orders.Total()-1 即最近一条。拿到真实订单后调用 SetActualProperties 把实际止损止盈写回请求对象。 若 req_obj.IsCompleted() 返回真,说明本次修改已全部对账成功,打印执行消息并 Delete(i) 移除挂单请求,随后 break 退出事件遍历。外汇与贵金属杠杆交易风险高,这类对账失败可能让保护单悬空,建议在 MT5 策略测试器用历史账户事件跑一遍验证。
for(class="type">int j=events_total-class="num">1; j>WRONG_VALUE; j--) { class=class="str">"cmt">//--- get the next trading event CEvent *event=list.At(j); if(event==NULL) class="kw">continue; class=class="str">"cmt">//--- If this is a change of the position&class="macro">#x27;s stop orders if(event.TypeEvent()>TRADE_EVENT_MODIFY_ORDER_TAKE_PROFIT) { class=class="str">"cmt">//--- If a position ticket in a trading event coincides with the ticket in a pending trading request if(event.PositionID()==req_obj.Position()) { class=class="str">"cmt">//--- Get a position object from the list of market positions CArrayObj *list_orders=this.m_market.GetList(ORDER_PROP_POSITION_ID,req_obj.Position(),EQUAL); if(list_orders==NULL || list_orders.Total()==class="num">0) class="kw">break; COrder *order=list_orders.At(list_orders.Total()-class="num">1); if(order==NULL) class="kw">break; class=class="str">"cmt">//--- Set actual position data to the pending request object this.SetActualProperties(req_obj,order); class=class="str">"cmt">//--- If all modifications have worked out - class=class="str">"cmt">//--- the request is handled: remove it and class="kw">break the loop by the list of account trading events if(req_obj.IsCompleted()) { if(this.m_log_level>LOG_LEVEL_NO_MSG) ::Print(req_obj.Header(),": ",CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_EXECUTED)); this.m_list_request.Delete(i); class="kw">break; } } } }
撤销与修改挂单的回执判定逻辑
在交易请求队列的循环处理里,若某个挂单请求对象已被交易事件列表在循环中销毁,直接用 CheckPointer 判 POINTER_INVALID 并 continue,避免对野指针操作。 对于 TRADE_ACTION_REMOVE 类型的撤单请求,先从历史挂单列表按 ORDER_STATUS_HISTORY_PENDING 筛出已撤销挂单,再用 ByOrderProperty 按 ticket 精确留单。若 list.Total()>0 说明撤单已在历史中落地,打印「挂单请求已执行」后从 m_list_request 删除该索引并 continue。 修改类请求(TRADE_ACTION_MODIFY)走另一条路:取全部账户交易事件列表,从末尾倒序遍历。事件类型落在 TRADE_EVENT_TRIGGERED_STOP_LIMIT_ORDER 与 TRADE_EVENT_MODIFY_POSITION_STOP_LOSS_TAKE_PROFIT 之间,即视为该挂单参数被改动过。外汇与贵金属市场跳空频繁,这类回执判定在高波动时段可能漏判,建议在 MT5 策略测试器用 2022-09-26 黄金跳空行情回放验证事件边界。
class=class="str">"cmt">//--- If a handled pending request object was removed by the trading event list in the loop, move on to the next one if(::CheckPointer(req_obj)==POINTER_INVALID) class="kw">continue; } class=class="str">"cmt">//--- If this is a pending order removal if(req_obj.Action()==TRADE_ACTION_REMOVE) { class=class="str">"cmt">//--- Get the list of removed pending orders from the historical list list=this.m_history.GetList(ORDER_PROP_STATUS,ORDER_STATUS_HISTORY_PENDING,EQUAL); if(::CheckPointer(list)==POINTER_INVALID) class="kw">continue; class=class="str">"cmt">//--- Leave a single order with the necessary ticket in the list list=CSelect::ByOrderProperty(list,ORDER_PROP_TICKET,req_obj.Order(),EQUAL); class=class="str">"cmt">//--- If the order is present, the request is handled: remove it and proceed to the next if(list.Total()>class="num">0) { if(this.m_log_level>LOG_LEVEL_NO_MSG) ::Print(req_obj.Header(),": ",CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_EXECUTED)); this.m_list_request.Delete(i); class="kw">continue; } } class=class="str">"cmt">//--- If this is a pending order modification if(req_obj.Action()==TRADE_ACTION_MODIFY) { class=class="str">"cmt">//--- Get the list of all account trading events list=this.m_events.GetList(); if(list==NULL) class="kw">continue; class=class="str">"cmt">//--- In the loop from the end of the account trading event list class="type">int events_total=list.Total(); for(class="type">int j=events_total-class="num">1; j>WRONG_VALUE; j--) { class=class="str">"cmt">//--- get the next trading event CEvent *event=list.At(j); if(event==NULL) class="kw">continue; class=class="str">"cmt">//--- If this event involves any change of modified pending order parameters if(event.TypeEvent()>TRADE_EVENT_TRIGGERED_STOP_LIMIT_ORDER && event.TypeEvent()<TRADE_EVENT_MODIFY_POSITION_STOP_LOSS_TAKE_PROFIT) {
◍ 挂单请求与成交事件的票据对齐
在异步交易框架里,挂单请求对象和账户交易事件靠订单票据(ticket)做关联。当交易事件里的订单票据和挂单请求对象的 Order() 返回一致时,说明这次事件正对应我们之前发出的那笔改单或撤单。 代码先用 GetList(ORDER_PROP_TICKET, req_obj.Order(), EQUAL) 按票据从市场对象拉出订单列表;若列表为空或取不到 COrder 指针就直接 break,避免空指针往下走。拿到订单后调用 SetActualProperties 把真实盘口数据写回请求对象,这是后续判断「请求是否已完结」的依据。 请求对象若 IsCompleted() 返回真,代表全部修饰已生效,打印一条执行消息后从 m_list_request 里删掉该请求并 break 事件循环。删除后代码用 CheckPointer(req_obj)==POINTER_INVALID 判断对象是否失效,失效就直接 return,防止后续访问悬空指针。 若请求还在等待,就按 TimeCreate()+WaitingMSC()*(CurrentAttempt()+1) 重算激活时间。当 symbol_obj.Time() 小于这个激活时间,说明还没到重试窗口,用 continue 跳到列表下一个请求——这意味着每次循环最多只处理到点的那几个挂单。外汇和贵金属市场跳空频繁,这种基于票据的精确对齐能降低重复发单概率,但异步失败仍可能发生,实际跑 MT5 时建议打开 LOG_LEVEL 观察被 break 的请求数量。
class=class="str">"cmt">//--- If an order ticket in a trading event coincides with the ticket in a pending trading request if(event.TicketOrderEvent()==req_obj.Order()) { class=class="str">"cmt">//--- Get an order object from the list CArrayObj *list_orders=this.m_market.GetList(ORDER_PROP_TICKET,req_obj.Order(),EQUAL); if(list_orders==NULL || list_orders.Total()==class="num">0) class="kw">break; COrder *order=list_orders.At(class="num">0); if(order==NULL) class="kw">break; class=class="str">"cmt">//--- Set actual order data to the pending request object this.SetActualProperties(req_obj,order); class=class="str">"cmt">//--- If all modifications have worked out - class=class="str">"cmt">//--- the request is handled: remove it and class="kw">break the loop by the list of account trading events if(req_obj.IsCompleted()) { if(this.m_log_level>LOG_LEVEL_NO_MSG) ::Print(req_obj.Header(),": ",CMessage::Text(MSG_LIB_TEXT_PEND_REQUEST_EXECUTED)); this.m_list_request.Delete(i); class="kw">break; } } class=class="str">"cmt">//--- Exit if the pending request object has been removed after checking its operation if(::CheckPointer(req_obj)==POINTER_INVALID) class="kw">return; class=class="str">"cmt">//--- Set the request activation time in the request object req_obj.SetTimeActivate(req_obj.TimeCreate()+req_obj.WaitingMSC()*(req_obj.CurrentAttempt()+class="num">1)); class=class="str">"cmt">//--- If the current time is less than the request activation time, class=class="str">"cmt">//--- this is not the request time - move on to the next request in the list if((class="type">long)symbol_obj.Time()<(class="type">long)req_obj.TimeActivate()) class="kw">continue;
「请求重试后按动作类型分派交易」
在交易请求对象里先给当前尝试次数加 1,用 SetCurrentAttempt 写入,便于在断网或报价延迟时追踪第几次重发。若日志级别高于 NO_MSG,就把『重试 N:』打进专家日志并附上请求摘要,实战中能直接看出某笔单子重发了几次。 随后用 switch 根据 request.action 分派:TRADE_ACTION_DEAL 下靠 position 是否为 0 区分开仓与平仓——为 0 调 OpenPosition,非 0 调 ClosePosition;SLTP 改止损止盈,CLOSE_BY 反手平,PENDING 挂单,MODIFY 改挂单,REMOVE 删挂单。 这套分派逻辑没有 default 处理分支,未知 action 直接 break。复制下面代码到 EA 里跑,能在 MT5 终端『专家』标签看到重试计数随网络抖动自然增长,外汇与贵金属杠杆高,重发机制只降低漏单概率,不消除滑点风险。
class=class="str">"cmt">//--- Set the attempt number in the request object req_obj.SetCurrentAttempt((class="type">uchar)(req_obj.CurrentAttempt()+class="num">1)); class=class="str">"cmt">//--- Display the number of a trading attempt in the journal if(this.m_log_level>LOG_LEVEL_NO_MSG) { ::Print(CMessage::Text(MSG_LIB_TEXT_RE_TRY_N)+(class="type">class="kw">string)req_obj.CurrentAttempt()+":"); req_obj.PrintShort(); } class=class="str">"cmt">//--- Depending on the type of action performed in the trading request class="kw">switch(request.action) { class=class="str">"cmt">//--- Opening/closing a position case TRADE_ACTION_DEAL : class=class="str">"cmt">//--- If no ticket is present in the request structure - this is opening a position if(request.position==class="num">0) this.OpenPosition((class="type">ENUM_POSITION_TYPE)request.type,request.volume,request.symbol,request.magic,request.sl,request.tp,request.comment,request.deviation,request.type_filling); class=class="str">"cmt">//--- If the ticket is present in the request structure - this is a position closure else this.ClosePosition(request.position,request.volume,request.comment,request.deviation); class="kw">break; class=class="str">"cmt">//--- Modify StopLoss/TakeProfit position case TRADE_ACTION_SLTP : this.ModifyPosition(request.position,request.sl,request.tp); class="kw">break; class=class="str">"cmt">//--- Close by an opposite one case TRADE_ACTION_CLOSE_BY : this.ClosePositionBy(request.position,request.position_by); class="kw">break; class=class="str">"cmt">//--- class=class="str">"cmt">//--- Place a pending order case TRADE_ACTION_PENDING : this.PlaceOrder(request.type,request.volume,request.symbol,request.price,request.stoplimit,request.sl,request.tp,request.magic,request.comment,request.expiration,request.type_time,request.type_filling); class="kw">break; class=class="str">"cmt">//--- Modify a pending order case TRADE_ACTION_MODIFY : this.ModifyOrder(request.order,request.price,request.sl,request.tp,request.stoplimit,request.expiration,request.type_time,request.type_filling); class="kw">break; class=class="str">"cmt">//--- Remove a pending order case TRADE_ACTION_REMOVE : this.DeleteOrder(request.order); class="kw">break; class=class="str">"cmt">//--- class="kw">default: class="kw">break; } } }
引擎头文件的引用拼装
一个交易引擎的 mqh 头文件,先把版权与版本元信息钉死:copyright 标到 2019 年某软件公司,version 写死 1.00,link 指向作者主页。这部分只是编译期标识,不影响运行时逻辑,但缺了会在 MT5 编辑器里报属性缺失警告。 真正决定能力边界的是后面那串 include。基础服务层引入 Services\TimerCounter.mqh 做计时;数据层把 HistoryCollection、MarketCollection、EventsCollection、AccountsCollection、SymbolsCollection、ResourceCollection 六个集合类全拉进来,覆盖历史、行情、事件、账户、品种、资源六类状态。 交易动作层有两版实现,注意上一份草稿里挂的是 Trading.mqh,定稿换成了 TradingControl.mqh——后者倾向把下单、撤单、风控开关收敛到统一控制类,而不是散在交易助手函数里。你在自己 EA 里接引擎时,改这一行就能切换交易执行骨架。 外汇与贵金属品种波动大、杠杆高,直接套用第三方引擎头文件前,先确认 TradingControl 里的仓位校验逻辑是否适配你的经纪商点差与停利距离限制,否则可能触发无效下单。
class="macro">#class="kw">property copyright "Copyright class="num">2019, 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">//| Include files | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Services\TimerCounter.mqh" class="macro">#include "Collections\HistoryCollection.mqh" class="macro">#include "Collections\MarketCollection.mqh" class="macro">#include "Collections\EventsCollection.mqh" class="macro">#include "Collections\AccountsCollection.mqh" class="macro">#include "Collections\SymbolsCollection.mqh" class="macro">#include "Collections\ResourceCollection.mqh" class="macro">#include "TradingControl.mqh"
◍ 引擎基类的成员布局
在 MT5 的 EA 架构里,把交易逻辑拆成集合类再统一挂到引擎基类,是降低耦合的常见做法。下面这段 CEngine 私有区就集中声明了历史、市场、事件、账户、品种、资源六类集合,以及一个交易对象与定时器计数器列表。 早期版本里交易对象用的是 CTrading,后来改成了 CTradingControl,从命名看是从单纯下单封装升级成了带风控管理的交易控制对象。你在自己移植老代码时,若链接库报找不到 CTrading,大概率就是版本错位,直接换成 CTradingControl 即可。 外汇与贵金属杠杆高,这类封装只是工程结构优化,不意味任何胜率保证,回测和实盘前都应在策略测试器里跑一遍样本外数据。
class CEngine { class="kw">private: CHistoryCollection m_history; class=class="str">"cmt">// Collection of historical orders and deals CMarketCollection m_market; class=class="str">"cmt">// Collection of market orders and deals CEventsCollection m_events; class=class="str">"cmt">// Event collection CAccountsCollection m_accounts; class=class="str">"cmt">// Account collection CSymbolsCollection m_symbols; class=class="str">"cmt">// Symbol collection CResourceCollection m_resource; class=class="str">"cmt">// Resource list CTradingControl m_trading; class=class="str">"cmt">// Trading management object CArrayObj m_list_counters; class=class="str">"cmt">// List of timer counters };
「禁用自动交易时延后请求怎么活下来」
把上一版的 EA 放到 \MQL5\Experts\TestDoEasy\Part30\ 下,命名 TestDoEasyPart30.mq5,编译后挂到模拟账户图表。先把 StopLoss in points 和 TakeProfit in points 两个参数都设成 0,这样挂单不会自带止损止盈,方便后面测修改逻辑。 点掉终端的“自动交易”按钮,用 EA 面板发一个 SellLimit 挂单。此时终端返回禁用交易的错误,但请求没丢——延后请求对象被创建,日志里能看到 Pending request created #1,品种 GBPUSD、手数 0.10、限价 1.30088。 重新点亮“自动交易”,延后请求立刻重试,Retry trading attempt #1 把单子真正发到服务器,执行完后对象自动删除。外汇和贵金属模拟盘本身就有滑点和拒绝风险,禁用开关只是人为制造失败条件,真金白银跑之前务必先在 demo 验三遍。 接着测挂单生成后的止损修改:再次禁用自动交易,点面板“Set StopLoss”,同样因禁用报错并生成延后请求;启用自动交易后止损被补上、请求删除。对止盈做同样操作也通过。这说明新版本对同一订单可连续做不同操作,旧版每张票只能干一次就当完事了。 下面这段日志是上面 SellLimit 延后创建到重试的真实输出,注意 Attempts 5 和 Wait 00:00:20 是对象重试图谱,不是收益承诺:
- 12.27 04:16:28.894 automated trading is disabled
- 12.27 04:16:33.177 CTrading::PlaceOrder<uint,int,uint,uint>: Invalid request:
- 12.27 04:16:33.177 There is no permission to conduct trading operations in the terminal (the "AutoTrading" button is disabled)
- 12.27 04:16:33.177 Correction of trade request parameters ...
- 12.27 04:16:33.178 Pending request created #1:
- 12.27 04:16:33.178 Pending request to place a pending order:
- 12.27 04:16:33.178 - GBPUSD 0.10 Pending order Sell Limit, Price 1.30088
- 12.27 04:16:33.178 - Pending request ID: #1, Created 2019.12.26 23:16:30.003, Attempts 5, Wait 00:00:20, End 2019.12.26 23:18:10.003
- 12.27 04:16:33.178
- 12.27 04:16:37.397 automated trading is enabled
- 12.27 04:16:37.472 Retry trading attempt #1:
- 12.27 04:16:37.472 Pending request to place a pending order:
- 12.27 04:16:37.472 - GBPUSD 0.10 Pending order Sell Limit, Price 1.30088
- 12.27 04:16:37.472 - Pending request ID: #1, Created 2019.12.26 23:16:10.003, Attempts 5, Wait 00:00:20, End 2019.12.26 23:17:50.003
class="num">2019.12.class="num">27 class="num">04:class="num">16:class="num">28.894 automated trading is disabled class="num">2019.12.class="num">27 class="num">04:class="num">16:class="num">33.177 CTrading::PlaceOrder<class="type">uint,class="type">int,class="type">uint,class="type">uint>: Invalid request: class="num">2019.12.class="num">27 class="num">04:class="num">16:class="num">33.177 There is no permission to conduct trading operations in the terminal(the "AutoTrading" button is disabled) class="num">2019.12.class="num">27 class="num">04:class="num">16:class="num">33.177 Correction of trade request parameters ... class="num">2019.12.class="num">27 class="num">04:class="num">16:class="num">33.178 Pending request created #class="num">1: class="num">2019.12.class="num">27 class="num">04:class="num">16:class="num">33.178 Pending request to place a pending order: class="num">2019.12.class="num">27 class="num">04:class="num">16:class="num">33.178 - GBPUSD class="num">0.10 Pending order Sell Limit, Price class="num">1.30088 class="num">2019.12.class="num">27 class="num">04:class="num">16:class="num">33.178 - Pending request ID: #class="num">1, Created class="num">2019.12.class="num">26 class="num">23:class="num">16:class="num">30.003, Attempts class="num">5, Wait class="num">00:class="num">00:class="num">20, End class="num">2019.12.class="num">26 class="num">23:class="num">18:class="num">10.003 class="num">2019.12.class="num">27 class="num">04:class="num">16:class="num">33.178 class="num">2019.12.class="num">27 class="num">04:class="num">16:class="num">37.397 automated trading is enabled class="num">2019.12.class="num">27 class="num">04:class="num">16:class="num">37.472 Retry trading attempt #class="num">1: class="num">2019.12.class="num">27 class="num">04:class="num">16:class="num">37.472 Pending request to place a pending order: class="num">2019.12.class="num">27 class="num">04:class="num">16:class="num">37.472 - GBPUSD class="num">0.10 Pending order Sell Limit, Price class="num">1.30088 class="num">2019.12.class="num">27 class="num">04:class="num">16:class="num">37.472 - Pending request ID: #class="num">1, Created class="num">2019.12.class="num">26 class="num">23:class="num">16:class="num">10.003, Attempts class="num">5, Wait class="num">00:class="num">00:class="num">20, End class="num">2019.12.class="num">26 class="num">23:class="num">17:class="num">50.003
自动交易被关后挂单改参数的pending兜底
- 12.27 04:16:37 这条日志里,EA 在 GBPUSD 以 1.30088 放了 0.10 手 Sell Limit 挂单(#500442708,Magic 26148987),G1=15、G2=8,随后同毫秒触发 OnDoEasyEvent 报「Pending order placed」。
约 7 分 34 秒后(04:24:11.671)终端 automated trading is disabled,紧接着 ModifyOrder 模板函数抛 Invalid request:终端禁止交易操作(AutoTrading 按钮关闭)。此时 EA 没有放弃,而是把改挂单参数的请求转成 pending request #1,记录 Price 1.30088、SL 1.30208、执行方式 Return、到期 GTC,并注明该请求创建于前一日 23:24:00.270、已尝试 5 次、等待 20 秒至 23:25:40.270。 到 04:24:25.803 出现 Retry trading attempt #1,说明 EA 在自动交易恢复后会按 ID 重放这些 pending 请求。外汇和贵金属波动快,这类「交易权限中断→请求挂起→恢复重试」的机制能避免漏单,但终端若长时间禁自动交易,挂单仍可能因价格跳过而失效,属正常高风险现象。 开 MT5 把日志级别调到含 OnDoEasyEvent 与 CTrading::ModifyOrder,断网或手动关 AutoTrading 复现一次,就能在「Experts」标签里看到 pending request 的创建与重试节奏。
◍ 挂单改止损的重试日志长这样
EA 在 2019.12.27 04:24:25.803 抛出一笔 GBPUSD 0.10 手的 Sell Limit #500442708 改参请求,原挂单价 1.30088、待补止损 1.30208,订单属性为 IOC 回退(未成交部分留场)且 GTC 不过期。这条请求关联的 Pending request ID #1 早在 2019.12.26 23:24:00 创建,已尝试 5 次、等待 20 秒后于 23:25:40 结束,说明前夜就改止损失败过一轮。 04:24:29.770 自动交易被重新启用,紧接着 04:24:30.022 触发 Retry trading attempt #1。重试再次提交同一笔 Sell Limit 的改参:价格 1.30088、止损 1.30208 不变,但日志里该 ID 的创建时间变成了 23:23:40、结束于 23:25:20——两个时间戳差异提示你,MT5 日志中重试请求的元数据可能来自不同内部队列,排查时要以『订单号+魔法数』为主键而非请求 ID。 真正落地在 04:24:30.405:止损由 0.00000 改为 1.30208,魔法数 26148987、附加标记 G1=15 / G2=8 / ID=1,并触发 OnDoEasyEvent 的 Modified order StopLoss。外汇与贵金属挂单改止损若卡在 broker 拒绝或自动化开关抖动,靠这种带 Attempts 计数的重试机制才能补单;GBPUSD 波动快,这类 20 秒级等待窗口在高波动时段可能仍不够,建议你在策略测试器里把重试间隔拉到实盘延迟量级验证一次。
「自动交易被关后挂单改参数的重试现场」
一段真实的 MT5 日志能拆出不少门道。2019.12.27 04:32:46 终端报出 automated trading is disabled,仅 4 秒后 EA 调用 CTrading::ModifyOrder 改挂单参数,立刻被终端驳回:没有权限,因为「AutoTrading」按钮是灰的。 被拒的不是行情判断,而是通道权限。日志里 GBPUSD 0.10 手的 Sell Limit #500442708 挂单价 1.30088、SL 1.30208、TP 1.29888 都还在,系统把它转成 Pending request #1,执行类型 Return、过期类型 GTC,等待 20 秒后到 23:34:27 仍未成交。 04:33:08 触发 Retry trading attempt #1,同样的改单参数被重新递交。说明 EA 内部有重试队列,但前提是自动交易按钮重新点亮,否则每次重试都会卡在同一句「no permission」。 外汇和贵金属波动快,这类被权限掐断的改单若赶上报价跳空,挂单可能以不利价位成交。开 MT5 按 F8 看「专家」标签,把 AutoTrading 状态变化和 ModifyOrder 报错对着看,比盲信 EA 稳定更能保命。
挂单改参重试的日志长什么样
下面是 GBPUSD 一笔 Sell Limit 在 2019.12.27 凌晨重发改单请求的真实日志片段。原挂单编号 500442708,手数 0.10,挂单价 1.30088,SL 1.30208,TP 1.29888,属于 Good till cancel 类型,执行方式为非全额即留市(Return)。 首次重试标记 Retry trading attempt #2 出现在 04:33:29.984,但当时 automated trading 尚未启用;直到 04:33:31.999 日志打出 automated trading is enabled,下一跳 04:33:32.250 才真正发出改单请求。两者相隔约 2.25 秒,说明 EA 会等交易开关就绪再动手。 注意 Pending request ID 始终是 #1,创建于 2019.12.26 23:32:47.943,Attempts 5,Wait 00:00:20,End 落在 23:34:27.943。也就是说底层挂单任务没变,只是上层重试机制在隔日重新拉起一次改参流程。 如果你在 MT5 终端碰到类似『重试改单不生效』,先翻日志确认 automated trading is enabled 的时间点,再看 Pending request ID 是否和昨天的任务同源,能省掉一半瞎猜。外汇与贵金属挂单受流动性与滑点影响,重试失败概率不低,需以实盘日志为准。
◍ 挂单改保本后的日志长什么样
下面这段 MT5 专家日志来自 2019.12.27 04:33:32 附近,记录了一张 GBPUSD 的 Sell Limit 挂单被动态修改止盈的全过程。从 04:33:32.250 到 .754 不到 0.5 秒,系统连续写了 5 条记录,说明事件触发与订单修改是同步完成的。
关键一行是 Modified order TakeProfit: [0.00000 --> 1.29888],止盈从零(未设)跳到 1.29888,Magic number 为 26148987,附加参数 G1=15、G2=8、ID=1。外汇与贵金属杠杆高,这类自动改 TP 的逻辑若参数错配,可能在剧烈波动中频繁发单,实盘前务必在策略测试器回放该时段验证。
最后一条灰色记录 Deleted due completed 表示本次改单请求已因执行完毕被清理,没有残留挂起请求。开 MT5 把日志级别调到 Trade 详细,就能看到同样格式的实时输出,对照检查你的 EA 是否漏捕 OnDoEasyEvent。
class="num">2019.12.class="num">27 class="num">04:class="num">33:class="num">32.250 class="num">2019.12.class="num">27 class="num">04:class="num">33:class="num">32.352 - Modified order TakeProfit: class="num">2019.12.class="num">26 class="num">23:class="num">24:class="num">26.509 - class="num">2019.12.class="num">27 class="num">04:class="num">33:class="num">32.352 GBPUSD Pending order Sell Limit #class="num">500442708: Modified order TakeProfit: [class="num">0.00000 --> class="num">1.29888], Magic number class="num">26148987 (class="num">123), G1: class="num">15, G2: class="num">8, ID: class="num">1 class="num">2019.12.class="num">27 class="num">04:class="num">33:class="num">32.352 OnDoEasyEvent: Modified order TakeProfit class="num">2019.12.class="num">27 class="num">04:class="num">33:class="num">32.754 Pending request to modify pending order parameters, ID #class="num">1: Deleted due completed
「延后请求封装的后续落点」
本篇把延后交易请求的对象类收了尾,当前函数库压缩包 MQL5.zip 体积 3642.78 KB、MQL4.zip 为 3642.77 KB,可直接拉到 MT5 里覆盖原有 Include 目录做编译验证。 有读者在 2022 年 8 月反馈,拉取第 31 篇附件后编译 TradingControl.mqh 报私有成员不可访问:OpenPosition 与 PlaceOrder 两个模板方法若处在 CTrading 私有段会触发 328 行与 344 行错误,作者建议挪到 protected 段即可绕开。 外汇与贵金属品种用这套延后机制挂单,遇报价跳空可能令请求对象排队超时,属高风险操作,实盘前务必在策略测试器跑满历史样本。 下一篇计划接着扩延后请求的执行逻辑,当前版本文件已够你先摸清请求对象生命周期,改一处访问权限就能复现编译现象。