轻松快捷开发 MetaTrader 程序的函数库(第八部分):订单和持仓修改事件·综合运用
- 事件属性枚举与排序基址的硬边界
- 事件排序枚举里的真实字段映射
- 事件排序枚举与基类的属性寻址
- 交易事件对象的内部属性与构造细节
- 把事件属性翻成可读的中文标签
- 订单属性在事件描述里怎么映射成文字
- 把交易事件属性翻成可读文本
- 交易事件属性与类型的中文映射
- 账户与订单事件的中文映射写法
- 交易事件映射成双语标签的写法
- 把平仓与反手事件映射成双语提示
- 成交与挂单改单事件的枚举分支
- 挂单与持仓的保护价改动事件映射
- 成交回报里的撤销与反转语义
- 平仓与资金事件的俄语英文双标签映射
- 账户事件原因的本地化文本映射
- 交易事件对象的属性读取接口
- 持仓翻转事件的属性读取接口
- 持仓切换事件的属性读取接口
- 订单事件里的价格与成交量取数接口
- 事件对象里的价格与品种取值接口
- 挂单与持仓改单的事件细分逻辑
- 挂单触发下的持仓反转与加仓判定
- 用事件标志区分反转、加仓与挂单激活
- 平仓事件标志的逐层判定逻辑
- 订单盈亏点数该怎么算
- 订单整型属性的可读化封装
- 订单属性多语种文本拼装的分支逻辑
- 订单毫秒级时间与状态字段的读取分支
- 订单属性多语标签的拼装逻辑
- 挂单修改事件的类封装与属性过滤
- 挂单改价与改价带止损的事件回显
- 挂单改价与止损止盈的事件分流
- 挂单与持仓的止损止盈变更事件分流
- 持仓止损止盈改动时的事件文本拼装
- 事件容器的私有成员与构造初始化
- 挂单与持仓改仓事件的位标记拆解
- 给持仓翻转事件塞满上下文属性
- 挂单事件对象的属性填充与去重
- 给测试EA补上止损止盈与尾随按钮
- 跟踪止损与改单参数的输入声明
- 初始化里把 trailing 和按钮状态钉死
- 按钮事件映射到持仓动作的实现细节
- 一键给所有持仓挂止损的逻辑
- 给缺失止盈的持仓与挂单补 TP
- 卖单与挂单的跟踪止损实现差异
- 挂单随价移动的改单逻辑
- 卖单挂单的跟随式重定价逻辑
- 挂单改价与面板按钮的衔接逻辑
- 挂单与风控参数的输入声明
- EA 初始化与Tick循环的骨架
- 定时器与图表事件的分流处理
- 按钮面板的布局与创建逻辑
- 给按钮挂上状态与文字的收尾逻辑
- 按钮事件怎么映射到下单动作
- 按钮触发后的挂单与市价单分支
- 挂单与平仓按钮的底层派发逻辑
- 盈利单的半平与对冲闭环写法
- 用对冲单和利润排序收掉仓位
- 用反向单平掉最赚的卖仓
- 按钮触发后的持仓与挂单处理逻辑
- 跟单面板的止损批量接管逻辑
- 批量补 TP 与最大利润单的移动止损
- 卖单与挂单的追踪修改逻辑
- 挂单尾随里的价格与止损重算逻辑
- 卖单挂单如何跟着价格做移动修正
- 空小节无内容可析
- 记住这一条就够了
「事件属性枚举与排序基址的硬边界」
在 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 里把这两个宏数值打印出来核对,外汇与贵金属杠杆高,事件顺序错乱可能让风控逻辑误触发。
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 返回顺序变化,是最快的验证路径。外汇与贵金属杠杆高,回测排序逻辑出错可能误导仓位统计,实盘前务必小资金校验。
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 会在升级后错位。用枚举名而非字面量,让编译器算偏移。
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 里,改改枚举就能直接复用事件描述输出。
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」这种输出,比光秃秃的数字好查得多。外汇与贵金属杠杆高,事件日志只辅助复盘,不构成任何方向暗示。
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 对照,验证持仓翻转追踪是否漏字段。
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 取值是否符合预期。
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 终端看到每笔动作的人类语言描述而非数字代号。
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 的后续分支,就能在日志里看到每类事件触发时的本地化文案,验证你账户实际产生的事件是否覆盖上述全部类型。外汇与贵金属交易杠杆高,账户事件里的佣金和利息变动会直接吃掉浮盈,核对明细是必要动作。
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 品种观察回执文本即可验证。
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 前做映射,就能在终端看清楚每一笔持仓到底怎么没的。
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 前做预判断,能在不依赖标准库的情况下,自己拼出带中文说明的成交流水。
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,可能漏掉挂单转市价的关键节点,外汇与贵金属波动大时这种漏听会拖慢风控响应。
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 跑一晚模拟单,就能对照终端‘交易’标签里的记录验证文本是否对得上。
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 分支做参照。
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),复制时别手改成正规拼写,否则编译器会报未定义标识符。外汇与贵金属交易本身杠杆高、风险大,这类事件文本仅用于排查账目异动,不构成任何方向判断。
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 直观得多。外汇与贵金属杠杆交易本身高风险,这类事件监控只用于排查账户流水异常,不预示任何价格方向。
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 就能验证属性是否随事件正确填充。
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 连续。
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 的拆分符合你的成交假设,再上模拟盘。
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 嵌套导致事件类型误判。
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 里打点,能验证同一笔改单在日志中落到的具体枚举值。
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 事件常量被命中。
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 后缀的事件类型。
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 用真实成交回放校验标志组合。
} 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 低于挂单价时的返回值,你会发现它恒为非负整数,验证上面说的绝对值逻辑。外汇与贵金属杠杆高,点数折算仅作技术参考,实盘风险自担。
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_DATE | TIME_MINUTES | TIME_SECONDS,输出精度到秒,便于复盘毫秒级滑点。 |
|---|
SupportProperty 先判该 broker 是否支持此字段,不支持就追加「Property not supported」,避免老平台返回乱码。外汇与贵金属订单属性受券商限制较大,同一段代码在 ECNC 与做市商环境可能差出 3~4 个不支持项,实盘前务必本地先跑一遍。
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 按 DATE | MINUTES | SECONDS 格式拼时间。 |
|---|
类型与方向走 TypeDescription、DirectionDescription 直接给文案;而 Reason、PositionID、DealTicket 等属性要先过 SupportProperty 这道闸,不支持就回「Property not supported」,避免在某些券商环境下读脏数据。 TimeOpenMsc 这类毫秒级字段同样套了 SupportProperty 守卫,说明 MT5 不同订单类型对属性的支持度不一致。你在写订单诊断面板时,若漏掉这个判断,可能在对冲账户上直接抛数组越界。外汇与贵金属杠杆高,历史订单结构差异大,跑前务必用真实账户回放验证。
(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 时进入特定逻辑,意味着浮盈点数属性只对市价挂单类状态有效。外汇与贵金属杠杆品种中这类状态切换极快,建议在策略测试器用逐笔成交模式验证该分支触发频率。
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 调这段,就能在日志看到该单是否由止盈触发关闭——外汇与贵金属杠杆高,这类标记只用于复盘,不代表未来平仓路径。
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,把下面代码原样贴进去,编译能过就说明你的事件枚举和基类接口是一致的。
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]」这种行。外汇与贵金属波动剧烈,这类修改事件高频出现时,图表自定义事件可能堆积,需自行限流。
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] 这种闭环。外汇与贵金属杠杆高,挂单改止损不代表风险同步可控,价格跳空可能使改单不成交。
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_PROFIT、TRADE_EVENT_MODIFY_ORDER_STOP_LOSS_TAKE_PROFIT、TRADE_EVENT_MODIFY_POSITION_STOP_LOSS、TRADE_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 的跳变是否符合预期。
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() 拼出完整提示。外汇与贵金属杠杆高,这类改单事件频繁出现时,建议用此逻辑接小布盯盘推送,人工漏看止损被挪的概率会下降。
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 终端看账户窗口的保证金模式,再决定要不要保留这个判断。
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 的挂单」这种细分动作。 外汇与贵金属杠杆高,改仓触发密集时事件流可能淹没真实信号,建议先单品种验证再扩到多品种。
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 是否一致——不一致往往说明抓取的是修改后价而非触发价。外汇与贵金属杠杆高,翻转信号误抓可能放大回撤,建议用历史数据先验证逻辑。
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 绑定是否串号。外汇与贵金属杠杆高,事件去重失败可能导致重复平仓信号,务必在策略测试器里先跑通再上真仓。
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 点。策略测试器里先挂单,按按钮补停止位,日志会打印对应项;开尾随后可见订单止损随价移动并打印。外汇与贵金属杠杆高,脚本仅验证库逻辑,实盘误用可能快速放大亏损。
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,能直接观察回撤容忍度变化。
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 有没有真正生效。
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 变量单位。
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 输出符合预期再上实盘。
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 的最小止损距限制。
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() 得出两倍最小止损距离,这是实盘里很容易被忽略的硬约束。
{
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 策略测试器用真实点差回放验证。
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 设太小会频繁触发修改请求,实盘前务必在策略测试器用真实点差跑一遍。
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 返回错误码时的处理分支没有吞异常。
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 能观察跟踪止损触发频率的明显变化。外汇与贵金属杠杆高,参数误设可能瞬间放大回撤,须先在策略测试器验证。
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 变化触发追踪,就能验证这套骨架是否和你现有库兼容。
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 不会执行,验证环境分流是否生效。外汇与贵金属杠杆高,事件驱动逻辑未经实盘观测前勿直接用于资金账户。
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() 看图表左上角是否按上述规则出两列按钮。外汇与贵金属波动剧烈,这类面板只做交互辅助,任何信号都不构成入场保证。
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」这种可读标签。外汇与贵金属波动剧烈,这类面板只降低操作失误率,不预示任何方向。
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 策略测试器用真实点差验证这几处校验函数返回值。
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,就能实测各品种在不同波动下的下单拒绝率。
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 上,而不是市价:
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 的枚举名就能直接测。
{
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 分支(原文截断)。外汇与贵金属杠杆高,部分平仓前先确认保证金占用,避免半仓后触发追加。
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()); } } }
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 看挂单是否从最旧的那张开始消失。
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 用策略测试器跑一遍无止损持仓样本最稳妥。
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 取值。
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(低于现价),则进入“随价抬升”分支计算新单距与止损级距,逻辑延续买仓的追踪思路。
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 跑一遍验证距离参数。
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 线才可能触发一次修正,外汇对点差扩大时触发频率更低。
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)); } } } }
「空小节无内容可析」
本小节原文仅含代码闭合括号与注释分隔符,未提供任何技术描述、参数说明或价格现象。 因无实质内容,无法生成可验证数据点,也无法做中文逐行拆解。 如你手头有该节完整原文(含函数体或指标逻辑),可补发,我会按硬核规范重写。
}
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+记住这一条就够了
本系列把订单、持仓、交易事件的对象化封装走完了全程,当前函数库压缩包 93.77 KB,含全部类文件与测试 EA,可直接丢进 MT5 跑一遍验证事件触发链路。 下一阶段重心是 MQL4 兼容层,意味着同一套封装逻辑要能在 MT4 上复用,跨平台差异主要卡在交易函数返回结构和历史存取接口。 读者在评论区反馈过一类真问题:同一段 PositionModify 代码在 64 位 Win10 实机不触发 SL/TP,进虚拟机或开跟踪反而正常——这种环境级异常,开 MT5 测之前先排除本机终端版本和权限干扰,比改参数更管用。