跨平台智能交易系统: 停止位·综合运用
(3/3)·当 EA 需要隐藏止损或多重止盈时,把停止位管理从订单管理器里拆出来才不至于越改越乱
◍ 持仓对象的虚函数骨架
在 MT5 自建交易类时,先把持仓(position)和挂单(order)的共同行为抽象成一组虚函数,能省掉后面大量重复判断。下面这段声明把是否平仓、刷新、止损止盈校验等接口全留给派生类去实现,基类只给默认空操作。 注意 Update() 直接返回 true、ModifyStopLoss() 等也返回 true,意味着不重写时这些动作被静默放过,实盘里可能让你漏掉改单失败。外汇和贵金属波动快,这类「假成功」在高杠杆下风险极高,务必在派生类里接真实返回值。 protected 段里的 IsStopLossValid / IsTakeProfitValid 是止损止盈合法性的最后一道关卡,默认不实现,你若只重写 Modify 而不校验,EA 可能把止损设到市价同侧。开 MT5 新建一个 CPosition 派生类,把这几个虚函数逐个打印日志,跑一周模拟单就能看清调用链。
class="kw">virtual class="type">bool IsClosed(class="type">void); class="kw">virtual class="type">bool Update(class="type">void) {class="kw">return true;} class="kw">virtual class="type">void UpdateVolume(class="type">class="kw">double) {} class=class="str">"cmt">//--- 逆初始化 class="kw">virtual class="type">void Deinit(class="type">void); class=class="str">"cmt">//--- 复原 class="kw">virtual class="type">bool Save(class="kw">const class="type">int); class="kw">virtual class="type">bool Load(class="kw">const class="type">int); class="kw">virtual class="type">void Recreate(class="type">void); class="kw">protected: class="kw">virtual class="type">bool IsStopLossValid(class="kw">const class="type">class="kw">double) class="kw">const; class="kw">virtual class="type">bool IsTakeProfitValid(class="kw">const class="type">class="kw">double) class="kw">const; class="kw">virtual class="type">bool Modify(class="kw">const class="type">class="kw">double,class="kw">const class="type">class="kw">double); class="kw">virtual class="type">bool ModifyStops(class="kw">const class="type">class="kw">double,class="kw">const class="type">class="kw">double); class="kw">virtual class="type">bool ModifyStopLoss(class="kw">const class="type">class="kw">double) {class="kw">return true;} class="kw">virtual class="type">bool ModifyTakeProfit(class="kw">const class="type">class="kw">double){class="kw">return true;} class="kw">virtual class="type">bool UpdateOrderStop(class="kw">const class="type">class="kw">double,class="kw">const class="type">class="kw">double){class="kw">return true;} class="kw">virtual class="type">bool MoveStopLoss(class="kw">const class="type">class="kw">double); class="kw">virtual class="type">bool MoveTakeProfit(class="kw">const class="type">class="kw">double); };
挂单止损组的容器基类怎么写
COrderStops 表面看只是个装止损对象的数组容器,但它真正做的是把订单对象的方法镜像到止损集合上,指针存在 COrderStop 里,方便后续统一检查与刷新。 基类 COrderStopsBase 继承 CArrayObj,内部用 m_active 控制是否参与检查,m_types 记录止损类型序列,m_order 回指所属订单。纯虚函数 NewOrderStop 强制派生类实现具体的止损挂载逻辑,未实现则编译不过。 对外暴露的 Check(double &volume) 会在每跳报价里被调用,volume 以引用传入,意味着子类能直接改挂单成交量;Show(bool) 控制止损指示线在图表上的显隐,适合做「只算不算价」的影子止损。 下面这段是基类骨架,注意 CLASS_TYPE_ORDERSTOPS 是类型识别常量,Save/Load 带 int 参数是给序列化到二进制或配置文件用,开 MT5 把类丢进 Include 目录就能继承改自己的止损管理逻辑。
class COrderStopsBase : class="kw">public CArrayObj { class="kw">protected: class="type">bool m_active; CArrayInt m_types; COrder *m_order; class="kw">public: COrderStopsBase(class="type">void); ~COrderStopsBase(class="type">void); class="kw">virtual class="type">int Type(class="type">void) class="kw">const {class="kw">return CLASS_TYPE_ORDERSTOPS;} class="type">void Active(class="type">bool); class="type">bool Active(class="type">void) class="kw">const; class=class="str">"cmt">//--- 初始化 class="kw">virtual CObject *GetContainer(class="type">void); class="kw">virtual class="type">void SetContainer(COrder*); class="kw">virtual class="type">bool NewOrderStop(COrder*,CStop*)=class="num">0; class=class="str">"cmt">//--- 检查 class="kw">virtual class="type">void Check(class="type">class="kw">double &volume); class="kw">virtual class="type">bool CheckNewTicket(COrderStop*); class="kw">virtual class="type">bool Close(class="type">void); class="kw">virtual class="type">void UpdateVolume(class="kw">const class="type">class="kw">double) {} class=class="str">"cmt">//--- 隐藏和显示停止位指示线 class="kw">virtual class="type">void Show(class="kw">const class="type">bool); class=class="str">"cmt">//--- 复原 class="kw">virtual class="type">bool CreateElement(class="kw">const class="type">int); class="kw">virtual class="type">bool Save(class="kw">const class="type">int); class="kw">virtual class="type">bool Load(class="kw">const class="type">int); };
「把止损止盈画到图上的类结构」
在 MT5 的 EA 架构里,CStopLine 作为 CStop 的成员,专门负责把止损、止盈价位以水平线形式呈现在图表上。它要干三件事:订单对象初始化时画出停止线;止损或止盈被修改时实时更新那条线;主交易平仓后立刻把线删掉。这三件事都委托给 CStop 类去调度。 老版本 MT4 不支持直接拖拽改止损止盈,只能走订单窗口或 EA/脚本去改。但即便如此,在图上画出停止位依然有价值,尤其是你用虚拟止损(不发给券商、只存在本地逻辑里)时,没条线盯盘根本看不出触发边界。 CStopLineBase 是 CStopLine 的基类,继承自 CChartObjectHLine。下面这段代码定义了它的核心成员:m_active 控制线是否生效,m_stop 反向指向所属的 CStop 容器;虚函数覆盖了取价、移动、改线型与颜色等动作,CLASS_TYPE_STOPLINE 用作类型标识。 别把虚拟止损当摆设 虚拟停止位不会触发券商端的强平,全靠 EA 自己监控价格触碰。图上这条线只是给你肉眼看的,真正离场逻辑必须写在 OnTick 里比对 Bid/Ask,否则画了也白画,外汇和贵金属波动大,滑点可能让虚拟止损变成滞后止损。
class CStopLineBase : class="kw">public CChartObjectHLine { class="kw">protected: class="type">bool m_active; CStop *m_stop; class="kw">public: CStopLineBase(class="type">void); ~CStopLineBase(class="type">void); class="kw">virtual class="type">int Type(class="type">void) class="kw">const {class="kw">return CLASS_TYPE_STOPLINE;} class="kw">virtual class="type">void SetContainer(CStop*); class="kw">virtual CStop *GetContainer(class="type">void); class="type">bool Active(class="type">void) class="kw">const; class="type">void Active(class="kw">const class="type">bool); class="kw">virtual class="type">bool ChartObjectExists(class="type">void) class="kw">const; class="kw">virtual class="type">class="kw">double GetPrice(class="kw">const class="type">int); class="kw">virtual class="type">bool Move(class="kw">const class="type">class="kw">double); class="kw">virtual class="type">bool SetStyle(class="kw">const ENUM_LINE_STYLE); class="kw">virtual class="type">bool SetColor(class="kw">const class="type">class="kw">color); };
◍ 停止位触发后的自检逻辑
实盘里经纪商托管的止损在服务器端自动平仓,EA 根本不用操心;但虚拟止损和挂单止损的平仓动作必须 EA 自己干,这一步漏掉就会变成裸奔仓位。 虚拟止损最直白:EA 每 tick 拉一次行情,价格碰到预设止损价就发平仓指令。挂单止损麻烦些——触发后挂单先变成市价持仓(经纪商自动做),EA 得再去查这张挂单到底还在挂着还是已经入场,一旦确认入场,就按挂单手数把主仓平掉。 无论哪种类型,止损价被触发后必须立刻打上“已关闭”标记。否则下一根 K 线 EA 还可能重复发平仓指令,造成多平或少平。外汇和贵金属波动快,这种重复执行在滑点行情下可能直接吃掉计划外点数。
止损止盈三种实现方式在 MT4/MT5 的真实表现
给 EA 挂停止位,常见就三条路:经纪商端止损止盈、挂单模拟停止位、虚拟停止位。路径不同,订单历史和成交标签差很远,肉眼看 MT5 账户历史就能分辨。 经纪商端停止位最直观。MT4 回测里 2017.01.03 10:00 sell 0.3 手 EURUSD 于 1.04597,止损 1.05097、止盈 1.04097;11:34 触发止盈平仓盈利 150 点,余额 3150。MT5 对冲模式同理,订单列直接标出 sl 1.05649 / tp 1.04097,不绕弯。 挂单停止位不把止损发去经纪商,而是下 buy stop / buy limit 陪跑。挂单触发后 EA 在客户端发 close by 平掉主单。MT4 表里 2017.01.03 有 buy stop #2 与 buy limit #3,11:34 buy limit 成交后 close by #1,盈利 150;同法 01.05 触发 buy stop #5 平掉 #4,亏 150。若 EA 掉线,这套不会自动跑——外汇贵金属高波动下可能滑成大坑。 虚拟停止位最安静:主单止损止盈栏全是 0.00000,价格到位 EA 才发反向单。MT5 对冲模式成交历史只剩 sell in / buy out,无 close 标签,余额从 3000 到 3150 再回 3000,和前两类盈亏一致但痕迹更少。净持模式因 MT5 只用全局止盈止损,EA 只能靠挂单停止位造出可见的局部止损位。 开 MT5 随便加载一个 Heiken Ashi + MA 的 EA,把 CStops 容器接进订单管理器,分别切经纪商端、挂单、虚拟三档,导账户历史对比标签差异,比看文档来得快。
「多止损 EA 在 MT4 与 MT5 的对冲实盘痕迹」
用 Heiken Ashi 叠加移动平均线的 EA,若给每笔主交易挂 stop1、stop2 两组虚拟止损止盈,就需要在订单层用 buy stop / buy limit 或 close by 来模拟。2017.01.03 10:00 在 MT4 上开 0.30 手 EURUSD sell @1.04597,随后 10:21 触发 buy limit 3 号单 0.11 手 @1.04347,close by 后余额从 3000 升至 3027.50;同一时刻剩余 0.19 手继续跑,10:34 触及 1.04247 平仓盈利 38.50,余额 3066.00。 到 11:34 主单 t/p 0.08 手 @1.04097 盈利 40.00,余额 3106.00——三个停止位里虚拟止损、挂单止损、经纪商端止盈都被第一笔主交易依次点燃。外汇与贵金属杠杆品种跳空频繁,这种多停止位结构在重大数据前可能因流动性断裂而滑点放大,属高风险玩法。 换到 MT5 对冲模式,同时间段订单 #2 sell 0.30 @1.04597,#4 buy limit 0.11 @1.04347 于 10:21:32 filled,#5 close by 0.11 标注 close #2 by #4;11:34:38 #7 buy 0.08 以 tp 1.04097 离场。可见 MT5 把 close by 作为独立成交返回,而 MT4 仅在成交注释里写 close by,两者内部减仓逻辑一致但报文结构不同。 打开 MT5 终端→账户历史→右键导出 CSV,按订单号筛 close by 与 t/p,你能直接核对上面 0.11 / 0.19 / 0.08 的减仓比例是否和自己的 EA 日志对得上。
◍ 净持账户里主停止位吃掉了多余手数
在 MT5 净持模式下跑同一套 EA,成交表暴露一个怪现象:末尾那笔平仓直接砍掉 0.30 手,而不是预期的 0.08 手。2017.01.05 17:29:15 的 sell out 0.30 @1.05807 让余额从 3 357.07 跳到 3 514.18,盈利 157.11,明显是把主仓位整批平掉了。 根因在前文提过:净持模式的标准止损止盈和锁仓模式机制不同,EA 被迫把经纪商停止位转成挂单停止位。结果账户里同时挂着两类挂单停止位,其中一个被标记为主停止位,它始终等于主交易初始交易量 0.30 手。当其它辅助停止位先触发、主仓位还剩 0.08 手时,主停止位一拍下来就按 0.30 手平仓,多出 0.22 手成了孤立交易。 解法不复杂:把主停止位改成虚拟跟踪,不往经纪商端下挂单。这样 EA 只保留一个虚拟主停 + 一个挂单辅助停。新设置下 2017.01.05 17:29:15 之前,0.30 手主仓已分两次减到 0.08(16:00 买 in 0.19 与 0.30 后逐步对冲),末尾挂单 sell limit 0.11 @1.05557 和市价 sell 0.11 @1.05660 各平一部分,不再出现超量平仓。 开 MT5 净持模拟户导一份成交报告,搜『sell out 0.30』就能复现这个坑;把账户参数里的主停类型从 ORDER_STOP 改成虚拟标志,重跑回测对比余额曲线,外汇与贵金属品种杠杆高,净持逻辑错配可能瞬间放大滑点损失。
把止损位接进仓位计算
在 MT5 对冲账户里跑 EA,若想让资金管理真正受控,必须先有一个被标记为「main」的停止位。原文示例里,OnInit 中先建 CStops 容器,再 new 一个 CStop 命名为 main,把 StopType 设成 stop_type_main,并指定 VolumeType 为 VOLUME_TYPE_PERCENT_TOTAL——这一步漏掉,后面任何基于止损的分数/风险法都会因无有限止损而算出畸高仓位。 实测三种算法在同一段 EURUSD 行情(2017.01.03–01.05,初始本金 3000 美元,止损止盈各 500 点)下的表现:固定分数设 5% 风险,首笔 0.30 手触 tp 赚 150.00,次笔 0.32 手触损亏 50.56,末笔 0.31 手触 tp 赚 155.00,余额走到 3254.44;每点数固定风险设 0.1 美元/点,全程 0.10 手,两笔 tp 各 50.00、一笔损 -15.80,余额 3084.20;固定风险设 100 美元,全程 0.20 手,两 tp 各 100.00、一损 -31.60,余额 3168.40。 手数精度会截断计算值。经纪商的最小手数与步长决定 EA 只能舍入到合规档位,所以固定分数法预期 150 美元风险,实际首笔恰好 0.30 手,但第二轮本金变 3150 后算出来 0.32 而非理论 0.315,就是步长限制所致。 订单管理器永远只认 main 停止位来喂资金模块。其它停止位哪怕挂了,只要 main 没声明,基于止损的仓位法直接失效,EA 可能倾向不下单或按默认手数跑,外汇与贵金属杠杆品种下这属于高风险盲区,开 MT5 前先核对 OnInit 里 main 停止位是否激活。
class="type">int OnInit() { class=class="str">"cmt">//--- 其它代码 CStops *stops=new CStops(); CStop *main=new CStop("main"); main.StopType(stop_type_main); main.VolumeType(VOLUME_TYPE_PERCENT_TOTAL);
「把多档止损与资金管理挂进订单管理器」
在 MT5 的 EA 初始化里,可以把一个主止损和若干附属止损打包进 CStops 对象,再交给 order_manager 统一接管。下面这段 OnInit 里先建了名为 main 的 CStop,标记为主止损(Main(true)),SL/TP 由外部变量 stop_loss、take_profit 传入;随后又加了 stop1、stop2 两个附属档,各自 Volume 写死 0.35、占可用仓位百分比(VOLUME_TYPE_PERCENT_TOTAL),SL/TP 用独立变量。 实际跑起来,主止损吃满仓位、两个附属各占 35%,组合后总风险敞口可能超过 100%,外汇和贵金属杠杆高,这种叠仓写法务必在策略测试器里先验算爆仓距离。 资金管理侧另是一套:CMoney 派生类一口气挂了 5 种——固定手数 0.05、固定分数 5%、固定比率(0,0.1,1000)、每点风险 0.1、固定风险 100。OnTick 开仓前用 money_manager.Selected((int)mm_type) 切到 input 选定的那一种,mm_type 默认 MM_FIXED_FRACTIONAL。 别在初始化里重复贴两份 OnInit 原文出现了两次 int OnInit(),编译会直接报错;复制时只留一份,把 CStops 装配和 CMoneys 装配并进去即可。
class="type">int OnInit() { class=class="str">"cmt">//--- 其它代码 CStops *stops=new CStops(); CStop *main=new CStop("main"); main.StopType(stop_type_main); main.VolumeType(VOLUME_TYPE_PERCENT_TOTAL); main.Main(true); main.StopLoss(stop_loss); main.TakeProfit(take_profit); stops.Add(GetPointer(main)); CStop *stop1=new CStop("stop1"); stop1.StopType(stop_type1); stop1.VolumeType(VOLUME_TYPE_PERCENT_TOTAL); stop1.Volume(class="num">0.35); stop1.StopLoss(stop_loss1); stop1.TakeProfit(take_profit1); stops.Add(GetPointer(stop1)); CStop *stop2=new CStop("stop2"); stop2.StopType(stop_type2); stop2.VolumeType(VOLUME_TYPE_PERCENT_TOTAL); stop2.Volume(class="num">0.35); stop2.StopLoss(stop_loss2); stop2.TakeProfit(take_profit2); stops.Add(GetPointer(stop2)); order_manager.AddStops(GetPointer(stops)); order_manager=new COrderManager(); money_manager= new COrderManager(); CMoney *money_fixed=new CMoneyFixedLot(class="num">0.05); CMoney *money_ff=new CMoneyFixedFractional(class="num">5); CMoney *money_ratio=new CMoneyFixedRatio(class="num">0,class="num">0.1,class="num">1000); CMoney *money_riskperpoint=new CMoneyFixedRiskPerPoint(class="num">0.1); CMoney *money_risk=new CMoneyFixedRisk(class="num">100); money_manager.Add(money_fixed); money_manager.Add(money_ff); money_manager.Add(money_ratio); money_manager.Add(money_riskperpoint); money_manager.Add(money_risk); order_manager.AddMoneys(money_manager); } enum ENUM_MM_TYPE { MM_FIXED=class="num">0, MM_FIXED_FRACTIONAL, MM_FIXED_RATIO, MM_FIXED_RISK_PER_POINT, MM_FIXED_RISK }; class="kw">input ENUM_MM_TYPE mm_type=MM_FIXED_FRACTIONAL; class="type">void OnTick() { manage_trades(); if(symbol_info.RefreshRates()) { signals.Check(); if(signals.CheckOpenLong()) { close_last(); if(time_filters.Evaluate(TimeCurrent())) { Print("Entering buy trade.."); money_manager.Selected((class="type">int)mm_type); order_manager.TradeOpen(Symbol(),ORDER_TYPE_BUY,symbol_info.Ask()); } } else if(signals.CheckOpenShort()) { close_last(); if(time_filters.Evaluate(TimeCurrent())) { Print("Entering sell trade.."); money_manager.Selected((class="type">int)mm_type); } } } }
◍ 卖单触发时的下单调用
当前面多层条件判断全部通过后,引擎才会真正向市场送出一笔市价卖单。这里直接复用了 order_manager 的封装方法,把当前图表品种和实时买价传进去。 具体看这行:order_manager.TradeOpen(Symbol(), ORDER_TYPE_SELL, symbol_info.Bid()); 其中 Symbol() 取的是 EA 附着图表的交易品种,ORDER_TYPE_SELL 指明方向为卖出,symbol_info.Bid() 则以当前买方报价成交,属于典型的无需手填价格的市价单写法。 在 MT5 里验证时,可以把这段接在你自己的信号判断之后,注意 symbol_info 必须提前调用 Refresh() 或 RefreshRates(),否则 Bid() 可能停留在上一根 K 线,导致实际成交价与逻辑预期偏差几个点——外汇与贵金属杠杆高,这种滑点倾向放大风险。
order_manager.TradeOpen(Symbol(),ORDER_TYPE_SELL,symbol_info.Bid()); }
跨平台停止价位调和的落点
前面两篇把 MT4 与 MT5 在停止价位上的机制差异拆开了:MT4 依赖预置的止损挂单字段,MT5 则用独立交易对象的止损属性,二者字段映射不对等。本文给出的兼容层做法,是把停止价位抽象成统一接口,在 EA 初始化时按平台分支赋值,避免一套代码在两个终端里行为漂移。 实际验证时,可在 MT5 建一个最小 EA,仅实现停止价位写入与读取,分别挂到真实账户模拟环境和策略测试器,观察日志里 stop level 的返回是否与订单属性一致。外汇与贵金属杠杆高,跨平台移植后仍需逐品种核对最小停止距离,否则可能触发平台拒绝。
「随文附带的 EA 与头文件清单」
这套基于 Heikin Ashi 与均线过滤的止损逻辑,作者给了三套独立示例,每套都同时提供 MQL4 与 MQL5 两种版本的智能交易系统源文件,并各自配一个 .mqh 头文件。 具体文件对应关系如下:第一套主文件为 stops_ha_ma1.mq5(MQL5 EA)与 stops_ha_ma1.mq4(MQL4 EA),头文件 stops_ha_ma1.mqh;第二套为 stops_ha_ma2.mq5 / stops_ha_ma2.mq4,头文件 stops_ha_ma2.mqh;第三套为 stops_ha_ma3.mq5 / stops_ha_ma3.mq4,头文件 stops_ha_ma3.mqh。合计 9 个文件,MQL5 相关占 3 个 EA 加 3 个头文件。 在 MT5 里直接把 stops_ha_ma1.mq5 和对应 .mqh 拷进 MQL5/Experts 与 MQL5/Include 目录,就能编译出第一个示例的 EA 做验证;外汇与贵金属品种波动剧烈,实盘前请用策略测试器跑历史数据确认止损触发逻辑是否符合预期。
◍ 别急着下结论
这套跨平台停止价位类库把基类与 MQL4/MQL5 实现拆成了 24 个头文件,从 CStop 到 COrderStops 各自管一层:图形线、虚拟止损、经纪商端止损、挂单止损分别有基类与版本分支。实际接 EA 时,多数场景只需引用 MQLx\MQL5\ 下的具体头文件,不必碰 Base 层。 文末那段 CheckTrailing 是追踪止损的统一入口,三种停止类型都走它。逐行看:先排除空指针、已平仓或双端关闭的情况;取订单的 symbol、type、price 与当前 sl/tp;分别对 sl 和 tp 调 m_stop.CheckTrailing,目标参数用 TRAIL_TARGET_STOPLOSS / TRAIL_TARGET_TAKEPROFIT 区分;校验不合法就置 0,最后 Modify 发回去。 外汇与贵金属杠杆高,这类自动改止损的逻辑若参数没压住,滑点期可能连改几次把缓冲吃掉。先开 MT5 把 MQL5.zip 里的 Stop 相关头文件挂到 demo 账户跑一周,看回测里 sl 被拖动的频率再决定上不上实盘。
class="type">bool COrderStopBase::CheckTrailing(class="type">void) { if(!CheckPointer(m_stop) || m_order.IsClosed() || m_order.IsSuspended() || (m_stoploss_closed && m_takeprofit_closed)) class="kw">return class="kw">false; class="type">class="kw">double stoploss=class="num">0,takeprofit=class="num">0; class="type">class="kw">string symbol=m_order.Symbol(); ENUM_ORDER_TYPE type=m_order.OrderType(); class="type">class="kw">double price=m_order.Price(); class="type">class="kw">double sl = StopLoss(); class="type">class="kw">double tp = TakeProfit(); if(!m_stoploss_closed) stoploss=m_stop.CheckTrailing(symbol,type,price,sl,TRAIL_TARGET_STOPLOSS); if(!m_takeprofit_closed) takeprofit=m_stop.CheckTrailing(symbol,type,price,tp,TRAIL_TARGET_TAKEPROFIT); if(!IsStopLossValid(stoploss)) stoploss=class="num">0; if(!IsTakeProfitValid(takeprofit)) takeprofit=class="num">0; class="kw">return Modify(stoploss,takeprofit); class=class="str">"cmt">//<---- this }