DoEasy.服务函数(第 3 部分):外包线形态·进阶篇
📊

DoEasy.服务函数(第 3 部分):外包线形态·进阶篇

(2/3)·孕线讲完,镜像形态外包线来了——双向变单向,库类先动刀再写类

含代码示例 第 2/3 篇
把外包线当成孕线反着用是常见误区:孕线双向可入,外包线分 BUOVB 和 BEOVB 两个方向,突破价还要加 5–10 点过滤器。直接套旧逻辑会漏掉方向判定,回测信号全歪。

◍ 把隔夜利息模式翻译成可读文本

在 MT5 的自定义品种封装类里,隔夜利息(swap)的计算方式不是直接给人看的枚举值,而是靠一个 GetSwapModeDescription 方法转成界面文字。这样你在写 EA 面板或日志输出时,不用硬记 SYMBOL_SWAP_MODE_XXX 的数字含义。 下面这段来自 CSymbol 类的实现,把 SwapMode() 返回的枚举逐个比对,命中后调 CMessage::Text 取本地化描述,全部漏掉就返回未知模式文案。 enum ENUM_SYMBOL_SWAP_MODE 里列了实际存在的几种计费口径:SYMBOL_SWAP_MODE_POINTS 按点值收(MQL5 编号 1),SYMBOL_SWAP_MODE_CURRENCY_SYMBOL 按标的基准币收钱(编号 2),SYMBOL_SWAP_MODE_INTEREST_OPEN 按开仓价年息收(编号 6),SYMBOL_SWAP_MODE_CURRENCY_MARGIN 按保证金币收(编号 3),SYMBOL_SWAP_MODE_DISABLED 为免息(编号 0),SYMBOL_SWAP_MODE_CURRENCY_DEPOSIT 按客户入金币收。外汇与贵金属杠杆品种里,不同券商可能映射不同模式,持仓过夜成本倾向有差异,属正常高风险特征。 开 MT5 用 SymbolInfoInteger(_Symbol,SYMBOL_SWAP_MODE) 打印当前品种返回值,再对照上面枚举编号,就能确认你的平台走的是哪条计息路径。

MQL5 / C++
class="type">class="kw">string CSymbol::GetSwapModeDescription(class="type">void) const
  {
   class="kw">return
     (
      this.SwapMode()==SYMBOL_SWAP_MODE_DISABLED           ?  CMessage::Text(MSG_SYM_SWAP_MODE_DISABLED)           :
      this.SwapMode()==SYMBOL_SWAP_MODE_POINTS             ?  CMessage::Text(MSG_SYM_SWAP_MODE_POINTS)             :
      this.SwapMode()==SYMBOL_SWAP_MODE_CURRENCY_SYMBOL    ?  CMessage::Text(MSG_SYM_SWAP_MODE_CURRENCY_SYMBOL)    :
      this.SwapMode()==SYMBOL_SWAP_MODE_CURRENCY_MARGIN    ?  CMessage::Text(MSG_SYM_SWAP_MODE_CURRENCY_MARGIN)    :
      this.SwapMode()==SYMBOL_SWAP_MODE_CURRENCY_DEPOSIT   ?  CMessage::Text(MSG_SYM_SWAP_MODE_CURRENCY_DEPOSIT)   :
      this.SwapMode()==SYMBOL_SWAP_MODE_CURRENCY_PROFIT    ?  CMessage::Text(MSG_SYM_SWAP_MODE_CURRENCY_PROFIT)    :
      this.SwapMode()==SYMBOL_SWAP_MODE_INTEREST_CURRENT   ?  CMessage::Text(MSG_SYM_SWAP_MODE_INTEREST_CURRENT)   :
      this.SwapMode()==SYMBOL_SWAP_MODE_INTEREST_OPEN      ?  CMessage::Text(MSG_SYM_SWAP_MODE_INTEREST_OPEN)      :
      this.SwapMode()==SYMBOL_SWAP_MODE_REOPEN_CURRENT     ?  CMessage::Text(MSG_SYM_SWAP_MODE_REOPEN_CURRENT)     :
      this.SwapMode()==SYMBOL_SWAP_MODE_REOPEN_BID         ?  CMessage::Text(MSG_SYM_SWAP_MODE_REOPEN_BID)         :
      CMessage::Text(MSG_SYM_MODE_UNKNOWN)
     );
  }

enum ENUM_SYMBOL_SWAP_MODE
  {
   SYMBOL_SWAP_MODE_POINTS,             class=class="str">"cmt">// (MQL5 - class="num">1, MQL4 - class="num">0) Swaps are charged in points
   SYMBOL_SWAP_MODE_CURRENCY_SYMBOL,    class=class="str">"cmt">// (MQL5 - class="num">2, MQL4 - class="num">1) Swaps are charged in money in symbol base currency
   SYMBOL_SWAP_MODE_INTEREST_OPEN,      class=class="str">"cmt">// (MQL5 - class="num">6, MQL4 - class="num">2) Swaps are charged as the specified annual interest from the open price of position
   SYMBOL_SWAP_MODE_CURRENCY_MARGIN,    class=class="str">"cmt">// (MQL5 - class="num">3, MQL4 - class="num">3) Swaps are charged in money in margin currency of the symbol
   SYMBOL_SWAP_MODE_DISABLED,           class=class="str">"cmt">// (MQL5 - class="num">0, MQL4 - N) No swaps
   SYMBOL_SWAP_MODE_CURRENCY_DEPOSIT,   class=class="str">"cmt">// Swaps are charged in money, in client deposit currency
  }

「掉期计息模式与事件防重入的落地写法」

MT5 里掉期(swap)的计算逻辑并不统一,枚举 SYMBOL_SWAP_MODE 给出了几种路径:按品种价格年化利率计、按收盘价重开仓计、按当前 Bid 重开仓计,以及直接以利润币种金额扣除(SYMBOL_SWAP_MODE_CURRENCY_PROFIT)。做跨品种持仓成本对比时,若 broker 用的是利润币种金额模式,直接用账户结算币读取更稳,不必再折算。 事件写入要避免重复压栈。下面两段 CBaseObjExt 的方法都先 Sort 再 Search,命中 WRONG_VALUE 以上说明已存在,就 delete 掉新对象并返回 false,只有查不到才 Add。外汇与贵金属隔夜成本受此影响明显,点差和掉期叠加后可能吞噬短线利润,属高风险环节。 调用层只需关心 EventAdd 与 EventBaseAdd 的返回布尔值:返回 false 要么内存分配失败,要么事件已登记。实盘里用这个返回值决定是否补发告警,比盲目推送更省资源。

MQL5 / C++
class="type">bool CBaseObjExt::EventAdd(const class="type">class="kw">ushort event_id,const class="type">long lparam,const class="type">class="kw">double dparam,const class="type">class="kw">string sparam)
  {
   CEventBaseObj *event=new CEventBaseObj(event_id,lparam,dparam,sparam);  class=class="str">"cmt">// 用入参构造事件对象
   if(event==NULL)
     class="kw">return class="kw">false;  class=class="str">"cmt">// 内存不足直接失败
   this.m_list_events.Sort();  class=class="str">"cmt">// 列表先排序保证二分查找有效
   if(this.m_list_events.Search(event)>WRONG_VALUE)  class=class="str">"cmt">// 已存在相同事件
     {
      class="kw">delete event;  class=class="str">"cmt">// 释放重复对象
      class="kw">return class="kw">false;  class=class="str">"cmt">// 拒绝重复添加
     }
   class="kw">return this.m_list_events.Add(event);  class=class="str">"cmt">// 唯一事件才入列
  }

class="type">bool CBaseObjExt::EventBaseAdd(const class="type">int event_id,const ENUM_BASE_EVENT_REASON reason,const class="type">class="kw">double value)
  {
   CBaseEvent* event=new CBaseEvent(event_id,reason,value);  class=class="str">"cmt">// 基础事件含原因与数值
   if(event==NULL)
     class="kw">return class="kw">false;
   this.m_list_events_base.Sort();
   if(this.m_list_events_base.Search(event)>WRONG_VALUE)
     {
      class="kw">delete event;
      class="kw">return class="kw">false;
     }
   class="kw">return this.m_list_events_base.Add(event);
  }

事件去重与持仓点数收益的底层写法

在扩展对象里塞事件,先搜后插是防重复的标准动作。Search 返回大于 WRONG_VALUE 说明已存在,Add 失败也直接 delete 掉 event 并返 false,避免悬空指针把 EA 拖崩。 基础事件列表走的是同一套逻辑,只是多了一句 Sort() 再 Search。MT5 里 CArrayObj 派生列表不自动排序,手动 Sort 能保证二分查找命中率,事件量上千时回测速度可能差出 20%~30%。 订单点数利润这个函数值得抄:历史单用 PriceClose-PriceOpen 除以 point 再 round,实时仓位用 tick.bid 或 tick.ask 减开仓价。注意 ORDER_TYPE_CLOSE_BY 和点值为 0 的直接返 0,否则除零会静默出错。 外汇与贵金属杠杆高、点值浮动,这套计算只反映名义点数,实盘盈亏还受滑点与点差挤压,验证时务必用真实品种跑一遍。

MQL5 / C++
if(this.m_list_events.Search(event)>WRONG_VALUE || !this.m_list_events.Add(event))
    {
      class="kw">delete event;
      class="kw">return class="kw">false;
    }
  class="kw">return true;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Add the object base event to the list                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CBaseObjExt::EventBaseAdd(const class="type">int event_id,const ENUM_BASE_EVENT_REASON reason,const class="type">class="kw">double value)
  {
   CBaseEvent* event=new CBaseEvent(event_id,reason,value);
   if(event==NULL)
      class="kw">return class="kw">false;
   this.m_list_events_base.Sort();
   if(this.m_list_events_base.Search(event)>WRONG_VALUE || !this.m_list_events_base.Add(event))
     {
       class="kw">delete event;
       class="kw">return class="kw">false;
     }
   class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Get order profit in points                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int COrder::ProfitInPoints(class="type">void) 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 ? (::round(this.PriceClose()-this.PriceOpen())/point) : type==ORDER_TYPE_SELL ? ::round((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)::round((tick.bid-this.PriceOpen())/point);
      else if(type==ORDER_TYPE_SELL)

◍ 反手时抓取旧单与新单的标识

在持仓反手事件里,系统需要同时拿到反手前与反手后的订单、仓位线索。上面这段接口把八个属性拆成独立取值函数,前四个对应订单类型与 ticket,后四个由订单类型反推仓位方向并取出 ticket。 注意两个被标红的函数返回类型写成了 ENUM_ORDER_TYPE,但变量名 TicketOrderPosPrevious / TicketOrderPosCurrent 实际应返回 long。若直接编译,类型不匹配会在运行时截断 ticket 高位,导致反手事件回查失败。 正确写法应改为 return (long)this.GetProperty(EVENT_PROP_TICKET_ORD_POS_BEFORE); 这类强制转换。开 MT5 把这段贴进自定义事件类,搜 TicketOrderPos 看编译器是否报 ENUM_ORDER_TYPE 与 long 冲突,就能复现该坑。外汇与贵金属杠杆高,事件回查错乱可能让风控逻辑误判仓位,实盘前务必验证。

MQL5 / C++
  ENUM_ORDER_TYPE   TypeOrderPosPrevious(class="type">void)                    const { class="kw">return (ENUM_ORDER_TYPE)this.GetProperty(EVENT_PROP_TYPE_ORD_POS_BEFORE);   }
  class="type">long              TicketOrderPosPrevious(class="type">void)                    const { class="kw">return (ENUM_ORDER_TYPE)this.GetProperty(EVENT_PROP_TICKET_ORD_POS_BEFORE); }
  ENUM_ORDER_TYPE   TypeOrderPosCurrent(class="type">void)                      const { class="kw">return (ENUM_ORDER_TYPE)this.GetProperty(EVENT_PROP_TYPE_ORD_POS_CURRENT);   }
  class="type">long              TicketOrderPosCurrent(class="type">void)                    const { class="kw">return (ENUM_ORDER_TYPE)this.GetProperty(EVENT_PROP_TICKET_ORD_POS_CURRENT);}
  class="type">ENUM_POSITION_TYPE TypePositionPrevious(class="type">void)                    const { class="kw">return PositionTypeByOrderType(this.TypeOrderPosPrevious());                    }
  class="type">class="kw">ulong             TicketPositionPrevious(class="type">void)                    const { class="kw">return this.TicketOrderPosPrevious();                                        }
  class="type">ENUM_POSITION_TYPE TypePositionCurrent(class="type">void)                     const { class="kw">return PositionTypeByOrderType(this.TypeOrderPosCurrent());                    }

「抓反向持仓切换时的前后订单票号」

MT5 的持仓事件里,如果策略在运行中把持仓方向翻转(比如多单平掉再开空单),引擎会同时吐出「切换前」和「切换后」两套订单与持仓属性。上面这组访问函数就是专门把这些信息拆出来用的。 TypeOrderPosPrevious 与 TicketOrderPosPrevious 返回翻转前那笔订单的类型和票号;TypeOrderPosCurrent 与 TicketOrderPosCurrent 对应翻转后当前订单。两者都走 GetProperty 读 EVENT_PROP_TYPE_ORD_POS_BEFORE / _CURRENT 这类事件属性。 TypePositionPrevious 和 TypePositionCurrent 不直接存持仓枚举,而是用 PositionTypeByOrderType 把前面的订单类型映射成 POSITION_TYPE_BUY 或 POSITION_TYPE_SELL。TicketPositionPrevious / TicketPositionCurrent 则直接复用对应订单票号,因为持仓票号在翻转场景下与订单票号一致。 在 EA 里监听 OnTradeTransaction 时,若检测到持仓方向变更事件,直接调这几个方法就能拿到前后八项数据(前订单类型、前票号、当前订单类型、当前票号、前持仓类型、前持仓票号、后持仓类型、后持仓票号),用来做对冲日志或反手统计都很顺手。外汇与贵金属杠杆高,翻转持仓可能瞬间放大敞口,实盘前务必在策略测试器跑通逻辑。

MQL5 / C++
class="type">class="kw">ulong TicketPositionCurrent(class="type">void) const { class="kw">return this.TicketOrderPosCurrent(); }
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) const { class="kw">return (ENUM_ORDER_TYPE)this.GetProperty(EVENT_PROP_TYPE_ORD_POS_BEFORE); }
class="type">long TicketOrderPosPrevious(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TICKET_ORD_POS_BEFORE); }
ENUM_ORDER_TYPE TypeOrderPosCurrent(class="type">void) const { class="kw">return (ENUM_ORDER_TYPE)this.GetProperty(EVENT_PROP_TYPE_ORD_POS_CURRENT); }
class="type">long TicketOrderPosCurrent(class="type">void) const { class="kw">return this.GetProperty(EVENT_PROP_TICKET_ORD_POS_CURRENT); }
class="type">ENUM_POSITION_TYPE TypePositionPrevious(class="type">void) const { class="kw">return PositionTypeByOrderType(this.TypeOrderPosPrevious()); }
class="type">class="kw">ulong TicketPositionPrevious(class="type">void) const { class="kw">return this.TicketOrderPosPrevious(); }
class="type">ENUM_POSITION_TYPE TypePositionCurrent(class="type">void) const { class="kw">return PositionTypeByOrderType(this.TypeOrderPosCurrent()); }
class="type">class="kw">ulong TicketPositionCurrent(class="type">void) const { class="kw">return this.TicketOrderPosCurrent(); }

事件状态映射与开仓封装的落地写法

把交易事件的状态码转成可读文本,核心是一个三元表达式链。CEvent::StatusDescription 先通过 GetProperty(EVENT_PROP_STATUS_EVENT) 取出整型状态,再强转为 ENUM_EVENT_STATUS,依次比对市价挂单、市价持仓、历史挂单、历史持仓、余额、修改六类,命中即返回对应多语言文案,全不命中回退到未知状态。 开仓函数 CTradeObj::OpenPosition 把类型、手数、止损、止盈、魔法码、注释、滑点、成交方式全收进参数表里,默认滑点和 deviation 给到 ULONG_MAX,意味着不强制覆盖终端设置。函数开头先拦掉指标和程序端服务这两种不能下单的运行环境,直接返回 true 跳过,实盘里若你抄这段代码做 EA,要留意这个早期 return 会吞掉日志。 下单前用 SymbolInfoTick 拉一次实时报价,失败就把 GetLastError 写进结果结构并取文案,说明报价中断时不会盲目发单。外汇和贵金属杠杆高,这类封装只解决调用规范,不替你管仓位风险,复制去 MT5 跑之前先把 m_log_level 的级别确认清楚。

MQL5 / C++
class="type">class="kw">string CEvent::StatusDescription(class="type">void) const
  {
   ENUM_EVENT_STATUS status=(ENUM_EVENT_STATUS)this.GetProperty(EVENT_PROP_STATUS_EVENT);
   class="kw">return
     (
       status==EVENT_STATUS_MARKET_PENDING     ?  CMessage::Text(MSG_EVN_STATUS_MARKET_PENDING)  :
       status==EVENT_STATUS_MARKET_POSITION    ?  CMessage::Text(MSG_EVN_STATUS_MARKET_POSITION) :
       status==EVENT_STATUS_HISTORY_PENDING    ?  CMessage::Text(MSG_EVN_STATUS_HISTORY_PENDING) :
       status==EVENT_STATUS_HISTORY_POSITION   ?  CMessage::Text(MSG_EVN_STATUS_HISTORY_POSITION) :
       status==EVENT_STATUS_BALANCE            ?  CMessage::Text(MSG_LIB_PROP_BALANCE)           :
       status==EVENT_STATUS_MODIFY             ?  CMessage::Text(MSG_EVN_REASON_MODIFY)          :
       CMessage::Text(MSG_EVN_STATUS_UNKNOWN)
     );
  }

class="type">bool CTradeObj::OpenPosition(const class="type">ENUM_POSITION_TYPE type,
                             const class="type">class="kw">double volume,
                             const class="type">class="kw">double sl=class="num">0,
                             const class="type">class="kw">double tp=class="num">0,
                             const class="type">class="kw">ulong magic=ULONG_MAX,
                             const class="type">class="kw">string comment=NULL,
                             const class="type">class="kw">ulong deviation=ULONG_MAX,
                             const ENUM_ORDER_TYPE_FILLING type_filling=WRONG_VALUE)
  {
   if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
      class="kw">return true;
   ::ResetLastError();
   if(!::SymbolInfoTick(this.m_symbol,this.m_tick))
     {
      this.m_result.retcode=::GetLastError();
      this.m_result.comment=CMessage::Text(this.m_result.retcode);
      if(this.m_log_level>LOG_LEVEL_NO_MSG)
把形态扫描交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到外包线方向标记,你只需核对突破过滤器是否到位。

常见问题

BUOVB 需突破信号柱最高价加 5–10 点过滤器,BEOVB 需跌破信号柱最低价减同样过滤器,二者方向互斥不可混用。
相邻烛形尺寸比例是判定形态图形的关键,新增真实属性把总数扩到 12,便于后续统一按变量检索形态。
可以,小布盯盘品种页已内置 AIGC 形态诊断,会自动区分看涨与看跌外包线并提示突破位。
该枚举值表示库存费以盈利货币计,加入 Data.mqh 消息索引后库能正确识别这类账户 swap 模式。
不能简单复用,本篇把访问方法收敛为单函数按变量分支,比上篇每形态独立方法更短平快。