本文讨论如何在跨平台智能交易系统中设置自定义停止价位。它还讨论了一种紧密相关的方法, 即随着时间的推移, 定义停止位的演化。(基础篇)
自定义止损位的演化逻辑
在 MT5 的智能交易系统里,停止价位不一定非要在下单那一刻写死。更实用的做法是把它当成一个随时间推移而演化的变量:开仓后根据价格行为逐步移动止损或止盈,而不是依赖平台自带的固定距离尾随。 原文给出的示例发布于 2017 年 10 月 24 日,累计浏览 6420 次、评论 15 条,说明这类自定义停止位的需求在 MQL5 社区一直有真实交易量级的使用场景。 具体实现上,作者把停止位拆成「自定义停止位」「修改停止价位」「盈亏平衡」「尾随止损」「尾随止盈」几个层次,并用 CTrails 容器统一管理多条 trail 的实例。对外汇与贵金属这类高波动品种,这种演化式止损可能比固定点数止损更贴合波段结构,但杠杆交易风险极高,任何止损都只是概率保护而非保本承诺。
◍ 用图表价格而非点距来设止损
上一节演示了用 CStop 类按点值或点数设置止损止盈,但实盘里止损位往往不是按入场价加减固定距离算出来的。很多策略的止损/止盈直接来自其他计算逻辑,并以图表上的具体价格呈现,而不是相对入场价的偏移量。 CStop 类本身就支持以图表价格直接指定停止位,不必绕回点距换算。本文要展开的就是这套用法,以及如何用配套函数库在持仓过程中修改这些停止位。 另一个重点是新增的 CTrail 类。它和自定义停止位写法很像,但定位不同:CTrail 专门处理随时间移动的跟踪停止位,而不是一次性写死的价位。外汇与贵金属杠杆高、滑点突发行情多,用价格级停止位前要自己在 MT5 里跑一遍验证逻辑。
「动态停止位怎么在 CStop 里落地」
很多策略的止损止盈不能写死成固定点数,得跟着 OHLC 或指标输出走。CStop 里留了两个扩展入口:StopLossCustom 和 TakeProfitCustom,程序员可以重写它们来算出任意想要的停止价位。 这两个方法其实是一对重载,简化版本只返回布尔值,用来判断类成员 m_stoploss 或 m_takeprofit 是否为零。为零就意味着走自定义计算,非零就按入场价叠加成员值。 CStop 的计算顺序是:m_stoploss 为零则用 StopLossCustom,非零则按成员值算真实止损;止盈对 m_takeprofit 同理。主停止位在订单管理器 TradeOpen 里调用,其余附加停止位在每笔 COrder 实例内处理。 在 MT4 与 MT5 对冲模式里,主停止位要随初始请求发给经纪商,所以 EA 只在发单前需要它。净持模式下不能这么干——止损会作用到整个持仓而不是单笔交易量,因此代码里会把 sl、tp 重置为 0,改由挂单模拟。 附加的多停止位必须等主交易进场后才创建,否则没有意义。COrder 的 CreateStops 会为每个 CStop 实例生成 COrderStop,但仅在主单成功入场后生效。外汇和贵金属杠杆高,自定义停止位算错可能直接放大回撤,上 MT5 前先单步跟一遍返回值。
class="type">bool CStopBase::StopLossCustom(class="type">void) { class="kw">return m_stoploss==class="num">0; } class="type">bool CStopBase::TakeProfitCustom(class="type">void) { class="kw">return m_takeprofit==class="num">0; } class="type">bool CStopBase::StopLossCustom(class="type">void) { class="kw">return m_stoploss==class="num">0; } class="type">bool CStopBase::TakeProfitCustom(class="type">void) { class="kw">return m_takeprofit==class="num">0; } class="type">bool COrderManager::TradeOpen(class="kw">const class="type">class="kw">string symbol,ENUM_ORDER_TYPE type,class="type">class="kw">double price,class="type">bool in_points=true) { class="type">int trades_total = TradesTotal(); class="type">int orders_total = OrdersTotal(); m_symbol=m_symbol_man.Get(symbol); if(!CheckPointer(m_symbol)) class="kw">return class="kw">false; if(!IsPositionAllowed(type)) class="kw">return true; if(m_max_orders>orders_total && (m_max_trades>trades_total || m_max_trades<=class="num">0)) { ENUM_ORDER_TYPE ordertype=type; if(in_points) price=PriceCalculate(type); class="type">class="kw">double sl=class="num">0,tp=class="num">0; if(CheckPointer(m_main_stop)==POINTER_DYNAMIC) { sl = m_main_stop.StopLossCustom()?m_main_stop.StopLossCustom(symbol,type,price):m_main_stop.StopLossCalculate(symbol,type,price); tp = m_main_stop.TakeProfitCustom()?m_main_stop.TakeProfitCustom(symbol,type,price):m_main_stop.TakeProfitCalculate(symbol,type,price); } class="type">class="kw">double lotsize=LotSizeCalculate(price,type,sl); if(CheckPointer(m_main_stop)==POINTER_DYNAMIC) { if (!m_main_stop.Broker()) { sl = class="num">0; tp = class="num">0; } } class="type">int ticket=(class="type">int)SendOrder(type,lotsize,price,sl,tp); if(ticket>class="num">0) { if(OrderSelect(ticket,SELECT_BY_TICKET)) {
从挂单回补到开仓风控的订单封装
这段逻辑把已有订单和新建订单统一塞进 COrder 对象,靠 CheckPointer 判断内存对象是否真实存在,避免空指针去读写票据号、品种和魔术码。 COrder *order = m_orders.NewOrder(OrderTicket(),OrderSymbol(),OrderMagicNumber(), (ENUM_ORDER_TYPE)::OrderType(),::OrderLots(),::OrderOpenPrice()); if (CheckPointer(order)) { LatestOrder(GetPointer(order)); return true; } 上面这段是把历史挂单转成本地订单缓存的标准写法,OrderTicket() 取票据、OrderLots() 取手数、OrderOpenPrice() 取开价,六个参数顺序不能错。 TradeOpen 里先卡两道闸:IsPositionAllowed 拒绝不允许的订单类型,m_max_orders>orders_total 且 m_max_trades>trades_total(或 m_max_trades<=0 表示不限制)才放行进单。外汇与贵金属杠杆高,这类计数闸能直接压住过度交易的概率。 若 m_main_stop 是动态指针,SL/TP 会走自定义或计算函数;但若 Broker() 返回假或不是对冲账户,代码强制把 sl、tp 归 0,等于放弃止损止盈发单。实盘前应在 MT5 策略测试器里打印 m_main_stop.Broker() 返回值,确认经纪商配置没悄悄吞掉你的止损。 lotsize=LotSizeCalculate(price,type,m_main_stop==NULL?0:m_main_stop.StopLossCalculate(symbol,type,price)); 这行把止损距离也喂给仓位计算,若止损计算返回 0,仓位模型会按无止损情形放大手数,风险敞口可能倾向陡增。
COrder *order = m_orders.NewOrder(OrderTicket(),OrderSymbol(),OrderMagicNumber(), (ENUM_ORDER_TYPE)::OrderType(),::OrderLots(),::OrderOpenPrice()); if (CheckPointer(order)) { LatestOrder(GetPointer(order)); class="kw">return true; } class="type">bool COrderManager::TradeOpen(class="kw">const class="type">class="kw">string symbol,ENUM_ORDER_TYPE type,class="type">class="kw">double price,class="type">bool in_points=true) { class="type">bool ret=class="kw">false; class="type">class="kw">double lotsize=class="num">0.0; class="type">int trades_total =TradesTotal(); class="type">int orders_total = OrdersTotal(); m_symbol=m_symbol_man.Get(symbol); if(!IsPositionAllowed(type)) class="kw">return true; if(m_max_orders>orders_total && (m_max_trades>trades_total || m_max_trades<=class="num">0)) { if(in_points) price=PriceCalculate(type); class="type">class="kw">double sl=class="num">0.0,tp=class="num">0.0; if(CheckPointer(m_main_stop)==POINTER_DYNAMIC) { sl = m_main_stop.StopLossCustom()?m_main_stop.StopLossCustom(symbol,type,price):m_main_stop.StopLossCalculate(symbol,type,price); tp = m_main_stop.TakeProfitCustom()?m_main_stop.TakeProfitCustom(symbol,type,price):m_main_stop.TakeProfitCalculate(symbol,type,price); } lotsize=LotSizeCalculate(price,type,m_main_stop==NULL?class="num">0:m_main_stop.StopLossCalculate(symbol,type,price)); if(CheckPointer(m_main_stop)==POINTER_DYNAMIC) { if (!m_main_stop.Broker() || !IsHedging()) { sl = class="num">0; tp = class="num">0; } } ret=SendOrder(type,lotsize,price,sl,tp); if(ret) { COrder *order = m_orders.NewOrder((class="type">int)m_trade.ResultOrder(),m_trade.RequestSymbol(),(class="type">int)m_trade.RequestMagic(), m_trade.RequestType(),m_trade.ResultVolume(),m_trade.ResultPrice()); if (CheckPointer(order)) { LatestOrder(GetPointer(order)); class="kw">return true; } } } class="kw">return ret; } if(CheckPointer(m_main_stop)==POINTER_DYNAMIC) { if (!m_main_stop.Broker()) { sl = class="num">0; tp = class="num">0; } } if(CheckPointer(m_main_stop)==POINTER_DYNAMIC) {
◍ 对冲账户下止损止盈的强制清零
在 MT5 的订单基类里,若当前账户不支持对冲(IsHedging() 返回 false)或主止损对象未通过 Broker() 校验,代码会直接把 sl 和 tp 置为 0。这意味着即便上层策略算出了止损位,在该条件下也不会真正挂出保护单,外汇与贵金属杠杆交易下这类静默失效可能放大回撤风险。 CreateStops 方法负责把外部传入的 CStops 集合逐个注册到订单止损管理器。它先判断指针有效性,再遍历 stops.Total() 个对象;任一子止损指针无效就跳过,有效则通过 NewOrderStop 绑定到当前订单实例。 开 MT5 把这段逻辑塞进自己的 EA 订单模块,打印 IsHedging() 与 Broker() 返回值,能立刻验证不同账户类型下 sl/tp 是否被吞掉。
if (!m_main_stop.Broker() || !IsHedging()) { sl = class="num">0; tp = class="num">0; } } 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); } } }
「停止位修改的调用链落在哪一层」
CStop 在扩展交易类里持有独立的 CTradeManager 实例,和管入场的订单管理器分开跑。实际改停止价不是订单管理器的事,而是 CStop 自己接手;但调用必须由 COrder 里的交易发起,因为修改按预订顺序执行,乱序会破坏持仓逻辑。 监视起点是 COrder::CheckStops,它只转调 COrderStops::Check。后者在容器里逐个跑 COrderStop 实例,至少做五件事:看是否已平单、按 trailing 实例更新、Update 刷新、检查行情是否触价、MT4 下核对逐笔 ID 变更。MT5 没有第五项,部分平仓时单号不变,逻辑更干净。 尾随真正改值发生在 COrderStop::CheckTrailing,它先排除已平或挂起,再向 CStop 要新 sl/tp。下面这段是核心调用: void COrderBase::CheckStops(void) { m_order_stops.Check(m_volume); } COrderStopsBase::Check(double &volume) { if(!Active()) return; for(int i=0;i<Total();i++) { COrderStop *order_stop=(COrderStop *)At(i); if(CheckPointer(order_stop)) { if(order_stop.IsClosed()) continue; order_stop.CheckTrailing(); order_stop.Update(); order_stop.Check(volume); if(!CheckNewTicket(order_stop)) return; } } } bool COrderStopBase::CheckTrailing(void) {
| if(!CheckPointer(m_stop) | m_order.IsClosed() | m_order.IsSuspended() |
|---|
(m_stoploss_closed && m_takeprofit_closed)) return false; double stoploss=0,takeprofit=0; string symbol=m_order.Symbol(); ENUM_ORDER_TYPE type=m_order.OrderType(); double price=m_order.Price(); double sl = StopLoss(); 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=0; if(!IsTakeProfitValid(takeprofit)) takeprofit=0; return Modify(stoploss,takeprofit); } double CStopBase::CheckTrailing(const string symbol,const ENUM_ORDER_TYPE type,const double entry_price,const double price,const ENUM_TRAIL_TARGET mode) { if(!CheckPointer(m_trails)) return 0; 逐行拆一下:CheckStops 只把成交量丢给 m_order_stops.Check;Check 里先判 Active,不活跃直接退,再遍历所有停止实例,已平的 skip,活的跑尾随、更新、触价检查、MT4 单号核对。CheckTrailing 先卡掉无效指针和平仓挂起状态,取品种、类型、开价、当前 sl/tp,分别问 CStop 算新值,无效就归零,最后 Modify。CStop::CheckTrailing 开头验 m_trails 指针,空就返 0,说明尾随容器没挂上就别想改价。 Modify 之前会用 IsStopLossValid / IsTakeProfitValid 卡一道,建议值等于当前值也判无效重置为 0。经纪商端停止可一次改完,挂单和虚拟停止必须分两次单独处理——虚拟的只动内部数据,不碰服务器。COrderStop 自己没交易对象指针,任何改价最终都绕回 CStop 里的方法。 开 MT5 把这段塞进自己的 EA 框架,先打印 m_trails 指针是否为空,能直接看出尾随为什么没生效。外汇和贵金属杠杆高,虚拟停止位断网就失效,真触价可能滑到你设的价位之外。
class="type">void COrderBase::CheckStops(class="type">void) { m_order_stops.Check(m_volume); } COrderStopsBase::Check(class="type">class="kw">double &volume) { if(!Active()) class="kw">return; for(class="type">int i=class="num">0;i<Total();i++) { COrderStop *order_stop=(COrderStop *)At(i); if(CheckPointer(order_stop)) { if(order_stop.IsClosed()) class="kw">continue; order_stop.CheckTrailing(); order_stop.Update(); order_stop.Check(volume); if(!CheckNewTicket(order_stop)) class="kw">return; } } } 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="type">class="kw">double CStopBase::CheckTrailing(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double entry_price,class="kw">const class="type">class="kw">double price,class="kw">const ENUM_TRAIL_TARGET mode) { if(!CheckPointer(m_trails)) class="kw">return class="num">0;