通用智能交易系统:自定义追踪止损(第六章)(基础篇)
「把追踪止损从EA里抽出来」
MetaTrader 5 自带的追踪止损只能挂在客户端手动单上,对通过 EA 开出的持仓常常鞭长莫及。把追踪逻辑写成独立的通用智能交易系统,就能让任何 EA 下的单都吃到同一套移动止损规则。 Vasiliy Sokolov 在 2016 年 7 月 5 日发布的示例被下载 12 463 次、收藏 10 次,说明这套思路在 MT5 社区里需求很实。它的核心不是重画指标,而是用 EA 的 OnTick 去轮询持仓、按自定义距离重设 SL。 做验证时,先在 MT5 策略测试器里挂一个裸 EA 下市价单,再加载这个通用追踪止损 EA,观察持仓止损是否随价格反向移动。外汇与贵金属杠杆高,回测没跑过的参数直接上真仓可能放大回撤,先用模拟盘确认距离设置。
本篇要拆的追踪止损模块清单
这一节先把整篇要落地的追踪止损相关模块列清楚,方便后面逐个去 MT5 里对照验证。 涉及的内容包括:追踪止损函数的底层实现方式、标准化封装后的 CTrailing 类、它和策略其他模块的调用关系,以及一个经典函数的实际用例。 后续还会讲到如何把追踪止损接进 CImpulse 策略、在 EA 输入参数里暴露开关与阈值、用移动平均线做追踪基准、对单一头寸做独立追踪,以及新版交易引擎的改动点。 你打开 MT5 的 MetaEditor,建一个空 EA,就可以按这个清单逐项把模块补全,先认结构再写逻辑。
◍ 把追踪止损做成外部模块
通用智能交易系统本质是一组类构成的交易引擎,用户能在上面拼装自己的策略。前面几篇已经把 CStrategy 类里的核心模型讲清了:不论是内建在类里的逻辑,还是挂在类实例上的实现对象,基本功能都能顺着这套骨架扩。 这一节要加的新能力是追踪止损。它和之前那些模块不一样——算法对引擎来说是外部的,挂上或撤掉都不该干扰 CStrategy 原本的运转。 实现手段是组合(composition)而非继承:继续用额外的类去接新功能,严格贴着面向对象的原则走,不让追踪止损的逻辑渗进引擎主体。这样你在 MT5 里改止损策略时,核心引擎文件可以一行不动。
「把追踪止损从基类里拆出去」
追踪止损本质是一套保护头寸、把止损推到特定价位的算法。它不属于交易引擎的必需环节,只是用来简化过程的外挂功能;缺了它,策略类不该失效,加了也不该拖垮基类可读性。 直接塞进 CStrategy 或 CPosition 都会出问题。示例代码里 TrailingMode1 既能被策略调用返回新止损价,也能挂在持仓对象上自己跑,但这么做只是把一堆怪异又有用的算法从策略类转移到了持仓类,变量越积越多。
<span style="class="type">color:rgb(class="num">0, class="num">0, class="num">255);">class </span>CStrategy { <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> TrailingMode1(CPosition* pos); }; <span class="keyword">class="type">void</span> CMovingAverage::SupportBuy(<span class="keyword">class="kw">const</span> MarketEvent &<span class="keyword">event</span>,CPosition *pos) { <span class="keyword">class="type">class="kw">double</span> new_sl = TrailingMode1(pos); pos.StopLossValue(new_sl); } <span class="keyword">class="type">void</span> CMovingAverage::SupportBuy(<span class="keyword">class="kw">const</span> MarketEvent &<span class="keyword">event</span>,CPosition *pos) { pos.TrailingMode1(); }
<span style="class="type">color:rgb(class="num">0, class="num">0, class="num">255);">class </span>CStrategy { <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> TrailingMode1(CPosition* pos); }; <span class="keyword">class="type">void</span> CMovingAverage::SupportBuy(<span class="keyword">class="kw">const</span> MarketEvent &<span class="keyword">event</span>,CPosition *pos) { <span class="keyword">class="type">class="kw">double</span> new_sl = TrailingMode1(pos); pos.StopLossValue(new_sl); } <span class="keyword">class="type">void</span> CMovingAverage::SupportBuy(<span class="keyword">class="kw">const</span> MarketEvent &<span class="keyword">event</span>,CPosition *pos) { pos.TrailingMode1(); }
把追踪止损收进一个基类里
追踪止损逻辑如果散落在策略主类里,会和 CStrategy、CPosition 这些核心对象搅在一起,后期调参和扩展都容易出岔子。把它单独抽成一个类,内部只存运行参数、把移动止损当成方法,结构就清爽了。 所有追踪算法都离不开两样东西:要改的那个头寸(用 CPosition 表示),以及一个统一名称的修改入口函数。我们把这两点提进基类 CTrailing,派生类各写各的算法,但对外都按同一个按钮触发。 基类里 Modify 是虚方法,默认返回 false,真实算法在子类重写;Copy 虚方法留作后面克隆实例用。参数不通过 Modify 传,而是在子类里用独立方法提前设好,调用 Modify 时所需数据已就位。 下面这段是基类骨架,含日志模块挂钩和最小接口。外汇、贵金属波动剧烈,追踪止损只降低回撤概率,不消除穿仓风险,上 MT5 跑之前先想清楚参数怎么注入。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Trailing.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 <Object.mqh> class="macro">#include "..\PositionMT5.mqh" class="macro">#include "..\Logs.mqh" class CPosition; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 追踪止损的基类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CTrailing : class="kw">public CObject { class="kw">protected: CPosition *m_position; class=class="str">"cmt">// 你要修改止损的头寸 CLog *Log; class="kw">public: CTrailing(class="type">void); class="type">void SetPosition(CPosition *position); CPosition *GetPosition(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">//+------------------------------------------------------------------+ CTrailing::CTrailing(class="type">void) { Log=CLog::GetLog(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 追踪止损方法 | class=class="str">"cmt">//| 应在派生类中重写 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CTrailing::Modify(class="type">void) { class="kw">return false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回实例的副本 | class=class="str">"cmt">//+------------------------------------------------------------------+ CTrailing* CTrailing::Copy(class="type">void) { class="kw">return new CTrailing(); }
◍ 给追踪止损类挂上当前持仓指针
在写自己的追踪止损模块时,最容易被忽略的一步是「谁在被拖」。上面这段把 CPosition 指针存进类成员 m_position,外部每次调 SetPosition 就把正在处理的订单对象喂进来,GetPosition 再原样吐出,类内部其余逻辑就能直接读持仓的开仓价、方向、当前止损位。 实际跑起来,EA 在 OnTick 里遍历 PositionSelect 后,应当先 SetPosition(&pos) 再调 trailing 计算;漏掉这步,m_position 可能是上一张单甚至空指针,回测里会看到止损完全不跟随,胜率虚高。外汇与贵金属波动大,这类指针错挂会在实盘放大滑点风险。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置要修改止损的头寸 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTrailing::SetPosition(CPosition *position) { m_position=position; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回要修改止损的头寸 | class=class="str">"cmt">//+------------------------------------------------------------------+ CPosition *CTrailing::GetPosition(class="type">void) { class="kw">return m_position; } class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置要修改止损的头寸 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTrailing::SetPosition(CPosition *position) { m_position=position; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回要修改止损的头寸 | class=class="str">"cmt">//+------------------------------------------------------------------+ CPosition *CTrailing::GetPosition(class="type">void) { class="kw">return m_position; } class=class="str">"cmt">//+------------------------------------------------------------------+