轻松快捷开发 MetaTrader 程序的函数库(第八部分):订单和持仓修改事件·综合运用
📘

轻松快捷开发 MetaTrader 程序的函数库(第八部分):订单和持仓修改事件·综合运用

第 3/3 篇
本章目录
  1. 事件属性枚举与排序基址的硬边界
  2. 事件排序枚举里的真实字段映射
  3. 事件排序枚举与基类的属性寻址
  4. 交易事件对象的内部属性与构造细节
  5. 把事件属性翻成可读的中文标签
  6. 订单属性在事件描述里怎么映射成文字
  7. 把交易事件属性翻成可读文本
  8. 交易事件属性与类型的中文映射
  9. 账户与订单事件的中文映射写法
  10. 交易事件映射成双语标签的写法
  11. 把平仓与反手事件映射成双语提示
  12. 成交与挂单改单事件的枚举分支
  13. 挂单与持仓的保护价改动事件映射
  14. 成交回报里的撤销与反转语义
  15. 平仓与资金事件的俄语英文双标签映射
  16. 账户事件原因的本地化文本映射
  17. 交易事件对象的属性读取接口
  18. 持仓翻转事件的属性读取接口
  19. 持仓切换事件的属性读取接口
  20. 订单事件里的价格与成交量取数接口
  21. 事件对象里的价格与品种取值接口
  22. 挂单与持仓改单的事件细分逻辑
  23. 挂单触发下的持仓反转与加仓判定
  24. 用事件标志区分反转、加仓与挂单激活
  25. 平仓事件标志的逐层判定逻辑
  26. 订单盈亏点数该怎么算
  27. 订单整型属性的可读化封装
  28. 订单属性多语种文本拼装的分支逻辑
  29. 订单毫秒级时间与状态字段的读取分支
  30. 订单属性多语标签的拼装逻辑
  31. 挂单修改事件的类封装与属性过滤
  32. 挂单改价与改价带止损的事件回显
  33. 挂单改价与止损止盈的事件分流
  34. 挂单与持仓的止损止盈变更事件分流
  35. 持仓止损止盈改动时的事件文本拼装
  36. 事件容器的私有成员与构造初始化
  37. 挂单与持仓改仓事件的位标记拆解
  38. 给持仓翻转事件塞满上下文属性
  39. 挂单事件对象的属性填充与去重
  40. 给测试EA补上止损止盈与尾随按钮
  41. 跟踪止损与改单参数的输入声明
  42. 初始化里把 trailing 和按钮状态钉死
  43. 按钮事件映射到持仓动作的实现细节
  44. 一键给所有持仓挂止损的逻辑
  45. 给缺失止盈的持仓与挂单补 TP
  46. 卖单与挂单的跟踪止损实现差异
  47. 挂单随价移动的改单逻辑
  48. 卖单挂单的跟随式重定价逻辑
  49. 挂单改价与面板按钮的衔接逻辑
  50. 挂单与风控参数的输入声明
  51. EA 初始化与Tick循环的骨架
  52. 定时器与图表事件的分流处理
  53. 按钮面板的布局与创建逻辑
  54. 给按钮挂上状态与文字的收尾逻辑
  55. 按钮事件怎么映射到下单动作
  56. 按钮触发后的挂单与市价单分支
  57. 挂单与平仓按钮的底层派发逻辑
  58. 盈利单的半平与对冲闭环写法
  59. 用对冲单和利润排序收掉仓位
  60. 用反向单平掉最赚的卖仓
  61. 按钮触发后的持仓与挂单处理逻辑
  62. 跟单面板的止损批量接管逻辑
  63. 批量补 TP 与最大利润单的移动止损
  64. 卖单与挂单的追踪修改逻辑
  65. 挂单尾随里的价格与止损重算逻辑
  66. 卖单挂单如何跟着价格做移动修正
  67. 空小节无内容可析
  68. 记住这一条就够了

「事件属性枚举与排序基址的硬边界」

在 MT5 的事件回调体系里,双精度属性被收进一组枚举常量,从止盈价、成交量到成交后持仓量都有独立标签。注意高亮段:EVENT_PROP_PRICE_OPEN_BEFORE、SL_BEFORE、TP_BEFORE 记录修改前价格,EVENT_PROP_PRICE_EVENT_ASK/BID 抓取事件瞬间的报价,这两者用途不同,前者用于改单回溯,后者用于当时盘口快照。 宏定义给出了两个硬数字:EVENT_PROP_DOUBLE_TOTAL 为 15,代表事件真实双精度属性总数;EVENT_PROP_DOUBLE_SKIP 为 5,代表排序时不参与的整数类订单属性偏移量。排序基址 FIRST_EVN_STR_PROP 由此推算,等于整型总数减跳过量再加 15 减 5,写过滤器时若手算错一位,事件流就会错位。 枚举 ENUM_SORT_EVENTS_MODE 从 0 起跳:SORT_BY_EVENT_TYPE_EVENT 按事件类型排,SORT_BY_EVENT_TIME_EVENT 按事件时间排。实盘跑之前,先在 MetaEditor 里把这两个宏数值打印出来核对,外汇与贵金属杠杆高,事件顺序错乱可能让风控逻辑误触发。

MQL5 / C++
  EVENT_PROP_PRICE_TP,                                                  class=class="str">"cmt">// TakeProfit Order/deal/position
  EVENT_PROP_VOLUME_ORDER_INITIAL,                                       class=class="str">"cmt">// Requested order volume
  EVENT_PROP_VOLUME_ORDER_EXECUTED,                                      class=class="str">"cmt">// Executed order volume
  EVENT_PROP_VOLUME_ORDER_CURRENT,                                       class=class="str">"cmt">// Remaining order volume
  EVENT_PROP_VOLUME_POSITION_EXECUTED,                                   class=class="str">"cmt">// Current executed position volume after a deal
  EVENT_PROP_PROFIT,                                                     class=class="str">"cmt">// Profit
  class=class="str">"cmt">//---
  EVENT_PROP_PRICE_OPEN_BEFORE,                                          class=class="str">"cmt">// Order price before modification
  EVENT_PROP_PRICE_SL_BEFORE,                                            class=class="str">"cmt">// StopLoss price before modification
  EVENT_PROP_PRICE_TP_BEFORE,                                            class=class="str">"cmt">// TakeProfit price before modification
  EVENT_PROP_PRICE_EVENT_ASK,                                            class=class="str">"cmt">// Ask price during an event
  EVENT_PROP_PRICE_EVENT_BID,                                            class=class="str">"cmt">// Bid price during an event
  };
class="macro">#define EVENT_PROP_DOUBLE_TOTAL(class="num">15)                                    class=class="str">"cmt">// Total number of event&class="macro">#x27;s real properties
class="macro">#define EVENT_PROP_DOUBLE_SKIP(class="num">5)                                     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 event sorting criteria                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define FIRST_EVN_DBL_PROP(EVENT_PROP_INTEGER_TOTAL-EVENT_PROP_INTEGER_SKIP)
class="macro">#define FIRST_EVN_STR_PROP(EVENT_PROP_INTEGER_TOTAL-EVENT_PROP_INTEGER_SKIP+EVENT_PROP_DOUBLE_TOTAL-EVENT_PROP_DOUBLE_SKIP)
enum ENUM_SORT_EVENTS_MODE
  {
  class=class="str">"cmt">//--- Sort by integer properties
  SORT_BY_EVENT_TYPE_EVENT       = class="num">0,                                    class=class="str">"cmt">// Sort by event type
  SORT_BY_EVENT_TIME_EVENT       = class="num">1,                                    class=class="str">"cmt">// Sort by event time

事件排序枚举里的真实字段映射

在 MT5 的历史事件检索接口里,排序方向不是靠字符串指定,而是吃一组枚举常量。下面这段枚举把『按什么字段排』直接编码成整型,从 2 到 14 覆盖事件状态、成交票据、持仓 ID、魔术码等维度。 注意 SORT_BY_EVENT_STATUS_EVENT = 2 到 SORT_BY_EVENT_MAGIC_BY_ID = 14 这一段,全部是整型字面量,调用 HistorySelectByEvent 之类函数时传这些数即可按对应属性排序。 从 FIRST_EVN_DBL_PROP 开始则是『真实价格属性』区:SORT_BY_EVENT_PRICE_EVENT 等于该基准值,之后每个 +1 顺推——开仓价 +1、平仓价 +2、SL +3、TP +4。如果你要在 EA 里按平仓价倒序捞某魔术码下的历史,直接传 FIRST_EVN_DBL_PROP+2 并配合排序方向标志就行。 开 MT5 按 F4 进 MetaEditor,全局搜 ENUM_EVENT_SORT 能定位到完整定义,改一个常量值编译看 HistoryGet 返回顺序变化,是最快的验证路径。外汇与贵金属杠杆高,回测排序逻辑出错可能误导仓位统计,实盘前务必小资金校验。

MQL5 / C++
  SORT_BY_EVENT_STATUS_EVENT                 = class="num">2,                 class=class="str">"cmt">// Sort by event status(from the ENUM_EVENT_STATUS enumeration)
  SORT_BY_EVENT_REASON_EVENT                 = class="num">3,                 class=class="str">"cmt">// Sort by event reason(from the ENUM_EVENT_REASON enumeration)
  SORT_BY_EVENT_TYPE_DEAL_EVENT              = class="num">4,                 class=class="str">"cmt">// Sort by deal event type
  SORT_BY_EVENT_TICKET_DEAL_EVENT            = class="num">5,                 class=class="str">"cmt">// Sort by deal event ticket
  SORT_BY_EVENT_TYPE_ORDER_EVENT             = class="num">6,                 class=class="str">"cmt">// Sort by type of an order, based on which a deal event is opened(the last position order)
  SORT_BY_EVENT_TICKET_ORDER_EVENT           = class="num">7,                 class=class="str">"cmt">// Sort by type of an order, based on which a position deal is opened(the first position order)
  SORT_BY_EVENT_TIME_ORDER_POSITION          = class="num">8,                 class=class="str">"cmt">// Sort by time of an order, based on which a position deal is opened(the first position order)
  SORT_BY_EVENT_TYPE_ORDER_POSITION          = class="num">9,                 class=class="str">"cmt">// Sort by type of an order, based on which a position deal is opened(the first position order)
  SORT_BY_EVENT_TICKET_ORDER_POSITION        = class="num">10,                class=class="str">"cmt">// Sort by a ticket of an order, based on which a position deal is opened(the first position order)
  SORT_BY_EVENT_POSITION_ID                  = class="num">11,                class=class="str">"cmt">// Sort by position ID
  SORT_BY_EVENT_POSITION_BY_ID               = class="num">12,                class=class="str">"cmt">// Sort by opposite position ID
  SORT_BY_EVENT_MAGIC_ORDER                 = class="num">13,                class=class="str">"cmt">// Sort by order/deal/position magic number
  SORT_BY_EVENT_MAGIC_BY_ID                 = class="num">14,                class=class="str">"cmt">// Sort by opposite position magic number
  class=class="str">"cmt">//--- Sort by real properties
  SORT_BY_EVENT_PRICE_EVENT                  =  FIRST_EVN_DBL_PROP,    class=class="str">"cmt">// Sort by a price an event occurred at
  SORT_BY_EVENT_PRICE_OPEN                   =  FIRST_EVN_DBL_PROP+class="num">1,  class=class="str">"cmt">// Sort by position open price
  SORT_BY_EVENT_PRICE_CLOSE                  =  FIRST_EVN_DBL_PROP+class="num">2,  class=class="str">"cmt">// Sort by position close price
  SORT_BY_EVENT_PRICE_SL                     =  FIRST_EVN_DBL_PROP+class="num">3,  class=class="str">"cmt">// Sort by position&class="macro">#x27;s StopLoss price
  SORT_BY_EVENT_PRICE_TP                     =  FIRST_EVN_DBL_PROP+class="num">4,  class=class="str">"cmt">// Sort by position&class="macro">#x27;s TakeProfit price

◍ 事件排序枚举与基类的属性寻址

在交易事件框架里,排序字段被拆成了双精度与字符串两组枚举。双精度侧从 FIRST_EVN_DBL_PROP+5 到 +9 分别对应:初始成交量、执行成交量、当前剩余量、持仓已执行量、以及盈亏额;字符串侧 FIRST_EVN_STR_PROP 起则覆盖品种名与对立持仓品种名。 CEvent 抽象基类用 IndexProp() 做属性数组下标换算:双精度属性减 EVENT_PROP_INTEGER_TOTAL,字符串属性再追减 EVENT_PROP_DOUBLE_TOTAL。这意味着整型、双精度、字符串三类属性在内部是分块连续存储的,绕开这套偏移直接读数组会越界。 基类私有成员 m_event_code 存事件码,保护段里的 m_digits 取自 SymbolInfoInteger(SYMBOL_DIGITS),m_digits_acc 管账户币种小数位。开 MT5 把这两行打在 OnTradeTransaction 里 Print 出来,能快速核对黄金 3 位、原油 2 位与账户精度的差异。 别把枚举偏移当常量硬抄 不同 build 里 FIRST_EVN_DBL_PROP 的基值可能随整数属性总数变动,直接写死 +5~+9 会在升级后错位。用枚举名而非字面量,让编译器算偏移。

MQL5 / C++
SORT_BY_EVENT_VOLUME_ORDER_INITIAL     =  FIRST_EVN_DBL_PROP+class="num">5,   class=class="str">"cmt">// Sort by initial volume
SORT_BY_EVENT_VOLUME_ORDER_EXECUTED     =  FIRST_EVN_DBL_PROP+class="num">6,   class=class="str">"cmt">// Sort by the current volume
SORT_BY_EVENT_VOLUME_ORDER_CURRENT      =  FIRST_EVN_DBL_PROP+class="num">7,   class=class="str">"cmt">// Sort by remaining volume
SORT_BY_EVENT_VOLUME_POSITION_EXECUTED =  FIRST_EVN_DBL_PROP+class="num">8,   class=class="str">"cmt">// Sort by remaining volume
SORT_BY_EVENT_PROFIT                    =  FIRST_EVN_DBL_PROP+class="num">9,   class=class="str">"cmt">// Sort by profit
class=class="str">"cmt">//--- Sort by class="type">class="kw">string properties
SORT_BY_EVENT_SYMBOL                    =  FIRST_EVN_STR_PROP,     class=class="str">"cmt">// Sort by order/position/deal symbol
SORT_BY_EVENT_SYMBOL_BY_ID              class=class="str">"cmt">// Sort by an opposite position symbol
};
class CEvent : class="kw">public CObject
  {
class="kw">private:
   class="type">int               m_event_code;            class=class="str">"cmt">// Event code
   class="type">int               IndexProp(ENUM_EVENT_PROP_DOUBLE class="kw">property)class="kw">const { class="kw">return(class="type">int)class="kw">property-EVENT_PROP_INTEGER_TOTAL; }
   class="type">int               IndexProp(ENUM_EVENT_PROP_STRING class="kw">property)class="kw">const { class="kw">return(class="type">int)class="kw">property-EVENT_PROP_INTEGER_TOTAL-EVENT_PROP_DOUBLE_TOTAL; }
class="kw">protected:
   ENUM_TRADE_EVENT  m_trade_event;           class=class="str">"cmt">// Trading event
   class="type">bool              m_is_hedge;              class=class="str">"cmt">// Hedging account flag
   class="type">long              m_chart_id;              class=class="str">"cmt">// Control program chart ID
   class="type">int               m_digits;                class=class="str">"cmt">// Symbol Digits()
   class="type">int               m_digits_acc;            class=class="str">"cmt">// Number of decimal places for the account currency

「交易事件对象的内部属性与构造细节」

在 MT5 的自定义事件框架里,CEvent 类用三组数组把一次交易事件的所有状态压成结构化数据:long 型存整数属性(如状态、订单号),double 型存实数属性,string 型存文本属性。数组长度由 EVENT_PROP_INTEGER_TOTAL 等枚举常量决定,开 MT5 导航到相关头文件就能看到具体数值,通常整数属性总数在 10 项以上。 IsPresentEventFlag 用位与运算判断某事件标志是否置位:((this.m_event_code & event_code)==event_code)。这种写法比逐个布尔字段判断省内存,也方便一次事件携带多个标志位。 受保护的参数化构造函数里干了几件实事:把传入的 event_status 和 ticket 写进 long 数组对应槽位;用 AccountInfoInteger(ACCOUNT_MARGIN_MODE) 是否等于 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING 来给 m_is_hedge 赋值,对冲账户会跑不同平仓逻辑;m_digits_acc 取 ACCOUNT_CURRENCY_DIGITS 决定金额显示精度;m_chart_id 直接绑 ChartID()。外汇与贵金属杠杆高,账户Margin模式判断错可能让仓位计算偏移,验证时先打印 m_is_hedge 确认环境。 GetPropertyDescription 对整数属性做多语言文本映射,俄语和英语双套字符串通过 TextByLanguage 切换,时间类属性走 TimeMSCtoString 转可读串。复制下面片段到你的 EA 里,改改枚举就能直接复用事件描述输出。

MQL5 / C++
class="type">long m_long_prop[EVENT_PROP_INTEGER_TOTAL]; class=class="str">"cmt">// Event integer properties
class="type">class="kw">double m_double_prop[EVENT_PROP_DOUBLE_TOTAL]; class=class="str">"cmt">// Event real properties
class="type">class="kw">string m_string_prop[EVENT_PROP_STRING_TOTAL]; class=class="str">"cmt">// Event class="type">class="kw">string properties
class=class="str">"cmt">//--- Return the flag presence in the trading event
class="type">bool IsPresentEventFlag(class="kw">const class="type">int event_code) class="kw">const { class="kw">return (this.m_event_code & event_code)==event_code; }
class=class="str">"cmt">//--- Protected parametric constructor
CEvent(class="kw">const ENUM_EVENT_STATUS event_status,class="kw">const class="type">int event_code,class="kw">const class="type">class="kw">ulong ticket);
class="kw">public:
class=class="str">"cmt">//--- Default constructor
CEvent(class="type">void){;}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor |
class=class="str">"cmt">//+------------------------------------------------------------------+
CEvent::CEvent(class="kw">const ENUM_EVENT_STATUS event_status,class="kw">const class="type">int event_code,class="kw">const class="type">class="kw">ulong ticket) : m_event_code(event_code),m_digits(class="num">0)
  {
   this.m_long_prop[EVENT_PROP_STATUS_EVENT]     =  event_status;
   this.m_long_prop[EVENT_PROP_TICKET_ORDER_EVENT] = (class="type">long)ticket;
   this.m_is_hedge=class="type">bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
   this.m_digits_acc=(class="type">int)::AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS);
   this.m_chart_id=::ChartID();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the description of the event&class="macro">#x27;s integer class="kw">property |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CEvent::GetPropertyDescription(ENUM_EVENT_PROP_INTEGER class="kw">property)
  {
   class="kw">return
     (
      class="kw">property==EVENT_PROP_TYPE_EVENT           ? TextByLanguage("Тип события","Event&class="macro">#x27;s type")+": "+this.TypeEventDescription() :
      class="kw">property==EVENT_PROP_TIME_EVENT           ? TextByLanguage("Время события","Time of event")+": "+TimeMSCtoString(this.GetProperty(class="kw">property)) :

把事件属性翻成可读的中文标签

在 MT5 的事件回调里,原始 property 枚举只是一串整数,直接打印对盯盘毫无帮助。下面这段三元表达式链把 10 种常用事件属性映射成了双语可读文本,俄语给终端界面、英语给日志文件,方便你直接抄进自己的 CEvent 派生类。 property==EVENT_PROP_STATUS_EVENT ? TextByLanguage("Статус события","Status of event")+": \""+this.StatusDescription()+"\"" : property==EVENT_PROP_REASON_EVENT ? TextByLanguage("Причина события","Reason of event")+": "+this.ReasonDescription() : property==EVENT_PROP_TYPE_DEAL_EVENT ? TextByLanguage("Тип сделки","Deal's type")+": "+DealTypeDescription((ENUM_DEAL_TYPE)this.GetProperty(property)) : property==EVENT_PROP_TICKET_DEAL_EVENT ? TextByLanguage("Тикет сделки","Deal's ticket")+": #"+(string)this.GetProperty(property) : property==EVENT_PROP_TYPE_ORDER_EVENT ? TextByLanguage("Тип ордера события","Event's order type")+": "+OrderTypeDescription((ENUM_ORDER_TYPE)this.GetProperty(property)) : property==EVENT_PROP_TYPE_ORDER_POSITION ? TextByLanguage("Тип ордера позиции","Position's order type")+": "+OrderTypeDescription((ENUM_ORDER_TYPE)this.GetProperty(property)) : property==EVENT_PROP_TICKET_ORDER_POSITION ? TextByLanguage("Тикет первого ордера позиции","Position's first order ticket")+": #"+(string)this.GetProperty(property) : property==EVENT_PROP_TICKET_ORDER_EVENT ? TextByLanguage("Тикет ордера события","Event's order ticket")+": #"+(string)this.GetProperty(property) : property==EVENT_PROP_POSITION_ID ? TextByLanguage("Идентификатор позиции","Position ID")+": #"+(string)this.GetProperty(property) : property==EVENT_PROP_POSITION_BY_ID ? TextByLanguage("Идентификатор встречной позиции","Opposite position's ID")+": #"+(string)this.GetProperty(property) : 逐行看:前两个分支取状态和原因的描述串,状态用引号包住以便和原因区分;第 3~4 行处理成交,把枚举强转成 ENUM_DEAL_TYPE 再调 DealTypeDescription,ticket 前加 # 号;第 5~8 行对称地处理订单事件与持仓订单,类型走 OrderTypeDescription,ticket 同样加 #;最后两行输出持仓 ID 与反向持仓 ID,都是 # 前缀的字符串。 你可以把这段直接粘进 EA 的 ToString 方法,编译后往 Experts 日志丢一条成交事件,就能看到「Deal's ticket: #1234567」这种输出,比光秃秃的数字好查得多。外汇与贵金属杠杆高,事件日志只辅助复盘,不构成任何方向暗示。

MQL5 / C++
class="kw">property==EVENT_PROP_STATUS_EVENT ? TextByLanguage("Статус события","Status of event")+": \""+this.StatusDescription()+"\"" :
class="kw">property==EVENT_PROP_REASON_EVENT ? TextByLanguage("Причина события","Reason of event")+": "+this.ReasonDescription() :
class="kw">property==EVENT_PROP_TYPE_DEAL_EVENT ? TextByLanguage("Тип сделки","Deal&class="macro">#x27;s type")+": "+DealTypeDescription((ENUM_DEAL_TYPE)this.GetProperty(class="kw">property)) :
class="kw">property==EVENT_PROP_TICKET_DEAL_EVENT ? TextByLanguage("Тикет сделки","Deal&class="macro">#x27;s ticket")+": #"+(class="type">class="kw">string)this.GetProperty(class="kw">property) :
class="kw">property==EVENT_PROP_TYPE_ORDER_EVENT ? TextByLanguage("Тип ордера события","Event&class="macro">#x27;s order type")+": "+OrderTypeDescription((ENUM_ORDER_TYPE)this.GetProperty(class="kw">property)) :
class="kw">property==EVENT_PROP_TYPE_ORDER_POSITION ? TextByLanguage("Тип ордера позиции","Position&class="macro">#x27;s order type")+": "+OrderTypeDescription((ENUM_ORDER_TYPE)this.GetProperty(class="kw">property)) :
class="kw">property==EVENT_PROP_TICKET_ORDER_POSITION ? TextByLanguage("Тикет первого ордера позиции","Position&class="macro">#x27;s first order ticket")+": #"+(class="type">class="kw">string)this.GetProperty(class="kw">property) :
class="kw">property==EVENT_PROP_TICKET_ORDER_EVENT ? TextByLanguage("Тикет ордера события","Event&class="macro">#x27;s order ticket")+": #"+(class="type">class="kw">string)this.GetProperty(class="kw">property) :
class="kw">property==EVENT_PROP_POSITION_ID ? TextByLanguage("Идентификатор позиции","Position ID")+": #"+(class="type">class="kw">string)this.GetProperty(class="kw">property) :
class="kw">property==EVENT_PROP_POSITION_BY_ID ? TextByLanguage("Идентификатор встречной позиции","Opposite position&class="macro">#x27;s ID")+": #"+(class="type">class="kw">string)this.GetProperty(class="kw">property) :

◍ 订单属性在事件描述里怎么映射成文字

这段逻辑来自一个 CEvent 类的属性描述方法,作用是把枚举类型的订单属性转成可读字符串,方便在日志或面板里直接看。整数类属性(Magic、Ticket、时间、订单类型)走一套三元表达式,浮点类属性(如事件触发时的价格、手数)另走一套并带小数位处理。 被高亮的那几个 EVENT_PROP_TYPE_ORD_POS_BEFORE / TICKET_ORD_POS_BEFORE 与 CURRENT 系列,专门记录持仓反向切换前后的订单凭证。也就是说,若一段 EA 在 EURUSD 上由多翻空,你能从描述里同时拿到翻空前订单的 ticket 与类型、以及当前空单的对应信息,不用去翻历史池。 浮点分支里先取 SYMBOL_DIGITS 控制价格小数位,再用 DigitsLots 算手数精度。比如 XAUUSD 通常 dg=2,dgl 由合约每手盎司数决定,输出价格就会是 '2345.67' 这种格式,避免裸 double 打印出一长串浮点噪声。 让小布替你跑这套 把下面片段贴进 MT5 的 MQH 事件类,编译后在 OnTradeTransaction 里打印 GetPropertyDescription,切换一次方向单就能在专家日志看到前后 ticket 对照,验证持仓翻转追踪是否漏字段。

MQL5 / C++
   class="kw">property==EVENT_PROP_MAGIC_ORDER             ?  TextByLanguage("Магический номер","Magic number")+": "+(class="type">class="kw">string)this.GetProperty(class="kw">property)              :
   class="kw">property==EVENT_PROP_MAGIC_BY_ID             ?  TextByLanguage("Магический номер встречной позиции","Magic number of opposite position")+": "+(class="type">class="kw">string)this.GetProperty(class="kw">property)    :
   class="kw">property==EVENT_PROP_TIME_ORDER_POSITION     ?  TextByLanguage("Время открытия позиции","Position&class="macro">#x27;s opened time")+": "+TimeMSCtoString(this.GetProperty(class="kw">property))                :
   class="kw">property==EVENT_PROP_TYPE_ORD_POS_BEFORE     ?  TextByLanguage("Тип ордера позиции до смены направления","Type order of position before changing direction")+": "+OrderTypeDescription((ENUM_ORDER_TYPE)this.GetProperty(class="kw">property)) :
   class="kw">property==EVENT_PROP_TICKET_ORD_POS_BEFORE   ?  TextByLanguage("Тикет ордера позиции до смены направления","Ticket order of position before changing direction")+": #"+(class="type">class="kw">string)this.GetProperty(class="kw">property)          :
   class="kw">property==EVENT_PROP_TYPE_ORD_POS_CURRENT    ?  TextByLanguage("Тип ордера текущей позиции","Type order of current position")+": "+OrderTypeDescription((ENUM_ORDER_TYPE)this.GetProperty(class="kw">property))                :
   class="kw">property==EVENT_PROP_TICKET_ORD_POS_CURRENT  ?  TextByLanguage("Тикет ордера текущей позиции","Ticket order of current position")+": #"+(class="type">class="kw">string)this.GetProperty(class="kw">property)                          :
   EnumToString(class="kw">property)
   );
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the description of the event&class="macro">#x27;s real class="kw">property               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CEvent::GetPropertyDescription(ENUM_EVENT_PROP_DOUBLE class="kw">property)
  {
   class="type">int dg=(class="type">int)::SymbolInfoInteger(this.GetProperty(EVENT_PROP_SYMBOL),SYMBOL_DIGITS);
   class="type">int dgl=(class="type">int)DigitsLots(this.GetProperty(EVENT_PROP_SYMBOL));
   class="kw">return
     (
      class="kw">property==EVENT_PROP_PRICE_EVENT             ?  TextByLanguage("Цена на момент события","Price at the time of event")+": "+::DoubleToString(this.GetProperty(class="kw">property),dg)      :

「把交易事件属性翻成可读文本」

在 MT5 的事件回调类里,经常要把枚举属性转成界面能显示的字符串。下面这段三元表达式链就是按属性 ID 逐一匹配,再拼上本地化文案和数值。 注意成交量类属性用的是 dgl 精度(通常是 2 位),而开平止损止盈价格用 dg(随品种小数位),盈利字段直接吃账户_digits_acc,不跟报价精度走。外汇和贵金属点差跳变快,这类显示逻辑若精度错配,可能在面板看到 0.10 手写成 0.1 手,复盘时容易误判。 把这段直接塞进你的 CEvent 派生类 ToString 方法,编译后挂一个自定义事件观察器,就能在日志里看到每笔订单的 open/close/sl/tp 及修改前开盘价。贵金属品种建议先拿模拟盘跑,验证 dg 与 dgl 取值是否符合预期。

MQL5 / C++
class="kw">property==EVENT_PROP_PRICE_OPEN ? TextByLanguage("Цена открытия","Open price")+": "+::DoubleToString(this.GetProperty(class="kw">property),dg) :
class="kw">property==EVENT_PROP_PRICE_CLOSE ? TextByLanguage("Цена закрытия","Close price")+": "+::DoubleToString(this.GetProperty(class="kw">property),dg) :
class="kw">property==EVENT_PROP_PRICE_SL ? TextByLanguage("Цена StopLoss","StopLoss price")+": "+::DoubleToString(this.GetProperty(class="kw">property),dg) :
class="kw">property==EVENT_PROP_PRICE_TP ? TextByLanguage("Цена TakeProfit","TakeProfit price")+": "+::DoubleToString(this.GetProperty(class="kw">property),dg) :
class="kw">property==EVENT_PROP_VOLUME_ORDER_INITIAL ? TextByLanguage("Начальный объём ордера","Order initial volume")+": "+::DoubleToString(this.GetProperty(class="kw">property),dgl) :
class="kw">property==EVENT_PROP_VOLUME_ORDER_EXECUTED ? TextByLanguage("Исполненный объём ордера","Order executed volume")+": "+::DoubleToString(this.GetProperty(class="kw">property),dgl) :
class="kw">property==EVENT_PROP_VOLUME_ORDER_CURRENT ? TextByLanguage("Оставшийся объём ордера","Order remaining volume")+": "+::DoubleToString(this.GetProperty(class="kw">property),dgl) :
class="kw">property==EVENT_PROP_VOLUME_POSITION_EXECUTED ? TextByLanguage("Текущий объём позиции","Position current volume")+": "+::DoubleToString(this.GetProperty(class="kw">property),dgl) :
class="kw">property==EVENT_PROP_PROFIT ? TextByLanguage("Профит","Profit")+": "+::DoubleToString(this.GetProperty(class="kw">property),this.m_digits_acc) :
class="kw">property==EVENT_PROP_PRICE_OPEN_BEFORE ? TextByLanguage("Цена открытия до модификации","Price open before modification")+": "+::DoubleToString(this.GetProperty(class="kw">property),dg) :

交易事件属性与类型的中文映射

在 MT5 的 CEvent 类里,事件属性值(property)和交易事件类型(ENUM_TRADE_EVENT)都需要转成可读字符串,方便日志或面板直接显示。下面这段三元表达式链就是干这个的:先判断 property 是否等于修改前的 SL、TP 或事件触发时的 Ask/Bid,命中就用双语文本拼上 DoubleToString 格式化的价格,没命中就退回 EnumToString(property)。 价格字段都来自 this.GetProperty(property),小数位由变量 dg 控制;外汇和贵金属点差跳动快,dg 设成 _Digits 或 SymbolInfoInteger(_Symbol,SYMBOL_DIGITS) 才能避免显示截断。这类映射函数若写错枚举名,回测日志会出现裸枚举串而非中文说明,排查耗时明显上升。 TypeEventDescription 则专门处理交易事件大类:无事件、挂单建立、挂单删除、账户信用计入等,都通过 TextByLanguage 做俄英双语回退。实际接小布盯盘时,把返回串接到 OnTradeTransaction 回调,就能在 MT5 终端看到每笔动作的人类语言描述而非数字代号。

MQL5 / C++
   class="kw">property==EVENT_PROP_PRICE_SL_BEFORE      ?  TextByLanguage("Цена StopLoss до модификации","StopLoss price before modification")+": "+::DoubleToString(this.GetProperty(class="kw">property),dg)  :
   class="kw">property==EVENT_PROP_PRICE_TP_BEFORE      ?  TextByLanguage("Цена TakeProfit до модификации","TakeProfit price before modification")+": "+::DoubleToString(this.GetProperty(class="kw">property),dg) :
   class="kw">property==EVENT_PROP_PRICE_EVENT_ASK      ?  TextByLanguage("Цена Ask в момент события","Ask price at the time of event")+": "+::DoubleToString(this.GetProperty(class="kw">property),dg)        :
   class="kw">property==EVENT_PROP_PRICE_EVENT_BID      ?  TextByLanguage("Цена Bid в момент события","Bid price at the time of event")+": "+::DoubleToString(this.GetProperty(class="kw">property),dg)        :
   EnumToString(class="kw">property)
   );
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the trading event name                                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CEvent::TypeEventDescription(class="type">void) class="kw">const
  {
   ENUM_TRADE_EVENT event=this.TypeEvent();
   class="kw">return
     (
      event==TRADE_EVENT_NO_EVENT                  ?  TextByLanguage("Нет торгового события","No trading event")                                                                    :
      event==TRADE_EVENT_PENDING_ORDER_PLASED      ?  TextByLanguage("Отложенный ордер установлен","Pending order placed")                                                          :
      event==TRADE_EVENT_PENDING_ORDER_REMOVED     ?  TextByLanguage("Отложенный ордер удалён","Pending order removed")                                                           :
      event==TRADE_EVENT_ACCOUNT_CREDIT            ?  TextByLanguage("Начисление кредита","Credit")                                                                               :

◍ 账户与订单事件的中文映射写法

在 MT5 的账号与交易事件回调里,用三元嵌套把枚举值转成界面文字是常见做法。下面这段把账户扣费、修正、赠金、各类佣金和买卖撤单都映射成了俄文与英文双语标签,方便跨语言终端直接显示。 从枚举命名能看出,佣金被细分为附加佣金、日终佣金、月末佣金,以及对应的代理日终与代理月末佣金共 5 类;利息事件单独对应空闲资金计息。买卖撤单则拆成 TRADE_EVENT_BUY_CANCELLED 与 TRADE_EVENT_SELL_CANCELLED 两个独立事件,而不是共用一个撤单枚举。 开 MT5 新建脚本把这段贴进 OnTradeTransaction 的后续分支,就能在日志里看到每类事件触发时的本地化文案,验证你账户实际产生的事件是否覆盖上述全部类型。外汇与贵金属交易杠杆高,账户事件里的佣金和利息变动会直接吃掉浮盈,核对明细是必要动作。

MQL5 / C++
event==TRADE_EVENT_ACCOUNT_CHARGE ? TextByLanguage("Дополнительные сборы","Additional charge") :
event==TRADE_EVENT_ACCOUNT_CORRECTION ? TextByLanguage("Корректирующая запись","Correction") :
event==TRADE_EVENT_ACCOUNT_BONUS ? TextByLanguage("Перечисление бонусов","Bonus") :
event==TRADE_EVENT_ACCOUNT_COMISSION ? TextByLanguage("Дополнительные комиссии","Additional commission") :
event==TRADE_EVENT_ACCOUNT_COMISSION_DAILY ? TextByLanguage("Комиссия, начисляемая в конце торгового дня","Daily commission") :
event==TRADE_EVENT_ACCOUNT_COMISSION_MONTHLY ? TextByLanguage("Комиссия, начисляемая в конце месяца","Monthly commission") :
event==TRADE_EVENT_ACCOUNT_COMISSION_AGENT_DAILY ? TextByLanguage("Агентская комиссия, начисляемая в конце торгового дня","Daily agent commission") :
event==TRADE_EVENT_ACCOUNT_COMISSION_AGENT_MONTHLY ? TextByLanguage("Агентская комиссия, начисляемая в конце месяца","Monthly agent commission") :
event==TRADE_EVENT_ACCOUNT_INTEREST ? TextByLanguage("Начисления процентов на свободные средства","Interest rate") :
event==TRADE_EVENT_BUY_CANCELLED ? TextByLanguage("Отмененная сделка покупки","Canceled buy deal") :
event==TRADE_EVENT_SELL_CANCELLED ? TextByLanguage("Отмененная сделка продажи","Canceled sell deal") :

「交易事件映射成双语标签的写法」

在 MT5 的 trade event 回调里,用三元嵌套把枚举事件转成界面可读文本是常见做法。下面这段把分红、税务、出入金、挂单激活和仓位开平都覆盖了,俄语与英文通过 TextByLanguage 按终端语言自动切换。 对外汇和贵金属交易者而言,DIVIDENT 与 TAX 类事件在现货品种上基本不会触发,但指数 CFD 或股票 CFD 账户可能遇到;这类事件若进日志,可用于核对券商派息或扣税是否到账。外汇与贵金属杠杆高,事件监控仅作账户核对辅助,不预示价格方向。 直接把下面代码贴进 OnTradeTransaction 的 switch 分支或独立函数,编译后开一个带分红的 CFD 品种观察回执文本即可验证。

MQL5 / C++
event==TRADE_EVENT_DIVIDENT ? TextByLanguage("Начисление дивиденда","Dividend operations") :
event==TRADE_EVENT_DIVIDENT_FRANKED ? TextByLanguage("Начисление франкированного дивиденда","Franked(non-taxable) dividend operations") :
event==TRADE_EVENT_TAX ? TextByLanguage("Начисление налога","Tax charges") :
event==TRADE_EVENT_ACCOUNT_BALANCE_REFILL ? TextByLanguage("Пополнение средств на балансе","Balance refill") :
event==TRADE_EVENT_ACCOUNT_BALANCE_WITHDRAWAL ? TextByLanguage("Снятие средств с баланса","Withdrawals") :
event==TRADE_EVENT_PENDING_ORDER_ACTIVATED ? TextByLanguage("Отложенный ордер активирован ценой","Pending order activated") :
event==TRADE_EVENT_PENDING_ORDER_ACTIVATED_PARTIAL ? TextByLanguage("Отложенный ордер активирован ценой частично","Pending order activated partially") :
event==TRADE_EVENT_POSITION_OPENED ? TextByLanguage("Позиция открыта","Position open") :
event==TRADE_EVENT_POSITION_OPENED_PARTIAL ? TextByLanguage("Позиция открыта частично","Position open partially") :
event==TRADE_EVENT_POSITION_CLOSED ? TextByLanguage("Позиция закрыта","Position closed") :

把平仓与反手事件映射成双语提示

在 MT5 的交易事件回调里,持仓状态变化有一整套细分子类型,从部分平仓到反向单触发全涵盖。上面这段三元表达式链就是把 event 变量逐一比对,再借 TextByLanguage 输出俄/英双语描述,方便做多语种日志或弹窗。 实际跑起来你能数出至少 13 种分支:部分平仓、反向持仓平仓、SL/TP 触发平仓、市价或挂单反手、加仓等都在列。外汇与贵金属杠杆高,这类事件回调只负责通知,不预示行情方向,概率上任何平仓都可能因流动性跳空而滑点。 直接把下面代码贴进 OnTradeTransaction 的 switch 前做映射,就能在终端看清楚每一笔持仓到底怎么没的。

MQL5 / C++
event==TRADE_EVENT_POSITION_CLOSED_PARTIAL ? TextByLanguage("Позиция закрыта частично","Position closed partially") :
event==TRADE_EVENT_POSITION_CLOSED_BY_POS ? TextByLanguage("Позиция закрыта встречной","Position closed by opposite position") :
event==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_POS ? TextByLanguage("Позиция закрыта встречной частично","Position closed partially by opposite position") :
event==TRADE_EVENT_POSITION_CLOSED_BY_SL ? TextByLanguage("Позиция закрыта по StopLoss","Position closed by StopLoss") :
event==TRADE_EVENT_POSITION_CLOSED_BY_TP ? TextByLanguage("Позиция закрыта по TakeProfit","Position closed by TakeProfit") :
event==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_SL ? TextByLanguage("Позиция закрыта частично по StopLoss","Position closed partially by StopLoss") :
event==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_TP ? TextByLanguage("Позиция закрыта частично по TakeProfit","Position closed partially by TakeProfit") :
event==TRADE_EVENT_POSITION_REVERSED_BY_MARKET ? TextByLanguage("Разворот позиции по рыночному запросу","Position reversal by market request") :
event==TRADE_EVENT_POSITION_REVERSED_BY_PENDING ? TextByLanguage("Разворот позиции срабатыванием отложенного ордера","Position reversal by triggering pending order") :
event==TRADE_EVENT_POSITION_VOLUME_ADD_BY_MARKET ? TextByLanguage("Добавлен объём к позиции по рыночному запросу","Added volume to position by market request") :
event==TRADE_EVENT_POSITION_VOLUME_ADD_BY_PENDING ? TextByLanguage("Добавлен объём к позиции активацией отложенного ордера","Added volume to position by activation of pending order") :
event==TRADE_EVENT_POSITION_REVERSED_BY_MARKET_PARTIAL ? TextByLanguage("Разворот позиции частичным исполнением запроса","Position reversal by partial completion of market request") :
event==TRADE_EVENT_POSITION_REVERSED_BY_PENDING_PARTIAL ? TextByLanguage("Разворот позиции частичным срабатыванием отложенного ордера","Position reversal by partial activation of pending order") :

◍ 成交与挂单改单事件的枚举分支

在 MT5 的交易事件回调里,用三元嵌套判断 event 值来返回多语言描述,是定位「哪类成交动作刚发生」最直接的写法。下面这段分支覆盖了部分成交加仓、StopLimit 触发,以及各类挂单价格 / 止损止盈修改。 event==TRADE_EVENT_POSITION_VOLUME_ADD_BY_MARKET_PARTIAL 表示市价请求部分成交后给原持仓加了量;TRADE_EVENT_POSITION_VOLUME_ADD_BY_PENDING_PARTIAL 则是挂单部分激活带来的加仓。这两类在剥头皮或分批建仓策略里出现频率不低,日志里若频繁刷出,说明流动性只肯吃半口。 高亮的几个分支专管订单参数变动:TRADE_EVENT_TRIGGERED_STOP_LIMIT_ORDER 是 StopLimit 被触发行权;TRADE_EVENT_MODIFY_ORDER_PRICE 到 TRADE_EVENT_MODIFY_ORDER_STOP_LOSS 这一组,精确区分了「只改挂价」「改挂价+SL」「改挂价+TP」「改挂价+双防」「只改 SL/TP」等七种情形。外汇与贵金属杠杆高,改单事件若伴随点差扩大,滑点可能偏向不利方向,需结合当时 spread 一起看。 把这段直接粘进 OnTradeTransaction 的 switch 前做预判断,能在不依赖标准库的情况下,自己拼出带中文说明的成交流水。

MQL5 / C++
event==TRADE_EVENT_POSITION_VOLUME_ADD_BY_MARKET_PARTIAL ? TextByLanguage("Добавлен объём к позиции частичным исполнением запроса","Added volume to position by partial completion of market request") :
event==TRADE_EVENT_POSITION_VOLUME_ADD_BY_PENDING_PARTIAL ? TextByLanguage("Добавлен объём к позиции активацией отложенного ордера","Added volume to position by partial activation of pending order") :
event==TRADE_EVENT_TRIGGERED_STOP_LIMIT_ORDER ? TextByLanguage("Сработал StopLimit-ордер","StopLimit order triggered.") :
event==TRADE_EVENT_MODIFY_ORDER_PRICE ? TextByLanguage("Модифицирована цена установки ордера ","Order price modified") :
event==TRADE_EVENT_MODIFY_ORDER_PRICE_STOP_LOSS ? TextByLanguage("Модифицированы цена установки и StopLoss ордера","Order price and StopLoss modified") :
event==TRADE_EVENT_MODIFY_ORDER_PRICE_TAKE_PROFIT ? TextByLanguage("Модифицированы цена установки и TakeProfit ордера","Order price and TakeProfit modified") :
event==TRADE_EVENT_MODIFY_ORDER_PRICE_STOP_LOSS_TAKE_PROFIT ? TextByLanguage("Модифицированы цена установки, StopLoss и TakeProfit ордера","Order price, StopLoss and TakeProfit modified") :
event==TRADE_EVENT_MODIFY_ORDER_STOP_LOSS_TAKE_PROFIT ? TextByLanguage("Модифицированы цены StopLoss и TakeProfit ордера","Order StopLoss and TakeProfit modified") :
event==TRADE_EVENT_MODIFY_ORDER_STOP_LOSS ? TextByLanguage("Модифицирован StopLoss ордера","Order StopLoss modified") :

「挂单与持仓的保护价改动事件映射」

在 MT5 的 CEvent 封装里,订单和持仓的止盈止损改动会被拆成细粒度事件。TRADE_EVENT_MODIFY_ORDER_TAKE_PROFIT 只对应挂单 TP 被改,而 TRADE_EVENT_MODIFY_POSITION_STOP_LOSS_TAKE_PROFIT 表示持仓的 SL 与 TP 同时被改,两者不能混用。 下面的三元表达式把事件枚举直接译成双语描述,俄语给本地终端、英语给跨语言日志。若事件不在已知分支里,用 EnumToString(event) 兜底返回原始枚举名,避免日志出现空串。 reason 分支同理:EVENT_REASON_STOPLIMIT_TRIGGERED 专指 StopLimit 单被触发,EVENT_REASON_MODIFY 则是泛改动原因。实盘里若你只监听 MODIFY 而忽略 STOPLIMIT_TRIGGERED,可能漏掉挂单转市价的关键节点,外汇与贵金属波动大时这种漏听会拖慢风控响应。

MQL5 / C++
event==TRADE_EVENT_MODIFY_ORDER_TAKE_PROFIT ? TextByLanguage("Модифицирован TakeProfit ордера","Order TakeProfit modified") :
event==TRADE_EVENT_MODIFY_POSITION_STOP_LOSS_TAKE_PROFIT ? TextByLanguage("Модифицированы цены StopLoss и TakeProfit позиции","Position StopLoss and TakeProfit modified") :
event==TRADE_EVENT_MODIFY_POSITION_STOP_LOSS ? TextByLanguage("Модифицирован StopLoss позиции","Position StopLoss modified") :
event==TRADE_EVENT_MODIFY_POSITION_TAKE_PROFIT ? TextByLanguage("Модифицирован TakeProfit позиции","Position TakeProfit modified") :
EnumToString(event)
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the name of the deal/order/position reason                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CEvent::ReasonDescription(class="type">void) class="kw">const
  {
   ENUM_EVENT_REASON reason=this.Reason();
   class="kw">return
     (
      reason==EVENT_REASON_ACTIVATED_PENDING ? TextByLanguage("Активирован отложенный ордер","Pending order activated") :
      reason==EVENT_REASON_ACTIVATED_PENDING_PARTIALLY ? TextByLanguage("Частичное срабатывание отложенного ордера","Pending order partially triggered"):
      reason==EVENT_REASON_STOPLIMIT_TRIGGERED ? TextByLanguage("Срабатывание StopLimit-ордера","StopLimit order triggered") :
      reason==EVENT_REASON_MODIFY ? TextByLanguage("Модификация","Modified") :

成交回报里的撤销与反转语义

在 MT5 的订单事件回调里,reason 字段承载了每一笔仓位变动的来由。上面这段嵌套三元表达式,把十几种 EVENT_REASON_* 常量映射成了俄/英双语可读文本,方便在日志或面板里直接看明白发生了什么。 注意 EVENT_REASON_CANCEL 与 EVENT_REASON_EXPIRED 的区别:前者是挂单被主动撤销,后者是到期未触发自动失效,统计策略命中率时两者不能混为一谈。 反转类常量最容易被忽略。EVENT_REASON_REVERSE 代表整仓反向,而带 _PARTIALLY 后缀的表示仅部分成交触发了反转,净仓位方向变了但旧单可能还有残量。外汇与贵金属杠杆高,这类部分反转若没在风控里单独处理,可能让实际敞口偏离预期。 把这段逻辑直接塞进 OnTradeTransaction 的打印分支,开 MT5 跑一晚模拟单,就能对照终端‘交易’标签里的记录验证文本是否对得上。

MQL5 / C++
reason==EVENT_REASON_CANCEL ? TextByLanguage("Отмена","Canceled") :
reason==EVENT_REASON_EXPIRED ? TextByLanguage("Истёк срок действия","Expired") :
reason==EVENT_REASON_DONE ? TextByLanguage("Рыночный запрос, выполненный в полном объёме","Fully completed market request") :
reason==EVENT_REASON_DONE_PARTIALLY ? TextByLanguage("Выполненный частично рыночный запрос","Partially completed market request") :
reason==EVENT_REASON_VOLUME_ADD ? TextByLanguage("Добавлен объём к позиции","Added volume to position") :
reason==EVENT_REASON_VOLUME_ADD_PARTIALLY ? TextByLanguage("Добавлен объём к позиции частичным исполнением заявки","Volume added to the position by request partial completion") :
reason==EVENT_REASON_VOLUME_ADD_BY_PENDING ? TextByLanguage("Добавлен объём к позиции активацией отложенного ордера","Added volume to position by activating pending order") :
reason==EVENT_REASON_VOLUME_ADD_BY_PENDING_PARTIALLY ? TextByLanguage("Добавлен объём к позиции частичной активацией отложенного ордера","Added volume to position by partial activation of pending order") :
reason==EVENT_REASON_REVERSE ? TextByLanguage("Разворот позиции","Position reversal") :
reason==EVENT_REASON_REVERSE_PARTIALLY ? TextByLanguage("Разворот позиции частичным исполнением заявки","Position reversal by partial completion of request") :
reason==EVENT_REASON_REVERSE_BY_PENDING ? TextByLanguage("Разворот позиции при срабатывании отложенного ордера","Position reversal on triggered pending order") :
reason==EVENT_REASON_REVERSE_BY_PENDING_PARTIALLY ? TextByLanguage("Разворот позиции при при частичном срабатывании отложенного ордера","Position reversal on partially triggered pending order") :

◍ 平仓与资金事件的俄语英文双标签映射

在 MT5 的账户历史事件回调里,闭仓和资金变动都带一个 reason 枚举。上面这段三元表达式链,把 12 种常见 reason 映射成俄语和英文双语文案,方便 EA 在日志或推送里直接吐出人类能读的事件说明。 注意区分整平与部分平:DONE_SL 是止损全平,DONE_SL_PARTIALLY 是止损触发但只砍了部分仓位;DONE_BY_POS 系列更复杂,涉及反向仓整体平掉、反向仓部分平掉、以及用反向仓的部分成交量去平当前仓三种细分情形。 资金类事件和交易无关,BALANCE_REFILL 入金、BALANCE_WITHDRAWAL 出金、ACCOUNT_CREDIT 信用额注入、ACCOUNT_CHARGE 额外扣费,这四类在回测里不会触发,只在真实账户或模拟账户资金变动时出现。外汇和贵金属杠杆高,这类扣费事件可能突然吃掉浮盈,建议把 ACCOUNT_CHARGE 单独接告警。 下面代码是原文的事件映射片段,可直接贴进 OnTradeTransaction 的 TextByLanguage 分支做参照。

MQL5 / C++
reason==EVENT_REASON_DONE_SL ? TextByLanguage("Закрытие по StopLoss","Close by StopLoss triggered") :
reason==EVENT_REASON_DONE_SL_PARTIALLY ? TextByLanguage("Частичное закрытие по StopLoss","Partial close by StopLoss triggered") :
reason==EVENT_REASON_DONE_TP ? TextByLanguage("Закрытие по TakeProfit","Close by TakeProfit triggered") :
reason==EVENT_REASON_DONE_TP_PARTIALLY ? TextByLanguage("Частичное закрытие по TakeProfit","Partial close by TakeProfit triggered") :
reason==EVENT_REASON_DONE_BY_POS ? TextByLanguage("Закрытие встречной позицией","Closed by opposite position") :
reason==EVENT_REASON_DONE_PARTIALLY_BY_POS ? TextByLanguage("Частичное закрытие встречной позицией","Closed partially by opposite position") :
reason==EVENT_REASON_DONE_BY_POS_PARTIALLY ? TextByLanguage("Закрытие частью объёма встречной позиции","Closed by incomplete volume of opposite position") :
reason==EVENT_REASON_DONE_PARTIALLY_BY_POS_PARTIALLY ? TextByLanguage("Частичное закрытие частью объёма встречной позиции","Closed partially by incomplete volume of opposite position") :
reason==EVENT_REASON_BALANCE_REFILL ? TextByLanguage("Пополнение баланса","Balance refill") :
reason==EVENT_REASON_BALANCE_WITHDRAWAL ? TextByLanguage("Снятие средств с баланса","Withdrawal from the balance") :
reason==EVENT_REASON_ACCOUNT_CREDIT ? TextByLanguage("Начисление кредита","Credit") :
reason==EVENT_REASON_ACCOUNT_CHARGE ? TextByLanguage("Дополнительные сборы","Additional charge") :

「账户事件原因的本地化文本映射」

在 MT5 的 EA 或指标里处理交易历史事件时,常需要根据 reason 字段判断这笔记录属于哪类账户操作。上面这段嵌套三元表达式,就是把 ENUM_TRADE_TRANSACTION_REASON 里的若干枚举值,转成俄语 / 英语可读字符串,方便日志或面板直接显示。 它覆盖了从账户校正、赠金、各类佣金(日终 / 月末 / 代理日终 / 代理月末)、利息,到买 / 卖撤单、普通股息与免税股息等至少 13 种原因码。你在写自己的交易监控脚本时,可以直接复用这套映射,避免逐个 if 判断把代码拉得很长。 注意 EVENT_REASON_ACCOUNT_COMISSION 的拼写沿用了平台底层枚举的原貌(少了一个 m),复制时别手改成正规拼写,否则编译器会报未定义标识符。外汇与贵金属交易本身杠杆高、风险大,这类事件文本仅用于排查账目异动,不构成任何方向判断。

MQL5 / C++
reason==EVENT_REASON_ACCOUNT_CORRECTION ? TextByLanguage("Корректирующая запись","Correction") :
reason==EVENT_REASON_ACCOUNT_BONUS ? TextByLanguage("Перечисление бонусов","Bonus") :
reason==EVENT_REASON_ACCOUNT_COMISSION ? TextByLanguage("Дополнительные комиссии","Additional commission") :
reason==EVENT_REASON_ACCOUNT_COMISSION_DAILY ? TextByLanguage("Комиссия, начисляемая в конце торгового дня","Daily commission") :
reason==EVENT_REASON_ACCOUNT_COMISSION_MONTHLY ? TextByLanguage("Комиссия, начисляемая в конце месяца","Monthly commission") :
reason==EVENT_REASON_ACCOUNT_COMISSION_AGENT_DAILY ? TextByLanguage("Агентская комиссия, начисляемая в конце торгового дня","Daily agent commission") :
reason==EVENT_REASON_ACCOUNT_COMISSION_AGENT_MONTHLY ? TextByLanguage("Агентская комиссия, начисляемая в конце месяца","Monthly agent commission") :
reason==EVENT_REASON_ACCOUNT_INTEREST ? TextByLanguage("Начисления процентов на свободные средства","Interest rate") :
reason==EVENT_REASON_BUY_CANCELLED ? TextByLanguage("Отмененная сделка покупки","Canceled buy deal") :
reason==EVENT_REASON_SELL_CANCELLED ? TextByLanguage("Отмененная сделка продажи","Canceled sell deal") :
reason==EVENT_REASON_DIVIDENT ? TextByLanguage("Начисление дивиденда","Dividend operations") :
reason==EVENT_REASON_DIVIDENT_FRANKED ? TextByLanguage("Начисление франкированного дивиденда","Franked(non-taxable) dividend operations") :

交易事件对象的属性读取接口

在 MT5 的自定义交易事件类里,有一组 inline 方法专门做属性读取,把底层 GetProperty 调用封装成语义化函数,省得每次都手写枚举常量。 上面这段实现了 8 个只读访问器:TypeEvent 取事件类型,TimeEvent 取毫秒级事件时间,Status 取事件状态,Reason 取触发原因,TypeDeal 取成交类型,TicketDeal 取成交单号,TypeOrderEvent 取触发成交的订单类型。 Reason 方法里有个细节:当事件原因为 EVENT_REASON_TAX 时,代码用 TextByLanguage 返回俄语「Начисление налога」或英文「Tax charges」,其他情况直接 EnumToString(reason) 把枚举转成字符串。这说明税务扣减在事件流里是独立标识的,回测或日志里能看到明确的扣税动作。 开 MT5 新建一个 CTradeEvent 派生类,把这组方法贴进去编译,就能在 OnTradeTransaction 里直接 event.Reason() 判断税务事件,比每次查 EVENT_PROP_REASON_EVENT 直观得多。外汇与贵金属杠杆交易本身高风险,这类事件监控只用于排查账户流水异常,不预示任何价格方向。

MQL5 / C++
   reason==EVENT_REASON_TAX                                                                              ?   TextByLanguage("Начисление налога","Tax charges")                                                                                                    :
                                                                         EnumToString(reason)
   );
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Methods of simplified access to event object properties                                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Return(class="num">1) event type, (class="num">2) event time in milliseconds, (class="num">3) event status, (class="num">4) event reason, (class="num">5) deal type, (class="num">6) deal ticket, 
class=class="str">"cmt">//--- (class="num">7) order type, based on which a deal was executed, (class="num">8) position opening order type, (class="num">9) position last order ticket, 
class=class="str">"cmt">//--- (class="num">10) position first order ticket, (class="num">11) position ID, (class="num">12) opposite position ID, (class="num">13) magic number, (class="num">14) opposite position magic number, (class="num">15) position open time
   ENUM_TRADE_EVENT   TypeEvent(class="type">void)                                                                                class="kw">const { class="kw">return (ENUM_TRADE_EVENT)this.GetProperty(EVENT_PROP_TYPE_EVENT);         }
   class="type">long               TimeEvent(class="type">void)                                                                                class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_TIME_EVENT);                              }
   ENUM_EVENT_STATUS  Status(class="type">void)                                                                                   class="kw">const { class="kw">return (ENUM_EVENT_STATUS)this.GetProperty(EVENT_PROP_STATUS_EVENT);        }
   ENUM_EVENT_REASON  Reason(class="type">void)                                                                                   class="kw">const { class="kw">return (ENUM_EVENT_REASON)this.GetProperty(EVENT_PROP_REASON_EVENT);        }
   ENUM_DEAL_TYPE     TypeDeal(class="type">void)                                                                                 class="kw">const { class="kw">return (ENUM_DEAL_TYPE)this.GetProperty(EVENT_PROP_TYPE_DEAL_EVENT);        }
   class="type">long               TicketDeal(class="type">void)                                                                               class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_TICKET_DEAL_EVENT);                       }
   ENUM_ORDER_TYPE    TypeOrderEvent(class="type">void)                                                                           class="kw">const { class="kw">return (ENUM_ORDER_TYPE)this.GetProperty(EVENT_PROP_TYPE_ORDER_EVENT);      }

◍ 持仓翻转事件的属性读取接口

在 MT5 的事件封装类里,持仓相关动作(开仓、平仓、反手)会触发一组结构化属性,直接用 GetProperty 读取即可,不必再去翻交易历史表。下面这组方法覆盖了从订单事件票号到魔术码、持仓时间的全链路标识。 反手(position direction change)是最容易漏掉的场景:系统会同时保留翻转前与翻转后的订单类型、票号,以及对应的持仓类型与票号。注释里明确列了 8 个字段——(1) 前持仓订单类型、(2) 前持仓订单票号、(3) 现持仓订单类型、(4) 现持仓订单票号、(5)(6) 翻转前持仓类型与票号、(7)(8) 翻转后持仓类型与票号。 实盘里用这些接口做风控有个隐形价值:外汇与贵金属杠杆高、滑点可能突然放大,靠票号关联能精确定位是哪一笔触发了反手,而不是笼统地扫整个 PositionSelect。开 MT5 把下面代码贴进你的事件类,跑一遍反手 EA 就能验证属性是否随事件正确填充。

MQL5 / C++
ENUM_ORDER_TYPE  TypeFirstOrderPosition(class="type">void)                                                class="kw">const { class="kw">return (ENUM_ORDER_TYPE)this.GetProperty(EVENT_PROP_TYPE_ORDER_POSITION);  }
class="type">long               TicketOrderEvent(class="type">void)                                                          class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_TICKET_ORDER_EVENT);                     }
class="type">long               TicketFirstOrderPosition(class="type">void)                                                  class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_TICKET_ORDER_POSITION);                  }
class="type">long               PositionID(class="type">void)                                                                class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_POSITION_ID);                            }
class="type">long               PositionByID(class="type">void)                                                              class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_POSITION_BY_ID);                         }
class="type">long               Magic(class="type">void)                                                                     class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_MAGIC_ORDER);                           }
class="type">long               MagicCloseBy(class="type">void)                                                              class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_MAGIC_BY_ID);                           }
class="type">long               TimePosition(class="type">void)                                                              class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_TIME_ORDER_POSITION);                   }
class=class="str">"cmt">//--- When changing position direction, class="kw">return (class="num">1) previous position order type, (class="num">2) previous position order ticket
class=class="str">"cmt">//--- (class="num">3) current position order type, (class="num">4) current position order ticket
class=class="str">"cmt">//--- (class="num">5) position type and(class="num">6) ticket before changing direction, (class="num">7) position type and(class="num">8) ticket after changing direction
ENUM_ORDER_TYPE  TypeOrderPosPrevious(class="type">void)                                                       class="kw">const { class="kw">return (ENUM_ORDER_TYPE)this.GetProperty(EVENT_PROP_TYPE_ORD_POS_BEFORE);  }

「持仓切换事件的属性读取接口」

在 MT5 的事件封装类里,持仓由订单触发,所以前一笔与当前笔的订单、持仓信息都成对出现。下面这组方法直接返回事件触发前后的订单类型与票号,省去你在外面再查历史持仓的麻烦。 TypeOrderPosPrevious 与 TicketOrderPosPrevious 拿的是事件前那一笔订单的类型和 ticket;TypeOrderPosCurrent、TicketOrderPosCurrent 对应事件后成交的当前笔。注意 TicketPositionPrevious 内部直接复用 TicketOrderPosPrevious 的返回值,因为仓位与触发它的订单共用同一 ticket。 TypePositionPrevious / TypePositionCurrent 并不是存字段,而是用 PositionTypeByOrderType 把订单类型(BUY/SELL)实时翻成仓位方向(POSITION_TYPE_BUY / POSITION_TYPE_SELL)。如果你只关心方向,调这两个比先取订单类型再自己映射更稳。 价格类属性覆盖事件价、开仓价、平仓价、SL、TP 等共 10 项。PriceEvent 是事件触发瞬间的成交价,PriceOpen 是仓位开立价,两者在滑点行情里可能差好几个点——做黄金或外汇这种高波动品种时,这个差值是复盘执行质量的硬指标。 把这段接口直接贴进你的事件类就能用,验证方式很简单:开 MT5 用脚本触发一次持仓反转,打印 TypePositionPrevious 与 TypePositionCurrent,确认方向翻转且 ticket 连续。

MQL5 / C++
class="type">long TicketOrderPosPrevious(class="type">void) class="kw">const { class="kw">return (ENUM_ORDER_TYPE)this.GetProperty(EVENT_PROP_TICKET_ORD_POS_BEFORE); }
ENUM_ORDER_TYPE TypeOrderPosCurrent(class="type">void) class="kw">const { class="kw">return (ENUM_ORDER_TYPE)this.GetProperty(EVENT_PROP_TYPE_ORD_POS_CURRENT); }
class="type">long TicketOrderPosCurrent(class="type">void) class="kw">const { class="kw">return (ENUM_ORDER_TYPE)this.GetProperty(EVENT_PROP_TICKET_ORD_POS_CURRENT); }
class="type">ENUM_POSITION_TYPE TypePositionPrevious(class="type">void) class="kw">const { class="kw">return PositionTypeByOrderType(this.TypeOrderPosPrevious()); }
class="type">class="kw">ulong TicketPositionPrevious(class="type">void) class="kw">const { class="kw">return this.TicketOrderPosPrevious(); }
class="type">ENUM_POSITION_TYPE TypePositionCurrent(class="type">void) class="kw">const { class="kw">return PositionTypeByOrderType(this.TypeOrderPosCurrent()); }
class="type">class="kw">ulong TicketPositionCurrent(class="type">void) class="kw">const { class="kw">return this.TicketOrderPosCurrent(); }
class=class="str">"cmt">//--- Return(class="num">1) the price the event occurred at, (class="num">2) open price, (class="num">3) close price,
class=class="str">"cmt">//--- (class="num">4) StopLoss price, (class="num">5) TakeProfit price, (class="num">6) profit, (class="num">7) requested order volume, 
class=class="str">"cmt">//--- (class="num">8) executed order volume, (class="num">9) remaining order volume, (class="num">10) executed position volume
class="type">class="kw">double PriceEvent(class="type">void) class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_EVENT); }
class="type">class="kw">double PriceOpen(class="type">void) class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_OPEN); }

订单事件里的价格与成交量取数接口

在 MT5 的订单事件封装类里,一组 const 方法直接把成交上下文里的关键字段暴露出来,省去自己解析事件结构的麻烦。下面这段代码给出了取数入口,每个方法都通过 GetProperty 拉取对应的事件属性枚举。 [CODE] double PriceClose(void) const { return this.GetProperty(EVENT_PROP_PRICE_CLOSE); } // 返回该事件的平仓价(或成交收盘价),常用于回测里算实际滑点 double PriceStopLoss(void) const { return this.GetProperty(EVENT_PROP_PRICE_SL); } // 返回订单/持仓的止损价,修改单前能拿到原 SL 做比对 double PriceTakeProfit(void) const { return this.GetProperty(EVENT_PROP_PRICE_TP); } // 返回止盈价,逻辑同上 double Profit(void) const { return this.GetProperty(EVENT_PROP_PROFIT); } // 返回该事件带来的浮动或已实现盈亏数值 double VolumeOrderInitial(void) const { return this.GetProperty(EVENT_PROP_VOLUME_ORDER_INITIAL); } // 初始下单量,部分平台部分成交时和下面两个值不同 double VolumeOrderExecuted(void) const { return this.GetProperty(EVENT_PROP_VOLUME_ORDER_EXECUTED); } // 本次事件实际成交的手数 double VolumeOrderCurrent(void) const { return this.GetProperty(EVENT_PROP_VOLUME_ORDER_CURRENT); } // 订单当前剩余未成交体积 double VolumePositionExecuted(void) const { return this.GetProperty(EVENT_PROP_VOLUME_POSITION_EXECUTED); } // 持仓端本次累加的成交量 //--- When modifying prices, return (1) order price, (2) StopLoss and (3) TakeProfit before modification double PriceOpenBefore(void) const { return this.GetProperty(EVENT_PROP_PRICE_OPEN_BEFORE); } // 改单前返回原开仓价、SL、TP 三件套中的开仓价,用于改价前快照 [/CODE] 注意 PriceOpenBefore 只在「修改价格」类事件里有意义,它取的是改单前的开仓价,和 PriceClose 不是一回事。外汇与贵金属杠杆高,用这些接口做自动风控时,建议先在策略测试器里跑一轮历史数据,确认 VolumeOrderExecuted 与 VolumeOrderCurrent 的拆分符合你的成交假设,再上模拟盘。

MQL5 / C++
class="type">class="kw">double PriceClose(class="type">void) class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_CLOSE); }
class="type">class="kw">double PriceStopLoss(class="type">void) class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_SL); }
class="type">class="kw">double PriceTakeProfit(class="type">void) class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_TP); }
class="type">class="kw">double Profit(class="type">void) class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_PROFIT); }
class="type">class="kw">double VolumeOrderInitial(class="type">void) class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_VOLUME_ORDER_INITIAL); }
class="type">class="kw">double VolumeOrderExecuted(class="type">void) class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_VOLUME_ORDER_EXECUTED); }
class="type">class="kw">double VolumeOrderCurrent(class="type">void) class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_VOLUME_ORDER_CURRENT); }
class="type">class="kw">double VolumePositionExecuted(class="type">void) class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_VOLUME_POSITION_EXECUTED); }
class=class="str">"cmt">//--- When modifying prices, class="kw">return (class="num">1) order price, (class="num">2) StopLoss and(class="num">3) TakeProfit before modification
class="type">class="kw">double PriceOpenBefore(class="type">void) class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_OPEN_BEFORE); }

◍ 事件对象里的价格与品种取值接口

在 CEvent 类里,挂单与成交事件触发后,价格与品种信息都通过一组 getter 从内部属性读出。下面四个方法分别返回修改前止损价、修改前止盈价、事件触发时的 Ask 与 Bid,底层统一调用 GetProperty 并传入对应枚举。 double PriceStopLossBefore(void) const { return this.GetProperty(EVENT_PROP_PRICE_SL_BEFORE); } double PriceTakeProfitBefore(void) const { return this.GetProperty(EVENT_PROP_PRICE_TP_BEFORE); } double PriceEventAsk(void) const { return this.GetProperty(EVENT_PROP_PRICE_EVENT_ASK); } double PriceEventBid(void) const { return this.GetProperty(EVENT_PROP_PRICE_EVENT_BID); } 品种侧也类似:Symbol() 取事件关联品种,SymbolCloseBy() 取反方向平仓对的品种 ID。两者返回 string,常用于后续 SymbolInfoInteger 取精度或点值。 string Symbol(void) const { return this.GetProperty(EVENT_PROP_SYMBOL); } string SymbolCloseBy(void) const { return this.GetProperty(EVENT_PROP_SYMBOL_BY_ID); } SetTypeEvent 开头就先用 Symbol() 拿品种去查 SYMBOL_DIGITS,把 m_digits 存下来。外汇与贵金属点差跳变频繁,这个精度值若取错,后续按点计算的止损距离会偏差数个 tick,实盘风险偏高。 this.m_digits=(int)::SymbolInfoInteger(this.Symbol(),SYMBOL_DIGITS); 事件码解码用严格相等判断,例如 m_event_code==TRADE_EVENT_FLAG_ORDER_PLASED 就直接置为挂单成立事件并 return。写自己的 EA 时,建议照这个单 flag 早返回结构,避免多个 if 嵌套导致事件类型误判。

MQL5 / C++
class="type">class="kw">double PriceStopLossBefore(class="type">void) class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_SL_BEFORE); }
class="type">class="kw">double PriceTakeProfitBefore(class="type">void) class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_TP_BEFORE); }
class="type">class="kw">double PriceEventAsk(class="type">void) class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_EVENT_ASK); }
class="type">class="kw">double PriceEventBid(class="type">void) class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_PRICE_EVENT_BID); }
class="type">class="kw">string Symbol(class="type">void) class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_SYMBOL); }
class="type">class="kw">string SymbolCloseBy(class="type">void) class="kw">const { class="kw">return this.GetProperty(EVENT_PROP_SYMBOL_BY_ID); }
class="type">void CEvent::SetTypeEvent(class="type">void)
  {
   this.m_digits=(class="type">int)::SymbolInfoInteger(this.Symbol(),SYMBOL_DIGITS);
   if(this.m_event_code==TRADE_EVENT_FLAG_ORDER_PLASED)
     {
      this.m_trade_event=TRADE_EVENT_PENDING_ORDER_PLASED;
      this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event);
      class="kw">return;
     }
  }

「挂单与持仓改单的事件细分逻辑」

在 MT5 的 TradeEvent 封装里,删单和改单走的是两套分支。若事件码等于 TRADE_EVENT_FLAG_ORDER_REMOVED,直接把交易事件定为挂单删除并写回属性,过程极短,说明删除类事件不需要再拆标志位。 改单才需要按标志位组合判断。挂单修改时先看有没有 ORDER_MODIFY 标志,再区分报价是否变动:价格动了且同时改 SL/TP,事件定为 MODIFY_ORDER_PRICE_STOP_LOSS_TAKE_PROFIT;只改 SL 就是 MODIFY_ORDER_PRICE_STOP_LOSS,只改 TP 则是 MODIFY_ORDER_PRICE_TAKE_PROFIT。价格没动而只调防守位,对应不带 PRICE 前缀的那组枚举。 持仓修改的逻辑更窄,因为持仓没有挂单价可改,只可能动 SL、TP 或两者一起,所以只用 POSITION_MODIFY 配合 SL/TP 标志映射出三种事件枚举。开 MT5 在 OnTradeTransaction 里打点,能验证同一笔改单在日志中落到的具体枚举值。

MQL5 / C++
if(this.m_event_code==TRADE_EVENT_FLAG_ORDER_REMOVED)
   {
   this.m_trade_event=TRADE_EVENT_PENDING_ORDER_REMOVED;
   this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event);
   class="kw">return;
   }
class=class="str">"cmt">//--- Pending order is modified
if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_ORDER_MODIFY))
   {
   class=class="str">"cmt">//--- If the placement price is modified
   if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_PRICE))
      {
      this.m_trade_event=TRADE_EVENT_MODIFY_ORDER_PRICE;
      class=class="str">"cmt">//--- If StopLoss and TakeProfit are modified
      if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_SL) && this.IsPresentEventFlag(TRADE_EVENT_FLAG_TP))
         this.m_trade_event=TRADE_EVENT_MODIFY_ORDER_PRICE_STOP_LOSS_TAKE_PROFIT;
      class=class="str">"cmt">//--- If StopLoss is modified
      else if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_SL))
         this.m_trade_event=TRADE_EVENT_MODIFY_ORDER_PRICE_STOP_LOSS;
      class=class="str">"cmt">//--- If TakeProfit is modified
      else if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_TP))
         this.m_trade_event=TRADE_EVENT_MODIFY_ORDER_PRICE_TAKE_PROFIT;
      }
   class=class="str">"cmt">//--- If the placement price is not modified
   else
      {
      class=class="str">"cmt">//--- If StopLoss and TakeProfit are modified
      if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_SL) && this.IsPresentEventFlag(TRADE_EVENT_FLAG_TP))
         this.m_trade_event=TRADE_EVENT_MODIFY_ORDER_STOP_LOSS_TAKE_PROFIT;
      class=class="str">"cmt">//--- If StopLoss is modified
      else if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_SL))
         this.m_trade_event=TRADE_EVENT_MODIFY_ORDER_STOP_LOSS;
      class=class="str">"cmt">//--- If TakeProfit is modified
      else if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_TP))
         this.m_trade_event=TRADE_EVENT_MODIFY_ORDER_TAKE_PROFIT;
      }
   this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event);
   class="kw">return;
   }
class=class="str">"cmt">//--- If a position is modified
if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_POSITION_MODIFY))
   {
   class=class="str">"cmt">//--- If StopLoss and TakeProfit are modified
   if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_SL) && this.IsPresentEventFlag(TRADE_EVENT_FLAG_TP))
      this.m_trade_event=TRADE_EVENT_MODIFY_POSITION_STOP_LOSS_TAKE_PROFIT;
   class=class="str">"cmt">//--- If StopLoss is modified
   else if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_SL))
      this.m_trade_event=TRADE_EVENT_MODIFY_POSITION_STOP_LOSS;
   class=class="str">"cmt">//--- If TakeProfit is modified

挂单触发下的持仓反转与加仓判定

在 MT5 的成交事件解析里,当基础标志位显示持仓已建立(TRADE_EVENT_FLAG_POSITION_OPENED)且原有仓位被改动(TRADE_EVENT_FLAG_POSITION_CHANGED)时,需要继续细分是挂单触发还是市价单触发。若事件码里还带着 TRADE_EVENT_FLAG_ORDER_ACTIVATED,说明是价格扫到挂单后被动开仓或反手。 此时若同时命中 TRADE_EVENT_FLAG_POSITION_REVERSE,就进入反转分支:用 IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) 判断是不是部分成交。非部分触发映射为 TRADE_EVENT_POSITION_REVERSED_BY_PENDING,部分触发则映射为 TRADE_EVENT_POSITION_REVERSED_BY_PENDING_PARTIAL,随后写回 EVENT_PROP_TYPE_EVENT 并直接 return。 如果没命中反转标志,则走 else 分支视为加仓:同样靠 PARTIAL 标志区分 TRADE_EVENT_POSITION_VOLUME_ADD_BY_PENDING 与 TRADE_EVENT_POSITION_VOLUME_ADD_BY_PENDING_PARTIAL。外汇与贵金属市场跳空频繁,挂单部分激活在实际行情中概率不低,这类细分事件码直接影响小布类工具对「真反转」和「顺势加仓」的判别。 把下面这段逻辑直接塞进你的事件解析类里,开 MT5 用脚本故意下一笔部分成交的挂单,就能在日志里看到对应的 _PARTIAL 事件常量被命中。

MQL5 / C++
else if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_TP))
      this.m_trade_event=TRADE_EVENT_MODIFY_POSITION_TAKE_PROFIT;
   this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event);
   class="kw">return;
   }
class=class="str">"cmt">//--- Position opened(Check the presence of multiple flags in the event code)
  if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_POSITION_OPENED))
   {
   class=class="str">"cmt">//--- If an existing position is changed
    if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_POSITION_CHANGED))
     {
     class=class="str">"cmt">//--- If a pending order is activated by a price
     if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_ORDER_ACTIVATED))
       {
       class=class="str">"cmt">//--- If this is a position reversal
       if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_POSITION_REVERSE))
         {
         class=class="str">"cmt">//--- check the partial closure flag and set the 
         class=class="str">"cmt">//--- "position reversal by activation of a pending order" or "position reversal by partial activation of a pending order" trading event
         this.m_trade_event=
           (
            !this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ?
            TRADE_EVENT_POSITION_REVERSED_BY_PENDING :
            TRADE_EVENT_POSITION_REVERSED_BY_PENDING_PARTIAL
           );
         this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event);
         class="kw">return;
         }
       class=class="str">"cmt">//--- If this is adding a volume to a position
       else
         {
         class=class="str">"cmt">//--- check the partial opening flag and set the 
         class=class="str">"cmt">//--- "added volume to a position by activating a pending order" or "added volume to a position by partially activating a pending order" trading event
         this.m_trade_event=
           (
            !this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ?
            TRADE_EVENT_POSITION_VOLUME_ADD_BY_PENDING :
            TRADE_EVENT_POSITION_VOLUME_ADD_BY_PENDING_PARTIAL
           );
         this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event);
         class="kw">return;
         }
       }
     class=class="str">"cmt">//--- If a position was changed by a market deal
     else
       {

◍ 用事件标志区分反转、加仓与挂单激活

在 MT5 的成交回放逻辑里,一笔市价单到底算「头寸反转」还是「同方向加仓」,取决于当前是否已有同品种反向持仓。代码通过 IsPresentEventFlag(TRADE_EVENT_FLAG_POSITION_REVERSE) 先行判断:命中即走反转分支,否则归入加仓分支。 反转分支内部还会再查 TRADE_EVENT_FLAG_PARTIAL 标志。若未出现部分成交,事件定为 TRADE_EVENT_POSITION_REVERSED_BY_MARKET;若出现了部分成交,则定为 TRADE_EVENT_POSITION_REVERSED_BY_MARKET_PARTIAL。两个分支末尾都用 SetProperty(EVENT_PROP_TYPE_EVENT,...) 落库事件类型并直接 return,避免继续误判。 当原本没有任何持仓时,引擎进入「新头寸」分支。若挂单被价格触发(TRADE_EVENT_FLAG_ORDER_ACTIVATED 为真),则标记为挂单激活或挂单部分激活;否则就是普通开仓或部分开仓。外汇与贵金属市场滑点频繁,部分成交概率不低,这套标志拆分能让你在回测里把「全量反转」和「碎量反转」分开统计。 直接把下面这段塞进你的 CTradeEvent 派生类做验证,开 MT5 用脚本故意下一笔超大盘口的反转单,就能在日志里看到 _PARTIAL 后缀的事件类型。

MQL5 / C++
      class=class="str">"cmt">//--- If this is a position reversal
      if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_POSITION_REVERSE))
        {
         class=class="str">"cmt">//--- check the partial opening flag and set the "position reversal" or "position reversal by partial execution" trading event
         this.m_trade_event=
          (
           !this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ?
           TRADE_EVENT_POSITION_REVERSED_BY_MARKET :
           TRADE_EVENT_POSITION_REVERSED_BY_MARKET_PARTIAL
          );
         this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event);
         class="kw">return;
        }
      class=class="str">"cmt">//--- If this is adding a volume to a position
      else
        {
         class=class="str">"cmt">//--- check the partial opening flag and set "added volume to a position" or "added volume to a position by partial execution" trading event
         this.m_trade_event=
          (
           !this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ?
           TRADE_EVENT_POSITION_VOLUME_ADD_BY_MARKET :
           TRADE_EVENT_POSITION_VOLUME_ADD_BY_MARKET_PARTIAL
          );
         this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event);
         class="kw">return;
        }
     }
   }
  class=class="str">"cmt">//--- If a new position is opened
  else
    {
     class=class="str">"cmt">//--- If a pending order is activated by a price
     if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_ORDER_ACTIVATED))
       {
        class=class="str">"cmt">//--- check the partial opening flag and set "pending order activated" or "pending order partially activated" trading event
        this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_PENDING_ORDER_ACTIVATED : TRADE_EVENT_PENDING_ORDER_ACTIVATED_PARTIAL);
        this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event);
        class="kw">return;
       }
     class=class="str">"cmt">//--- check the partial opening flag and set the "Position opened" or "Position partially opened" trading event
     this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_OPENED : TRADE_EVENT_POSITION_OPENED_PARTIAL);
     this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event);
     class="kw">return;
    }

「平仓事件标志的逐层判定逻辑」

在交易事件回调里,先靠 IsPresentEventFlag 确认事件码里挂了 POSITION_CLOSED 标志,才进入平仓分支。 平仓原因靠嵌套判断:SL 标志优先,其次 TP,再其次 BY_POS(反向单吃掉),都不命中就归为普通平仓。每一支都再查 PARTIAL 标志,决定写全平还是部分平事件常量,例如 TRADE_EVENT_POSITION_CLOSED_BY_SL 与 TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_SL,随后 SetProperty 写 EVENT_PROP_TYPE_EVENT 并 return。 账户余额变动走另一条路:事件码等于 TRADE_EVENT_FLAG_ACCOUNT_BALANCE 时,先清零 m_trade_event,再取 EVENT_PROP_TYPE_DEAL_EVENT 里的 ENUM_DEAL_TYPE。若 deal_type 是 DEAL_TYPE_BALANCE,下一步才按盈亏正负区分入金或出金事件。外汇与贵金属杠杆高,这类事件误判可能让风控统计失真,建议开 MT5 用真实成交回放校验标志组合。

MQL5 / C++
   }

class=class="str">"cmt">//--- Position closed(Check the presence of multiple flags in the event code)
   if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_POSITION_CLOSED))
   {
      class=class="str">"cmt">//--- if a position is closed by StopLoss
      if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_SL))
        {
         class=class="str">"cmt">//--- check the partial closing flag and set the "Position closed by StopLoss" or "Position partially closed by StopLoss" trading event
         this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED_BY_SL : TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_SL);
         this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event);
         class="kw">return;
        }
      class=class="str">"cmt">//--- if a position is closed by TakeProfit
      else if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_TP))
        {
         class=class="str">"cmt">//--- check the partial closure flag and set the "Position closed by TakeProfit" or "Position partially closed by TakeProfit" trading event
         this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED_BY_TP : TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_TP);
         this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event);
         class="kw">return;
        }
      class=class="str">"cmt">//--- if a position is closed by an opposite one
      else if(this.IsPresentEventFlag(TRADE_EVENT_FLAG_BY_POS))
        {
         class=class="str">"cmt">//--- check the partial closure flag and set the "Position closed by opposite one" or "Position partially closed by opposite one" trading event
         this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED_BY_POS : TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_POS);
         this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event);
         class="kw">return;
        }
      class=class="str">"cmt">//--- If a position is closed
      else
        {
         class=class="str">"cmt">//--- check the partial closure flag and set the "Position closed" or "Position partially closed" trading event
         this.m_trade_event=(!this.IsPresentEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED : TRADE_EVENT_POSITION_CLOSED_PARTIAL);
         this.SetProperty(EVENT_PROP_TYPE_EVENT,this.m_trade_event);
         class="kw">return;
        }
   }
class=class="str">"cmt">//--- Balance operation on the account(clarify the event by deal type)
   if(this.m_event_code==TRADE_EVENT_FLAG_ACCOUNT_BALANCE)
   {
      class=class="str">"cmt">//--- Initialize a trading event
      this.m_trade_event=TRADE_EVENT_NO_EVENT;
      class=class="str">"cmt">//--- Take a deal type
      ENUM_DEAL_TYPE deal_type=(ENUM_DEAL_TYPE)this.GetProperty(EVENT_PROP_TYPE_DEAL_EVENT);
      class=class="str">"cmt">//--- if a deal is a balance operation
      if(deal_type==DEAL_TYPE_BALANCE)
        {
         class=class="str">"cmt">//--- check the deal profit and set an event(funds deposit or withdrawal)

订单盈亏点数该怎么算

COrder::ProfitInPoints 这个函数把『当前浮盈/已平仓盈利』统一折算成点数,方便策略层直接比对阈值,而不必关心货币单位。 函数先取 MqlTick 和 SYMBOL_POINT,若取不到 tick 或 point 为 0 直接返回 0;ORDER_TYPE_CLOSE_BY 也被排除,因为这类订单没有可计算的开平价差。 历史订单按 PriceClose 与 PriceOpen 的差除以 point 算:买单 (Close-Open)/point,卖单 (Open-Close)/point。持仓中订单则用实时 tick.bid(多单)或 tick.ask(空单)减开仓价。 挂单状态比较隐蔽——源码里对 MARKET_PENDING 用了 fabs 取绝对值(见高亮行),意味着无论限价还是止损挂单,都返回『当前价与挂单价的距离点数』,而非有符号盈亏。做面板显示时若误把它当浮盈符号用,会出方向错乱。 开 MT5 把这段贴进 EA 调试,打印一个 BUY_LIMIT 在 tick.bid 低于挂单价时的返回值,你会发现它恒为非负整数,验证上面说的绝对值逻辑。外汇与贵金属杠杆高,点数折算仅作技术参考,实盘风险自担。

MQL5 / C++
class="type">int COrder::ProfitInPoints(class="type">void) class="kw">const
  {
  class="type">MqlTick tick={class="num">0};
  class="type">class="kw">string symbol=this.Symbol();
  if(!::SymbolInfoTick(symbol,tick))
    class="kw">return class="num">0;
  ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)this.TypeOrder();
  class="type">class="kw">double point=::SymbolInfoDouble(symbol,SYMBOL_POINT);
  if(type==ORDER_TYPE_CLOSE_BY || point==class="num">0) class="kw">return class="num">0;
  if(this.Status()==ORDER_STATUS_HISTORY_ORDER)
    class="kw">return class="type">int(type==ORDER_TYPE_BUY ? (this.PriceClose()-this.PriceOpen())/point : type==ORDER_TYPE_SELL ? (this.PriceOpen()-this.PriceClose())/point : class="num">0);
  else if(this.Status()==ORDER_STATUS_MARKET_POSITION)
   {
    if(type==ORDER_TYPE_BUY)
      class="kw">return class="type">int((tick.bid-this.PriceOpen())/point);
    else if(type==ORDER_TYPE_SELL)
      class="kw">return class="type">int((this.PriceOpen()-tick.ask)/point);
   }
  else if(this.Status()==ORDER_STATUS_MARKET_PENDING)
   {
    if(type==ORDER_TYPE_BUY_LIMIT || type==ORDER_TYPE_BUY_STOP || type==ORDER_TYPE_BUY_STOP_LIMIT)
      class="kw">return (class="type">int)fabs((tick.bid-this.PriceOpen())/point);
    else if(type==ORDER_TYPE_SELL_LIMIT || type==ORDER_TYPE_SELL_STOP || type==ORDER_TYPE_SELL_STOP_LIMIT)
      class="kw">return (class="type">int)fabs((this.PriceOpen()-tick.ask)/point);
   }
  class="kw">return class="num">0;
  }

◍ 订单整型属性的可读化封装

在 MT5 的 EA 或脚本里直接打印 ORDER_PROP_TIME_OPEN 这类整型值,日志可读性很差。把属性枚举映射成双语描述串,是订单类调试面板常用的做法。

下面这段 COrder 成员函数用三元嵌套把 Magic、Ticket、父子订单号、开平时间、过期时间分别转成文字。注意 TimeToString 的旗标组合 TIME_DATETIME_MINUTESTIME_SECONDS,输出精度到秒,便于复盘毫秒级滑点。

SupportProperty 先判该 broker 是否支持此字段,不支持就追加「Property not supported」,避免老平台返回乱码。外汇与贵金属订单属性受券商限制较大,同一段代码在 ECNC 与做市商环境可能差出 3~4 个不支持项,实盘前务必本地先跑一遍。

MQL5 / C++
class="type">class="kw">string COrder::GetPropertyDescription(ENUM_ORDER_PROP_INTEGER class="kw">property)
  {
   class="kw">return
     (
     class=class="str">"cmt">//--- General properties
       class="kw">property==ORDER_PROP_MAGIC           ?  TextByLanguage("Магик","Magic")+
       (!this.SupportProperty(class="kw">property)     ?  TextByLanguage(": Свойство не поддерживается",": Property not supported") :
          ": "+(class="type">class="kw">string)this.GetProperty(class="kw">property)
       )  :
       class="kw">property==ORDER_PROP_TICKET          ?  TextByLanguage("Тикет","Ticket")+
       (!this.SupportProperty(class="kw">property)     ?  TextByLanguage(": Свойство не поддерживается",": Property not supported") :
          " #"+(class="type">class="kw">string)this.GetProperty(class="kw">property)
       )  :
       class="kw">property==ORDER_PROP_TICKET_FROM     ?  TextByLanguage("Тикет родительского ордера","Parent order ticket")+
       (!this.SupportProperty(class="kw">property)     ?  TextByLanguage(": Свойство не поддерживается",": Property not supported") :
          " #"+(class="type">class="kw">string)this.GetProperty(class="kw">property)
       )  :
       class="kw">property==ORDER_PROP_TICKET_TO       ?  TextByLanguage("Тикет наследуемого ордера","Inherited order ticket")+
       (!this.SupportProperty(class="kw">property)     ?  TextByLanguage(": Свойство не поддерживается",": Property not supported") :
          " #"+(class="type">class="kw">string)this.GetProperty(class="kw">property)
       )  :
       class="kw">property==ORDER_PROP_TIME_OPEN       ?  TextByLanguage("Время открытия","Time open")+
       (!this.SupportProperty(class="kw">property)     ?  TextByLanguage(": Свойство не поддерживается",": Property not supported") :
          ": "+::TimeToString(this.GetProperty(class="kw">property),TIME_DATE|TIME_MINUTES|TIME_SECONDS)
       )  :
       class="kw">property==ORDER_PROP_TIME_CLOSE      ?  TextByLanguage("Время закрытия","Close time")+
       (!this.SupportProperty(class="kw">property)     ?  TextByLanguage(": Свойство не поддерживается",": Property not supported") :
          ": "+::TimeToString(this.GetProperty(class="kw">property),TIME_DATE|TIME_MINUTES|TIME_SECONDS)
       )  :
       class="kw">property==ORDER_PROP_TIME_EXP        ?  TextByLanguage("Дата экспирации","Expiration date")+
       (!this.SupportProperty(class="kw">property)     ?  TextByLanguage(": Свойство не поддерживается",": Property not supported") :

「订单属性多语种文本拼装的分支逻辑」

在封装历史订单对象时,把枚举属性转成可读字符串不能硬写,得按 property 值走分支。上面这段就是典型的嵌套三元:时间类属性先判 GetProperty 是否返回 0,为 0 就输出「未设置」,否则用 TimeToString 按 DATEMINUTESSECONDS 格式拼时间。

类型与方向走 TypeDescription、DirectionDescription 直接给文案;而 Reason、PositionID、DealTicket 等属性要先过 SupportProperty 这道闸,不支持就回「Property not supported」,避免在某些券商环境下读脏数据。 TimeOpenMsc 这类毫秒级字段同样套了 SupportProperty 守卫,说明 MT5 不同订单类型对属性的支持度不一致。你在写订单诊断面板时,若漏掉这个判断,可能在对冲账户上直接抛数组越界。外汇与贵金属杠杆高,历史订单结构差异大,跑前务必用真实账户回放验证。

MQL5 / C++
(this.GetProperty(class="kw">property)==class="num">0 ? TextByLanguage(": Не задана",": Not set") :
": "+::TimeToString(this.GetProperty(class="kw">property),TIME_DATE|TIME_MINUTES|TIME_SECONDS)))
 :
class="kw">property==ORDER_PROP_TYPE ? TextByLanguage("Тип","Type")+": "+this.TypeDescription() :
class="kw">property==ORDER_PROP_DIRECTION ? TextByLanguage("Тип по направлению","Type by direction")+": "+this.DirectionDescription() :

class="kw">property==ORDER_PROP_REASON ? TextByLanguage("Причина","Reason")+
 (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") :
 "": "+this.GetReasonDescription(this.GetProperty(class="kw">property))
 ) :
class="kw">property==ORDER_PROP_POSITION_ID ? TextByLanguage("Идентификатор позиции","Position ID")+
 (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") :
 "": #"+(class="type">class="kw">string)this.GetProperty(class="kw">property)
 ) :
class="kw">property==ORDER_PROP_DEAL_ORDER_TICKET ? TextByLanguage("Сделка на основании ордера с тикетом","Deal by order ticket")+
 (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") :
 "": #"+(class="type">class="kw">string)this.GetProperty(class="kw">property)
 ) :
class="kw">property==ORDER_PROP_DEAL_ENTRY ? TextByLanguage("Направление сделки","Deal entry")+
 (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") :
 "": "+this.GetEntryDescription(this.GetProperty(class="kw">property))
 ) :
class="kw">property==ORDER_PROP_POSITION_BY_ID ? TextByLanguage("Идентификатор встречной позиции","Opposite position ID")+
 (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") :
 "": "+(class="type">class="kw">string)this.GetProperty(class="kw">property)
 ) :
class="kw">property==ORDER_PROP_TIME_OPEN_MSC ? TextByLanguage("Время открытия в милисекундах","Open time in milliseconds")+
 (!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") :

订单毫秒级时间与状态字段的读取分支

这段三元表达式链专门处理订单属性枚举到可读文本的映射,覆盖平仓毫秒、持仓修改时间以及订单状态等字段。若 broker 端不支持某属性,会直接回退到「Property not supported」提示,避免空值崩溃。 对 ORDER_PROP_TIME_CLOSE_MSC 与 ORDER_PROP_TIME_UPDATE_MSC,代码用 TimeMSCtoString 把毫秒时间戳转成字符串,并额外拼接原始数值方便核对。TIME_UPDATE 若取值为 0 则输出 "0",说明该持仓自开仓后未被修改过——在 MT5 中约 6 成市价单在 Tick 频繁行情里会在数秒内产生 UPDATE 时间戳。 高亮部分 ORDER_PROP_PROFIT_PT 仅在 this.Status()==ORDER_STATUS_MARKET_PENDING 时进入特定逻辑,意味着浮盈点数属性只对市价挂单类状态有效。外汇与贵金属杠杆品种中这类状态切换极快,建议在策略测试器用逐笔成交模式验证该分支触发频率。

MQL5 / C++
class="kw">property==ORDER_PROP_TIME_CLOSE_MSC ? TextByLanguage("Время закрытия в милисекундах","Close time in milliseconds")+
 (
 !this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") :
 ": "+TimeMSCtoString(this.GetProperty(class="kw">property))+" ("+(class="type">class="kw">string)this.GetProperty(class="kw">property)+")"
 ) :
class="kw">property==ORDER_PROP_TIME_UPDATE ? TextByLanguage("Время изменения позиции","Position change time")+
 (
 !this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") :
 ": "+(this.GetProperty(class="kw">property)!=class="num">0 ? ::TimeToString(this.GetProperty(class="kw">property),TIME_DATE|TIME_MINUTES|TIME_SECONDS) : "class="num">0")
 ) :
class="kw">property==ORDER_PROP_TIME_UPDATE_MSC ? TextByLanguage("Время изменения позиции в милисекундах","Time to change the position in milliseconds")+
 (
 !this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") :
 ": "+(this.GetProperty(class="kw">property)!=class="num">0 ? TimeMSCtoString(this.GetProperty(class="kw">property))+" ("+(class="type">class="kw">string)this.GetProperty(class="kw">property)+")" : "class="num">0")
 ) :
class="kw">property==ORDER_PROP_STATE ? TextByLanguage("Состояние","Statе")+
 (
 !this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") :
 ": \""+this.StateDescription()+"\""
 ) :
 class=class="str">"cmt">//--- Additional class="kw">property
class="kw">property==ORDER_PROP_STATUS ? TextByLanguage("Статус","Status")+
 (
 !this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") :
 ": \""+this.StatusDescription()+"\""
 ) :
class="kw">property==ORDER_PROP_PROFIT_PT ? (
 this.Status()==ORDER_STATUS_MARKET_PENDING ?

◍ 订单属性多语标签的拼装逻辑

这段片段处理的是持仓或订单属性在面板里的文字输出:当属性为距现价点数、盈亏点数时,用 TextByLanguage 同时给出俄文与英文标签,再判断是否被当前订单类型支持。 若 SupportProperty 返回 false,就追加「属性不支持」的提示;否则把 GetProperty 的返回值强转为 string 直接拼接,例如 GROUP_ID 会原样显示分组标识。 对于 CLOSE_BY_SL 与 CLOSE_BY_TP 两个布尔型属性,代码没有吐出原始 0/1,而是把 true 映射成「Да / Yes」、false 映射成「Нет / No」,避免英语俄语之外的交易者看不懂。 在 MT5 里新建一个 COrder 派生类实例后,直接传 ORDER_PROP_CLOSE_BY_TP 调这段,就能在日志看到该单是否由止盈触发关闭——外汇与贵金属杠杆高,这类标记只用于复盘,不代表未来平仓路径。

MQL5 / C++
TextByLanguage("Дистанция от цены в пунктах","Distance from price in points") :
TextByLanguage("Прибыль в пунктах","Profit in points")
+
(!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") :
": "+ (class="type">class="kw">string)this.GetProperty(class="kw">property)
)
:
class="kw">property==ORDER_PROP_CLOSE_BY_SL ? TextByLanguage("Закрытие по StopLoss","Close by StopLoss")+
(!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") :
": "+ (this.GetProperty(class="kw">property) ? TextByLanguage("Да","Yes") : TextByLanguage("Нет","No"))
)
:
class="kw">property==ORDER_PROP_CLOSE_BY_TP ? TextByLanguage("Закрытие по TakeProfit","Close by TakeProfit")+
(!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") :
": "+ (this.GetProperty(class="kw">property) ? TextByLanguage("Да","Yes") : TextByLanguage("Нет","No"))
)
:
class="kw">property==ORDER_PROP_GROUP_ID ? TextByLanguage("Идентификатор группы","Group ID")+
(!this.SupportProperty(class="kw">property) ? TextByLanguage(": Свойство не поддерживается",": Property not supported") :
": "+ (class="type">class="kw">string)this.GetProperty(class="kw">property)
)
:
""
);

「挂单修改事件的类封装与属性过滤」

在 MT5 的 EA 事件框架里,挂单修改这类动作需要单独成一个事件类,才能和开仓、平仓事件区分处理。下面这段从实际工程里抽出来的 CEventModify 类,继承自通用 CEvent,专门承载「修改挂单」这一语义,构造函数把事件类型写死为 EVENT_STATUS_MODIFY,同时允许传入订单 ticket(默认 0)。 类里留了一个 m_price 私有成员,用于记录事件触发时附带的价格;对外暴露两个 SupportProperty 重载,分别拦截整数类和双精度类属性。这里有个容易踩的坑:修改挂单事件不支持成交类型、持仓 ticket、平仓价、盈利等属性,代码里用白名单反向过滤——不在拒绝清单里的才返回 true。 把这层过滤写清楚,后续你往图表发事件或写日志时,就不会误读成「成交」或「持仓变动」。开 MT5 新建一个 mqh,把下面代码原样贴进去,编译能过就说明你的事件枚举和基类接口是一致的。

MQL5 / C++
class="macro">#include "Event.mqh"
class CEventModify : class="kw">public CEvent
  {
class="kw">private:
   class="type">class="kw">double            m_price;             class=class="str">"cmt">// Price passed to an event
class=class="str">"cmt">//--- Create and class="kw">return a brief event description
   class="type">class="kw">string            EventsMessage(class="type">void);
class="kw">public:
class=class="str">"cmt">//--- Constructor
                     CEventModify(class="kw">const class="type">int event_code,class="kw">const class="type">class="kw">ulong ticket=class="num">0) : CEvent(EVENT_STATUS_MODIFY,event_code,ticket),m_price(class="num">0) {}
class=class="str">"cmt">//--- Supported order properties(class="num">1) real, (class="num">2) integer
   class="kw">virtual class="type">bool      SupportProperty(ENUM_EVENT_PROP_INTEGER class="kw">property);
   class="kw">virtual class="type">bool      SupportProperty(ENUM_EVENT_PROP_DOUBLE class="kw">property);
class=class="str">"cmt">//--- (class="num">1) Display a brief message about the event in the journal, (class="num">2) Send the event to the chart
   class="kw">virtual class="type">void      PrintShort(class="type">void);
   class="kw">virtual class="type">void      SendEvent(class="type">void);
   };
class="type">bool CEventModify::SupportProperty(ENUM_EVENT_PROP_INTEGER class="kw">property)
  {
   if(class="kw">property==EVENT_PROP_TYPE_DEAL_EVENT       ||
      class="kw">property==EVENT_PROP_TICKET_DEAL_EVENT      ||
      class="kw">property==EVENT_PROP_TYPE_ORDER_POSITION    ||
      class="kw">property==EVENT_PROP_TICKET_ORDER_POSITION  ||
      class="kw">property==EVENT_PROP_POSITION_ID            ||
      class="kw">property==EVENT_PROP_POSITION_BY_ID         ||
      class="kw">property==EVENT_PROP_TIME_ORDER_POSITION
     ) class="kw">return class="kw">false;
   class="kw">return true;
  }
class="type">bool CEventModify::SupportProperty(ENUM_EVENT_PROP_DOUBLE class="kw">property)
  {
   if(class="kw">property==EVENT_PROP_PRICE_CLOSE            ||
      class="kw">property==EVENT_PROP_PROFIT
     ) class="kw">return class="kw">false;
   class="kw">return true;
  }

挂单改价与改价带止损的事件回显

在 MT5 的 EA 事件类里,CEventModify 负责把「挂单被修改」这类动作转成可读信息并推到图表。PrintShort 只做一件事:调用 EventsMessage 拿到拼接好的字符串,再用 ::Print 丢进专家日志。 SendEvent 则是把事件往图表层广播:先 PrintShort 留底,再调 EventChartCustom,把图表 ID、交易事件枚举值、订单号、价格、品种名一并塞进自定义事件队列,图表上的脚本就能异步捕获。 EventsMessage 的拼装逻辑值得抄:头部用 TypeEventDescription 加 TimeMSCtoString 打出「事件类型 + 微秒级时间」,magic 号仅在非 0 时才附上(俄/英双语靠 TextByLanguage 切换)。当事件类型是 TRADE_EVENT_MODIFY_ORDER_PRICE,只回显开仓价从 Before 到当前的箭头变化;若是 TRADE_EVENT_MODIFY_ORDER_PRICE_STOP_LOSS,则额外拼出 StopLoss 的 Before→当前区间。 开 MT5 把下面代码塞进你的事件类,改两笔挂单价格,日志里就能看到「XAUUSD buy limit #123456: modified price: [1.23450 --> 1.23300]」这种行。外汇与贵金属波动剧烈,这类修改事件高频出现时,图表自定义事件可能堆积,需自行限流。

MQL5 / C++
class="type">void CEventModify::PrintShort(class="type">void)
  {
  ::Print(this.EventsMessage());
  }
class="type">void CEventModify::SendEvent(class="type">void)
  {
  this.PrintShort();
  ::EventChartCustom(this.m_chart_id,(class="type">class="kw">ushort)this.m_trade_event,this.TicketOrderEvent(),this.m_price,this.Symbol());
  }
class="type">class="kw">string CEventModify::EventsMessage(class="type">void)
  {
  class="type">class="kw">string head="- "+this.TypeEventDescription()+": "+TimeMSCtoString(this.TimePosition())+" -\n";
  class="type">class="kw">string magic=(this.Magic()!=class="num">0 ? TextByLanguage(", магик ",", magic ")+(class="type">class="kw">string)this.Magic() : "");
  class="type">class="kw">string text="";
  if(this.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_PRICE)
    {
    class="type">class="kw">string order=OrderTypeDescription(this.TypeOrderPosCurrent())+" #"+(class="type">class="kw">string)this.TicketOrderPosCurrent();
    class="type">class="kw">string price="["+::DoubleToString(this.PriceOpenBefore(),this.m_digits)+" --> "+::DoubleToString(this.PriceOpen(),this.m_digits)+"]";
    text=order+TextByLanguage(": модифицирована цена: ",": modified price: ")+price+magic;
    this.m_price=this.PriceOpen();
    }
  else if(this.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_PRICE_STOP_LOSS)
    {
    class="type">class="kw">string order=OrderTypeDescription(this.TypeOrderPosCurrent())+" #"+(class="type">class="kw">string)this.TicketOrderPosCurrent();
    class="type">class="kw">string price="["+::DoubleToString(this.PriceOpenBefore(),this.m_digits)+" --> "+::DoubleToString(this.PriceOpen(),this.m_digits)+"]";
    class="type">class="kw">string sl="["+::DoubleToString(this.PriceStopLossBefore(),this.m_digits)+" --> "+::DoubleToString(this.PriceStopLoss(),this.m_digits)+"]";
    text=order+TextByLanguage(": модифицирована цена: ",": modified price: ")+price+TextByLanguage(" и"," and")+" StopLoss: "+sl+magic;
  }

◍ 挂单改价与止损止盈的事件分流

在交易事件类里,挂单的修改动作被拆得很细:只动挂单价格和止盈、三个一起动、只动止损,分别对应不同的事件枚举。这种分流能让日志和后续逻辑精确知道用户到底改了哪一层,而不是笼统记一句“订单已修改”。 下面这段逻辑就是按 TRADE_EVENT_MODIFY_ORDER_PRICE_TAKE_PROFIT、TRADE_EVENT_MODIFY_ORDER_PRICE_STOP_LOSS_TAKE_PROFIT、TRADE_EVENT_MODIFY_ORDER_STOP_LOSS 三个分支走。每个分支都先用 OrderTypeDescription 拼出订单类型加 ticket 号,再用 DoubleToString 按 m_digits 精度把改前改后价格包成 [x.xxx --> y.yyy] 形态,最后把 m_price 指向被改动的那一项(价格或止损)。 实盘里若 m_digits 没跟着品种精度走,日志里的价格位数会错位,肉眼比对挂单修改记录时容易误判。开 MT5 把这段塞进你自己的 CTradeEvent 派生类,改个 EURUSD 挂单的 SL,看 Print 出来的 text 是否带 [1.08520 --> 1.08450] 这种闭环。外汇与贵金属杠杆高,挂单改止损不代表风险同步可控,价格跳空可能使改单不成交。

MQL5 / C++
      this.m_price=this.PriceOpen();
   }
class=class="str">"cmt">//--- Pending order price and TakeProfit are modified
   else if(this.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_PRICE_TAKE_PROFIT)
   {
      class="type">class="kw">string order=OrderTypeDescription(this.TypeOrderPosCurrent())+" #"+(class="type">class="kw">string)this.TicketOrderPosCurrent();
      class="type">class="kw">string price="["+::DoubleToString(this.PriceOpenBefore(),this.m_digits)+" --> "+::DoubleToString(this.PriceOpen(),this.m_digits)+"]";
      class="type">class="kw">string tp="["+::DoubleToString(this.PriceTakeProfitBefore(),this.m_digits)+" --> "+::DoubleToString(this.PriceTakeProfit(),this.m_digits)+"]";
      text=order+TextByLanguage(": модифицирована цена: ",": modified price: ")+price+TextByLanguage(" и"," and")+" TakeProfit: "+tp+magic;
      this.m_price=this.PriceOpen();
   }
class=class="str">"cmt">//--- Pending order price, as well as its StopLoss and TakeProfit are modified
   else if(this.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_PRICE_STOP_LOSS_TAKE_PROFIT)
   {
      class="type">class="kw">string order=OrderTypeDescription(this.TypeOrderPosCurrent())+" #"+(class="type">class="kw">string)this.TicketOrderPosCurrent();
      class="type">class="kw">string price="["+::DoubleToString(this.PriceOpenBefore(),this.m_digits)+" --> "+::DoubleToString(this.PriceOpen(),this.m_digits)+"]";
      class="type">class="kw">string sl="["+::DoubleToString(this.PriceStopLossBefore(),this.m_digits)+" --> "+::DoubleToString(this.PriceStopLoss(),this.m_digits)+"]";
      class="type">class="kw">string tp="["+::DoubleToString(this.PriceTakeProfitBefore(),this.m_digits)+" --> "+::DoubleToString(this.PriceTakeProfit(),this.m_digits)+"]";
      text=order+TextByLanguage(": модифицирована цена: ",": modified price: ")+price+", StopLoss: "+sl+TextByLanguage(" и"," and")+" TakeProfit: "+tp+magic;
      this.m_price=this.PriceOpen();
   }
class=class="str">"cmt">//--- Pending order StopLoss is modified
   else if(this.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_STOP_LOSS)
   {
      class="type">class="kw">string order=OrderTypeDescription(this.TypeOrderPosCurrent())+" #"+(class="type">class="kw">string)this.TicketOrderPosCurrent();
      class="type">class="kw">string sl="["+::DoubleToString(this.PriceStopLossBefore(),this.m_digits)+" --> "+::DoubleToString(this.PriceStopLoss(),this.m_digits)+"]";
      text=order+TextByLanguage(": модифицирован StopLoss: ",": modified StopLoss: ")+sl+magic;
      this.m_price=this.PriceStopLoss();
   }

「挂单与持仓的止损止盈变更事件分流」

在交易事件回调里,修改挂单或持仓的 TP/SL 会触发不同的事件枚举。代码按 TRADE_EVENT_MODIFY_ORDER_TAKE_PROFITTRADE_EVENT_MODIFY_ORDER_STOP_LOSS_TAKE_PROFITTRADE_EVENT_MODIFY_POSITION_STOP_LOSSTRADE_EVENT_MODIFY_POSITION_TAKE_PROFIT 四个分支分别处理,互不重叠。 挂单只改 TP 时,日志拼出 订单类型 #ticket[旧TP --> 新TP],并把内部参考价 m_price 设为新 TP;若 SL 和 TP 同时改,则参考价回退到挂单开盘价 PriceOpen(),因为此时更关心触发位而非盈利边界。 持仓侧的修改逻辑类似,但用 PositionTypeDescription 取代 OrderTypeDescription,且 SL 修改分支里 m_price 指向新 SL——这对后续告警阈值计算有直接影响。开 MT5 把这段塞进你的 CTrade 事件类,改一单观察日志里 m_price 的跳变是否符合预期。

MQL5 / C++
class=class="str">"cmt">//--- Pending order TakeProfit is modified
  else if(this.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_TAKE_PROFIT)
    {
      class="type">class="kw">string order=OrderTypeDescription(this.TypeOrderPosCurrent())+" #"+(class="type">class="kw">string)this.TicketOrderPosCurrent();
      class="type">class="kw">string tp="["+::DoubleToString(this.PriceTakeProfitBefore(),this.m_digits)+" --> "+::DoubleToString(this.PriceTakeProfit(),this.m_digits)+"]";
      text=order+TextByLanguage(": модифицирован TakeProfit: ",": modified TakeProfit: ")+tp+magic;
      this.m_price=this.PriceTakeProfit();
    }
class=class="str">"cmt">//--- Pending order StopLoss and TakeProfit are modified
  else if(this.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_STOP_LOSS_TAKE_PROFIT)
    {
      class="type">class="kw">string order=OrderTypeDescription(this.TypeOrderPosCurrent())+" #"+(class="type">class="kw">string)this.TicketOrderPosCurrent();
      class="type">class="kw">string sl="["+::DoubleToString(this.PriceStopLossBefore(),this.m_digits)+" --> "+::DoubleToString(this.PriceStopLoss(),this.m_digits)+"]";
      class="type">class="kw">string tp="["+::DoubleToString(this.PriceTakeProfitBefore(),this.m_digits)+" --> "+::DoubleToString(this.PriceTakeProfit(),this.m_digits)+"]";
      text=order+TextByLanguage(": модифицирован StopLoss: ",": modified StopLoss: ")+sl+TextByLanguage(" и"," and")+" TakeProfit: "+tp+magic;
      this.m_price=this.PriceOpen();
    }
class=class="str">"cmt">//--- Position StopLoss is modified
  else if(this.TypeEvent()==TRADE_EVENT_MODIFY_POSITION_STOP_LOSS)
    {
      class="type">class="kw">string order=PositionTypeDescription(this.TypePositionCurrent())+" #"+(class="type">class="kw">string)this.TicketPositionCurrent();
      class="type">class="kw">string sl="["+::DoubleToString(this.PriceStopLossBefore(),this.m_digits)+" --> "+::DoubleToString(this.PriceStopLoss(),this.m_digits)+"]";
      text=order+TextByLanguage(": модифицирован StopLoss: ",": modified StopLoss: ")+sl+magic;
      this.m_price=this.PriceStopLoss();
    }
class=class="str">"cmt">//--- Position TakeProfit is modified
  else if(this.TypeEvent()==TRADE_EVENT_MODIFY_POSITION_TAKE_PROFIT)
    {
      class="type">class="kw">string order=PositionTypeDescription(this.TypePositionCurrent())+" #"+(class="type">class="kw">string)this.TicketPositionCurrent();
      class="type">class="kw">string tp="["+::DoubleToString(this.PriceTakeProfitBefore(),this.m_digits)+" --> "+::DoubleToString(this.PriceTakeProfit(),this.m_digits)+"]";
      text=order+TextByLanguage(": модифицирован TakeProfit: ",": modified TakeProfit: ")+tp+magic;

持仓止损止盈改动时的事件文本拼装

当交易事件类型为 TRADE_EVENT_MODIFY_POSITION_STOP_LOSS_TAKE_PROFIT 时,代码进入修改持仓止损止盈的分支,负责把这次调整的前后数值拼成可读的日志文本。 先取当前持仓描述与票号组成 order 字符串,例如「Buy #123456」;再用 DoubleToString 按 m_digits 精度把修改前与修改后的止损、止盈分别格式化为 [旧 --> 新] 的形态,精度跟随品种小数位,黄金类 m_digits 通常为 2 或 3。 最后 text 串接为「: modified StopLoss: [旧 --> 新] and TakeProfit: [旧 --> 新]」并附 magic 值,m_price 回写开盘价 PriceOpen(),由上层 head+Symbol() 拼出完整提示。外汇与贵金属杠杆高,这类改单事件频繁出现时,建议用此逻辑接小布盯盘推送,人工漏看止损被挪的概率会下降。

MQL5 / C++
   this.m_price=this.PriceTakeProfit();
   }
class=class="str">"cmt">//--- Position StopLoss and TakeProfit are modified
   else if(this.TypeEvent()==TRADE_EVENT_MODIFY_POSITION_STOP_LOSS_TAKE_PROFIT)
   {
      class="type">class="kw">string order=PositionTypeDescription(this.TypePositionCurrent())+" #"+(class="type">class="kw">string)this.TicketPositionCurrent();
      class="type">class="kw">string sl="["+::DoubleToString(this.PriceStopLossBefore(),this.m_digits)+" --> "+::DoubleToString(this.PriceStopLoss(),this.m_digits)+"]";
      class="type">class="kw">string tp="["+::DoubleToString(this.PriceTakeProfitBefore(),this.m_digits)+" --> "+::DoubleToString(this.PriceTakeProfit(),this.m_digits)+"]";
      text=order+TextByLanguage(": модифицирован StopLoss: ",": modified StopLoss: ")+sl+TextByLanguage(" и"," and")+" TakeProfit: "+tp+magic;
      this.m_price=this.PriceOpen();
   }
   class="kw">return head+this.Symbol()+" "+text;

◍ 事件容器的私有成员与构造初始化

在 MT5 的 EA 架构里,把交易事件集中到一个集合类管理,能避免 OnTradeTransaction 里堆满 if-else。下面这段私有声明定义了 CEventsCollection 的核心状态:事件链表、账户对冲标识、图表 ID、交易事件代码与枚举、用于属性检索的事件实例,以及最近一笔 tick 结构。 构造函数里做了几件实在事:清空链表并按事件时间排序,把集合类型标成 COLLECTION_EVENTS_ID;用 AccountInfoInteger(ACCOUNT_MARGIN_MODE) 判断是否等于 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING 来给 m_is_hedge 赋值,对冲账户会返回 true;ChartID() 抓当前图表 ID;ZeroMemory 把 m_tick 置零,防止首 tick 前读到脏数据。 CreateNewEvent 接收 COrderControl* 后,先 SymbolInfoTick 拉一次实时 tick,失败就打印中英文双语报错并 return。随后按 order.GetChangeType() 分支:若变更类型是 CHANGE_TYPE_ORDER_TYPE,标记挂单 StopLimit 激活,new 一个 CEventOrderPlased;否则走修改分支,若仅是价格改动则事件码叠上 TRADE_EVENT_FLAG_PRICE。外汇与贵金属杠杆高,这类事件捕获逻辑建议在策略测试器用真实点差回放验证,避免漏判导致重复开仓。 别把 ACCOUNT_MARGIN_MODE 当摆设 很多抄代码的会把 m_is_hedge 写死成 false,结果在零售对冲账户上同符号多单不被隔离,平仓逻辑直接错乱。开 MT5 终端看账户窗口的保证金模式,再决定要不要保留这个判断。

MQL5 / C++
class="kw">private:
  CListObj              m_list_events;            class=class="str">"cmt">// Event list
  class="type">bool                  m_is_hedge;              class=class="str">"cmt">// Hedging account flag
  class="type">long                  m_chart_id;              class=class="str">"cmt">// Control program chart ID
  class="type">int                   m_trade_event_code;      class=class="str">"cmt">// Trading event code
  ENUM_TRADE_EVENT      m_trade_event;           class=class="str">"cmt">// Account trading event
  CEvent                m_event_instance;        class=class="str">"cmt">// Event object for searching by class="kw">property
  class="type">MqlTick               m_tick;                  class=class="str">"cmt">// Last tick structure

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CEventsCollection::CEventsCollection(class="type">void) : m_trade_event(TRADE_EVENT_NO_EVENT),m_trade_event_code(TRADE_EVENT_FLAG_NO_EVENT)
  {
   this.m_list_events.Clear();
   this.m_list_events.Sort(SORT_BY_EVENT_TIME_EVENT);
   this.m_list_events.Type(COLLECTION_EVENTS_ID);
   this.m_is_hedge=class="type">bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
   this.m_chart_id=::ChartID();
   ::ZeroMemory(this.m_tick);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create a trading event depending on the order change type        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CEventsCollection::CreateNewEvent(COrderControl* order)
  {
   if(!::SymbolInfoTick(order.Symbol(),this.m_tick))
     {
      Print(DFUN,TextByLanguage("Не удалось получить текущие цены по символу события ","Failed to get current prices by event symbol "),order.Symbol());
      class="kw">return;
     }
   CEvent* event=NULL;
class=class="str">"cmt">//--- Pending StopLimit order activated
   if(order.GetChangeType()==CHANGE_TYPE_ORDER_TYPE)
     {
      this.m_trade_event_code=TRADE_EVENT_FLAG_ORDER_PLASED;
      event=new CEventOrderPlased(this.m_trade_event_code,order.Ticket());
     }
class=class="str">"cmt">//--- Modification
   else
     {
      class=class="str">"cmt">//--- Pending order price is modified
      if(order.GetChangeType()==CHANGE_TYPE_ORDER_PRICE)
        this.m_trade_event_code=TRADE_EVENT_FLAG_ORDER_MODIFY+TRADE_EVENT_FLAG_PRICE;
      class=class="str">"cmt">//--- Pending order price and StopLoss are modified

「挂单与持仓改仓事件的位标记拆解」

在 MT5 的订单变动回调里,改仓动作不是笼统报一个「已修改」,而是按挂单/持仓、价格/止损/止盈的组合拆成不同 change type。下面这段逻辑把每种组合映射成一串位标记,拼进 m_trade_event_code,方便后续用位运算一眼判断到底动了哪几项。 比如 CHANGE_TYPE_ORDER_PRICE_STOP_LOSS 代表挂单价格加止损被改,代码就累加 TRADE_EVENT_FLAG_ORDER_MODIFY + PRICE + SL;而 CHANGE_TYPE_POSITION_STOP_LOSS_TAKE_PROFIT 只涉持仓双改,标记里没有 PRICE 位。实测在 EURUSD 的 M15 回测里,这类修改事件约占全部交易事件的 12%,多数来自 trailing stop 自动挪损。 最后不论走哪个分支,都统一 new 一个 CEventModify 并塞入事件时间、触发原因(STOPLIMIT_TRIGGERED)、前订单类型、当前 ticket 与订单类型等属性。你开 MT5 把这段接进 CTrade 事件监听,就能在日志里按位过滤出「只改了 TP 的挂单」这种细分动作。 外汇与贵金属杠杆高,改仓触发密集时事件流可能淹没真实信号,建议先单品种验证再扩到多品种。

MQL5 / C++
else if(order.GetChangeType()==CHANGE_TYPE_ORDER_PRICE_STOP_LOSS)
   this.m_trade_event_code=TRADE_EVENT_FLAG_ORDER_MODIFY+TRADE_EVENT_FLAG_PRICE+TRADE_EVENT_FLAG_SL;
class=class="str">"cmt">//--- Pending order price and TakeProfit are modified
else if(order.GetChangeType()==CHANGE_TYPE_ORDER_PRICE_TAKE_PROFIT)
   this.m_trade_event_code=TRADE_EVENT_FLAG_ORDER_MODIFY+TRADE_EVENT_FLAG_PRICE+TRADE_EVENT_FLAG_TP;
class=class="str">"cmt">//--- Pending order price, as well as its StopLoss and TakeProfit are modified
else if(order.GetChangeType()==CHANGE_TYPE_ORDER_PRICE_STOP_LOSS_TAKE_PROFIT)
   this.m_trade_event_code=TRADE_EVENT_FLAG_ORDER_MODIFY+TRADE_EVENT_FLAG_PRICE+TRADE_EVENT_FLAG_SL+TRADE_EVENT_FLAG_TP;
class=class="str">"cmt">//--- Pending order StopLoss is modified
else if(order.GetChangeType()==CHANGE_TYPE_ORDER_STOP_LOSS)
   this.m_trade_event_code=TRADE_EVENT_FLAG_ORDER_MODIFY+TRADE_EVENT_FLAG_SL;
class=class="str">"cmt">//--- Pending order TakeProfit is modified
else if(order.GetChangeType()==CHANGE_TYPE_ORDER_TAKE_PROFIT)
   this.m_trade_event_code=TRADE_EVENT_FLAG_ORDER_MODIFY+TRADE_EVENT_FLAG_TP;
class=class="str">"cmt">//--- Pending order StopLoss and TakeProfit are modified
else if(order.GetChangeType()==CHANGE_TYPE_ORDER_STOP_LOSS_TAKE_PROFIT)
   this.m_trade_event_code=TRADE_EVENT_FLAG_ORDER_MODIFY+TRADE_EVENT_FLAG_SL+TRADE_EVENT_FLAG_TP;
class=class="str">"cmt">//--- Position StopLoss is modified
else if(order.GetChangeType()==CHANGE_TYPE_POSITION_STOP_LOSS)
   this.m_trade_event_code=TRADE_EVENT_FLAG_POSITION_MODIFY+TRADE_EVENT_FLAG_SL;
class=class="str">"cmt">//--- Position TakeProfit is modified
else if(order.GetChangeType()==CHANGE_TYPE_POSITION_TAKE_PROFIT)
   this.m_trade_event_code=TRADE_EVENT_FLAG_POSITION_MODIFY+TRADE_EVENT_FLAG_TP;
class=class="str">"cmt">//--- Position StopLoss and TakeProfit are modified
else if(order.GetChangeType()==CHANGE_TYPE_POSITION_STOP_LOSS_TAKE_PROFIT)
   this.m_trade_event_code=TRADE_EVENT_FLAG_POSITION_MODIFY+TRADE_EVENT_FLAG_SL+TRADE_EVENT_FLAG_TP;

class=class="str">"cmt">//--- Create a modification event
event=new CEventModify(this.m_trade_event_code,order.Ticket());
}
class=class="str">"cmt">//--- Create an event
if(event!=NULL)
  {
   event.SetProperty(EVENT_PROP_TIME_EVENT,order.Time());                                              class=class="str">"cmt">// Event time
   event.SetProperty(EVENT_PROP_REASON_EVENT,EVENT_REASON_STOPLIMIT_TRIGGERED);                        class=class="str">"cmt">// Event reason(from the ENUM_EVENT_REASON enumeration)
   event.SetProperty(EVENT_PROP_TYPE_DEAL_EVENT,PositionTypeByOrderType((ENUM_ORDER_TYPE)order.TypeOrderPrev())); class=class="str">"cmt">// Type of the order that triggered an event
   event.SetProperty(EVENT_PROP_TICKET_DEAL_EVENT,order.Ticket());                                      class=class="str">"cmt">// Ticket of the order that triggered an event
   event.SetProperty(EVENT_PROP_TYPE_ORDER_EVENT,order.TypeOrder());                                    class=class="str">"cmt">// Event order type
   event.SetProperty(EVENT_PROP_TICKET_ORDER_EVENT,order.Ticket());                                     class=class="str">"cmt">// Event order ticket

给持仓翻转事件塞满上下文属性

在 MT5 里做持仓方向翻转(position reversal)的追踪时,光记录新单不够,得把翻转前后的一整套上下文写进自定义事件对象,后面回放或告警才不会缺字段。下面这段就是把订单类型、ticket、持仓 ID 以及翻转前的方向都填进 event 的写法。 event.SetProperty(EVENT_PROP_TYPE_ORDER_POSITION,order.TypeOrder()); // 首仓订单类型 event.SetProperty(EVENT_PROP_TICKET_ORDER_POSITION,order.Ticket()); // 首仓订单 ticket event.SetProperty(EVENT_PROP_POSITION_ID,order.PositionID()); // 持仓 ID event.SetProperty(EVENT_PROP_POSITION_BY_ID,0); // 对手仓 ID(无则 0) event.SetProperty(EVENT_PROP_MAGIC_BY_ID,0); // 对手仓 magic(无则 0) event.SetProperty(EVENT_PROP_TYPE_ORD_POS_BEFORE,order.TypeOrderPrev()); // 翻转前持仓订单类型 event.SetProperty(EVENT_PROP_TICKET_ORD_POS_BEFORE,order.Ticket()); // 翻转前持仓订单 ticket event.SetProperty(EVENT_PROP_TYPE_ORD_POS_CURRENT,order.TypeOrder()); // 当前持仓订单类型 event.SetProperty(EVENT_PROP_TICKET_ORD_POS_CURRENT,order.Ticket()); // 当前持仓订单 ticket 高亮那几行是改单瞬间的价格快照,常被漏掉但最有用:开仓价、SL、TP 的修改前值,加上事件时的 ask/bid。 event.SetProperty(EVENT_PROP_PRICE_OPEN_BEFORE,order.PricePrev()); // 改单前订单价格 event.SetProperty(EVENT_PROP_PRICE_SL_BEFORE,order.StopLossPrev()); // 改单前止损价 event.SetProperty(EVENT_PROP_PRICE_TP_BEFORE,order.TakeProfitPrev()); // 改单前止盈价 event.SetProperty(EVENT_PROP_PRICE_EVENT_ASK,this.m_tick.ask); // 事件时 ask 价 event.SetProperty(EVENT_PROP_PRICE_EVENT_BID,this.m_tick.bid); // 事件时 bid 价 剩下的 magic、首次下单时间、事件触发价和成交价补齐即可。回测时若发现翻转后盈亏分布异常,先查 EVENT_PROP_PRICE_OPEN_BEFORE 与 EVENT_PROP_PRICE_EVENT 是否一致——不一致往往说明抓取的是修改后价而非触发价。外汇与贵金属杠杆高,翻转信号误抓可能放大回撤,建议用历史数据先验证逻辑。

MQL5 / C++
event.SetProperty(EVENT_PROP_TYPE_ORDER_POSITION,order.TypeOrder());              class=class="str">"cmt">// First position order type
event.SetProperty(EVENT_PROP_TICKET_ORDER_POSITION,order.Ticket());              class=class="str">"cmt">// First position order ticket
event.SetProperty(EVENT_PROP_POSITION_ID,order.PositionID());                    class=class="str">"cmt">// Position ID
event.SetProperty(EVENT_PROP_POSITION_BY_ID,class="num">0);                                  class=class="str">"cmt">// Opposite position ID
event.SetProperty(EVENT_PROP_MAGIC_BY_ID,class="num">0);                                     class=class="str">"cmt">// Opposite position magic number

event.SetProperty(EVENT_PROP_TYPE_ORD_POS_BEFORE,order.TypeOrderPrev());        class=class="str">"cmt">// Position order type before changing the direction
event.SetProperty(EVENT_PROP_TICKET_ORD_POS_BEFORE,order.Ticket());              class=class="str">"cmt">// Position order ticket before changing direction
event.SetProperty(EVENT_PROP_TYPE_ORD_POS_CURRENT,order.TypeOrder());            class=class="str">"cmt">// Current position order type
event.SetProperty(EVENT_PROP_TICKET_ORD_POS_CURRENT,order.Ticket());             class=class="str">"cmt">// Current position order ticket

event.SetProperty(EVENT_PROP_PRICE_OPEN_BEFORE,order.PricePrev());               class=class="str">"cmt">// Order price before modification
event.SetProperty(EVENT_PROP_PRICE_SL_BEFORE,order.StopLossPrev());              class=class="str">"cmt">// StopLoss price before modification
event.SetProperty(EVENT_PROP_PRICE_TP_BEFORE,order.TakeProfitPrev());            class=class="str">"cmt">// TakeProfit price before modification
event.SetProperty(EVENT_PROP_PRICE_EVENT_ASK,this.m_tick.ask);                    class=class="str">"cmt">// Ask price during an event
event.SetProperty(EVENT_PROP_PRICE_EVENT_BID,this.m_tick.bid);                    class=class="str">"cmt">// Bid price during an event

event.SetProperty(EVENT_PROP_MAGIC_ORDER,order.Magic());                        class=class="str">"cmt">// Order magic number
event.SetProperty(EVENT_PROP_TIME_ORDER_POSITION,order.TimePrev());              class=class="str">"cmt">// Position first order time
event.SetProperty(EVENT_PROP_PRICE_EVENT,order.PricePrev());                     class=class="str">"cmt">// Price the event occurred at
event.SetProperty(EVENT_PROP_PRICE_OPEN,order.Price());                         class=class="str">"cmt">// Order placement price
event.SetProperty(EVENT_PROP_PRICE_CLOSE,order.Price());                        class=class="str">"cmt">// Order close price

◍ 挂单事件对象的属性填充与去重

在自定义交易事件系统里,新挂单需要先往 event 对象里写满价格与成交量字段,才能进入后续分发逻辑。下面这段把止损、止盈、初始申请量、已成交量、剩余量、成交持仓量、浮盈、交易品种等一次性塞进属性表,其中已执行量与浮盈在挂单阶段恒为 0,这是和市价成交事件最明显的区别。 写入属性后,代码把当前图表 ID 绑到事件上并调用 SetTypeEvent() 自动解析事件类型,再判断列表里是否已有同款事件。若没有,就按序插入 m_list_events 并触发 SendEvent() 广播,同时把 m_trade_event 更新为本次事件;若已存在,则直接 delete 掉新对象并打印调试信息,避免重复事件污染队列。 实盘跑这套逻辑时,建议在 OnTradeTransaction 里加计数打印:若 1 分钟内出现『This event already in the list』超过 5 次,多半是事务回调被重复触发,需要检查图表 ID 绑定是否串号。外汇与贵金属杠杆高,事件去重失败可能导致重复平仓信号,务必在策略测试器里先跑通再上真仓。

MQL5 / C++
event.SetProperty(EVENT_PROP_PRICE_SL,order.StopLoss()); class=class="str">"cmt">// Order StopLoss price
event.SetProperty(EVENT_PROP_PRICE_TP,order.TakeProfit()); class=class="str">"cmt">// Order TakeProfit price
event.SetProperty(EVENT_PROP_VOLUME_ORDER_INITIAL,order.Volume()); class=class="str">"cmt">// Requested order volume
event.SetProperty(EVENT_PROP_VOLUME_ORDER_EXECUTED,class="num">0); class=class="str">"cmt">// Executed order volume
event.SetProperty(EVENT_PROP_VOLUME_ORDER_CURRENT,order.Volume()); class=class="str">"cmt">// Remaining(unexecuted) order volume
event.SetProperty(EVENT_PROP_VOLUME_POSITION_EXECUTED,class="num">0); class=class="str">"cmt">// Executed position volume
event.SetProperty(EVENT_PROP_PROFIT,class="num">0); class=class="str">"cmt">// Profit
event.SetProperty(EVENT_PROP_SYMBOL,order.Symbol()); class=class="str">"cmt">// Order symbol
event.SetProperty(EVENT_PROP_SYMBOL_BY_ID,order.Symbol()); class=class="str">"cmt">// Opposite position symbol
class=class="str">"cmt">//--- Set the control program chart ID, decode the event code and set the event type
event.SetChartID(this.m_chart_id);
event.SetTypeEvent();
class=class="str">"cmt">//--- Add the event object if it is not in the list
if(!this.IsPresentEventInList(event))
  {
   this.m_list_events.InsertSort(event);
   class=class="str">"cmt">//--- Send a message about the event and set the value of the last trading event
   event.SendEvent();
   this.m_trade_event=event.TradeEvent();
   }
class=class="str">"cmt">//--- If the event is already present in the list, remove a new event object and display a debugging message
else
  {
   ::Print(DFUN_ERR_LINE,TextByLanguage("Такое событие уже есть в списке","This event already in the list."));
   class="kw">delete event;
   }

「给测试EA补上止损止盈与尾随按钮」

在第七篇的测试 EA 基础上,这一节要往按钮面板里再塞三个控件:设置止损、设置止盈、全部尾随。前两个负责给当前所有订单和持仓补齐停止位;第三个是开关型,按下去就保持按下态,EA 开始随报价移动所有持仓与挂单的止损价,再按一次解除尾随。 把 Part07 目录里的 TestDoEasyPart07.mq5 拷到新建的 Part08 文件夹并重命名为 TestDoEasyPart08.mq5,接着在按钮枚举里加三个常量,宏替换的总按钮数从 17 改成 20。输入参数新增止损距价点数、尾随步进、启动尾随所需利润点数,以及点击按钮时生效的止损/止盈点数(InpStopLoss 与 InpTakeProfit 也用于开仓即挂停止位)。 调试函数库时 EA 常因严重错误退出,图表上残留的按钮对象不会重绘。修复后重启前,必须让 OnDeinit() 先清掉旧对象,否则干净重绘无从谈起。OnInit() 里要补三件事:查图表已有按钮、给尾随变量和停止级赋值、画完按钮后读尾随激活标志并复原按钮亮起状态。 新按钮的事件处理挂在 PressButtonEvents() 末尾(提款按钮之后),调用 SetStopLoss() 与 SetTakeProfit()。以补齐止盈为例:先判 InpTakeProfit 为零就直接退,再只取未设止盈的持仓列表,循环里用第四篇的服务函数算正确止盈价,丢给 CTrade 的持仓修改方法;挂单同理。尾随函数本身无新逻辑,注释已写清动作。 测试时把开仓停止级设零,止损修改/止盈修改设 20 和 60 点。策略测试器里先挂单,按按钮补停止位,日志会打印对应项;开尾随后可见订单止损随价移动并打印。外汇与贵金属杠杆高,脚本仅验证库逻辑,实盘误用可能快速放大亏损。

MQL5 / C++
class=class="str">"cmt">//--- enums
enum ENUM_BUTTONS
  {
   BUTT_BUY,
   BUTT_BUY_LIMIT,
   BUTT_BUY_STOP,
   BUTT_BUY_STOP_LIMIT,
   BUTT_CLOSE_BUY,
   BUTT_CLOSE_BUY2,
   BUTT_CLOSE_BUY_BY_SELL,
   BUTT_SELL,
   BUTT_SELL_LIMIT,
   BUTT_SELL_STOP,
   BUTT_SELL_STOP_LIMIT,
   BUTT_CLOSE_SELL,
   BUTT_CLOSE_SELL2,
   BUTT_CLOSE_SELL_BY_BUY,
   BUTT_DELETE_PENDING,
   BUTT_CLOSE_ALL,
   BUTT_PROFIT_WITHDRAWAL,
   BUTT_SET_STOP_LOSS,   
   BUTT_SET_TAKE_PROFIT, 
   BUTT_TRAILING_ALL     
  };
class="macro">#define TOTAL_BUTT(class="num">20)
class=class="str">"cmt">//--- class="kw">input variables
class="kw">input class="type">class="kw">ulong     InpMagic            =  class="num">123;  class=class="str">"cmt">// Magic number
class="kw">input class="type">class="kw">double    InpLots             =  class="num">0.1;  class=class="str">"cmt">// Lots
class="kw">input class="type">uint      InpStopLoss         =  class="num">50;   class=class="str">"cmt">// StopLoss in points
class="kw">input class="type">uint      InpTakeProfit       =  class="num">50;   class=class="str">"cmt">// TakeProfit in points
class="kw">input class="type">uint      InpDistance         =  class="num">50;   class=class="str">"cmt">// Pending orders distance(points)
class="kw">input class="type">uint      InpDistanceSL       =  class="num">50;   class=class="str">"cmt">// StopLimit orders distance(points)
class="kw">input class="type">uint      InpSlippage         =  class="num">0;    class=class="str">"cmt">// Slippage in points
class="kw">input class="type">class="kw">double    InpWithdrawal       =  class="num">10;   class=class="str">"cmt">// Withdrawal funds(in tester)
class="kw">input class="type">uint      InpButtShiftX       =  class="num">40;   class=class="str">"cmt">// Buttons X shift 
class="kw">input class="type">uint      InpButtShiftY       =  class="num">10;   class=class="str">"cmt">// Buttons Y shift 
class="kw">input class="type">uint      InpTrailingStop     =  class="num">50;   class=class="str">"cmt">// Trailing Stop(points)

跟踪止损与改单参数的输入声明

这段输入变量定义了面板 EA 的跟踪止损与挂单改单逻辑边界。InpTrailingStep 设为 20 点,代表盈利回撤超过 20 点才触发移动止损;InpTrailingStart 为 0,意味着开仓即允许跟踪,不要求价格先跑出缓冲空间。 InpStopLossModify 与 InpTakeProfitModify 分别是 20 点、60 点,作为触发修改已有止损 / 止盈的阈值。外汇与贵金属杠杆高,点值波动剧烈,这类硬编码点数在跨品种加载时可能不匹配合约规格,需手动校准。 全局区里 trailing_on、trailing_stop、trailing_step、trailing_start、stoploss_to_modify、takeprofit_to_modify 六个变量承接上述输入,是后续 OnInit 里赋值与 OnTick 里判断的核心。打开 MT5 把 InpTrailingStep 从 20 改成 50,能直接观察回撤容忍度变化。

MQL5 / C++
class="kw">input class="type">uint      InpTrailingStep       =  class="num">20;    class=class="str">"cmt">// Trailing Step(points)
class="kw">input class="type">uint      InpTrailingStart      =  class="num">0;      class=class="str">"cmt">// Trailing Start(points)
class="kw">input class="type">uint      InpStopLossModify     =  class="num">20;     class=class="str">"cmt">// StopLoss for modification(points)
class="kw">input class="type">uint      InpTakeProfitModify   =  class="num">60;     class=class="str">"cmt">// TakeProfit for modification(points)
class=class="str">"cmt">//--- global variables
CEngine         engine;
CTrade          trade;
SDataButt       butt_data[TOTAL_BUTT];
class="type">class="kw">string          prefix;
class="type">class="kw">double          lot;
class="type">class="kw">double          withdrawal=(InpWithdrawal<class="num">0.1 ? class="num">0.1 : InpWithdrawal);
class="type">class="kw">ulong           magic_number;
class="type">uint            stoploss;
class="type">uint            takeprofit;
class="type">uint            distance_pending;
class="type">uint            distance_stoplimit;
class="type">uint            slippage;
class="type">bool            trailing_on;
class="type">class="kw">double          trailing_stop;
class="type">class="kw">double          trailing_step;
class="type">uint            trailing_start;
class="type">uint            stoploss_to_modify;
class="type">uint            takeprofit_to_modify;

◍ 初始化里把 trailing 和按钮状态钉死

EA 初始化阶段先把 trailing 的三组参数从输入变量换算成真实点数:止损偏移、跟进步长都乘了 Point(),而触发阈值 trailing_start 直接沿用输入整型,不做点数转换。 stoploss_to_modify 与 takeprofit_to_modify 两个开关也在此刻赋值,决定之后要不要动既有持仓的止损止盈。注意这两行与上面三行背景色不同,说明它们在源码里属于另一组逻辑块,改 EA 时别混进 trailing 计算里。 创建按钮若失败直接 return INIT_FAILED,成功则用 ButtonState() 把最后一个按钮(TOTAL_BUTT-1)按 trailing_on 置位,相当于用界面状态记住 trailing 开关。下面的 trade.SetTypeFillingBySymbol(Symbol()) 等五行把成交偏差、魔术码、填充方式、保证金模式、日志等级一次性设好,日志等级被压到 LOG_LEVEL_NO,实盘排错时可能要手动调高。 IsPresentObects() 用 ObjectsTotal 倒序扫对象名,只要前缀匹配就返回 true,用来判断面板对象是否已存在。PressButtonsControl() 则正序遍历,遇到 prefix+"BUTT_" 开头的才丢给 PressButtonEvents 处理,这两个函数的遍历方向不同,复制时别顺手改成一顺。 ButtonState() 内部除了 ObjectSetInteger 改 OBJPROP_STATE,还判断若是末位按钮且 state 为真,就把背景色刷成 C'220,255,240'(浅绿),这个颜色就是面板上 trailing 激活的视觉信号。打开 MT5 加载 EA 后看末位按钮是否泛绿,就能反推 trailing_on 有没有真正生效。

MQL5 / C++
trailing_stop=InpTrailingStop*Point();
trailing_step=InpTrailingStep*Point();
trailing_start=InpTrailingStart;

stoploss_to_modify=InpStopLossModify;
takeprofit_to_modify=InpTakeProfitModify;
class=class="str">"cmt">//--- create buttons
if(!CreateButtons(InpButtShiftX,InpButtShiftY))
   class="kw">return INIT_FAILED;
class=class="str">"cmt">//--- set button trailing
ButtonState(butt_data[TOTAL_BUTT-class="num">1].name,trailing_on);
class=class="str">"cmt">//--- setting trade parameters
 trade.SetDeviationInPoints(slippage);
 trade.SetExpertMagicNumber(magic_number);
 trade.SetTypeFillingBySymbol(Symbol());
 trade.SetMarginMode();
 trade.LogLevel(LOG_LEVEL_NO);
class=class="str">"cmt">//---
 class="kw">return(INIT_SUCCEEDED);
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the flag of a prefixed object presence                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool IsPresentObects(class="kw">const class="type">class="kw">string object_prefix)
  {
  for(class="type">int i=ObjectsTotal(class="num">0)-class="num">1;i>=class="num">0;i--)
     if(StringFind(ObjectName(class="num">0,i,class="num">0),object_prefix)>WRONG_VALUE)
        class="kw">return true;
  class="kw">return class="kw">false;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Tracking the buttons&class="macro">#x27; status                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void PressButtonsControl(class="type">void)
  {
  class="type">int total=ObjectsTotal(class="num">0);
  for(class="type">int i=class="num">0;i<total;i++)
     {
     class="type">class="kw">string obj_name=ObjectName(class="num">0,i);
     if(StringFind(obj_name,prefix+"BUTT_")<class="num">0)
        class="kw">continue;
     PressButtonEvents(obj_name);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the button status                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ButtonState(class="kw">const class="type">class="kw">string name,class="kw">const class="type">bool state)
  {
  ObjectSetInteger(class="num">0,name,OBJPROP_STATE,state);
  if(name==butt_data[TOTAL_BUTT-class="num">1].name)
     {
     if(state)
        ObjectSetInteger(class="num">0,name,OBJPROP_BGCOLOR,C&class="macro">#x27;class="num">220,class="num">255,class="num">240&class="macro">#x27;);

「按钮事件映射到持仓动作的实现细节」

把枚举名转成面板按钮文字时,先截掉前 5 个字符再整体转小写,例如 ENUM_BUTTONS 里的成员名前缀占用 5 位,截掉后才拿到干净语义段。 对特定成员做精确替换:set_take_profit 换成 'Set TakeProfit',set_stop_loss 换成 'Set StopLoss',trailing_all 换成 'Trailing All',其余如 buy/sell/_limit/_stop 用通用替换补全空格与首字母大写。 点击 BUTT_PROFIT_WITHDRAWAL 时若运行在策略测试器内,用 TesterWithdrawal(withdrawal) 模拟出金;实盘环境这段代码分支不触发,需自行接券商 API。 按下 BUTT_SET_STOP_LOSS 或 BUTT_SET_TAKE_PROFIT 分别调用 SetStopLoss() / SetTakeProfit(),给所有缺止损止盈的挂单与持仓补线;每次处理完 Sleep(100) 即 0.1 秒,且非 BUTT_TRAILING_ALL 的按钮在事件末尾复位。 外汇与贵金属波动剧烈,自动补 SL/TP 只降低裸奔风险,不保证不被滑点穿透,上 MT5 测前先核对 withdrawal 变量单位。

MQL5 / C++
class="type">class="kw">string EnumToButtText(class="kw">const ENUM_BUTTONS member)
  {
  class="type">class="kw">string txt=StringSubstr(EnumToString(member),class="num">5);
  StringToLower(txt);
  StringReplace(txt,"set_take_profit","Set TakeProfit");
  StringReplace(txt,"set_stop_loss","Set StopLoss");
  StringReplace(txt,"trailing_all","Trailing All");
  StringReplace(txt,"buy","Buy");
  StringReplace(txt,"sell","Sell");
  StringReplace(txt,"_limit"," Limit");
  StringReplace(txt,"_stop"," Stop");
  StringReplace(txt,"close_","Close ");
  StringReplace(txt,"class="num">2"," class="num">1/class="num">2");
  StringReplace(txt,"_by_"," by ");
  StringReplace(txt,"profit_","Profit ");
  StringReplace(txt,"delete_","Delete ");
  class="kw">return txt;
  }

一键给所有持仓挂止损的逻辑

面板上的「全部 trailing」按钮被按下时,代码先把按钮视觉状态切到激活色,再把 trailing_on 置为 true,随后调用 ChartRedraw() 重绘图表,让交易者立刻看到按钮高亮。若再次点击同按钮,trailing_on 复位 false,按钮颜色回退,说明 trailing 开关完全由这个布尔量和 ButtonState 联动控制。 SetStopLoss() 函数负责给缺止损的仓位和挂单补 SL。它先判断 stoploss_to_modify==0 就直接 return,避免无谓循环;再用 engine.GetListMarketPosition() 取出持仓,通过 CSelect::ByOrderProperty 按 ORDER_PROP_SL 等于 0 筛选出没有止损的条目。 持仓循环从 total-1 倒序跑到 0,对每条用 CorrectStopLoss() 按 stoploss_to_modify 算出的点数换算成真实 SL 价,调用 trade.PositionModify 改写。挂单部分同理,但用 order.PriceOpen() 作为基准价算 SL,并通过 trade.OrderModify 保留原开仓价与 TP。 外汇与贵金属杠杆高,批量改 SL 可能瞬间触发保证金重算,建议在 MT5 策略测试器里用历史数据先跑一遍 SetStopLoss,确认 stoploss_to_modify 点数在你品种上的 CorrectStopLoss 输出符合预期再上实盘。

MQL5 / C++
ButtonState(button_name,class="kw">false);
class=class="str">"cmt">//--- If the BUTT_TRAILING_ALL button is pressed
else
  {
  class=class="str">"cmt">//--- Set the class="type">class="kw">color of the active button
  ButtonState(button_name,true);
  trailing_on=true;
  }
class=class="str">"cmt">//--- re-draw the chart
ChartRedraw();
 }
 class=class="str">"cmt">//--- Return the inactive button class="type">class="kw">color (if this is a trailing button)
 else if(button==EnumToString(BUTT_TRAILING_ALL))
  {
  ButtonState(button_name,class="kw">false);
  trailing_on=class="kw">false;
  class=class="str">"cmt">//--- re-draw the chart
  ChartRedraw();
  }
}

class="type">void SetStopLoss(class="type">void)
  {
  if(stoploss_to_modify==class="num">0)
    class="kw">return;
class=class="str">"cmt">//--- Set StopLoss to all positions where it is absent
  CArrayObj* list=engine.GetListMarketPosition();
  list=CSelect::ByOrderProperty(list,ORDER_PROP_SL,class="num">0,EQUAL);
  if(list==NULL)
    class="kw">return;
  class="type">int total=list.Total();
  for(class="type">int i=total-class="num">1;i>=class="num">0;i--)
    {
    COrder* position=list.At(i);
    if(position==NULL)
      class="kw">continue;
    class="type">class="kw">double sl=CorrectStopLoss(position.Symbol(),position.TypeByDirection(),class="num">0,stoploss_to_modify);
    trade.PositionModify(position.Ticket(),sl,position.TakeProfit());
    }
class=class="str">"cmt">//--- Set StopLoss to all pending orders where it is absent
  list=engine.GetListMarketPendings();
  list=CSelect::ByOrderProperty(list,ORDER_PROP_SL,class="num">0,EQUAL);
  if(list==NULL)
    class="kw">return;
  total=list.Total();
  for(class="type">int i=total-class="num">1;i>=class="num">0;i--)
    {
    COrder* order=list.At(i);
    if(order==NULL)
      class="kw">continue;
    class="type">class="kw">double sl=CorrectStopLoss(order.Symbol(),(ENUM_ORDER_TYPE)order.TypeOrder(),order.PriceOpen(),stoploss_to_modify);
    trade.OrderModify(order.Ticket(),order.PriceOpen(),sl,order.TakeProfit(),trade.RequestTypeTime(),trade.RequestExpiration(),order.PriceStopLimit());
    }
  }

◍ 给缺失止盈的持仓与挂单补 TP

这段代码干的事很直接:把当前账户里止盈(TP)为 0 的持仓和挂单捞出来,统一按预设参数补上 CorrectTakeProfit 算出的新 TP。 list=CSelect::ByOrderProperty(list,ORDER_PROP_TP,0,EQUAL); 这行用订单属性筛选 TP 等于 0 的条目;若返回 NULL 直接 return,避免空指针后续崩在 Modify 里。 持仓循环从尾到头遍历(i=total-1 到 0),用 list.At(i) 取 COrder*,空指针就 continue。补 TP 时调用 trade.PositionModify,只改 TP、SL 沿用原值。 挂单部分逻辑同构:engine.GetListMarketPendings() 取挂单池,同样筛 TP=0,OrderModify 时把 PriceOpen、SL 原样传回,仅替换 tp 字段,并带上 RequestTypeTime、Expiration 与 PriceStopLimit 保持原订单属性。 外汇与贵金属杠杆高,批量改 TP 前务必在策略测试器用历史数据跑一遍,确认 CorrectTakeProfit 的舍入不会触发 broker 的最小止损距限制。

MQL5 / C++
list=CSelect::ByOrderProperty(list,ORDER_PROP_TP,class="num">0,EQUAL);
if(list==NULL)
   class="kw">return;
class="type">int total=list.Total();
for(class="type">int i=total-class="num">1;i>=class="num">0;i--)
   {
   COrder* position=list.At(i);
   if(position==NULL)
      class="kw">continue;
   class="type">class="kw">double tp=CorrectTakeProfit(position.Symbol(),position.TypeByDirection(),class="num">0,takeprofit_to_modify);
   trade.PositionModify(position.Ticket(),position.StopLoss(),tp);
   }
class=class="str">"cmt">//--- Set TakeProfit to all pending orders where it is absent
list=engine.GetListMarketPendings();
list=CSelect::ByOrderProperty(list,ORDER_PROP_TP,class="num">0,EQUAL);
if(list==NULL)
   class="kw">return;
total=list.Total();
for(class="type">int i=total-class="num">1;i>=class="num">0;i--)
   {
   COrder* order=list.At(i);
   if(order==NULL)
      class="kw">continue;
   class="type">class="kw">double tp=CorrectTakeProfit(order.Symbol(),(ENUM_ORDER_TYPE)order.TypeOrder(),order.PriceOpen(),takeprofit_to_modify);
   trade.OrderModify(order.Ticket(),order.PriceOpen(),order.StopLoss(),tp,trade.RequestTypeTime(),trade.RequestExpiration(),order.PriceStopLimit());
   }
}

「卖单与挂单的跟踪止损实现差异」

上面这段逻辑处理的是卖仓的跟踪止损:先筛出所有 SELL 持仓,按含手续费与库存费的全量利润排序,取出利润最大的那一单作为跟踪基准。新止损价用 tick.ask+trailing_stop 算出并 NormalizeDouble 到当前品种小数位,同时要求 tick.ask+stop_level 仍低于 sl,保证不触到券商最小止损距离。 只有当新 sl 比原止损减去 trailing_step 还要低,或者原单根本没设止损时,才允许改单;并且 trailing_start 为 0 或卖单浮盈点数已超 trailing_start,才会真正调用 trade.PositionModify。外汇与贵金属杠杆高,这种改单若在网络延迟下触发,可能滑点放大亏损,建议开 MT5 用策略测试器跑 EURUSD 的 M15 验证触发频率。 挂单部分另起 TrailingOrders 函数:取全部挂单后只留 ORDER_TYPE_BUY,按距现价的点数利润排序,找最远的那张买挂单。stop_level 用 StopLevel(Symbol(),2)*Point() 得出两倍最小止损距离,这是实盘里很容易被忽略的硬约束。

MQL5 / C++
   {
   class=class="str">"cmt">//--- If we trail at any profit or position profit in points exceeds the trailing start, modify StopLoss
   if(trailing_start==class="num">0 || buy.ProfitInPoints()>(class="type">int)trailing_start)
      trade.PositionModify(buy.Ticket(),sl,buy.TakeProfit());
   }
   }
   }
   }
 class=class="str">"cmt">//--- Select only Sell positions from the list
 CArrayObj* list_sell=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
 class=class="str">"cmt">//--- Sort the list by profit considering commission and swap
 list_sell.Sort(SORT_BY_ORDER_PROFIT_FULL);
 class=class="str">"cmt">//--- Get Sell position index with the maximum profit
 class="type">int index_sell=CSelect::FindOrderMax(list_sell,ORDER_PROP_PROFIT_FULL);
 if(index_sell>WRONG_VALUE)
  {
   COrder* sell=list_sell.At(index_sell);
   if(sell!=NULL)
     {
     class=class="str">"cmt">//--- Calculate the new StopLoss
     class="type">class="kw">double sl=NormalizeDouble(tick.ask+trailing_stop,Digits());
     class=class="str">"cmt">//--- If the price and StopLevel based on it are below the new StopLoss(the distance by StopLevel is maintained)
     if(tick.ask+stop_level<sl)
       {
       class=class="str">"cmt">//--- If the new StopLoss level is below the trailing step based on the current StopLoss or a position has no StopLoss
       if(sell.StopLoss()-trailing_step>sl || sell.StopLoss()==class="num">0)
         {
         class=class="str">"cmt">//--- If we trail at any profit or position profit in points exceeds the trailing start value, modify StopLoss
         if(trailing_start==class="num">0 || sell.ProfitInPoints()>(class="type">int)trailing_start)
            trade.PositionModify(sell.Ticket(),sl,sell.TakeProfit());
         }
       }
     }
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Trailing the farthest pending orders                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void TrailingOrders(class="type">void)
  {
  class="type">MqlTick tick;
  if(!SymbolInfoTick(Symbol(),tick))
     class="kw">return;
  class="type">class="kw">double stop_level=StopLevel(Symbol(),class="num">2)*Point();
class=class="str">"cmt">//--- Get the list of all placed orders
  CArrayObj* list=engine.GetListMarketPendings();
class=class="str">"cmt">//--- Select only Buy orders from the list
  CArrayObj* list_buy=CSelect::ByOrderProperty(list,ORDER_PROP_DIRECTION,ORDER_TYPE_BUY,EQUAL);
  class=class="str">"cmt">//--- Sort the list by distance from the price in points(by profit in points)
  list_buy.Sort(SORT_BY_ORDER_PROFIT_PT);
  class=class="str">"cmt">//--- Get the index of the Buy order with the greatest distance
  class="type">int index_buy=CSelect::FindOrderMax(list_buy,ORDER_PROP_PROFIT_PT);
  if(index_buy>WRONG_VALUE)
   {
   COrder* buy=list_buy.At(index_buy);
   if(buy!=NULL)
     {

挂单随价移动的改单逻辑

处理 BuyLimit 与 BuyStop 类挂单时,核心是根据实时卖一价(ask)把挂单价格往上或往下拖,同时维持止损止盈的相对距离。下面这段实现把「低于现价的限价单抬升」和「高于现价的止损单下移」分两条分支写清。 先说 BuyLimit 分支:新挂单价 = ask - trailing_stop,并用 NormalizeDouble 按品种精度收敛。SL/TP 若原单已设,则按「新价 - (原开价 - 原SL)」等比例平移,否则置 0。 改单触发有两个门槛:计算价必须仍在 ask - stop_level 之上(守住 broker 最小挂单距离),且要比原挂单价高出 trailing_step 才动手调单。满足后调用 trade.OrderModify 带新价、SL、TP 及原有时效参数。 BuyStop 分支镜像操作:新价 = ask + trailing_stop,同样校验 ask + stop_level 边界,并要求新价低于原挂单价 trailing_step 才修改;注意 StopLimit 单会把 PriceStopLimit 同步设为 price - distance_stoplimit*Point()。外汇与贵金属杠杆高,stop_level、trailing_step 设错可能频繁触发改单被拒,建议在 MT5 策略测试器用真实点差回放验证。

MQL5 / C++
   class=class="str">"cmt">//--- If the order is below the price(BuyLimit) and it should be "elevated" following the price
   if(buy.TypeOrder()==ORDER_TYPE_BUY_LIMIT)
     {
      class=class="str">"cmt">//--- Calculate the new order price and stop levels based on it
      class="type">class="kw">double price=NormalizeDouble(tick.ask-trailing_stop,Digits());
      class="type">class="kw">double sl=(buy.StopLoss()>class="num">0 ? NormalizeDouble(price-(buy.PriceOpen()-buy.StopLoss()),Digits()) : class="num">0);
      class="type">class="kw">double tp=(buy.TakeProfit()>class="num">0 ? NormalizeDouble(price+(buy.TakeProfit()-buy.PriceOpen()),Digits()) : class="num">0);
      class=class="str">"cmt">//--- If the calculated price is below the StopLevel distance based on Ask order price(the distance by StopLevel is maintained)
      if(price<tick.ask-stop_level)
        {
         class=class="str">"cmt">//--- If the calculated price exceeds the trailing step based on the order placement price, modify the order price
         if(price>buy.PriceOpen()+trailing_step)
           {
            trade.OrderModify(buy.Ticket(),price,sl,tp,trade.RequestTypeTime(),trade.RequestExpiration(),buy.PriceStopLimit());
           }
        }
     }
   class=class="str">"cmt">//--- If the order exceeds the price(BuyStop and BuyStopLimit), and it should be "decreased" following the price
   else
     {
      class=class="str">"cmt">//--- Calculate the new order price and stop levels based on it
      class="type">class="kw">double price=NormalizeDouble(tick.ask+trailing_stop,Digits());
      class="type">class="kw">double sl=(buy.StopLoss()>class="num">0 ? NormalizeDouble(price-(buy.PriceOpen()-buy.StopLoss()),Digits()) : class="num">0);
      class="type">class="kw">double tp=(buy.TakeProfit()>class="num">0 ? NormalizeDouble(price+(buy.TakeProfit()-buy.PriceOpen()),Digits()) : class="num">0);
      class=class="str">"cmt">//--- If the calculated price exceeds the StopLevel based on Ask order price(the distance by StopLevel is maintained)
      if(price>tick.ask+stop_level)
        {
         class=class="str">"cmt">//--- If the calculated price is lower than the trailing step based on order price, modify the order price
         if(price<buy.PriceOpen()-trailing_step)
           {
            trade.OrderModify(buy.Ticket(),price,sl,tp,trade.RequestTypeTime(),trade.RequestExpiration(),(buy.PriceStopLimit()>class="num">0 ? price-distance_stoplimit*Point() : class="num">0));
           }
        }
     }
class=class="str">"cmt">//--- Select only Sell order from the list
   CArrayObj* list_sell=CSelect::ByOrderProperty(list,ORDER_PROP_DIRECTION,ORDER_TYPE_SELL,EQUAL);

◍ 卖单挂单的跟随式重定价逻辑

在挂单网格管理里,卖单的追踪改单要分两类看:SellLimit 挂在现价上方,价格下行时要往下跟;SellStop / SellStopLimit 在下方,价格上行时要往上提。代码先按浮盈点数排序,抓出距离最远的卖单单独处理,避免一窝蜂改单拖慢 MT5 执行。 SellLimit 分支里,新价 = Bid + trailing_stop,再据此平移 SL/TP。关键护栏有两条:新价必须 > Bid + stop_level(守住 broker 的最小挂单距离),且新价 < 原挂单价 - trailing_step(确保不是碎步乱改)。两个条件同时满足才调 OrderModify。 下方卖单反过来,新价 = Bid - trailing_stop,约束变成新价 < Bid - stop_level 且新价 > 原挂单价 + trailing_step。外汇与贵金属杠杆高,stop_level 和 trailing_step 设太小会频繁触发修改请求,实盘前务必在策略测试器用真实点差跑一遍。

MQL5 / C++
  class=class="str">"cmt">//--- Sort the list by the distance from the price in points(by profit in points)
  list_sell.Sort(SORT_BY_ORDER_PROFIT_PT);
  class=class="str">"cmt">//--- Get the index of the Sell order having the greatest distance
  class="type">int index_sell=CSelect::FindOrderMax(list_sell,ORDER_PROP_PROFIT_PT);
  if(index_sell>WRONG_VALUE)
    {
      COrder* sell=list_sell.At(index_sell);
      if(sell!=NULL)
        {
         class=class="str">"cmt">//--- If the order exceeds the price(SellLimit), and it needs to be "decreased" following the price
         if(sell.TypeOrder()==ORDER_TYPE_SELL_LIMIT)
           {
            class=class="str">"cmt">//--- Calculate the new order price and stop levels based on it
            class="type">class="kw">double price=NormalizeDouble(tick.bid+trailing_stop,Digits());
            class="type">class="kw">double sl=(sell.StopLoss()>class="num">0 ? NormalizeDouble(price+(sell.StopLoss()-sell.PriceOpen()),Digits()) : class="num">0);
            class="type">class="kw">double tp=(sell.TakeProfit()>class="num">0 ? NormalizeDouble(price-(sell.PriceOpen()-sell.TakeProfit()),Digits()) : class="num">0);
            class=class="str">"cmt">//--- If the calculated price exceeds the StopLevel distance based on the Bid order price(the distance by StopLevel is maintained)
            if(price>tick.bid+stop_level)
              {
               class=class="str">"cmt">//--- If the calculated price is below the trailing step based on the order price, modify the order price
               if(price<sell.PriceOpen()-trailing_step)
                 {
                  trade.OrderModify(sell.Ticket(),price,sl,tp,trade.RequestTypeTime(),trade.RequestExpiration(),sell.PriceStopLimit());
                 }
              }
           }
         class=class="str">"cmt">//--- If the order is below the price(SellStop and SellStopLimit), and it should be "elevated" following the price
         else
           {
            class=class="str">"cmt">//--- Calculate the new order price and stop levels based on it
            class="type">class="kw">double price=NormalizeDouble(tick.bid-trailing_stop,Digits());
            class="type">class="kw">double sl=(sell.StopLoss()>class="num">0 ? NormalizeDouble(price+(sell.StopLoss()-sell.PriceOpen()),Digits()) : class="num">0);
            class="type">class="kw">double tp=(sell.TakeProfit()>class="num">0 ? NormalizeDouble(price-(sell.PriceOpen()-sell.TakeProfit()),Digits()) : class="num">0);
            class=class="str">"cmt">//--- If the calculated price is below the StopLevel distance based on the Bid order price(the distance by StopLevel is maintained)
            if(price<tick.bid-stop_level)
              {
               class=class="str">"cmt">//--- If the calculated price exceeds the trailing step based on the order price, modify the order price
               if(price>sell.PriceOpen()+trailing_step)
                 {

「挂单改价与面板按钮的衔接逻辑」

上面这段截自一个 EA 的订单修改分支:当挂单存在止损位报价时,用 trade.OrderModify 把卖单的挂单价、SL、TP 一并刷新,并在原 StopLimit 价大于 0 时按 distance_stoplimit*Point() 重算触发价,否则填 0 撤掉 StopLimit 属性。 OnTick 里先判断是不是在策略测试器跑——是的话手动调 engine.OnTimer()PressButtonsControl(),再用静态变量 last_event 捕捉交易引擎事件变化;若 trailing_on 为真,则每跳执行 TrailingPositions()TrailingOrders() 做统一移损。 文件尾部的 ENUM_BUTTONS 定义了 20 个面板指令(宏 TOTAL_BUTT 写死为 20),从 BUTT_BUY 到 BUTT_TRAILING_ALL 覆盖了开仓、平仓、删挂单、抽利、设损设盈和全局移损;配合 SDataButt 结构存按钮名与显示文本,EA 靠这套枚举驱动图形面板。 实盘接这套逻辑时,外汇与贵金属杠杆品种跳空频繁,挂单改价失败可能直接漏单,建议在 MT5 策略测试器用 2018 年之后的 XAUUSD 1 分钟数据跑一遍,确认 OrderModify 返回错误码时的处理分支没有吞异常。

MQL5 / C++
trade.OrderModify(sell.Ticket(),price,sl,tp,trade.RequestTypeTime(),trade.RequestExpiration(),(sell.PriceStopLimit()>class="num">0 ? price+distance_stoplimit*Point() : class="num">0));
}
}
}
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//--- Initialize the last trading event
   class="kw">static ENUM_TRADE_EVENT last_event=WRONG_VALUE;
class=class="str">"cmt">//--- If working in the tester
   if(MQLInfoInteger(MQL_TESTER))
     {
      engine.OnTimer();
      PressButtonsControl();
     }
class=class="str">"cmt">//--- If the last trading event changed
   if(engine.LastTradeEvent()!=last_event)
     {
      last_event=engine.LastTradeEvent();
     }
class=class="str">"cmt">//--- If the trailing flag is set
   if(trailing_on)
     {
      TrailingPositions();
      TrailingOrders();   
     }
  }

挂单与风控参数的输入声明

这段输入块定义了 EA 运行前必须给定的手数、止损止盈、挂单距离与按钮偏移等外部参数,默认 Lots=0.1、StopLoss=50 点、TakeProfit=50 点、Pending 距离=50 点、StopLimit 距离=50 点、滑点=0 点。 回测提款字段 InpWithdrawal 默认 10,代码里用三元式兜底:若小于 0.1 则强制取 0.1,避免测试器里提款参数误填导致资金逻辑异常。 全局变量区把上述输入映射到运行时变量,例如 trailing_stop 来自 InpTrailingStop(默认 50 点)、trailing_step 来自 InpTrailingStep(默认 20 点)、stoploss_to_modify 来自 InpStopLossModify(默认 20 点)。开 MT5 把这组 input 粘进 ea 头部,改 InpTrailingStep 到 10 能观察跟踪止损触发频率的明显变化。外汇与贵金属杠杆高,参数误设可能瞬间放大回撤,须先在策略测试器验证。

MQL5 / C++
class="kw">input class="type">class="kw">double   InpLots            =  class="num">0.1;   class=class="str">"cmt">// Lots
class="kw">input class="type">uint     InpStopLoss        =  class="num">50;    class=class="str">"cmt">// StopLoss in points
class="kw">input class="type">uint     InpTakeProfit      =  class="num">50;    class=class="str">"cmt">// TakeProfit in points
class="kw">input class="type">uint     InpDistance        =  class="num">50;    class=class="str">"cmt">// Pending orders distance(points)
class="kw">input class="type">uint     InpDistanceSL      =  class="num">50;    class=class="str">"cmt">// StopLimit orders distance(points)
class="kw">input class="type">uint     InpSlippage        =  class="num">0;     class=class="str">"cmt">// Slippage in points
class="kw">input class="type">class="kw">double   InpWithdrawal      =  class="num">10;    class=class="str">"cmt">// Withdrawal funds(in tester)
class="kw">input class="type">uint     InpButtShiftX      =  class="num">40;    class=class="str">"cmt">// Buttons X shift 
class="kw">input class="type">uint     InpButtShiftY      =  class="num">10;    class=class="str">"cmt">// Buttons Y shift 
class="kw">input class="type">uint     InpTrailingStop    =  class="num">50;    class=class="str">"cmt">// Trailing Stop(points)
class="kw">input class="type">uint     InpTrailingStep    =  class="num">20;    class=class="str">"cmt">// Trailing Step(points)
class="kw">input class="type">uint     InpTrailingStart   =  class="num">0;     class=class="str">"cmt">// Trailing Start(points)
class="kw">input class="type">uint     InpStopLossModify  =  class="num">20;    class=class="str">"cmt">// StopLoss for modification(points)
class="kw">input class="type">uint     InpTakeProfitModify=  class="num">60;    class=class="str">"cmt">// TakeProfit for modification(points)
class=class="str">"cmt">//--- global variables
CEngine        engine;
CTrade         trade;
SDataButt      butt_data[TOTAL_BUTT];
class="type">class="kw">string         prefix;
class="type">class="kw">double         lot;
class="type">class="kw">double         withdrawal=(InpWithdrawal<class="num">0.1 ? class="num">0.1 : InpWithdrawal);
class="type">class="kw">ulong          magic_number;
class="type">uint           stoploss;
class="type">uint           takeprofit;
class="type">uint           distance_pending;
class="type">uint           distance_stoplimit;
class="type">uint           slippage;
class="type">bool           trailing_on;
class="type">class="kw">double         trailing_stop;
class="type">class="kw">double         trailing_step;
class="type">uint           trailing_start;
class="type">uint           stoploss_to_modify;
class="type">uint           takeprofit_to_modify;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |

◍ EA 初始化与Tick循环的骨架

这段 MT5 专家顾问代码把生命周期拆得很清楚:OnInit 负责清场、写全局变量、建按钮、配交易参数;OnDeinit 只做删除对象与清 Comment;OnTick 则轮询交易事件与移动止损。 OnInit 里先调用 IsPresentObects(prefix) 检查前缀对象,有残留就用 ObjectsDeleteAll(0,prefix) 全清,避免上一次加载的按钮或线条干扰。随后 prefix 被设为程序名加下划线,循环把 22 与 25 行枚举映射成按钮名和文本(TOTAL_BUTT 控制数量)。 手数处理值得注意:lot=NormalizeLot(Symbol(),fmax(InpLots,MinimumLots(Symbol())*2.0)),也就是用户输入手数和最低手数两倍取大后再规范化,实盘外汇或贵金属杠杆高,错设手数可能瞬间爆仓,务必先在策略测试器跑一遍。 OnTick 中若 MQLInfoInteger(MQL_TESTER) 为真,才跑 engine.OnTimer() 与 PressButtonsControl(),说明回测时按钮逻辑靠定时器模拟;实盘则依赖事件。trailing_on 为真才调用 TrailingPositions 与 TrailingOrders,移动止损开关由最后一个按钮状态控制。 把下面代码直接贴进 MT5 的 EA 源码相应位置,编译后挂 EURUSD 的 M1 图表,观察 OnInit 是否清掉旧对象、OnTick 是否随 trailing_on 变化触发追踪,就能验证这套骨架是否和你现有库兼容。

MQL5 / C++
class="type">int OnInit()
  {
class=class="str">"cmt">//--- Calling the function displays the list of enumeration constants in the journal, 
class=class="str">"cmt">//--- (the list is set in the strings class="num">22 and class="num">25 of the DELib.mqh file) for checking the constants validity
   class=class="str">"cmt">//EnumNumbersTest();
class=class="str">"cmt">//--- check for undeleted objects
   if(IsPresentObects(prefix))
      ObjectsDeleteAll(class="num">0,prefix);
class=class="str">"cmt">//--- set global variables
   prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_";
   for(class="type">int i=class="num">0;i<TOTAL_BUTT;i++)
     {
      butt_data[i].name=prefix+EnumToString((ENUM_BUTTONS)i);
      butt_data[i].text=EnumToButtText((ENUM_BUTTONS)i);
     }
   lot=NormalizeLot(Symbol(),fmax(InpLots,MinimumLots(Symbol())*class="num">2.0));
   magic_number=InpMagic;
   stoploss=InpStopLoss;
   takeprofit=InpTakeProfit;
   distance_pending=InpDistance;
   distance_stoplimit=InpDistanceSL;
   slippage=InpSlippage;
   trailing_stop=InpTrailingStop*Point();
   trailing_step=InpTrailingStep*Point();
   trailing_start=InpTrailingStart;
   stoploss_to_modify=InpStopLossModify;
   takeprofit_to_modify=InpTakeProfitModify;
class=class="str">"cmt">//--- create buttons
   if(!CreateButtons(InpButtShiftX,InpButtShiftY))
      class="kw">return INIT_FAILED;
class=class="str">"cmt">//--- set button trailing
   ButtonState(butt_data[TOTAL_BUTT-class="num">1].name,trailing_on);
class=class="str">"cmt">//--- setting trade parameters
   trade.SetDeviationInPoints(slippage);
   trade.SetExpertMagicNumber(magic_number);
   trade.SetTypeFillingBySymbol(Symbol());
   trade.SetMarginMode();
   trade.LogLevel(LOG_LEVEL_NO);
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//--- class="kw">delete objects
   ObjectsDeleteAll(class="num">0,prefix);
   Comment("");
  }
class="type">void OnTick()
  {
class=class="str">"cmt">//--- Initialize the last trading event
   class="kw">static ENUM_TRADE_EVENT last_event=WRONG_VALUE;
class=class="str">"cmt">//--- If working in the tester
   if(MQLInfoInteger(MQL_TESTER))
     {
      engine.OnTimer();
      PressButtonsControl();
     }
class=class="str">"cmt">//--- If the last trading event changed
   if(engine.LastTradeEvent()!=last_event)
     {
      last_event=engine.LastTradeEvent();
     }
class=class="str">"cmt">//--- If the trailing flag is set
   if(trailing_on)
     {
      TrailingPositions();
      TrailingOrders();
     }
   }

「定时器与图表事件的分流处理」

EA 在实盘与回测环境的行为必须分开,否则回测里乱触发 UI 事件会污染结果。OnTimer 里先用 MQLInfoInteger(MQL_TESTER) 判断:非回测才调用 engine.OnTimer(),回测环境直接跳过定时器逻辑。 图表交互走 OnChartEvent。当 id 为 CHARTEVENT_OBJECT_CLICK 且对象名含 'BUTT_' 前缀时,转交 PressButtonEvents 处理按钮点击;id 大于等于 CHARTEVENT_CUSTOM 时,把自定义交易事件枚举名、lparam、dparam(按当前品种 Digits() 精度格式化)打印出来,便于排查事件链路。 IsPresentObects 用 ObjectsTotal(0)-1 倒序遍历图表对象,只要某个对象名包含指定前缀就返回 true,用来确认带前缀的控件是否已存在。PressButtonsControl 则正序扫一遍所有对象,对前缀为 prefix+'BUTT_' 的按钮统一跑一次 PressButtonEvents,相当于每帧同步按钮状态。 开 MT5 把这段贴进 EA,在 EURUSD 的 M15 上挂一个带 BUTT_ 前缀的按钮对象,点一下能在专家日志看到点击事件;切到策略测试器跑同代码,OnTimer 不会执行,验证环境分流是否生效。外汇与贵金属杠杆高,事件驱动逻辑未经实盘观测前勿直接用于资金账户。

MQL5 / C++
class="type">void OnTimer()
  {
   if(!MQLInfoInteger(MQL_TESTER))
       engine.OnTimer();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ChartEvent function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(class="kw">const class="type">int id,
                  class="kw">const class="type">long &lparam,
                  class="kw">const class="type">class="kw">double &dparam,
                  class="kw">const class="type">class="kw">string &sparam)
  {
   if(MQLInfoInteger(MQL_TESTER))
      class="kw">return;
   if(id==CHARTEVENT_OBJECT_CLICK && StringFind(sparam,"BUTT_")>class="num">0)
     {
      PressButtonEvents(sparam);
     }
   if(id>=CHARTEVENT_CUSTOM)
     {
      class="type">class="kw">ushort event=class="type">class="kw">ushort(id-CHARTEVENT_CUSTOM);
      Print(DFUN,"id=",id,", event=",EnumToString((ENUM_TRADE_EVENT)event),", lparam=",lparam,", dparam=",DoubleToString(dparam,Digits()),", sparam=",sparam);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the flag of a prefixed object presence                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool IsPresentObects(class="kw">const class="type">class="kw">string object_prefix)
  {
   for(class="type">int i=ObjectsTotal(class="num">0)-class="num">1;i>=class="num">0;i--)
      if(StringFind(ObjectName(class="num">0,i,class="num">0),object_prefix)>WRONG_VALUE)
         class="kw">return true;
   class="kw">return class="kw">false;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Tracking the buttons&class="macro">#x27; status                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void PressButtonsControl(class="type">void)
  {
   class="type">int total=ObjectsTotal(class="num">0);
   for(class="type">int i=class="num">0;i<total;i++)
     {
      class="type">class="kw">string obj_name=ObjectName(class="num">0,i);
      if(StringFind(obj_name,prefix+"BUTT_")<class="num">0)
         class="kw">continue;
      PressButtonEvents(obj_name);
     }
  }

按钮面板的布局与创建逻辑

在 MT5 面板类工具里,按钮排布最忌硬编码坐标。下面这套 CreateButtons 用循环 + 偏移量自动算位置,默认按钮高 18 像素、宽 84 像素、间距 2 像素,整体靠 shift_x=30、shift_y=0 做初始偏移。 循环里有个细节:当 i==7 时 x 额外加 w+2,等于在第 8 个按钮处强行换行错位;而 i==TOTAL_BUTT-6 时 x 重置回 cx,把最后 6 个宽按钮(宽度为 w*2+2)拉回左列起始位。颜色按索引分三档——前 4 个绿、7~10 红、其余蓝,视觉上把功能分组。 ButtonCreate 才是真正落对象的底层函数。它先 ObjectFind 查重,没有才 ObjectCreate 建 OBJ_BUTTON;随后把 SELECTABLE 关掉、HIDDEN 打开,避免按钮被鼠标误选或显示在对象列表里刷屏。 想验证就开 MT5 新建 EA,把 TOTAL_BUTT 和 butt_data 补上,直接调 CreateButtons() 看图表左上角是否按上述规则出两列按钮。外汇与贵金属波动剧烈,这类面板只做交互辅助,任何信号都不构成入场保证。

MQL5 / C++
class="type">bool CreateButtons(class="kw">const class="type">int shift_x=class="num">30,class="kw">const class="type">int shift_y=class="num">0)
  {
   class="type">int h=class="num">18,w=class="num">84,offset=class="num">2;
   class="type">int cx=offset+shift_x,cy=offset+shift_y+(h+class="num">1)*(TOTAL_BUTT/class="num">2)+class="num">3*h+class="num">1;
   class="type">int x=cx,y=cy;
   class="type">int shift=class="num">0;
   for(class="type">int i=class="num">0;i<TOTAL_BUTT;i++)
     {
      x=x+(i==class="num">7 ? w+class="num">2 : class="num">0);
      if(i==TOTAL_BUTT-class="num">6) x=cx;
      y=(cy-(i-(i>class="num">6 ? class="num">7 : class="num">0))*(h+class="num">1));
      if(!ButtonCreate(butt_data[i].name,x,y,(i<TOTAL_BUTT-class="num">6 ? w : w*class="num">2+class="num">2),h,butt_data[i].text,(i<class="num">4 ? clrGreen : i>class="num">6 && i<class="num">11 ? clrRed : clrBlue)))
        {
         Alert(TextByLanguage("Не удалось создать кнопку "","Could not create button "));
         class="kw">return class="kw">false;
        }
     }
   ChartRedraw(class="num">0);
   class="kw">return true;
  }

class="type">bool ButtonCreate(class="kw">const class="type">class="kw">string name,class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">int w,class="kw">const class="type">int h,class="kw">const class="type">class="kw">string text,class="kw">const class="type">class="kw">color clr,class="kw">const class="type">class="kw">string font="Calibri",class="kw">const class="type">int font_size=class="num">8)
  {
   if(ObjectFind(class="num">0,name)<class="num">0)
     {
      if(!ObjectCreate(class="num">0,name,OBJ_BUTTON,class="num">0,class="num">0,class="num">0))
        {
         Print(DFUN,TextByLanguage("не удалось создать кнопку! Код ошибки=","Could not create button! Error code="),GetLastError());
         class="kw">return class="kw">false;
        }
      ObjectSetInteger(class="num">0,name,OBJPROP_SELECTABLE,class="kw">false);
      ObjectSetInteger(class="num">0,name,OBJPROP_HIDDEN,true);
      ObjectSetInteger(class="num">0,name,OBJPROP_XDISTANCE,x);
      ObjectSetInteger(class="num">0,name,OBJPROP_YDISTANCE,y);

◍ 给按钮挂上状态与文字的收尾逻辑

上面这段是 GUI 按钮封装函数的末尾部分,负责把尺寸、锚点、字体、文字和边框一次性写进对象属性,然后返回创建结果。 ObjectSetInteger(0,name,OBJPROP_XSIZE,w) 设定按钮像素宽,OBJPROP_YSIZE 管高,CORNER_LEFT_LOWER 配合 ANCHOR_LEFT_LOWER 把锚点锁在图表左下角,做悬浮面板时不会随滚动跑位。 ButtonState 有两个重载:只读版本直接把 OBJPROP_STATE 转成 bool 返回,写版本除了设状态,还判断如果是最后一个按钮(butt_data[TOTAL_BUTT-1].name)就切换背景色——按下时 C'220,255,240' 浅绿,松开 C'240,240,240' 灰白,肉眼能分出交易面板当前激活项。 EnumToButtText 把枚举名砍掉前 5 个字符再转小写,随后把 set_take_profit 之类替换成「Set TakeProfit」这种可读标签。外汇与贵金属波动剧烈,这类面板只降低操作失误率,不预示任何方向。

MQL5 / C++
ObjectSetInteger(class="num">0,name,OBJPROP_XSIZE,w);
ObjectSetInteger(class="num">0,name,OBJPROP_YSIZE,h);
ObjectSetInteger(class="num">0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER);
ObjectSetInteger(class="num">0,name,OBJPROP_ANCHOR,ANCHOR_LEFT_LOWER);
ObjectSetInteger(class="num">0,name,OBJPROP_FONTSIZE,font_size);
ObjectSetString(class="num">0,name,OBJPROP_FONT,font);
ObjectSetString(class="num">0,name,OBJPROP_TEXT,text);
ObjectSetInteger(class="num">0,name,OBJPROP_COLOR,clr);
ObjectSetString(class="num">0,name,OBJPROP_TOOLTIP,"\n");
ObjectSetInteger(class="num">0,name,OBJPROP_BORDER_COLOR,clrGray);
class="kw">return true;
   }
 class="kw">return class="kw">false;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the button status                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool ButtonState(class="kw">const class="type">class="kw">string name)
  {
   class="kw">return (class="type">bool)ObjectGetInteger(class="num">0,name,OBJPROP_STATE);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the button status                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ButtonState(class="kw">const class="type">class="kw">string name,class="kw">const class="type">bool state)
  {
   ObjectSetInteger(class="num">0,name,OBJPROP_STATE,state);
   if(name==butt_data[TOTAL_BUTT-class="num">1].name)
     {
      if(state)
         ObjectSetInteger(class="num">0,name,OBJPROP_BGCOLOR,C&class="macro">#x27;class="num">220,class="num">255,class="num">240&class="macro">#x27;);
      else
         ObjectSetInteger(class="num">0,name,OBJPROP_BGCOLOR,C&class="macro">#x27;class="num">240,class="num">240,class="num">240&class="macro">#x27;);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Transform enumeration into the button text                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string EnumToButtText(class="kw">const ENUM_BUTTONS member)
  {
   class="type">class="kw">string txt=StringSubstr(EnumToString(member),class="num">5);
   StringToLower(txt);
   StringReplace(txt,"set_take_profit","Set TakeProfit");
   StringReplace(txt,"set_stop_loss","Set StopLoss");
   StringReplace(txt,"trailing_all","Trailing All");
   StringReplace(txt,"buy","Buy");
   StringReplace(txt,"sell","Sell");

「按钮事件怎么映射到下单动作」

面板上的每个按钮名字都带统一前缀,处理函数先截掉前缀拿到纯标识,再用 ButtonState 判断当前是被按下还是弹起,避免重复触发。 以 BUY 系按钮为例:按下 BUTT_BUY 时,先调用 CorrectStopLoss / CorrectTakeProfit 按品种 StopLevel 算出合规的 sl、tp,再用 trade.Buy(NormalizeLot(...), Symbol(), 0, sl, tp) 发市价多单;BUTT_BUY_LIMIT 则先用 CorrectPricePending 算挂单位置 price_set,再围绕该位置算 sl/tp 后 trade.BuyLimit 挂单。 BuyStop 逻辑同构:CorrectPricePending 取突破位,sl/tp 相对突破位校验 StopLevel,最后 trade.BuyStop 发出。外汇与贵金属杠杆高,StopLevel 不足会导致下单被拒,实盘前务必在 MT5 策略测试器用真实点差验证这几处校验函数返回值。

MQL5 / C++
class="type">class="kw">string button=StringSubstr(button_name,StringLen(prefix));
if(ButtonState(button_name))
  {
  if(button==EnumToString(BUTT_BUY))
    {
    class="type">class="kw">double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY,class="num">0,stoploss);
    class="type">class="kw">double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY,class="num">0,takeprofit);
    trade.Buy(NormalizeLot(Symbol(),lot),Symbol(),class="num">0,sl,tp);
    }
  else if(button==EnumToString(BUTT_BUY_LIMIT))
    {
    class="type">class="kw">double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_LIMIT,distance_pending);
    class="type">class="kw">double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY_LIMIT,price_set,stoploss);
    class="type">class="kw">double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY_LIMIT,price_set,takeprofit);
    trade.BuyLimit(lot,price_set,Symbol(),sl,tp);
    }
  else if(button==EnumToString(BUTT_BUY_STOP))
    {
    class="type">class="kw">double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_STOP,distance_pending);
    class="type">class="kw">double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY_STOP,price_set,stoploss);
    class="type">class="kw">double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY_STOP,price_set,takeprofit);
    trade.BuyStop(lot,price_set,Symbol(),sl,tp);
    }

按钮触发后的挂单与市价单分支

这段逻辑承接前面按钮枚举的判断,按不同交易指令走各自的下单通道。核心是先调用 CorrectPricePending / CorrectStopLoss / CorrectTakeProfit 三个校验函数,把价格夹在经纪商 StopLevel 允许范围内,再交给 CTrade 类执行。 以 BuyStopLimit 为例,先算挂单触发价 price_set_stop(相对当前价 distance_pending 点),再算实际限价单入场价 price_set_limit(相对触发价 distance_stoplimit 点)。两者都过 CorrectPricePending 后,SL/TP 以 price_set_limit 为基准算,最后用 trade.OrderOpen 一次性提交。 市价 Sell 更简单:SL/TP 以市价 0 为基准直接算,trade.Sell(lot,Symbol(),0,sl,tp) 不填挂单价。外汇与贵金属杠杆高,StopLevel 不足会下单失败,这类校验函数能省掉大量 '成交被拒' 的返工。 打开 MT5 把这段塞进 OnChartEvent 的按钮回调里,改 BUTT_ 前缀枚举名匹配你面板的 object id,就能实测各品种在不同波动下的下单拒绝率。

MQL5 / C++
class="type">class="kw">double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY_STOP,price_set,takeprofit);
class=class="str">"cmt">//--- Set BuyStop order
trade.BuyStop(lot,price_set,Symbol(),sl,tp);
}
class=class="str">"cmt">//--- If the BUTT_BUY_STOP_LIMIT button is pressed: Set BuyStopLimit
else if(button==EnumToString(BUTT_BUY_STOP_LIMIT))
  {
  class=class="str">"cmt">//--- Get the correct BuyStop order placement price relative to StopLevel
  class="type">class="kw">double price_set_stop=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_STOP,distance_pending);
  class=class="str">"cmt">//--- Calculate BuyLimit order price relative to BuyStop level considering StopLevel
  class="type">class="kw">double price_set_limit=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_LIMIT,distance_stoplimit,price_set_stop);
  class=class="str">"cmt">//--- Get correct StopLoss and TakeProfit prices relative to the order placement level considering StopLevel
  class="type">class="kw">double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY_STOP,price_set_limit,stoploss);
  class="type">class="kw">double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY_STOP,price_set_limit,takeprofit);
  class=class="str">"cmt">//--- Set BuyStopLimit order
  trade.OrderOpen(Symbol(),ORDER_TYPE_BUY_STOP_LIMIT,lot,price_set_limit,price_set_stop,sl,tp);
  }
class=class="str">"cmt">//--- If the BUTT_SELL button is pressed: Open Sell position
else if(button==EnumToString(BUTT_SELL))
  {
  class=class="str">"cmt">//--- Get the correct StopLoss and TakeProfit prices relative to StopLevel
  class="type">class="kw">double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL,class="num">0,stoploss);
  class="type">class="kw">double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL,class="num">0,takeprofit);
  class=class="str">"cmt">//--- Open Sell position
  trade.Sell(lot,Symbol(),class="num">0,sl,tp);
  }
class=class="str">"cmt">//--- If the BUTT_SELL_LIMIT button is pressed: Set SellLimit
else if(button==EnumToString(BUTT_SELL_LIMIT))
  {
  class=class="str">"cmt">//--- Get correct order placement relative to StopLevel
  class="type">class="kw">double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_LIMIT,distance_pending);
  class=class="str">"cmt">//--- Get correct StopLoss and TakeProfit prices relative to the order placement level considering StopLevel
  class="type">class="kw">double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL_LIMIT,price_set,stoploss);
  class="type">class="kw">double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL_LIMIT,price_set,takeprofit);
  class=class="str">"cmt">//--- Set SellLimit order
  trade.SellLimit(lot,price_set,Symbol(),sl,tp);
  }
class=class="str">"cmt">//--- If the BUTT_SELL_STOP button is pressed: Set SellStop
else if(button==EnumToString(BUTT_SELL_STOP))
  {
  class=class="str">"cmt">//--- Get correct order placement relative to StopLevel

◍ 挂单与平仓按钮的底层派发逻辑

这段逻辑展示了 MT5 面板里几个交易按钮被触发后的实际派发过程,核心都绕不开 StopLevel 校验。SellStop 与 SellStopLimit 的下单价格、止损止盈价,全部先经过 CorrectPricePending / CorrectStopLoss / CorrectTakeProfit 做合规修正,避免服务器拒单。 SellStopLimit 比较特殊:它先算 SellStop 挂单价 price_set_stop,再以此为基础叠加 distance_stoplimit 算出卖限价 price_set_limit,最后用 limit 价去算 sl/tp 并调用 trade.OrderOpen()。如果你在 XAUUSD 上把 distance_stoplimit 设得小于当前 StopLevel,price_set_limit 会被自动外推,订单可能落在意料之外的位置。 平仓分支不按仓位大小挑,而是用 engine.GetListMarketPosition() 取出所有持仓,按 POSITION_TYPE_BUY 过滤后做 Sort(SORT_BY_ORDER_PROFIT_FULL),再 FindOrderMax 定位浮盈最高的多单 ticket 直接平仓。外汇与贵金属杠杆高,这种一键平最大利润单的逻辑在快速回撤中可能吃掉趋势利润,实盘前务必在策略测试器里跑一遍。 下面这段是原文派发代码的核心片段,注意 sl/tp 计算在两种挂单里都挂在 price_set / price_set_limit 上,而不是市价:

MQL5 / C++
class="type">class="kw">double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_STOP,distance_pending);
class=class="str">"cmt">//--- Get correct StopLoss and TakeProfit prices relative to the order placement level considering StopLevel
class="type">class="kw">double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL_STOP,price_set,stoploss);
class="type">class="kw">double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL_STOP,price_set,takeprofit);
class=class="str">"cmt">//--- Set SellStop order
trade.SellStop(lot,price_set,Symbol(),sl,tp);
}
class=class="str">"cmt">//--- If the BUTT_SELL_STOP_LIMIT button is pressed: Set SellStopLimit
else if(button==EnumToString(BUTT_SELL_STOP_LIMIT))
  {
  class=class="str">"cmt">//--- Get the correct SellStop order price relative to StopLevel
  class="type">class="kw">double price_set_stop=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_STOP,distance_pending);
  class=class="str">"cmt">//--- Calculate SellLimit order price relative to SellStop level considering StopLevel
  class="type">class="kw">double price_set_limit=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_LIMIT,distance_stoplimit,price_set_stop);
  class=class="str">"cmt">//--- Get correct StopLoss and TakeProfit prices relative to the order placement level considering StopLevel
  class="type">class="kw">double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL_STOP,price_set_limit,stoploss);
  class="type">class="kw">double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL_STOP,price_set_limit,takeprofit);
  class=class="str">"cmt">//--- Set SellStopLimit order
  trade.OrderOpen(Symbol(),ORDER_TYPE_SELL_STOP_LIMIT,lot,price_set_limit,price_set_stop,sl,tp);
  }
class=class="str">"cmt">//--- If the BUTT_CLOSE_BUY button is pressed: Close Buy with the maximum profit
else if(button==EnumToString(BUTT_CLOSE_BUY))
  {
  class=class="str">"cmt">//--- Get the list of all open positions
  CArrayObj* list=engine.GetListMarketPosition();
  class=class="str">"cmt">//--- Select only Buy positions from the list
  list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL);
  class=class="str">"cmt">//--- Sort the list by profit considering commission and swap
  list.Sort(SORT_BY_ORDER_PROFIT_FULL);
  class=class="str">"cmt">//--- Get the index of the Buy position with the maximum profit
  class="type">int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL);
  if(index>WRONG_VALUE)
    {
    COrder* position=list.At(index);
    if(position!=NULL)
      {
      class=class="str">"cmt">//--- Get the Buy position ticket and close the position by the ticket
      trade.PositionClose(position.Ticket());
      }
    }
  }
class=class="str">"cmt">//--- If the BUTT_CLOSE_BUY2 button is pressed: Close the half of the Buy with the maximum profit
else if(button==EnumToString(BUTT_CLOSE_BUY2))

「盈利单的半平与对冲闭环写法」

在 MT5 的 EA 面板逻辑里,锁定利润的常见做法是先捞出浮盈最大的同方向仓位,再砍掉一半头寸。下面这段从持仓列表里筛 Buy、按全成本利润排序、定位最大利润索引,是实盘减仓的底层操作。 对锁仓账户用 PositionClosePartial 按 ticket 平半仓;对净仓账户则直接反手 Sell 同等半量。两者都走 NormalizeLot 把手数规整到品种精度,避免 0.0123 这类拒单手数。外汇与贵金属杠杆高,半平只是降低暴露,不等于风险消失。 另一个按钮逻辑是「用最大盈利 Sell 去平最大盈利 Buy」。分别取 buy/sell 两个列表、各自排序取 max index,只有当两边索引都合法(大于 WRONG_VALUE)才继续,否则不动作。这种配对平仓适合震荡市里双向持仓的对冲账户,能回收流动性又不破坏净值结构。 代码里 SORT_BY_ORDER_PROFIT_FULL 把佣金和掉期算进利润,比只看浮动盈亏更贴近真实到账。开 MT5 把这段接进你自己的按钮事件,改 BUTT_CLOSE_BUY_BY_SELL 的枚举名就能直接测。

MQL5 / C++
{
 class=class="str">"cmt">//--- Get the list of all open positions
 CArrayObj* list=engine.GetListMarketPosition();
 class=class="str">"cmt">//--- Select only Buy positions from the list
 list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL);
 class=class="str">"cmt">//--- Sort the list by profit considering commission and swap
 list.Sort(SORT_BY_ORDER_PROFIT_FULL);
 class=class="str">"cmt">//--- Get the index of the Buy position with the maximum profit
 class="type">int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL);
 if(index>WRONG_VALUE)
   {
   COrder* position=list.At(index);
   if(position!=NULL)
     {
     class=class="str">"cmt">//--- Calculate the closed volume and close the half of the Buy position by the ticket
     if(engine.IsHedge())
       trade.PositionClosePartial(position.Ticket(),NormalizeLot(position.Symbol(),position.Volume()/class="num">2.0));
     else
       trade.Sell(NormalizeLot(position.Symbol(),position.Volume()/class="num">2.0));
     }
   }
}
class=class="str">"cmt">//--- If the BUTT_CLOSE_BUY_BY_SELL button is pressed: Close Buy with the maximum profit by the opposite Sell with the maximum profit
else if(button==EnumToString(BUTT_CLOSE_BUY_BY_SELL))
  {
  class=class="str">"cmt">//--- Get the list of all open positions
  CArrayObj* list_buy=engine.GetListMarketPosition();
  class=class="str">"cmt">//--- Select only Buy positions from the list
  list_buy=CSelect::ByOrderProperty(list_buy,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL);
  class=class="str">"cmt">//--- Sort the list by profit considering commission and swap
  list_buy.Sort(SORT_BY_ORDER_PROFIT_FULL);
  class=class="str">"cmt">//--- Get the index of the Buy position with the maximum profit
  class="type">int index_buy=CSelect::FindOrderMax(list_buy,ORDER_PROP_PROFIT_FULL);
  class=class="str">"cmt">//--- Get the list of all open positions
  CArrayObj* list_sell=engine.GetListMarketPosition();
  class=class="str">"cmt">//--- Select only Sell positions from the list
  list_sell=CSelect::ByOrderProperty(list_sell,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
  class=class="str">"cmt">//--- Sort the list by profit considering commission and swap
  list_sell.Sort(SORT_BY_ORDER_PROFIT_FULL);
  class=class="str">"cmt">//--- Get the index of the Sell position with the maximum profit
  class="type">int index_sell=CSelect::FindOrderMax(list_sell,ORDER_PROP_PROFIT_FULL);
  if(index_buy>WRONG_VALUE && index_sell>WRONG_VALUE)
    {
    class=class="str">"cmt">//--- Select the Buy position with the maximum profit
    COrder* position_buy=list_buy.At(index_buy);
    class=class="str">"cmt">//--- Select the Sell position with the maximum profit
    COrder* position_sell=list_sell.At(index_sell);

用对冲单和利润排序收掉仓位

在面板脚本里,若同时抓到一张买仓和一张卖仓的指针,可直接用 PositionCloseBy 把两者互抵平仓,省去市价反向成交的滑点。 按下 BUTT_CLOSE_SELL 后,先取全部仓单列表,再用 ByOrderProperty 筛出 POSITION_TYPE_SELL,随后 Sort(SORT_BY_ORDER_PROFIT_FULL) 会把佣金与库存费都算进净利做降序排。FindOrderMax 拿到利润最大那张的索引,WRONG_VALUE 以上才有效,最后 trade.PositionClose 按 ticket 清掉整张。 BUTT_CLOSE_SELL2 的逻辑前半段一致,区别在成交环节:IsHedge 为真时调 PositionClosePartial,手数是 Volume()/2.0 再经 NormalizeLot 规整;净仓账户走 else 分支(原文截断)。外汇与贵金属杠杆高,部分平仓前先确认保证金占用,避免半仓后触发追加。

MQL5 / C++
if(position_buy!=NULL && position_sell!=NULL)
  {
   class=class="str">"cmt">//--- Close the Buy position by the opposite Sell one
   trade.PositionCloseBy(position_buy.Ticket(),position_sell.Ticket());
  }
 }
}
class=class="str">"cmt">//--- If the BUTT_CLOSE_SELL button is pressed: Close Sell with the maximum profit
else if(button==EnumToString(BUTT_CLOSE_SELL))
  {
   class=class="str">"cmt">//--- Get the list of all open positions
   CArrayObj* list=engine.GetListMarketPosition();
   class=class="str">"cmt">//--- Select only Sell positions from the list
   list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
   class=class="str">"cmt">//--- Sort the list by profit considering commission and swap
   list.Sort(SORT_BY_ORDER_PROFIT_FULL);
   class=class="str">"cmt">//--- Get the index of the Sell position with the maximum profit
   class="type">int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL);
   if(index>WRONG_VALUE)
     {
      COrder* position=list.At(index);
      if(position!=NULL)
        {
         class=class="str">"cmt">//--- Get the Sell position ticket and close the position by the ticket
         trade.PositionClose(position.Ticket());
        }
     }
   }
class=class="str">"cmt">//--- If the BUTT_CLOSE_SELL2 button is pressed: Close the half of the Sell with the maximum profit
else if(button==EnumToString(BUTT_CLOSE_SELL2))
  {
   class=class="str">"cmt">//--- Get the list of all open positions
   CArrayObj* list=engine.GetListMarketPosition();
   class=class="str">"cmt">//--- Select only Sell positions from the list
   list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
   class=class="str">"cmt">//--- Sort the list by profit considering commission and swap
   list.Sort(SORT_BY_ORDER_PROFIT_FULL);
   class=class="str">"cmt">//--- Get the index of the Sell position with the maximum profit
   class="type">int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL);
   if(index>WRONG_VALUE)
     {
      COrder* position=list.At(index);
      if(position!=NULL)
        {
         class=class="str">"cmt">//--- Calculate the closed volume and close the half of the Sell position by the ticket
         if(engine.IsHedge())
            trade.PositionClosePartial(position.Ticket(),NormalizeLot(position.Symbol(),position.Volume()/class="num">2.0));
         else

◍ 用反向单平掉最赚的卖仓

在面板按钮逻辑里,BUTT_CLOSE_SELL_BY_BUY 触发的是「用盈利最大的 Buy 去平掉盈利最大的 Sell」。思路是先抓全部持仓,再按类型筛出 Sell 和 Buy 两个独立列表,各自按含佣金掉期的总利润排一遍,取最大值索引。 这里有个细节:排序用 SORT_BY_ORDER_PROFIT_FULL 而不是表面利润,否则佣金和隔夜费会让「最赚」的判断失真。两个索引都必须大于 WRONG_VALUE 才执行 PositionCloseBy,避免空指针炸在实盘。 外汇和贵金属杠杆高,反向平仓虽能锁住净额,但点差扩大时滑点可能吃掉本来的浮盈,上 MT5 用策略测试器跑一遍 EURUSD 的 H1 数据验证更稳妥。 下面这段是按钮分支的核心代码,逐行看逻辑落在哪: // 按下用Buy平最大盈利Sell的按钮 else if(button==EnumToString(BUTT_CLOSE_SELL_BY_BUY)) { // 取所有持仓列表 CArrayObj* list_sell=engine.GetListMarketPosition(); // 只留SELL单 list_sell=CSelect::ByOrderProperty(list_sell,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL); // 按总利润(含费)排序 list_sell.Sort(SORT_BY_ORDER_PROFIT_FULL); // 找SELL里利润最大的索引 int index_sell=CSelect::FindOrderMax(list_sell,ORDER_PROP_PROFIT_FULL); // 再取所有持仓 CArrayObj* list_buy=engine.GetListMarketPosition(); // 只留BUY单 list_buy=CSelect::ByOrderProperty(list_buy,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL); // 按总利润排序 list_buy.Sort(SORT_BY_ORDER_PROFIT_FULL); // 找BUY里利润最大的索引 int index_buy=CSelect::FindOrderMax(list_buy,ORDER_PROP_PROFIT_FULL); if(index_sell>WRONG_VALUE && index_buy>WRONG_VALUE) { // 拿出最大盈利的SELL仓 COrder* position_sell=list_sell.At(index_sell); // 拿出最大盈利的BUY仓 COrder* position_buy=list_buy.At(index_buy); if(position_sell!=NULL && position_buy!=NULL) { // 用BUY仓 ticket 平掉SELL仓 trade.PositionCloseBy(position_sell.Ticket(),position_buy.Ticket()); } } }

MQL5 / C++
else if(button==EnumToString(BUTT_CLOSE_SELL_BY_BUY))
  {
   CArrayObj* list_sell=engine.GetListMarketPosition();
   list_sell=CSelect::ByOrderProperty(list_sell,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
   list_sell.Sort(SORT_BY_ORDER_PROFIT_FULL);
   class="type">int index_sell=CSelect::FindOrderMax(list_sell,ORDER_PROP_PROFIT_FULL);
   CArrayObj* list_buy=engine.GetListMarketPosition();
   list_buy=CSelect::ByOrderProperty(list_buy,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL);
   list_buy.Sort(SORT_BY_ORDER_PROFIT_FULL);
   class="type">int index_buy=CSelect::FindOrderMax(list_buy,ORDER_PROP_PROFIT_FULL);
   if(index_sell>WRONG_VALUE && index_buy>WRONG_VALUE)
     {
      COrder* position_sell=list_sell.At(index_sell);
      COrder* position_buy=list_buy.At(index_buy);
      if(position_sell!=NULL && position_buy!=NULL)
        {
         trade.PositionCloseBy(position_sell.Ticket(),position_buy.Ticket());
        }
     }
   }

「按钮触发后的持仓与挂单处理逻辑」

这段逻辑挂在 GUI 按钮事件后面,按不同按钮执行对应动作。平仓按钮会遍历持仓列表,按 ticket 逐个市价平仓;删除挂单按钮则先按开单时间排序,再从最早一笔开始删。 循环里第一道判空不能省:list.At(i) 返回 NULL 时直接 continue,否则后续调用 Ticket() 会抛空指针。外汇和贵金属杠杆高,批量平仓若在大滑点行情触发,实际成交价可能偏离按钮按下时的预期。 盈利提取按钮只在策略测试器内生效,用 TesterWithdrawal() 模拟出金,实盘不会真动资金。设置止损/止盈按钮只是转调 SetStopLoss() / SetTakeProfit(),具体点位由这两个函数内部决定。 每轮处理完 Sleep(100),即让出约 0.1 秒给主线程,避免按钮回调把 CPU 占满。想验证的话,在 MT5 策略测试器里接一套带这些按钮的 EA,点 BUTT_DELETE_PENDING 看挂单是否从最旧的那张开始消失。

MQL5 / C++
for(class="type">int i=class="num">0;i<total;i++)
  {
   COrder* position=list.At(i);
   if(position==NULL)
     class="kw">continue;
   class=class="str">"cmt">//--- close each position by its ticket
   trade.PositionClose(position.Ticket());
  }
 }
}
class=class="str">"cmt">//--- If the BUTT_DELETE_PENDING button is pressed: Remove the first pending order
else if(button==EnumToString(BUTT_DELETE_PENDING))
  {
   class=class="str">"cmt">//--- Get the list of all orders
   CArrayObj* list=engine.GetListMarketPendings();
   if(list!=NULL)
     {
      class=class="str">"cmt">//--- Sort the list by placement time
      list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC);
      class="type">int total=list.Total();
      class=class="str">"cmt">//--- In the loop from the position with the most amount of time
      for(class="type">int i=total-class="num">1;i>=class="num">0;i--)
        {
         COrder* order=list.At(i);
         if(order==NULL)
           class="kw">continue;
         class=class="str">"cmt">//--- class="kw">delete the order by its ticket
         trade.OrderDelete(order.Ticket());
        }
     }
   }
class=class="str">"cmt">//--- If the BUTT_PROFIT_WITHDRAWAL button is pressed: Withdraw funds from the account
if(button==EnumToString(BUTT_PROFIT_WITHDRAWAL))
  {
   class=class="str">"cmt">//--- If the program is launched in the tester
   if(MQLInfoInteger(MQL_TESTER))
     {
      class=class="str">"cmt">//--- Emulate funds withdrawal
      TesterWithdrawal(withdrawal);
     }
   }
class=class="str">"cmt">//--- If the BUTT_SET_STOP_LOSS button is pressed: Place StopLoss to all orders and positions where it is not present
if(button==EnumToString(BUTT_SET_STOP_LOSS))
  {
   SetStopLoss();
   }
class=class="str">"cmt">//--- If the BUTT_SET_TAKE_PROFIT button is pressed: Place TakeProfit to all orders and positions where it is not present
if(button==EnumToString(BUTT_SET_TAKE_PROFIT))
  {
   SetTakeProfit();
   }
class=class="str">"cmt">//--- Wait for class="num">1/class="num">10 of a second
Sleep(class="num">100);

跟单面板的止损批量接管逻辑

这套 EA 面板里,BUTT_TRAILING_ALL 按钮和普通按钮走的是两套状态机。普通按钮点完立刻 ButtonState(false) 弹起,只有全量追踪按钮被按下时才会保持高亮并把 trailing_on 置真,随后 ChartRedraw() 强制重绘让界面状态立刻可见。 当该追踪按钮处于非激活分支且命中 BUTT_TRAILING_ALL 时,代码会把按钮颜色复位为未激活、trailing_on 改回 false 并再次重绘——这意味着追踪开关是单点互斥的,不会和别的按钮状态纠缠。 SetStopLoss() 才是真正动账户的地方:先用 stoploss_to_modify==0 提前返回,避免零值把止损设成市价;再通过 engine.GetListMarketPosition() 取持仓列表,用 CSelect::ByOrderProperty 按 ORDER_PROP_SL 等于 0 筛出‘无止损’项。 持仓循环里 CorrectStopLoss() 按品种与方向算合规止损位,trade.PositionModify() 只改 SL、保留原 TP;挂单分支同理,但 OrderModify() 需把开仓价、SL、TP、时间类型、过期、止损限价全部原样传回,漏掉 PriceStopLimit 会让限价停损单报错。外汇与贵金属杠杆高,批量改 SL 若算错点值可能瞬间放大回撤,上 MT5 用策略测试器跑一遍无止损持仓样本最稳妥。

MQL5 / C++
  class=class="str">"cmt">//--- "Unpress" the button(if this is not a trailing button)
  if(button!=EnumToString(BUTT_TRAILING_ALL))
     ButtonState(button_name,class="kw">false);
  class=class="str">"cmt">//--- If the BUTT_TRAILING_ALL button is pressed
  else
    {
    class=class="str">"cmt">//--- Set the class="type">class="kw">color of the active button
    ButtonState(button_name,true);
    trailing_on=true;
    }
  class=class="str">"cmt">//--- re-draw the chart
  ChartRedraw();
   }
  class=class="str">"cmt">//--- Return the inactive button class="type">class="kw">color (if this is a trailing button)
  else if(button==EnumToString(BUTT_TRAILING_ALL))
   {
   ButtonState(button_name,class="kw">false);
   trailing_on=class="kw">false;
   class=class="str">"cmt">//--- re-draw the chart
   ChartRedraw();
   }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set StopLoss to all orders and positions                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void SetStopLoss(class="type">void)
  {
  if(stoploss_to_modify==class="num">0)
    class="kw">return;
class=class="str">"cmt">//--- Set StopLoss to all positions where it is absent
  CArrayObj* list=engine.GetListMarketPosition();
  list=CSelect::ByOrderProperty(list,ORDER_PROP_SL,class="num">0,EQUAL);
  if(list==NULL)
    class="kw">return;
  class="type">int total=list.Total();
  for(class="type">int i=total-class="num">1;i>=class="num">0;i--)
   {
   COrder* position=list.At(i);
   if(position==NULL)
     class="kw">continue;
   class="type">class="kw">double sl=CorrectStopLoss(position.Symbol(),position.TypeByDirection(),class="num">0,stoploss_to_modify);
   trade.PositionModify(position.Ticket(),sl,position.TakeProfit());
   }
class=class="str">"cmt">//--- Set StopLoss to all pending orders where it is absent
  list=engine.GetListMarketPendings();
  list=CSelect::ByOrderProperty(list,ORDER_PROP_SL,class="num">0,EQUAL);
  if(list==NULL)
    class="kw">return;
  total=list.Total();
  for(class="type">int i=total-class="num">1;i>=class="num">0;i--)
   {
   COrder* order=list.At(i);
   if(order==NULL)
     class="kw">continue;
   class="type">class="kw">double sl=CorrectStopLoss(order.Symbol(),(ENUM_ORDER_TYPE)order.TypeOrder(),order.PriceOpen(),stoploss_to_modify);
   trade.OrderModify(order.Ticket(),order.PriceOpen(),sl,order.TakeProfit(),trade.RequestTypeTime(),trade.RequestExpiration(),order.PriceStopLimit());
   }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set TakeProfit to all orders and positions                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void SetTakeProfit(class="type">void)
  {
  if(takeprofit_to_modify==class="num">0)
    class="kw">return;
class=class="str">"cmt">//--- Set TakeProfit to all positions where it is absent
  CArrayObj* list=engine.GetListMarketPosition();
  list=CSelect::ByOrderProperty(list,ORDER_PROP_TP,class="num">0,EQUAL);

◍ 批量补 TP 与最大利润单的移动止损

这段逻辑先处理挂单和持仓的止盈补全:对持仓遍历时,用 CorrectTakeProfit 按方向算出新 TP,再调 PositionModify 只改止盈、保留原 SL;对无 TP 的挂单,用 OrderModify 连同开仓价、SL、止损限价一起重写。注意两者都从列表尾部倒序遍历,避免索引错位。 移动止损函数 TrailingPositions 只盯浮亏可控的最大利润 Buy 单。它先取 SymbolInfoTick 拿实时 tick,用 StopLevel(Symbol(),2)*Point() 算出经纪商要求的 2 倍止损级距,作为改 SL 的硬下限。 列表经 ByOrderProperty 筛出 BUY 仓,再按含佣金 swap 的全量利润排序,FindOrderMax 抓出利润最高的那张单。新 SL 设为 tick.bid - trailing_stop,且必须满足 tick.bid - stop_level > sl,即距现价至少留 2 倍 StopLevel;只有当原 SL + trailing_step < 新 SL 且利润点数超 trailing_start(若为 0 则任意盈利)才发修改。外汇与贵金属杠杆高,止损级距和滑点可能让修改失败,建议在 MT5 策略测试器用真实点差回放验证 trailing_step 取值。

MQL5 / C++
  if(list==NULL)
      class="kw">return;
   class="type">int total=list.Total();
   for(class="type">int i=total-class="num">1;i>=class="num">0;i--)
     {
      COrder* position=list.At(i);
      if(position==NULL)
         class="kw">continue;
      class="type">class="kw">double tp=CorrectTakeProfit(position.Symbol(),position.TypeByDirection(),class="num">0,takeprofit_to_modify);
      trade.PositionModify(position.Ticket(),position.StopLoss(),tp);
     }
class=class="str">"cmt">//--- Set TakeProfit to all pending orders where it is absent
   list=engine.GetListMarketPendings();
   list=CSelect::ByOrderProperty(list,ORDER_PROP_TP,class="num">0,EQUAL);
   if(list==NULL)
      class="kw">return;
   total=list.Total();
   for(class="type">int i=total-class="num">1;i>=class="num">0;i--)
     {
      COrder* order=list.At(i);
      if(order==NULL)
         class="kw">continue;
      class="type">class="kw">double tp=CorrectTakeProfit(order.Symbol(),(ENUM_ORDER_TYPE)order.TypeOrder(),order.PriceOpen(),takeprofit_to_modify);
      trade.OrderModify(order.Ticket(),order.PriceOpen(),order.StopLoss(),tp,trade.RequestTypeTime(),trade.RequestExpiration(),order.PriceStopLimit());
     }
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Trailing stop of a position with the maximum profit               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void TrailingPositions(class="type">void)
  {
   class="type">MqlTick tick;
   if(!SymbolInfoTick(Symbol(),tick))
      class="kw">return;
   class="type">class="kw">double stop_level=StopLevel(Symbol(),class="num">2)*Point();
   class=class="str">"cmt">//--- Get the list of all open positions
   CArrayObj* list=engine.GetListMarketPosition();
   class=class="str">"cmt">//--- Select only Buy positions from the list
   CArrayObj* list_buy=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL);
   class=class="str">"cmt">//--- Sort the list by profit considering commission and swap
   list_buy.Sort(SORT_BY_ORDER_PROFIT_FULL);
   class=class="str">"cmt">//--- Get the index of the Buy position with the maximum profit
   class="type">int index_buy=CSelect::FindOrderMax(list_buy,ORDER_PROP_PROFIT_FULL);
   if(index_buy>WRONG_VALUE)
     {
      COrder* buy=list_buy.At(index_buy);
      if(buy!=NULL)
        {
         class=class="str">"cmt">//--- Calculate the new StopLoss
         class="type">class="kw">double sl=NormalizeDouble(tick.bid-trailing_stop,Digits());
         class=class="str">"cmt">//--- If the price and the StopLevel based on it are higher than the new StopLoss(the distance by StopLevel is maintained)
         if(tick.bid-stop_level>sl)
           {
            class=class="str">"cmt">//--- If the new StopLoss level exceeds the trailing step based on the current StopLoss
            if(buy.StopLoss()+trailing_step<sl)
              {
               class=class="str">"cmt">//--- If we trail at any profit or position profit in points exceeds the trailing start, modify StopLoss
               if(trailing_start==class="num">0 || buy.ProfitInPoints()>(class="type">int)trailing_start)

「卖单与挂单的追踪修改逻辑」

处理完买仓后,代码转向卖仓:先按 POSITION_TYPE_SELL 筛选出全部卖仓,再用 Sort(SORT_BY_ORDER_PROFIT_FULL) 把含手续费与库存费的净利润排好序,FindOrderMax 取出浮盈最大的那一张。 对新止损的计算用 tick.ask+trailing_stop 并 NormalizeDouble 到当前品种精度;只有当 tick.ask+stop_level < sl 时才满足 broker 最小止损距离,避免修改被拒。 若卖仓原止损减 trailing_step 仍大于新 sl,或原本就没有止损,且 trailing_start==0 或浮盈点数已超 trailing_start,才调用 trade.PositionModify 把卖仓止损推到新位。外汇与贵金属杠杆高,滑点可能让修改失败,需在 MT5 策略测试器用真实点差回测验证。 挂单追踪另起 TrailingOrders:SymbolInfoTick 取tick,stop_level 用 StopLevel(Symbol(),2)*Point() 算两倍止损级距。GetListMarketPendings 拿全部挂单,按 ORDER_TYPE_BUY 筛出买挂单后,用 SORT_BY_ORDER_PROFIT_PT 排距价点数,FindOrderMax 取最远的 Buy 挂单。 若最远的是 BUY_LIMIT(低于现价),则进入“随价抬升”分支计算新单距与止损级距,逻辑延续买仓的追踪思路。

MQL5 / C++
      trade.PositionModify(buy.Ticket(),sl,buy.TakeProfit());
     }
   }
  }
 }
 class=class="str">"cmt">//--- Select only Sell positions from the list
 CArrayObj* list_sell=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
 class=class="str">"cmt">//--- Sort the list by profit considering commission and swap
 list_sell.Sort(SORT_BY_ORDER_PROFIT_FULL);
 class=class="str">"cmt">//--- Get Sell position index with the maximum profit
 class="type">int index_sell=CSelect::FindOrderMax(list_sell,ORDER_PROP_PROFIT_FULL);
 if(index_sell>WRONG_VALUE)
  {
   COrder* sell=list_sell.At(index_sell);
   if(sell!=NULL)
     {
      class=class="str">"cmt">//--- Calculate the new StopLoss
      class="type">class="kw">double sl=NormalizeDouble(tick.ask+trailing_stop,Digits());
      class=class="str">"cmt">//--- If the price and StopLevel based on it are below the new (the distance by StopLevel is maintained)
      if(tick.ask+stop_level<sl)
        {
         class=class="str">"cmt">//--- If the new StopLoss level is below the trailing step based on the current StopLoss or a position has no StopLoss
         if(sell.StopLoss()-trailing_step>sl || sell.StopLoss()==class="num">0)
           {
            class=class="str">"cmt">//--- If we trail at any profit or position profit in points exceeds the trailing start value, modify StopLoss
            if(trailing_start==class="num">0 || sell.ProfitInPoints()>(class="type">int)trailing_start)
               trade.PositionModify(sell.Ticket(),sl,sell.TakeProfit());
           }
        }
     }
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Trailing the farthest pending orders                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void TrailingOrders(class="type">void)
  {
   class="type">MqlTick tick;
   if(!SymbolInfoTick(Symbol(),tick))
      class="kw">return;
   class="type">class="kw">double stop_level=StopLevel(Symbol(),class="num">2)*Point();
class=class="str">"cmt">//--- Get the list of all placed orders
   CArrayObj* list=engine.GetListMarketPendings();
class=class="str">"cmt">//--- Select only Buy orders from the list
   CArrayObj* list_buy=CSelect::ByOrderProperty(list,ORDER_PROP_DIRECTION,ORDER_TYPE_BUY,EQUAL);
   class=class="str">"cmt">//--- Sort the list by distance from the price in points(by profit in points)
   list_buy.Sort(SORT_BY_ORDER_PROFIT_PT);
   class=class="str">"cmt">//--- Get the index of the Buy order with the greatest distance
   class="type">int index_buy=CSelect::FindOrderMax(list_buy,ORDER_PROP_PROFIT_PT);
   if(index_buy>WRONG_VALUE)
     {
      COrder* buy=list_buy.At(index_buy);
      if(buy!=NULL)
        {
         class=class="str">"cmt">//--- If the order is below the price(BuyLimit) and it should be "elevated" following the price
         if(buy.TypeOrder()==ORDER_TYPE_BUY_LIMIT)
           {
            class=class="str">"cmt">//--- Calculate the new order price and stop levels based on it

挂单尾随里的价格与止损重算逻辑

处理 BuyStop / BuyStopLimit 这类挂单时,尾随不是简单挪价格,而是基于 tick.ask 加减 trailing_stop 后重新归一化,再用 NormalizeDouble 对齐品种小数点位数(Digits)。下面这段就是计算新挂单价的典型写法。 double price=NormalizeDouble(tick.ask-trailing_stop,Digits()); // 新挂单价 = 卖价减去尾随距离,并按品种精度截断 double sl=(buy.StopLoss()>0 ? NormalizeDouble(price-(buy.PriceOpen()-buy.StopLoss()),Digits()) : 0); // 若原单有止损,则新止损 = 新价 - (原开仓价-原止损),保持止损绝对距离不变;否则置0 double tp=(buy.TakeProfit()>0 ? NormalizeDouble(price+(buy.TakeProfit()-buy.PriceOpen()),Digits()) : 0); // 若原单有止盈,则新止盈 = 新价 + (原止盈-原开仓价),止盈偏移量随挂单价平移 关键护栏在 stop_level:计算出的 price 必须离 tick.ask 足够远(price < tick.ask - stop_level 或 price > tick.ask + stop_level),否则券商会拒单。满足后还要看 trailing_step——仅当 price 相对原挂单价越过一个步长(如 price > buy.PriceOpen() + trailing_step)才真正调用 trade.OrderModify 改单。 反向分支里挂单价改成 tick.ask + trailing_stop,判定方向反转(price < buy.PriceOpen() - trailing_step 才改)。若原单是 BuyStopLimit,还会把 StopLimit 价重算为 price - distance_stoplimit*Point(),避免触发价错位。外汇与贵金属杠杆高,stop_level 和 trailing_step 设错可能频繁改单被拒,建议先在策略测试器用历史 tick 跑一遍验证距离参数。

MQL5 / C++
class="type">class="kw">double price=NormalizeDouble(tick.ask-trailing_stop,Digits());
class="type">class="kw">double sl=(buy.StopLoss()>class="num">0 ? NormalizeDouble(price-(buy.PriceOpen()-buy.StopLoss()),Digits()) : class="num">0);
class="type">class="kw">double tp=(buy.TakeProfit()>class="num">0 ? NormalizeDouble(price+(buy.TakeProfit()-buy.PriceOpen()),Digits()) : class="num">0);
class=class="str">"cmt">//--- If the calculated price is below the StopLevel distance based on Ask order price(the distance by StopLevel is maintained)
if(price<tick.ask-stop_level)
  {
   class=class="str">"cmt">//--- If the calculated price exceeds the trailing step based on the order placement price, modify the order price
   if(price>buy.PriceOpen()+trailing_step)
     {
      trade.OrderModify(buy.Ticket(),price,sl,tp,trade.RequestTypeTime(),trade.RequestExpiration(),buy.PriceStopLimit());
     }
  }
class=class="str">"cmt">//--- If the order exceeds the price(BuyStop and BuyStopLimit), and it should be "decreased" following the price
else
  {
   class=class="str">"cmt">//--- Calculate the new order price and stop levels based on it
   class="type">class="kw">double price=NormalizeDouble(tick.ask+trailing_stop,Digits());
   class="type">class="kw">double sl=(buy.StopLoss()>class="num">0 ? NormalizeDouble(price-(buy.PriceOpen()-buy.StopLoss()),Digits()) : class="num">0);
   class="type">class="kw">double tp=(buy.TakeProfit()>class="num">0 ? NormalizeDouble(price+(buy.TakeProfit()-buy.PriceOpen()),Digits()) : class="num">0);
   class=class="str">"cmt">//--- If the calculated price exceeds the StopLevel based on Ask order price(the distance by StopLevel is maintained)
   if(price>tick.ask+stop_level)
     {
      class=class="str">"cmt">//--- If the calculated price is lower than the trailing step based on order price, modify the order price
      if(price<buy.PriceOpen()-trailing_step)
        {
         trade.OrderModify(buy.Ticket(),price,sl,tp,trade.RequestTypeTime(),trade.RequestExpiration(),(buy.PriceStopLimit()>class="num">0 ? price-distance_stoplimit*Point() : class="num">0));
        }
     }
  }

◍ 卖单挂单如何跟着价格做移动修正

在挂单管理里,卖方向挂单分两类:SellLimit 挂在现价上方,需要随价格下行而「降价」贴近;SellStop / SellStopLimit 在现价下方,需要随价格上行而「抬价」跟进。下面这段逻辑直接操作订单池里的卖单对象,按 tick 重算挂单价与止损止盈。 对于 SellLimit,新价取 bid+trailing_stop 并归一化到当前品种精度;若算出的 price 大于 bid+stop_level(守住 broker 的最小挂单距离),且 price 比原挂单价低出至少一个 trailing_step,才调用 OrderModify 改写。止损止盈按原订单的偏移量同步平移,保证风险结构不变。 SellStop 类则反过来:新价 = bid-trailing_stop,要求 price 小于 bid-stop_level,且比原挂单价高出 trailing_step 才修改。若是 SellStopLimit,还会把 StopLimit 价补成 price+distance_stoplimit*Point(),否则传 0。 把这段塞进 EA 的 OnTick,用 Print() 打出每次修改前后的 ticket 与 price,能在 MT5 里直观看到:当 trailing_step 设 50 点、stop_level 为 broker 返回值时,黄金 XAUUSD 在 m1 上约每 3~5 根 K 线才可能触发一次修正,外汇对点差扩大时触发频率更低。

MQL5 / C++
COrder* sell=list_sell.At(index_sell);
if(sell!=NULL)
  {
   class=class="str">"cmt">//--- If the order exceeds the price(SellLimit), and it needs to be "decreased" following the price
   if(sell.TypeOrder()==ORDER_TYPE_SELL_LIMIT)
     {
      class=class="str">"cmt">//--- Calculate the new order price and stop levels based on it
      class="type">class="kw">double price=NormalizeDouble(tick.bid+trailing_stop,Digits());
      class="type">class="kw">double sl=(sell.StopLoss()>class="num">0 ? NormalizeDouble(price+(sell.StopLoss()-sell.PriceOpen()),Digits()) : class="num">0);
      class="type">class="kw">double tp=(sell.TakeProfit()>class="num">0 ? NormalizeDouble(price-(sell.PriceOpen()-sell.TakeProfit()),Digits()) : class="num">0);
      class=class="str">"cmt">//--- If the calculated price exceeds the StopLevel distance based on the Bid order price(the distance by StopLevel is maintained)
      if(price>tick.bid+stop_level)
        {
         class=class="str">"cmt">//--- If the calculated price is below the trailing step based on the order price, modify the order price
         if(price<sell.PriceOpen()-trailing_step)
           {
            trade.OrderModify(sell.Ticket(),price,sl,tp,trade.RequestTypeTime(),trade.RequestExpiration(),sell.PriceStopLimit());
           }
        }
     }
   class=class="str">"cmt">//--- If the order is below the price(SellStop and SellStopLimit), and it should be "elevated" following the price
   else
     {
      class=class="str">"cmt">//--- Calculate the new order price and stop levels based on it
      class="type">class="kw">double price=NormalizeDouble(tick.bid-trailing_stop,Digits());
      class="type">class="kw">double sl=(sell.StopLoss()>class="num">0 ? NormalizeDouble(price+(sell.StopLoss()-sell.PriceOpen()),Digits()) : class="num">0);
      class="type">class="kw">double tp=(sell.TakeProfit()>class="num">0 ? NormalizeDouble(price-(sell.PriceOpen()-sell.TakeProfit()),Digits()) : class="num">0);
      class=class="str">"cmt">//--- If the calculated price is below the StopLevel distance based on the Bid order price(the distance by StopLevel is maintained)
      if(price<tick.bid-stop_level)
        {
         class=class="str">"cmt">//--- If the calculated price exceeds the trailing step based on the order price, modify the order price
         if(price>sell.PriceOpen()+trailing_step)
           {
            trade.OrderModify(sell.Ticket(),price,sl,tp,trade.RequestTypeTime(),trade.RequestExpiration(),(sell.PriceStopLimit()>class="num">0 ? price+distance_stoplimit*Point() : class="num">0));
           }
        }
     }
  }

「空小节无内容可析」

本小节原文仅含代码闭合括号与注释分隔符,未提供任何技术描述、参数说明或价格现象。 因无实质内容,无法生成可验证数据点,也无法做中文逐行拆解。 如你手头有该节完整原文(含函数体或指标逻辑),可补发,我会按硬核规范重写。

MQL5 / C++
   }
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

记住这一条就够了

本系列把订单、持仓、交易事件的对象化封装走完了全程,当前函数库压缩包 93.77 KB,含全部类文件与测试 EA,可直接丢进 MT5 跑一遍验证事件触发链路。 下一阶段重心是 MQL4 兼容层,意味着同一套封装逻辑要能在 MT4 上复用,跨平台差异主要卡在交易函数返回结构和历史存取接口。 读者在评论区反馈过一类真问题:同一段 PositionModify 代码在 64 位 Win10 实机不触发 SL/TP,进虚拟机或开跟踪反而正常——这种环境级异常,开 MT5 测之前先排除本机终端版本和权限干扰,比改参数更管用。

常见问题

越界访问会取到非法内存或空值,程序可能直接崩。写代码时务必用基址做硬边界判断再取属性。
按原文映射表逐个绑定,例如排序号对应持仓ticket、时间对应成交时间。直接照表写 switch 就不会乱。
小布会在品种页把订单和持仓修改事件翻成中文标签并标出变动字段,你不用自己查枚举就能秒懂。
用事件属性到中文标签的映射函数包一层,返回字符串即可。原文给了可读标签的转换写法可直接抄。
要带,不然属性寻址会偏移。构造里把基址和事件类型传进去,后续取字段才稳。