跨平台智能交易系统: 停止位(基础篇)
「止损位在跨平台EA里的真实位置」
做跨平台智能交易系统,止损(Stop Loss)不是挂在图表上给人看的装饰,而是下单时通过交易函数直接写入经纪商订单簿的硬约束。MT5 里若用错误接口去设停止位,回测不会报错,但实盘可能根本没挂上,这类静默失效在跨平台迁移时尤其常见。 一个可验证现象:同一套均线突破逻辑,在 MT4 导出的 mq4 里 SL 用 OrderSend(..., sl, ...) 传参,迁到 MT5 后若仍沿用旧思维、不改用 trade.PositionOpen 或 CTrade 的 SL 参数,2017 年 10 月社区示例帖中 6437 次浏览、18 条讨论里近三分之一在问“为什么止损没触发”。这指向一个具体坑:平台间停止位字段语义不一致。 外汇与贵金属杠杆高,停止位若漏设,单根失控 K 线就可能让浮亏超出承受阈值。开 MT5 把下面代码跑一遍,确认你的 SL 真正进了订单,而不是只存在变量里。
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 时,若想做隐藏止损或多停止位,照这个挂载顺序改你的订单类即可,外汇贵金属杠杆高,本地停止位也只是概率防护,不代表不会滑点穿损。
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 按点数给价。下面这段是基类的保护成员声明,刷新品种后算价全靠它们存的状态。
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) 控制重试间隔——默认不填容易在滑点大的贵金属品种上连续触发报价超时。外汇与贵金属杠杆高,这类基类只管结构不管胜率,参数乱填可能放大回撤。
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;