如何创建自己的追踪止损·进阶篇
(2/3)· 标准追踪止损之外,用指标逻辑自动拖止损线到底差在哪
◍ 按趋势方向推止损的实盘逻辑
这段代码实现了一个跟随指标趋势移动持仓止损的核心方法 DoStoploss()。当追踪开关 m_onoff 为 false 时直接放行,不做任何操作;若设为逐根 K 线模式(m_eachtick 为 false),先用 CopyTime 取最新柱时间,若与上次执行时间相同则跳过,避免同一根 bar 内重复发单。 趋势判断交给 Trend():返回 1 代表上升,只处理买入持仓。此时用 BuyStoploss() 算出理论止损位,再与 SYMBOL_BID 减去 stops level 点数得到的最小允许位做 MathMin,保证不违反经纪商止损距离限制。仅当新 sl 大于原持仓 sl 才填 request 并 OrderSend,否则不动——这意味着止损只会向上垫,不会回撤。 下降趋势返回 -1 时对称处理卖单:理论位由 SellStoploss() 给出后需加上 ask-bid 点差(卖单靠买价平仓),再用 MathMax 锁住最小允许位。这里判断条件写成 sl<possl 或 possl==0,所以原止损为空(0)时也会首次挂上。 外汇与贵金属杠杆高、滑点突发行情多,止损被拒单(retcode!=TRADE_RETCODE_DONE)的概率不低。上面两段都在发送失败时用 printf 打出错误码并 return(false),开 MT5 把这段接进 EA,跑 EURUSD M15 就能看到日志里具体的 #错误码,据此调 SYMBOL_TRADE_STOPS_LEVEL 容忍度。
class="type">bool DoStoploss() { class=class="str">"cmt">//--- 如果追踪止损被关闭 if(!m_onoff) { class="kw">return(true); } class="type">class="kw">datetime tm[class="num">1]; class=class="str">"cmt">//--- 在每个bar模式下,获取最近一个柱形的时间 if(!m_eachtick) { class=class="str">"cmt">//--- 如果没能成功复制时间,结束方法,在下一个tick到来时重复 if(CopyTime(m_symbol,m_timeframe,class="num">0,class="num">1,tm)==-class="num">1) { class="kw">return(class="kw">false); } class=class="str">"cmt">//--- 如果柱形的时间和本方法最后执行时间一致,结束 if(tm[class="num">0]==m_lasttime) { class="kw">return(true); } } class=class="str">"cmt">//--- 获取指标值 if(!Refresh()) { class="kw">return(class="kw">false); } class="type">class="kw">double sl; class=class="str">"cmt">//--- 取决于指标显示的趋势,进行多种操作 class="kw">switch(Trend()) { class=class="str">"cmt">//--- 上升趋势 case class="num">1: class=class="str">"cmt">//--- 选择持仓如果成功,那么持仓存在 if(PositionSelect(m_symbol)) { class=class="str">"cmt">//---如果为买入持仓 if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY) { class=class="str">"cmt">//--- 获取买入持仓的止损值 sl=BuyStoploss(); class=class="str">"cmt">//--- 找到买入持仓被允许设置止损的位置 class="type">class="kw">double minimal=SymbolInfoDouble(m_symbol,SYMBOL_BID)-m_point*SymbolInfoInteger(m_symbol,SYMBOL_TRADE_STOPS_LEVEL); class=class="str">"cmt">//--- 对值进行标准化 sl=NormalizeDouble(sl,m_digits); class=class="str">"cmt">//--- 对值进行标准化 minimal=NormalizeDouble(minimal,m_digits); class=class="str">"cmt">//---如果不能在由指标获得的水平上设置止损, class=class="str">"cmt">// 那么止损将被放置在离可能止损位最近的地方 sl=MathMin(sl,minimal); class=class="str">"cmt">//--- 持仓的止损值 class="type">class="kw">double possl=PositionGetDouble(POSITION_SL); class=class="str">"cmt">//--- 对值进行标准化 possl=NormalizeDouble(possl,m_digits); class=class="str">"cmt">//--- 如果新的止损值大于当前止损值, class=class="str">"cmt">// 那么将尝试移动止损到新的水平上 if(sl>possl) { class=class="str">"cmt">//--- 填充请求结构体 m_request.sl=sl; class=class="str">"cmt">//--- 填充请求结构体 m_request.tp=PositionGetDouble(POSITION_TP); class=class="str">"cmt">//--- 请求 OrderSend(m_request,m_result); class=class="str">"cmt">//--- 检查请求结果 if(m_result.retcode!=TRADE_RETCODE_DONE) { class=class="str">"cmt">//--- 日志错误信息 printf("Unable to move Stop Loss of position %s, error #%I64u",m_symbol,m_result.retcode); class=class="str">"cmt">//--- 未能移动止损,结束 class="kw">return(class="kw">false); } } } } class="kw">break; class=class="str">"cmt">//--- 下降趋势 case -class="num">1: if(PositionSelect(m_symbol)) { if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL) { sl=SellStoploss(); class=class="str">"cmt">//--- 加上点差,因为卖单是通过买价平仓的 sl+=(SymbolInfoDouble(m_symbol,SYMBOL_ASK)-SymbolInfoDouble(m_symbol,SYMBOL_BID)); class="type">class="kw">double minimal=SymbolInfoDouble(m_symbol,SYMBOL_ASK)+m_point*SymbolInfoInteger(m_symbol,SYMBOL_TRADE_STOPS_LEVEL); sl=NormalizeDouble(sl,m_digits); minimal=NormalizeDouble(minimal,m_digits); sl=MathMax(sl,minimal); class="type">class="kw">double possl=PositionGetDouble(POSITION_SL); possl=NormalizeDouble(possl,m_digits); if(sl<possl || possl==class="num">0) { m_request.sl=sl; m_request.tp=PositionGetDouble(POSITION_TP); OrderSend(m_request,m_result); if(m_result.retcode!=TRADE_RETCODE_DONE) { printf("Unable to move Stop Loss of position %s, error #%I64u",m_symbol,m_result.retcode); class="kw">return(class="kw">false); } } } } class="kw">break; } class=class="str">"cmt">//--- 记住方法的最近一次执行时间 m_lasttime=tm[class="num">0]; class="kw">return(true); }
「用 SAR 线做追踪止损的子类落地」
把抛物线转向(SAR)接进 CTrailingStop 体系,核心就是写一个叫 CParabolicStop 的子类。声明里写 class CParabolicStop: public CTrailingStop,继承后基类的虚拟方法调用会自动走继承版本,不用在子类里重写调度逻辑。
构造函数 CParabolicStop() 只干一件事:把 m_typename 设成 "SAR"。这个变量后面被基类 Init() 拿去生成按钮名和标题,所以追踪止损在面板里显示成什么,全看这里赋的字符串。
SetParameters() 接收 sarstep 和 sarmaximum 两个参数,默认 0.02 和 0.2,然后 iSAR() 加载指标句柄。句柄若返回 -1 直接返回 false;m_indicator 为真时用 ChartIndicatorAdd(0,0,m_handle) 把 SAR 挂到当前图表 0 号窗口。
Refresh() 里 pricebuf 和 indbuf 都只存 1 个元素,靠 m_shift 决定取的是正在形成还是已形成的柱。CopyBuffer 取指标值、CopyClose 取收盘价,任一失败就返回 false,这保证了止损计算前数据是最新的。
Trend() 的判断很直白:pricebuf[0] 大于 indbuf[0] 返回 1(偏多),小于返回 -1(偏空),相等返回 0。价格贴着 SAR 线跑的概率虽低,但实盘里偶尔会出现,返回 0 能让调用方决定不翻转止损。
因为 SAR 只有一条线,BuyStoploss() 和 SellStoploss() 都直接 return indbuf[0],多空止损参考同一数值。写完这个类存成 Sample_TrailingStop.mqh 扔进 .\MQL5\Include,EA 里 include 一下就能跑,外汇和贵金属波动大,SAR 步进参数设太大会让止损被快速扫掉,建议先在 MT5 策略测试器用默认 0.02/0.2 验证一次再上真仓。
下面这段是子类完整代码,逐行已经标了注释,复制进 include 目录即可编译:
class CParabolicStop: public CTrailingStop —— 声明继承自 CTrailingStop 的 SAR 追踪止损类
void CParabolicStop() { m_typename="SAR"; } —— 构造函数,把类型名写成 SAR 供面板使用
bool SetParameters(double sarstep=0.02,double sarmaximum=0.2) —— 设步进与上限并加载指标
m_handle=iSAR(m_symbol,m_timeframe,sarstep,sarmaximum); —— 按品种周期和参数取 SAR 句柄
if(m_handle==-1) return(false); —— 句柄失败直接退出
if(m_indicator) ChartIndicatorAdd(0,0,m_handle); —— 允许时把 SAR 画到图表
bool Refresh() —— 刷新价格和指标缓冲
CopyBuffer(m_handle,0,m_shift,1,indbuf) —— 拷 1 根 SAR 值到 indbuf
CopyClose(m_symbol,m_timeframe,m_shift,1,pricebuf) —— 拷 1 根收盘价到 pricebuf
int Trend() —— 判趋势方向
pricebuf[0]>indbuf[0] return(1) —— 价在线上偏多
pricebuf[0]<indbuf[0] return(-1) —— 价在线下偏空
return(0) —— 相等情形
virtual double BuyStoploss() return(indbuf[0]) —— 多单止损取 SAR 值
virtual double SellStoploss() return(indbuf[0]) —— 空单止损同取 SAR 值
class CParabolicStop: class="kw">public CTrailingStop class="type">void CParabolicStop() { m_typename="SAR"; class=class="str">"cmt">// 设置追踪止损类型的名称 }; class=class="str">"cmt">// 设置参数和加载指标的方法 class="type">bool SetParameters(class="type">class="kw">double sarstep=class="num">0.02,class="type">class="kw">double sarmaximum=class="num">0.2) { m_handle=iSAR(m_symbol,m_timeframe,sarstep,sarmaximum); class=class="str">"cmt">// 加载指标 if(m_handle==-class="num">1) { class="kw">return(class="kw">false); class=class="str">"cmt">// 如果未能加载指标,方法返回false } if(m_indicator) { ChartIndicatorAdd(class="num">0,class="num">0,m_handle); class=class="str">"cmt">// 将指标添加到图标上 } class="kw">return(true); } class=class="str">"cmt">//获取指标值的方法 class="type">bool Refresh() { if(CopyBuffer(m_handle,class="num">0,m_shift,class="num">1,indbuf)==-class="num">1) { class="kw">return(class="kw">false); class=class="str">"cmt">// 如果不能复制数组值,返回false } if(CopyClose(m_symbol,m_timeframe,m_shift,class="num">1,pricebuf)==-class="num">1) { class="kw">return(class="kw">false); class=class="str">"cmt">// 如果不能复制数组值,返回false } class="kw">return(true); } class=class="str">"cmt">// 找到趋势的方法 class="type">int Trend() { if(pricebuf[class="num">0]>indbuf[class="num">0]) { class=class="str">"cmt">// 价格比指标线高,上升趋势 class="kw">return(class="num">1); } if(pricebuf[class="num">0]<indbuf[class="num">0]) { class=class="str">"cmt">// 价格比指标线低,下降趋势 class="kw">return(-class="num">1); } class="kw">return(class="num">0); } class=class="str">"cmt">// 找到买入持仓止损水平的方法 class="kw">virtual class="type">class="kw">double BuyStoploss() { class="kw">return(indbuf[class="num">0]); }; class=class="str">"cmt">// 找到卖出持仓止损水平的方法 class="kw">virtual class="type">class="kw">double SellStoploss() { class="kw">return(indbuf[class="num">0]); };
给初学版 EA 挂上抛物线止损
把抛物线追踪止损接进现成 EA,最省事的做法是另存改名的办法。以 My_First_EA 为例,在 MetaEditor 里另存为 My_First_EA_SARTrailing,避免动到原文件。 代码顶部先引头文件,再声明一个 CParabolicStop 实例。外部变量里放 Step 和 Maximum 两个输入参数,默认 0.02 与 0.2,对应 SAR 的加速步长和上限,这两个值直接决定止损跟价的快慢。 OnInit 里做类初始化:Init 设定品种与周期并打开开仓/平仓控制,setparameters 把上面两个参数灌进去,失败就弹窗并返回 -1;随后 StartTimer 与 On 打开计时器和激活开关。 原 EA 没有 OnTimer,要自己补一个只调 Refresh 的函数,让止损线随行情刷新;OnTick 顶部加一句 DoStoploss 调用即生效。编译后回测对比:无追踪止损见原图 7,挂上后见原图 8,权益曲线回撤明显被压扁,效果在测试环境里很直观。 外汇与贵金属波动大,抛物线止损被毛刺扫掉的概率不低,实盘前请在 MT5 策略测试器用历史数据跑一遍验证参数。
class="macro">#include <Sample_TrailingStop.mqh> class=class="str">"cmt">// 包含追踪止损类 CParabolicStop Trailing; class=class="str">"cmt">// 创建类的实例 input class="type">class="kw">double TrailingSARStep=class="num">0.02; input class="type">class="kw">double TrailingSARMaximum=class="num">0.2; Trailing.Init(_Symbol,PERIOD_CURRENT,true,true,class="kw">false); class=class="str">"cmt">//初始化(设置基本参数) if(!trailing.setparameters(TrailingSARStep,TrailingSARMaximum)) { class=class="str">"cmt">// 设置所使用的追踪止损类型的参数 Alert("trailing error"); class="kw">return(-class="num">1); } Trailing.StartTimer(); class=class="str">"cmt">// 启动计时器 Trailing.On(); class=class="str">"cmt">// 激活 class="type">void OnTimer() { Trailing.Refresh(); }
◍ 在 MT5 里把 NRTR 接成追踪止损类
NRTR(Nick Rypock Trailing Reverse)本身画两条线:基线(支撑或阻力)和目标线。价格碰穿目标线,基线就顺着价格方向迁移,小幅振荡被过滤掉;一旦价格回穿基线,视为趋势反转,两线整体换位。上行时用蓝线、下行用红线,这种结构天然适合拿来做 trailing stop 的触发参考。 我们建一个 CNRTRStop 类,继承自通用追踪止损基类 CTrailingStop。它和抛物线版本的方法集一致,只是构造函数里把 m_typename 写死成 "NRTR",方便运行时识别类型。 SetParameters() 接收 period 和 k 两个参数,用 iCustom 加载 "NRTR" 指标;句柄为 -1 就直接返回 false。若 m_indicator 为真,还会用 ChartIndicatorAdd(0,0,m_handle) 把线挂到当前图表上,方便肉眼核对。 Refresh() 负责把指标两个缓冲区拷进类内数组 sup[] 和 res[]:缓冲区 0 是支撑线、缓冲区 1 是阻力线,各取 1 根柱(m_shift 偏移)。任何一次 CopyBuffer 返回 -1 都判失败。 Trend() 看 sup[0] 非零就返回 1(偏多),res[0] 非零返回 -1(偏空),都零则返回 0。BuyStoploss() 直接吐 sup[0],SellStoploss() 吐 res[0]——也就是说多单止损挂在支撑线、空单挂在阻力线,外汇与贵金属波动大,这种止损被扫的概率随波动率上升而升高,实盘前先在策略测试器跑一轮。 下面这段是类的核心代码,逐行拆完就能直接塞进 EA 的 include 里编译验证。
class CNRTRStop: class="kw">public CTrailingStop class="type">void CNRTRStop() { m_typename="NRTR"; class=class="str">"cmt">// 设置追踪止损类型的名称 }; class=class="str">"cmt">// 设置参数和加载指标的方法 class="type">bool SetParameters(class="type">int period,class="type">class="kw">double k) { m_handle=iCustom(m_symbol,m_timeframe,"NRTR",period,k); class=class="str">"cmt">// 加载指标 if(m_handle==-class="num">1) { class=class="str">"cmt">// 如果未能加载指标,方法返回false class="kw">return(class="kw">false); } if(m_indicator) { ChartIndicatorAdd(class="num">0,class="num">0,m_handle); class=class="str">"cmt">// 将指标添加到图标上 } class="kw">return(true); } class=class="str">"cmt">//获取指标值的方法 class="type">bool Refresh() { if(CopyBuffer(m_handle,class="num">0,m_shift,class="num">1,sup)==-class="num">1) { class="kw">return(class="kw">false); class=class="str">"cmt">// 如果不能复制数组值,返回false } if(CopyBuffer(m_handle,class="num">1,m_shift,class="num">1,res)==-class="num">1) { class="kw">return(class="kw">false); class=class="str">"cmt">// 如果不能复制数组值,返回false } class="kw">return(true); } class="kw">protected: class="type">class="kw">double sup[class="num">1]; class=class="str">"cmt">// 支撑位的值 class="type">class="kw">double res[class="num">1]; class=class="str">"cmt">// 阻力位的值 class=class="str">"cmt">// 找到趋势的方法 class="type">int Trend() { if(sup[class="num">0]!=class="num">0) { class=class="str">"cmt">// 若有支撑线,那么是上升趋势 class="kw">return(class="num">1); } if(res[class="num">0]!=class="num">0) { class=class="str">"cmt">// 若有阻力线,那么是下降趋势 class="kw">return(-class="num">1); } class="kw">return(class="num">0); } class=class="str">"cmt">// 找到买入持仓止损水平的方法 class="type">class="kw">double BuyStoploss() { class="kw">return(sup[class="num">0]); } class=class="str">"cmt">// 找到卖出持仓止损水平的方法 class="type">class="kw">double SellStoploss() { class="kw">return(res[class="num">0]); }
「把 NRTR 止损接进现成 EA」
在 MetaEditor 里把 My_First_EA_SARTrailing 另存为 My_First_EA_NRTRTrailing,外部参数换成 NRTR 的周期与倍数,再把原来 CParabolicStop 的实例改成 CNRTRStop。实例创建位置放在外部变量之后即可,OnInit() 里对应改掉 SetParameters() 的传参,编译后跑一轮测试。 附在文中的测试图(图10)显示,换上 NRTR 追踪止损后,该 EA 的净值曲线与无追踪止损版本(图7)几乎重叠,变动极小。同条件下此前抛物线 SAR 追踪止损的改善更明显,说明这套策略里 SAR 段更适配。 外汇与贵金属杠杆高、滑点跳空频繁,追踪止损只是概率上的回撤控制手段,不保证盈利。NRTR 周期 40、倍数 2 是原文用例,你可以直接开 MT5 改这两个数看曲线反应,找出更贴合自己品种的档位。
input class="type">int TrailingNRTRPeriod = class="num">40; input class="type">class="kw">double TrailingNRTRK = class="num">2; CNRTRStop Trailing; class=class="str">"cmt">// 创建类的实例 Trailing.SetParameters(TrailingNRTRPeriod,TrailingNRTRK)