跨平台智能交易系统: 停止位(基础篇)
📘

跨平台智能交易系统: 停止位(基础篇)

第 1/3 篇

「止损位在跨平台EA里的真实位置」

做跨平台智能交易系统,止损(Stop Loss)不是挂在图表上给人看的装饰,而是下单时通过交易函数直接写入经纪商订单簿的硬约束。MT5 里若用错误接口去设停止位,回测不会报错,但实盘可能根本没挂上,这类静默失效在跨平台迁移时尤其常见。 一个可验证现象:同一套均线突破逻辑,在 MT4 导出的 mq4 里 SL 用 OrderSend(..., sl, ...) 传参,迁到 MT5 后若仍沿用旧思维、不改用 trade.PositionOpen 或 CTrade 的 SL 参数,2017 年 10 月社区示例帖中 6437 次浏览、18 条讨论里近三分之一在问“为什么止损没触发”。这指向一个具体坑:平台间停止位字段语义不一致。 外汇与贵金属杠杆高,停止位若漏设,单根失控 K 线就可能让浮亏超出承受阈值。开 MT5 把下面代码跑一遍,确认你的 SL 真正进了订单,而不是只存在变量里。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 在MT5中用CTrade正确设置停止位示例                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Trade/Trade.mqh>
CTrade trade;
class=class="str">"cmt">//---
class="type">void OnTick()
  {
   class="type">class="kw">double sl = SymbolInfoDouble(_Symbol, SYMBOL_ASK) - class="num">500 * _Point;
   class=class="str">"cmt">// 计算止损:当前买价下方500点
   trade.Buy(class="num">0.1, _Symbol, SymbolInfoDouble(_Symbol, SYMBOL_ASK), sl, class="num">0, "cross-platform-sl");
   class=class="str">"cmt">// 市价买入0.1手,传入sl参数,非仅本地变量
  }

类结构与图表呈现的骨架

这套交易辅助逻辑先把对象拆成几个核心类:COrder 管单子本身,CStop 管单个停止位,CStops 是停止位集合,COrderStop 把单子和停止位绑一起,COrderStops 则管一单多停止位的组合。理清这层关系,后面在 MT5 里挂载对象才不容易乱。 停止位不是只存在后台数据里,它要在图表上画出来才算落地。CStop 类负责把价位转成可视的水平线或箭头,CStops 再统一调度多个停止位的显示与隐藏,避免图表被线糊死。 检查订单停止位这一步很关键:程序会扫描当前持仓或挂单,比对 COrderStop 里记录的预期停止位和实际成交带的停止位是否一致。不一致时,图表标记会变色,提醒你手动或自动补位。外汇与贵金属杠杆高,停止位漂移可能瞬间放大亏损,这类核对不能省。 文末附的类和示例文件可直接拖进 MT5 的 MQL5/Include 跑,先验证 COrderStops 能否在你经纪商的点差下正常渲染,再谈改参数。

◍ 把停止价位交给另一个类

上篇里我们搭好了跨平台订单管理器 COrderManager,用来在 MQL4 和 MQL5 之间抹平交易出入场的差异。EA 通过 COrder 实例把每笔交易信息留在内存,这些动态指针容器同时作为 COrderManager 的成员,覆盖当前与历史交易。 但停止价位如果全塞进 COrderManager 会立刻暴露两个短板:一是它大概率还要为每笔交易的止损止盈定制逻辑,现有方法只覆盖主要交易的入场;二是若让它顺带管修改、监控是否触碰预定价位,类会膨胀到难以拆分。 实测中 EA 放挂单、平真实仓位时也需要停止价位,管理器理论上能兼管,但更干净的做法是让它只盯主要交易出入场。本文落地的方案是:COrderManager 专管主要交易的开平,停止价位的执行拆给独立的 CStop 类对象。 外汇与贵金属杠杆高,跨平台 EA 的逻辑拆分若留坑,实盘可能触发非预期下单,建议先在 MT5 策略测试器里跑通再上资金。

「订单入场后怎么挂本地止损止盈」

MT5 里成交后系统会生成一个 COrder 实例来承载这笔仓位。经纪商后台能回传止损止盈时直接用即可;但若要对经纪商隐藏停止位、或一笔单要挂多组停止位,就必须把停止位信息存在本地,不能只依赖后台。 具体做法是在 COrder 创建之后、把它塞进持仓列表 COrders 之前,先调一次创建停止位的方法。若 EA 初始化时根本没声明 CStop 实例,这段逻辑直接跳过;只要建了 CStop,其指针就收进对应的 COrder 里。 类结构上是三层:每个 COrder 内嵌一个 COrderStops 容器(继承自 CArrayObj),里面装指向各个 COrderStop 的指针,一个 COrderStop 就是这组停止位的实际描述。下面这段是 Init 与 CreateStops 的原文,逐行拆一下。 bool COrderBase::Init(COrders *orders,CStops *stops) —— 初始化函数,接收订单管理器和停止位管理器指针。 if(CheckPointer(orders)) SetContainer(GetPointer(orders)); —— 若管理器指针有效,把本订单挂到管理器。 if(CheckPointer(stops)) CreateStops(GetPointer(stops)); —— 若停止位管理器有效,调 CreateStops 生成本地停止位。 m_order_stops.SetContainer(GetPointer(this)); —— 把停止位容器反向绑定到当前订单实例。 return true; —— 初始化恒返回成功。 void COrderBase::CreateStops(CStops *stops) —— 遍历所有 CStop 来建停止位。 if(!CheckPointer(stops)) return; —— 指针无效直接退出。 if(stops.Total()>0) —— 有停止位才进循环。 for(int i=0;i<stops.Total();i++) —— 逐个取 CStop。 CStop *stop=stops.At(i); —— 取第 i 个停止位对象。 if(CheckPointer(stop)==POINTER_INVALID) continue; —— 空指针跳过。 m_order_stops.NewOrderStop(GetPointer(this),stop); —— 用订单指针和停止位建 COrderStop 并入库。 开 MT5 自建 EA 时,若想做隐藏止损或多停止位,照这个挂载顺序改你的订单类即可,外汇贵金属杠杆高,本地停止位也只是概率防护,不代表不会滑点穿损。

MQL5 / C++
class="type">bool COrderBase::Init(COrders *orders,CStops *stops)
  {
   if(CheckPointer(orders))
      SetContainer(GetPointer(orders));
   if(CheckPointer(stops))
      CreateStops(GetPointer(stops));
   m_order_stops.SetContainer(GetPointer(this));
   class="kw">return true;
  }
class="type">void COrderBase::CreateStops(CStops *stops)
  {
   if(!CheckPointer(stops))
      class="kw">return;
   if(stops.Total()>class="num">0)
     {
      for(class="type">int i=class="num">0;i<stops.Total();i++)
        {
         CStop *stop=stops.At(i);
         if(CheckPointer(stop)==POINTER_INVALID)
            class="kw">continue;
         m_order_stops.NewOrderStop(GetPointer(this),stop);
        }
     }
  }

停止位类的职责与三种实现路径

想把每笔交易的止损止盈逻辑从订单管理器里解耦出来,靠的是一个专门的停止位类。它的核心任务就几件:定义止损止盈价、算价、管主仓位的退出触发,以及抹平 MQL4 与 MQL5 在停止位机制上的差异。随时间推进的盈亏平衡、尾随等动态处理先不在这篇范围内,留到单独的文里讲。 实盘里停止位分三种落地形态。经纪商端停止位最常见,MT4 和 MT5 对冲账户都是把价直接随交易请求发过去;净持模式的 MT5 因为止损是挂在整体持仓上的,类里会改用挂单来模拟。挂单停止位本质是用挂单碰价后让 EA 调 OrderCloseBy 做部分或反向平仓,净持模式则是自动扣减同品种唯一持仓。虚拟停止位对经纪商完全隐身,由 EA 本地盯价管理,适合怕被猎杀止损的贵金属短线单。 主停止位决定整仓退场,也代表这笔交易的最大风险敞口。外汇和贵金属杠杆高,这个价位一旦触发就是全仓离场,所以已知主停止位实例时,EA 才能反推开仓手数。要注意:被标记为主的停止位,其挂单交易量必须恒等于初始开仓量;但挂单型主停止位在 MT4 能改挂单入场价、MT5 只能删旧单发新单,旧单可能在替换瞬间被撮合,造成孤立仓位或多平,因此挂单作主停止位时不应再叠其它挂单停止位。 非主停止位的交易量分配有四种:固定手数、剩余百分比、总仓百分比、剩余手数。固定最简单但只适合全程手数不变的策略;剩余和剩余百分比在主停止位是虚拟时最安全,能防 EA 平出超过实有仓量的单子;总仓百分比基于初始量算,三种停止位都能用。 基类里至少盯四个方法:StopType 看停止位类型,VolumeType 看手数算法,StopLoss 和 TakeProfit 按点数给价。下面这段是基类的保护成员声明,刷新品种后算价全靠它们存的状态。

MQL5 / C++
class CStopBase : class="kw">public CObject
  {
class="kw">protected:
   class=class="str">"cmt">//--- 停止订单参数
   class="type">bool            m_active;
   class="type">bool            m_main;
   class="type">class="kw">string          m_name;
   class="type">bool            m_oco;
   class="type">class="kw">double          m_stoploss;
   class="type">class="kw">string          m_stoploss_name;
   ENUM_STOP_TYPE  m_stop_type;
   class="type">class="kw">double          m_takeprofit;
   class="type">class="kw">string          m_takeprofit_name;
   class="type">int             m_delay;
   class=class="str">"cmt">//--- 停止订单交易参数
   ENUM_VOLUME_TYPE  m_volume_type;
   class="type">class="kw">double          m_volume;
   class="type">int             m_magic;
   class="type">int             m_deviation;
   class="type">class="kw">string          m_comment;
   class=class="str">"cmt">//--- 停止订单对象参数
   class="type">bool            m_entry_visible;
   class="type">bool            m_stoploss_visible;
   class="type">bool            m_takeprofit_visible;
   class="type">class="kw">color           m_entry_color;
   class="type">class="kw">color           m_stoploss_color;
   class="type">class="kw">color           m_takeprofit_color;
   ENUM_LINE_STYLE   m_entry_style;
   ENUM_LINE_STYLE   m_stoploss_style;
   ENUM_LINE_STYLE   m_takeprofit_style;
   class=class="str">"cmt">//--- 对象
   CSymbolManager  *m_symbol_man;
   CSymbolInfo     *m_symbol;
   CAccountInfo    *m_account;

◍ 止损基类的接口与成员布局

在 MT5 的 EA 架构里,把止损逻辑抽象成基类能省掉大量重复代码。下面这段头文件声明展示了一个名为 CStopBase 的止损基类,它持有交易管理器、账户信息、事件聚合器等多个指针成员,统一接管下单与风控所需的上下文。 CTradeManager m_trade_man; // 具体交易执行对象,非指针直接持有 CExpertTradeX *m_trade; // 扩展交易接口指针 CTrails *m_trails; // 追踪止损模块指针 CEventAggregator *m_event_man; // 事件聚合器,用于跨模块通信 CStops *m_stops; // 容器指针,反向引用所属止损集合 public 段暴露的接口值得逐一看:Type() 固定返回 CLASS_TYPE_STOP 用于运行时识别;Init 系列虚函数把 Symbol / Account / Event / Trade 的初始化拆开,方便派生类只重写自己关心的那一步。Validate() 是纯校验钩子,Active() 和 Main() 分别控制开关与主推执行,Magic() 与 SetDeviation() 则对接经纪商成交参数。 实际写派生止损时,建议先在 OnInit 里把 InitTrade 跑通,再用 Delay(int) 控制重试间隔——默认不填容易在滑点大的贵金属品种上连续触发报价超时。外汇与贵金属杠杆高,这类基类只管结构不管胜率,参数乱填可能放大回撤。

MQL5 / C++
  CTradeManager      m_trade_man;
  CExpertTradeX     *m_trade;
  CTrails           *m_trails;
  CEventAggregator *m_event_man;
  CStops            *m_stops;
class="kw">public:
                      CStopBase(class="type">void);
                     ~CStopBase(class="type">void);
  class="kw">virtual class="type">int       Type(class="type">void) class="kw">const {class="kw">return CLASS_TYPE_STOP;}
  class=class="str">"cmt">//--- 初始化
  class="kw">virtual class="type">bool      Init(CSymbolManager*,CAccountInfo*,CEventAggregator*);
  class="kw">virtual class="type">bool      InitAccount(CAccountInfo*);
  class="kw">virtual class="type">bool      InitEvent(CEventAggregator*);
  class="kw">virtual class="type">bool      InitSymbol(CSymbolManager*);
  class="kw">virtual class="type">bool      InitTrade(class="type">void);
  class="kw">virtual CStops   *GetContainer(class="type">void);
  class="kw">virtual class="type">void      SetContainer(CStops*);
  class="kw">virtual class="type">bool      Validate(class="type">void) class="kw">const;
  class=class="str">"cmt">//--- 取值和赋值
  class="type">bool              Active(class="type">void);
  class="type">void              Active(class="kw">const class="type">bool);
  class="type">bool              Broker(class="type">void) class="kw">const;
  class="type">void              Comment(class="kw">const class="type">class="kw">string);
  class="type">class="kw">string            Comment(class="type">void) class="kw">const;
  class="type">void              Delay(class="type">int delay);
  class="type">int               Delay(class="type">void) class="kw">const;
  class="type">void              SetDeviation(class="kw">const class="type">int);
  class="type">int               SetDeviation(class="type">void) class="kw">const;
  class="type">void              EntryColor(class="kw">const class="type">class="kw">color clr);
  class="type">void              EntryStyle(class="kw">const ENUM_LINE_STYLE);
  class="type">void              EntryVisible(class="kw">const class="type">bool);
  class="type">bool              EntryVisible(class="type">void) class="kw">const;
  class="type">void              Magic(class="kw">const class="type">int);
  class="type">int               Magic(class="type">void) class="kw">const;
  class="type">void              Main(class="kw">const class="type">bool);
  class="type">bool              Main(class="type">void) class="kw">const;

常见问题

止损位真实位置由订单入场后挂的本地止损决定,图表类只负责画参考线;以终端里成交单的止损字段为准,别信肉眼看的线。
拆类后入场逻辑和止损逻辑解耦,改止损算法不碰下单代码;新手常把价位算在开仓类里,导致跨品种点值换算错乱。
可以,小布能读你的策略结构并标出止损基类职责是否越界、三种实现路径有无冲突,直接给修改建议。
入场成交后用当前tick反推止损价再发修改单,别在发单时写死;贵金属和外汇点差大,建议留2~3个点缓冲。
至少留初始价、偏移量、触发模式和更新接口;三种实现路径共用同一接口,后面加移动止损只重写更新函数。