本文讨论如何在跨平台智能交易系统中设置自定义停止价位。它还讨论了一种紧密相关的方法, 即随着时间的推移, 定义停止位的演化。·进阶篇
(2/3)· 当距离入场价的固定点数不够用时,如何用 CStop 动态算价并让停止位随行情演化
不少跨平台 EA 开发者仍把止损止盈写死成入场价加减固定点数,遇到依据指标或 OHLC 动态算价的策略就卡壳。更麻烦的是,行情走出来之后停止位还按原样挂着,不会随时间移动。本文接着前篇的 CStop 地基,把自定义停止位和随时间演化的尾随逻辑一次讲清。
◍ 追踪止损的基类判定逻辑
这段基类代码把「多策略追踪止损」的合并判定拆得很干净:CTrailsBase::Check 会遍历所有 trail 实例,只挑出与目标模式(止损或止盈)匹配的活跃对象,再按买卖方向取极值。买单的止损取最大回撤位,卖单的止损取最小回撤位,反向对止盈处理,避免多个 trail 互相打架。 COrderStopBase::CheckTrailing 是实际挂单调用的入口。它先排除订单已关闭、挂起或双向都已锁死的情况,再分别算 sl/tp,最后用 IsStopLossValid / IsTakeProfitValid 做「是否真变了」的过滤——返回值等于原值时直接判无效,不触发无谓的 Modify。 外汇与贵金属杠杆高,追踪止损只降低回撤概率,不保证不被滑点击穿。把下面代码贴进 MT5 的 include 层,断点跑一遍 CheckTrailing,能看到 ret 在多个 trail 共存时如何收敛成单值。
class="kw">return m_trails.Check(symbol,type,entry_price,price,mode); } class="type">class="kw">double CTrailsBase::Check(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(!Active()) class="kw">return class="num">0; class="type">class="kw">double val=class="num">0.0,ret=class="num">0.0; for(class="type">int i=class="num">0;i<Total();i++) { CTrail *trail=At(i); if(!CheckPointer(trail)) class="kw">continue; if(!trail.Active()) class="kw">continue; class="type">int trail_target=trail.TrailTarget(); if(mode!=trail_target) class="kw">continue; val=trail.Check(symbol,type,entry_price,price,mode); if((type==ORDER_TYPE_BUY && trail_target==TRAIL_TARGET_STOPLOSS) || (type==ORDER_TYPE_SELL && trail_target==TRAIL_TARGET_TAKEPROFIT)) { if(val>ret || ret==class="num">0.0) ret=val; } else if((type==ORDER_TYPE_SELL && trail_target==TRAIL_TARGET_STOPLOSS) || (type==ORDER_TYPE_BUY && trail_target==TRAIL_TARGET_TAKEPROFIT)) { if(val<ret || ret==class="num">0.0) ret=val; } } class="kw">return ret; } 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">bool COrderStopBase::IsStopLossValid(class="kw">const class="type">class="kw">double stoploss) class="kw">const { class="kw">return stoploss!=StopLoss(); } class="type">bool COrderStopBase::IsTakeProfitValid(class="kw">const class="type">class="kw">double takeprofit) class="kw">const { class="kw">return takeprofit!=TakeProfit(); } class="type">bool COrderStopBase::Modify(class="kw">const class="type">class="kw">double stoploss,class="kw">const class="type">class="kw">double takeprofit) { class="type">bool stoploss_modified=class="kw">false,takeprofit_modified=class="kw">false; if(stoploss>class="num">0 && takeprofit>class="num">0) {
「止损止盈的分支改写逻辑」
这段 COrderStopBase 的成员函数展示了订单止损止盈修改的三种分支处理:同时改、只改止损、只改止盈。若 stoploss 与 takeprofit 均大于 0,调用 ModifyStops 并同时置两个 modified 标志为 true;若仅其一大于 0,则只调用对应的单边修改函数。
| 函数最终返回 stoploss_modified | takeprofit_modified,意味着只要任一边修改成功即算本次操作有效。外汇与贵金属杠杆高,修改挂单止损止盈前务必确认点差与冻结距离,否则 ModifyStopLoss 可能返回 false 导致逻辑漏改。 |
|---|
ModifyStops 内部用 ModifyStopLoss(stoploss) && ModifyTakeProfit(takeprofit) 做与运算,任一边失败整体即失败——这和上层「或」返回形成对比,写 EA 时要清楚双边改失败不会回退已成功的那一边。
if(ModifyStops(stoploss,takeprofit)) { stoploss_modified=true; takeprofit_modified=true; } } else if(stoploss>class="num">0 && takeprofit==class="num">0) stoploss_modified=ModifyStopLoss(stoploss); else if(takeprofit>class="num">0 && stoploss==class="num">0) takeprofit_modified=ModifyTakeProfit(takeprofit); class="kw">return stoploss_modified || takeprofit_modified; } class="type">bool COrderStopBase::ModifyStops(class="kw">const class="type">class="kw">double stoploss,class="kw">const class="type">class="kw">double takeprofit) { class="kw">return ModifyStopLoss(stoploss) && ModifyTakeProfit(takeprofit); }
把止损推到入场价才算真正脱险
盈亏平衡指一笔交易的盈利收入刚好覆盖成本(点差,或点差加佣金),此时离场盈亏为零。对不收佣金的做市商账户,这个点通常就是入场价;带佣金的账户,多头盈亏平衡价要高于入场价,空头则低于入场价。 在 EA 里把止损设到入场价,这笔交易就被标记为盈亏平衡,最大风险降为零,但后续仍可能按离场信号平仓(往往已是盈利)。这里有两道硬约束:动态止损方向限制与经纪商最小距离。多头止损必须低于市价、空头必须高于市价,所以 EA 只能在浮盈状态下把止损移向平衡点;最小距离在改单时同样生效,刚入场往往不能直接拉到平衡点,行情得朝持仓方向走出足够距离才行。 设计平衡功能至少要盯两个输入:激活价(触发把止损移向平衡点的行情价位,需逐笔按入场价评估)和激活后的新止损(通常是入场价,佣金账户则偏移)。下面这段 MQL5 函数按流程图预先算激活、失活和新止损价:若现价 ≥ 初始要求的最低价位,就暂用新止损;再比当前止损更优才返回 true 并输出该价,否则返零。 外汇与贵金属为高杠杆高风险品种,盈亏平衡仅消除该笔最大亏损风险,不预示后续必然盈利,请务必在 MT5 策略测试器实测参数。
class="type">class="kw">double CalculateBreakEven(class="type">class="kw">double open_price, class="type">class="kw">double current_price, class="type">class="kw">double commission_per_lot, class="type">class="kw">double tick_size, class="type">class="kw">double min_distance) { class="type">class="kw">double activation = open_price + min_distance * class="num">2; class=class="str">"cmt">// 触发价:入场价加两倍最小距离 class="type">class="kw">double deactivation = open_price; class=class="str">"cmt">// 失活参考:原入场价 class="type">class="kw">double new_stop = open_price + commission_per_lot * tick_size; class=class="str">"cmt">// 新止损:入场价加佣金折算跳数 if(current_price >= activation) class=class="str">"cmt">// 现价达到触发条件 { class="type">class="kw">double tentative_stop = new_stop; class=class="str">"cmt">// 暂定新止损 if(tentative_stop < current_price - min_distance) class=class="str">"cmt">// 新止损优于当前且合规 class="kw">return tentative_stop; class=class="str">"cmt">// 返回计算止损价 } class="kw">return class="num">0; class=class="str">"cmt">// 不满足条件返零 }
◍ 尾随止损的三道闸门与失活点
尾随止损本质是一种可反复触发的特殊盈亏平衡。普通盈亏平衡一般只用一次,而尾随止损在激活后能贯穿持仓剩余生命周期,按设定节奏不断把止损向上推。设计它至少要盯住三个变量:激活价格、激活后的新止损位、以及尾随步长(频率)。 和前两类止损不同,尾随的激活价可以落在图表上一段浮亏或“资金超支”的区域。也就是说,单子还在亏的时候,止损一样能被改、能被尾随;但若行情直接扫到有效止损,因为还没跨过盈亏平衡门槛,该亏还是亏。 第三个变量决定初始尾随启动后,当前止损到下一档的距离(以点值计),逻辑上和激活价很像,但每一阶段算出的新止损会随步长持续抬升。部分系统还有第四个因素——失活价格:触达后 EA 停止尾随,需逐笔评估。 实操上,MT5 里把激活价设得离进场太近,容易在毛刺行情里过早锁损;建议先用历史数据看某品种日均波幅,再把步长设为 ATR(14) 的 0.5~1 倍做对照测试。外汇与贵金属杠杆高,尾随只是缩小回撤概率,不承诺避免亏损。
「把止盈也做成会跑的线」
尾随通常挂在止损上,但止盈一样能尾随:价格越靠近原止盈位,就把止盈往同方向推。理论上这条线永远碰不到,可一旦遇到暴涨或跳空、行情比 EA 反应条件跑得还快,尾随止盈仍可能被扫。 要让尾随止盈真有用,得卡住两条:激活价必须落在当前止盈价内侧——多头的激活价低于止盈、空头的高于止盈;下一阶段触发价也得在原止盈内侧,且只在首次激活后才逐级生效。否则价格会先撞原止盈,仓位在尾随启动前就平了。 步长不能太窄。尾随越频繁,止盈被推得越勤,行情触及止盈的概率反而越低。外汇与贵金属杠杆高、跳空常见,这类机制在重大数据行情里可能直接失效,开 MT5 前先想清楚滑点承受度。
尾随止损类的骨架与触发逻辑
CTrailBase 作为所有尾随类的基类, 把激活价、步长、终点等参数都收进类成员。Set 方法本质就是类构造的替代入口, 你可以在自定义构造函数里直接调它, 省去外部逐个赋值的麻烦。 它内部持有一个品种管理器实例 (CSymbolManager), 任何计算前必须先刷新品种数据, 否则拿到的报价可能是上一根 K 线的残留值。激活价由 start、step、end 这些点数参数结合图表现价反推, 失活价用 m_end 算; 若 m_end 设为零, 尾随会一直跑到主交易平仓才停。 Check 方法是对外暴露的公有接口, 由调用方在需要更新止损/止盈时触发。通过 ENUM_TRAIL_TARGET 枚举区分目标: 尾随止损在每个阶段向趋势方向收紧, 尾随止盈则反向把价位推离现价。外汇和贵金属波动剧烈, 这类逻辑实盘前请在 MT5 策略测试器用历史数据验证参数敏感性。 下面这段基类声明值得直接贴进你的 MQL5 项目对照看, 重点留意 m_symbol_man 和 m_target 这两个成员如何贯穿后续计算。
class CTrailBase : class="kw">public CObject { class="kw">protected: class="type">bool m_active; ENUM_TRAIL_TARGET m_target; class="type">class="kw">double m_trail; class="type">class="kw">double m_start; class="type">class="kw">double m_end; class="type">class="kw">double m_step; CSymbolManager *m_symbol_man; CSymbolInfo *m_symbol; CEventAggregator *m_event_man; CObject *m_container; class="kw">public: CTrailBase(class="type">void); ~CTrailBase(class="type">void); class="kw">virtual class="type">int Type(class="type">void) class="kw">const {class="kw">return CLASS_TYPE_TRAIL;} class=class="str">"cmt">//--- 初始化 class="kw">virtual class="type">bool Init(CSymbolManager*,CEventAggregator*); class="kw">virtual CObject *GetContainer(class="type">void); class="kw">virtual class="type">void SetContainer(CObject*); 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="kw">const; class="type">void Active(class="kw">const class="type">bool); class="type">class="kw">double End(class="type">void) class="kw">const; class="type">void End(class="kw">const class="type">class="kw">double); class="type">void Set(class="kw">const class="type">class="kw">double,class="kw">const class="type">class="kw">double,class="kw">const class="type">class="kw">double,class="kw">const class="type">class="kw">double); class="type">class="kw">double Start(class="type">void) class="kw">const; class="type">void Start(class="kw">const class="type">class="kw">double); class="type">class="kw">double Step(class="type">void) class="kw">const; class="type">void Step(class="kw">const class="type">class="kw">double); class="type">class="kw">double Trail(class="type">void) class="kw">const; class="type">void Trail(class="kw">const class="type">class="kw">double); class="type">int TrailTarget(class="type">void) class="kw">const;