通用智能交易系统:自定义追踪止损(第六章)·进阶篇
(2/3)· 当追踪止损逻辑塞进基类,代码可读性与扩展性双双崩坏,本文给出干净解法
◍ 每个头寸配独立追踪实例才不串味
把 CTrailing 指针塞进 CPosition,头寸就能像管自己一样管止损。但真正落地时,CStrategy 层若只挂一个默认 Trailing 指针,所有头寸会共享同一个实例——这是坑。 共享实例会在内部保存计算变量,下一次循环传入的头寸变了,逻辑就错乱,出现难以排查的诡异 bug。解法是给每个头寸 Copy() 出独立实例,生命周期内不变。 Copy() 必须写在 CTrailing 派生类里。下面这段 CTrailingClassic::Copy 建新对象、搬参数、返指针,CStrategy 靠它自动给未平仓头寸配副本。 若你只在 BuySupport / SellSupport 里手动调 Trailing,不重写 Copy 也行;但想让 CStrategy 自动托管,就必须重写,否则 EA 跑起来管不了仓。 CallSupport 里有个细节:Modify 之后立刻查 pos.IsActive(),头寸若在追踪中途被平,循环直接跳下一单,不崩。这意味着任何平仓逻辑都能伪装成追踪函数用——在特定条件平掉仓位,策略照常跑。外汇与贵金属杠杆高,这类自动托管逻辑先开 MT5 用 demo 验一遍再上实盘。
class CPosition { class="kw">public: CTrailing* Trailing; class=class="str">"cmt">// 追踪止损模块 }; class="type">void CMovingAverage::SupportBuy(class="kw">const MarketEvent &event,CPosition *pos) { pos.Trailing.Modify(); } class CStrategy { class="kw">public: CTrailing* Trailing; class=class="str">"cmt">// 用于所有头寸的追踪止损模块 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 调用头寸管理逻辑,如果交易状态不等于 | class=class="str">"cmt">//| TRADE_WAIT | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CStrategy::CallSupport(class="kw">const MarketEvent &event) { m_trade_state=m_state.GetTradeState(); if(m_trade_state == TRADE_WAIT)class="kw">return; SpyEnvironment(); for(class="type">int i=ActivePositions.Total()-class="num">1; i>=class="num">0; i--) { CPosition *pos=ActivePositions.At(i); if(pos.ExpertMagic()!=m_expert_magic)class="kw">continue; if(pos.Symbol()!=ExpertSymbol())class="kw">continue; if(CheckPointer(Trailing)!=POINTER_INVALID) { if(CheckPointer(Trailing)==POINTER_INVALID) pos.Trailing=Trailing.Copy(); pos.Trailing.Modify(); if(!pos.IsActive()) class="kw">continue; } if(pos.Direction()==POSITION_TYPE_BUY) SupportBuy(event,pos); else SupportSell(event,pos); if(m_trade_state==TRADE_STOP && pos.IsActive()) ExitByStopRegim(pos); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回一个实例的副本 | class=class="str">"cmt">//+------------------------------------------------------------------+ CTrailing *CTrailingClassic::Copy(class="type">void) { CTrailingClassic *tral=new CTrailingClassic(); tral.SetDiffExtremum(m_diff_extremum); tral.SetStepModify(m_step_modify); tral.SetPosition(m_position); class="kw">return tral; }
「经典算法怎么管住止损乱跳」
经典追踪止损的逻辑很直白:多头盯着新高价、空头盯着新低价去推止损,价格回撤时止损原地不动,始终和极值保持一段由参数决定的距离。这么做的目的是让利润跑、把风险钉死在回撤边界上。 但有个坑得提前堵:如果不加限制,价格每晃一下 EA 就可能去改一次止损。在 FORTS 这类交易所,频繁改单却很少真实成交会被判定为“低效交易”并加收手续费。所以实例里引入了 StepModify——新止损和旧止损的最小变动点数,不到这个距离就不动单。 下面这段是基于 CTrailing 派生出的第一个实例,核心计算落在 Modify 和 FindExtremum 两个方法里。FindExtremum 负责按持仓方向捞到最高/最低价,因此即便 EA 重启或空闲,止损也能算回正确位置,不会丢状态。 代码里用 SHOW_TRAILING_CLASSIC_PARAMS 宏和 input 参数把差值距离(默认 0.00200)与最小步长(默认 0.00005)暴露给面板,具体怎么接进 EA 设置后面单说。外汇和贵金属波动大,这类追踪止损在高波动段可能频繁触及 StepModify 边界,实盘前建议在 MT5 策略测试器用历史数据验证改单频率。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TrailingClassic.mqh | class=class="str">"cmt">//| Copyright class="num">2016, Vasiliy Sokolov. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2016, Vasiliy Sokolov." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#include "Trailing.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 将追踪止损参数集成在EA的参数列表中 | class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#ifdef SHOW_TRAILING_CLASSIC_PARAMS class="kw">input class="type">class="kw">double PointsModify=class="num">0.00200; class="kw">input class="type">class="kw">double StepModify=class="num">0.00005; class="macro">#endif class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 一个经典的追踪止损算法 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CTrailingClassic : class="kw">public CTrailing { class="kw">private: class="type">class="kw">double m_diff_extremum; class=class="str">"cmt">// 价格到达的极值和止损位置之间的距离点数 class="type">class="kw">double m_step_modify; class=class="str">"cmt">// 修改止损的最小差异点数 class="type">class="kw">double FindExtremum(CPosition *pos); class="kw">public: CTrailingClassic(class="type">void); class="type">void SetDiffExtremum(class="type">class="kw">double points); class="type">class="kw">double GetDiffExtremum(class="type">void); class="type">void SetStepModify(class="type">class="kw">double points_step); class="type">class="kw">double GetStepModify(class="type">void); class="kw">virtual class="type">bool Modify(class="type">void); class="kw">virtual CTrailing *Copy(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 构造函数初始化默认参数 | class=class="str">"cmt">//+------------------------------------------------------------------+ CTrailingClassic::CTrailingClassic(class="type">void) : m_diff_extremum(class="num">0.0),
经典追踪止损类的接口与执行逻辑
CTrailingClassic 把经典追踪止损封装成可复制的对象。Copy() 会 new 一个同型实例,把 m_diff_extremum、m_step_modify 和 m_position 逐一搬过去,返回指针——这意味着你在 EA 里可以随时克隆一套独立参数跑对照测试。 两个setter很直白:SetDiffExtremum(double points) 存的是价格极值到止损位的点数距离,SetStepModify(double points_step) 存的是触发修改的最小点数差。GetDiffExtremum() 与 GetStepModify() 原样回读,方便在面板或日志里核对实参。 真正下单前卡了两道闸。Modify() 先查 m_position 指针是否有效,再查 m_diff_extremum 是否大于 0.0,任意一项不成立就写一条 MESSAGE_WARNING 并返回 false,不会动止损。 过闸后由 FindExtremum(m_position) 取极值,若为 0.0 直接退出;买仓用 extremum - m_diff_extremum 算新止损,卖仓走 else 分支(原文截断)。外汇与贵金属杠杆高,追踪止损只是概率性保护,极端跳空时仍可能大幅滑点。
m_step_modify(class="num">0.0) { class="macro">#ifdef SHOW_TRAILING_CLASSIC_PARAMS m_diff_extremum=PointsModify; m_step_modify=StepModify; class="macro">#endif } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回实例的副本 | class=class="str">"cmt">//+------------------------------------------------------------------+ CTrailing *CTrailingClassic::Copy(class="type">void) { CTrailingClassic *tral=new CTrailingClassic(); tral.SetDiffExtremum(m_diff_extremum); tral.SetStepModify(m_step_modify); tral.SetPosition(m_position); class="kw">return tral; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置价格极值同止损位置的距离点数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTrailingClassic::SetDiffExtremum(class="type">class="kw">double points) { m_diff_extremum=points; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置修改止损的最小差异点数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTrailingClassic::SetStepModify(class="type">class="kw">double points_step) { m_step_modify=points_step; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回距离价格极值的点数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CTrailingClassic::GetDiffExtremum(class="type">void) { class="kw">return m_diff_extremum; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回最小修改点数的值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CTrailingClassic::GetStepModify(class="type">void) { class="kw">return m_step_modify; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据经典追踪止损算法按修改止损位置 | class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CTrailingClassic::Modify(class="type">void) { if(CheckPointer(m_position)==POINTER_INVALID) { class="type">class="kw">string text="Invalid position for current trailing-stop. Set position with &class="macro">#x27;SetPosition&class="macro">#x27; method"; CMessage *msg=new CMessage(MESSAGE_WARNING,__FUNCTION__,text); Log.AddMessage(msg); class="kw">return false; } if(m_diff_extremum<=class="num">0.0) { class="type">class="kw">string text="Set points trailing-stop with &class="macro">#x27;SetDiffExtremum&class="macro">#x27; method"; CMessage *msg=new CMessage(MESSAGE_WARNING,__FUNCTION__,text); Log.AddMessage(msg); class="kw">return false; } class="type">class="kw">double extremum=FindExtremum(m_position); if(extremum == class="num">0.0)class="kw">return false; class="type">class="kw">double n_sl = class="num">0.0; if(m_position.Direction()==POSITION_TYPE_BUY) n_sl=extremum-m_diff_extremum; else
◍ 用持仓周期极值重挂止损
经典跟踪止损类里,止损位不是盯一根K线,而是从开仓时刻拉到当前,找这段区间里的极端价。多头取 M1 周期自开仓以来的最高价,空头取最低价,再叠加上一段算出的偏移量 m_diff_extremum 得到 n_sl。 下面这段是极值查找的实现核心。若当前止损已不等于算出的 n_sl,就调用 StopLossValue(n_sl) 重设并返回;否则返回 false 表示无需改动。 FindExtremum 先按持仓方向分流:买仓用 CopyHigh 把 pos.TimeOpen() 到 TimeCurrent() 的 M1 高价拷进数组,只要拷贝到的柱数大于 1(即不止当前未闭合的一根),就用 ArrayMaximum 取最高价;卖仓对称走 CopyLow 与 ArrayMinimum 取最低价。拷贝失败或不足两根则返回 0.0,调用方需自行处理这种边界。 实盘里这种写法对外汇与贵金属都有效,但 M1 重算频率高,点差扩大时滑点可能让重挂失败概率上升,属高风险操作,建议先在 MT5 策略测试器用历史数据验证极值窗口是否符合你的持仓逻辑。
n_sl=extremum+m_diff_extremum; if(n_sl!=m_position.StopLossValue()) class="kw">return m_position.StopLossValue(n_sl); class="kw">return false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 当持有头寸时,返回价格极值 | class=class="str">"cmt">//| 对于多头头寸,将返回最高价。 | class=class="str">"cmt">//| 对于空头头寸,返回最低价。 | class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CTrailingClassic::FindExtremum(CPosition *pos) { class="type">class="kw">double prices[]; if(pos.Direction()==POSITION_TYPE_BUY) { if(CopyHigh(pos.Symbol(),PERIOD_M1,pos.TimeOpen(),TimeCurrent(),prices)>class="num">1) class="kw">return prices[ArrayMaximum(prices)]; } else { if(CopyLow(pos.Symbol(),PERIOD_M1,pos.TimeOpen(),TimeCurrent(),prices)>class="num">1) class="kw">return prices[ArrayMinimum(prices)]; } class="kw">return class="num">0.0; } class=class="str">"cmt">//+------------------------------------------------------------------+
「把均线平仓换成经典追踪止损」
早先的 CImpulse 用移动平均做头寸管理:柱线开盘价跌破均线平多、升破均线平空,逻辑写死在 SupportBuy / SupportSell 里。这套规则在震荡市里容易被毛刺洗掉,实战中持仓体验偏脆。 直接把那两段基于均线的平仓代码从策略类里删掉,构造函数改成挂一个 CTrailingClassic 实例,默认 diff_extremum 设为 0.001。改名 CImpulseTrailingAuto 后,平仓完全交给追踪止损:从价格极值回撤 0.001 才触发,不再看均线脸色。 下面这段是原文里被替换掉的老逻辑和新的构造写法,逐行拆一下你能看清差异在哪。外汇与贵金属杠杆高,追踪止损只是缩小回撤概率,不等于锁定利润,实盘前请在 MT5 策略测试器用历史数据跑一遍。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据移动平均管理多头头寸 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CImpulse::SupportBuy(class="kw">const MarketEvent &event,CPosition *pos) { if(!IsTrackEvents(event))class="kw">return; ENUM_ACCOUNT_MARGIN_MODE mode = (ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE); if(mode != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) { class="type">class="kw">double target = Bid() - Bid()*(m_percent/class="num">100.0); if(target < Moving.OutValue(class="num">0)) pos.StopLossValue(target); else pos.StopLossValue(class="num">0.0); } if(Bid() < Moving.OutValue(class="num">0)) pos.CloseAtMarket(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据移动平均管理空头头寸 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CImpulse::SupportSell(class="kw">const MarketEvent &event,CPosition *pos) { if(!IsTrackEvents(event))class="kw">return; ENUM_ACCOUNT_MARGIN_MODE mode = (ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE); if(mode != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) { class="type">class="kw">double target = Ask() + Ask()*(m_percent/class="num">100.0); if(target > Moving.OutValue(class="num">0)) pos.StopLossValue(target); else pos.StopLossValue(class="num">0.0); } if(Ask() > Moving.OutValue(class="num">0)) pos.CloseAtMarket(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 策略初始化并在启动时配置追踪止损 | class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ CImpulseTrailing::CImpulseTrailing(class="type">void) { CTrailingClassic* classic = new CTrailingClassic(); classic.SetDiffExtremum(class="num">0.00100); Trailing = classic; }
用条件编译收住追踪止损的参数歧义
经典追踪止损模块本身好用,但参数总得在策略里手动接一次。麻烦在于:即便这次根本不打算开追踪止损,那几个 input 参数还是会挂在 EA 设置窗口里,交易者容易误以为生效了。 解法是在头文件里套一层条件编译。定义宏 SHOW_TRAILING_CLASSIC_PARAMS 时,PointsModify 与 StepModify 才作为 input 编译进 EA;宏被注释掉,参数直接从设置里消失,界面干净,也不会产生「开了还是没开」的歧义。 宏不只控制显隐,还会在 CTrailingClassic 构造函数里把这两个值直接喂给成员变量。也就是说,只要策略编译时带了这个宏,EA 设置窗口里填的数在对象创建那一刻就已经落进算法内部,不用再写额外的传参代码。 外汇与贵金属波动剧烈,追踪止损只是风控的一环,实际触发概率和滑点损耗都随行情变化,参数请结合回测与实盘验证再定。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TrailingClassic.mqh | class=class="str">"cmt">//| Copyright class="num">2016, Vasiliy Sokolov. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2016, Vasiliy Sokolov." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#include "Trailing.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 将追踪止损参数集成在EA的参数列表中 | class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#ifdef SHOW_TRAILING_CLASSIC_PARAMS class="kw">input class="type">class="kw">double PointsModify = class="num">0.00100; class="kw">input class="type">class="kw">double StepModify = class="num">0.00005; class="macro">#endif class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 一个经典的追踪止损算法 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CTrailingClassic : class="kw">public CTrailing { ... class="kw">public: CTrailingClassic(class="type">void); ... }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 构造函数。初始化参数 | class=class="str">"cmt">//+------------------------------------------------------------------+ CTrailingClassic::CTrailingClassic(class="type">void) : m_diff_extremum(class="num">0.0), m_step_modify(class="num">0.0) { class="macro">#ifdef SHOW_TRAILING_CLASSIC_PARAMS m_diff_extremum = PointsModify; m_step_modify = StepModify; class="macro">#endif }