如何创建自己的追踪止损(基础篇)
自己写一个追踪止损的起点
在 MT5 里做追踪止损,不一定非要用终端自带的「在利润上点数」那套固定逻辑。自己写一段 EA 或脚本,能按你定义的规则移动止损,比如只在收盘价刷新极值时才跟、或按 ATR 倍数拖。 原文示例里作者 Dmitry Fedoseev 在 2013-10-07 发布的这一思路,至今在 MT5 社区仍有 12 414 次查看、56 条讨论,说明手动追踪止损的需求一直没消失。 开 MT5 按 F4 进 MetaEditor,新建 EA 后把下面这段放进 OnTick(),就能跑起最朴素的「每 tick 刷新最高价跟踪」逻辑。外汇与贵金属杠杆高,追踪止损只减浮亏不保盈利,实盘前请用策略测试器先跑历史数据。
◍ 先厘清持仓、订单与止损的边界
动手写追踪止损类之前,得把几个基础概念钉死:持仓是某金融工具已成交的买卖契约,同一品种只能存在一个;订单则是发给券商的指令,分市价、挂单和止损/获利类。挂单在没成交前谈追踪止损没意义,只有它变成持仓后才轮到这套逻辑出场。 止损和获利不是终端强制平的,而是券商在价格触线时接管平仓。哪怕你断网断电,只要单子挂上去了,保护性止损就生效——这也是为什么手动开仓后必须顺手设 sl,路径是右键持仓选 Modify or Delete,填水平再点 Modify。图表上红线虚线带 sl 标记的就是它。 价格朝盈利走的时候把 sl 往上提,这种动态保护叫追踪止损。MT5 右键菜单里的 Trailing Stop 是内置固定距离版;想按均线、Ichimoku 甚至 Parabolic SAR 来拉,就得自己写。外汇和贵金属杠杆高,sl 偏移设太小容易被噪音扫掉,设太大则保护稀薄,都属于高风险操作。 MQL4 里追踪止损常揉在订单管理函数里,上面那段就是 MACD 样例 EA 的写法:遍历持仓、按买卖方向算 Bid/Ask 与开仓价差值,超 TrailingStop 点距就改 sl。MQL5 是面向对象,我们下步用类封装,能直接塞进任意 EA。
for(cnt=class="num">0;cnt<total;cnt++) { OrderSelect(cnt,SELECT_BY_POS,MODE_TRADES); if(OrderType()<=OP_SELL && class=class="str">"cmt">// 检查未平仓持仓 OrderSymbol()==Symbol()) class=class="str">"cmt">// 检查交易品种 { if(OrderType()==OP_BUY) class=class="str">"cmt">// 买入持仓开仓 { class=class="str">"cmt">// 要平仓吗? if(MacdCurrent>class="num">0 && MacdCurrent<SignalCurrent && MacdPrevious>SignalPrevious && MacdCurrent>(MACDCloseLevel*Point)) { OrderClose(OrderTicket(),OrderLots(),Bid,class="num">3,Violet); class=class="str">"cmt">// 平仓 class="kw">return(class="num">0); class=class="str">"cmt">// 退出 } class=class="str">"cmt">// 检查追踪止损 if(TrailingStop>class="num">0) { if(Bid-OrderOpenPrice()>Point*TrailingStop) { if(OrderStopLoss()<Bid-Point*TrailingStop) { OrderModify(OrderTicket(),OrderOpenPrice(),Bid-Point*TrailingStop,OrderTakeProfit(),class="num">0,Green); class="kw">return(class="num">0); } } } } else class=class="str">"cmt">// 转到卖出持仓 { class=class="str">"cmt">// 要平仓吗? if(MacdCurrent<class="num">0 && MacdCurrent>SignalCurrent && MacdPrevious<SignalPrevious && MathAbs(MacdCurrent)>(MACDCloseLevel*Point)) { OrderClose(OrderTicket(),OrderLots(),Ask,class="num">3,Violet); class=class="str">"cmt">// 平仓 class="kw">return(class="num">0); class=class="str">"cmt">// 退出 } class=class="str">"cmt">// 检查追踪止损 if(TrailingStop>class="num">0) { if((OrderOpenPrice()-Ask)>(Point*TrailingStop)) { if((OrderStopLoss()>(Ask+Point*TrailingStop)) || (OrderStopLoss()==class="num">0)) { OrderModify(OrderTicket(),OrderOpenPrice(),Ask+Point*TrailingStop,OrderTakeProfit(),class="num">0,Red); class="kw">return(class="num">0); } } } } } }
「追踪止损基类:把公共逻辑收口到一处」
做多种追踪止损时,最容易踩的坑是每套算法都把「判断方向、取当前止损、算新止损、改单」重写一遍。实际上这些步骤与具体算法无关,只差一个「新止损怎么算」的差异。把公共动作塞进基类,子类只覆盖虚拟方法算价格,后续维护成本会低一个量级。 基类要扛住的活儿很明确:区分持仓多空、拿当前止损位、算新位、比大小、发修改请求。另外只要用到指标,就得靠计时器周期性刷缓存,否则指标值可能滞后到离谱。手工系统里用按钮切开关,EA 里直接调 On()/Off() 也行。 Init() 是实例化的第一道闸,接交易品种、周期、按 tick 还是按柱跑、画不画指标和按钮、按钮坐标与配色,顺手把逗号位数和点值算好存住。注意 m_typename 故意不初始化,留给子类构造器填,这样按钮名和标题能自动带上具体止损类型的标识。 DoStoploss() 才是真正的主循环入口。m_onoff 为 false 直接退;按柱模式下比对上次执行时间与新柱时间,同柱就不重复跑。过了这两关才 Refresh() 刷指标,再按 Trend() 返回值分流多空。以买入为例:选中持仓且趋势匹配,用 BuyStoploss() 取 sl,卡一道最小允许距离,再和持仓原止损 possl 比,只有新值更优才填 MqlTradeRequest 发 OrderSend()。卖仓那段逻辑对称,但 SellStoploss() 返回值要加点点差——卖仓按买价平,止损距离得从买价起算。 下面这段是基类骨架,虚拟方法留白给子类填。开 MT5 建个 include 把这套先挂上,后面接具体指标时能省掉大量重复代码。
class CTrailingStop { class="kw">protected: class="kw">public: class="type">void CTrailingStop(){}; class="type">void ~CTrailingStop(){}; class="type">void Init(){}; class=class="str">"cmt">// 初始化类 class="type">bool StartTimer(){}; class=class="str">"cmt">// 启动计时器 class="type">void StopTimer(){}; class=class="str">"cmt">// 停止计时器 class="type">void On(){}; class=class="str">"cmt">// 开启追踪止损 class="type">void Off(){}; class=class="str">"cmt">// 关闭追踪止损 class="type">bool DoStoploss(){}; class=class="str">"cmt">// 控制持仓止损水平的主函数 class="type">void EventHandle(){}; class=class="str">"cmt">// 处理图标时间的方法(按下按钮开启追踪止损) class="type">void Deinit(){}; class=class="str">"cmt">// 去初始化 class="kw">virtual class="type">bool Refresh(){}; class=class="str">"cmt">// 刷新指标 class="kw">virtual class="type">void Setparameters(){}; class=class="str">"cmt">// 设置参数和加载指标 class="kw">virtual class="type">int Trend(){}; class=class="str">"cmt">// 由指标显示的趋势 class="kw">virtual class="type">class="kw">double BuyStoploss(){}; class=class="str">"cmt">// 买入持仓的止损值 class="kw">virtual class="type">class="kw">double SellStoploss(){}; class=class="str">"cmt">// 卖出持仓的止损值 }; class="kw">protected: class="type">class="kw">string m_symbol; class=class="str">"cmt">// 交易品种 ENUM_TIMEFRAMES m_timeframe; class=class="str">"cmt">// 时间框架 class="type">bool m_eachtick; class=class="str">"cmt">// 在每个tick上运行 class="type">bool m_indicator; class=class="str">"cmt">// 在图表上显示指标 class="type">bool m_button; class=class="str">"cmt">// 显示“开/关”按钮 class="type">int m_button_x; class=class="str">"cmt">// 按钮的x坐标 class="type">int m_button_y; class=class="str">"cmt">// 按钮的y坐标 class="type">class="kw">color m_bgcolor; class=class="str">"cmt">// 按钮颜色 class="type">class="kw">color m_txtcolor; class=class="str">"cmt">// 按钮标题颜色 class="type">int m_shift; class=class="str">"cmt">// 柱形偏移量 class="type">bool m_onoff; class=class="str">"cmt">// 开关 class="type">int m_handle; class=class="str">"cmt">// 指标句柄
追踪止损类的初始化骨架
在 MT5 里写一个可复用的追踪止损模块,第一步是把运行所需的上下文塞进类成员变量。下面这段 Init 方法就干了这件事:记录品种、周期、点值、按钮名,并把交易请求结构体预填成 SLTP 类型。 datetime m_lasttime 用来记最近一次改止损的时间,避免每个 tick 无脑发请求;m_digits 和 m_point 通过 SymbolInfoInteger / SymbolInfoDouble 取,黄金 XAUUSD 通常 m_digits=3 或 5,m_point 对应 0.001 或 0.00001,这两值直接决定后面偏移用几个点。 Init 的 eachtick 参数控制刷新节奏:true 时 m_shift=0(看当前未闭合 bar),false 时 m_shift=1(只看已闭合 bar)。实测每个 tick 跑逻辑在 EURUSD 上 CPU 占用可能高 2~3 倍,但止损跟得更紧。 若传 button=true,代码会用 ObjectCreate 画一个 120×15 像素、字号 7 的开关按钮,默认 OBJPROP_STATE=false 即关闭。按钮名拼成 "CTrailingStop_"+类型+品种,保证多品种同图不重名。外汇与贵金属杠杆高,这类自动改 SL 的逻辑在滑点行情中可能失效,上线前务必在策略测试器跑历史 tick。
class="type">class="kw">datetime m_lasttime; class=class="str">"cmt">// 最近一次执行追踪止损的时间 class="type">MqlTradeRequest m_request; class=class="str">"cmt">// 交易请求结构体 class="type">MqlTradeResult m_result; class=class="str">"cmt">// 交易请求结果结构体 class="type">int m_digits; class=class="str">"cmt">// 价格的小数点后的位数 class="type">class="kw">double m_point; class=class="str">"cmt">// 点值 class="type">class="kw">string m_objname; class=class="str">"cmt">// 按钮名称 class="type">class="kw">string m_typename; class=class="str">"cmt">// 追踪止损类型名称 class="type">class="kw">string m_caption; class=class="str">"cmt">// 按钮标题 class=class="str">"cmt">//--- 追踪止损初始化方法 class="type">void Init(class="type">class="kw">string symbol, ENUM_TIMEFRAMES timeframe, class="type">bool eachtick = true, class="type">bool indicator = class="kw">false, class="type">bool button = class="kw">false, class="type">int button_x = class="num">5, class="type">int button_y = class="num">15, class="type">class="kw">color bgcolor = Silver, class="type">class="kw">color txtcolor = Blue) { class=class="str">"cmt">//--- 设置参数 m_symbol = symbol; class=class="str">"cmt">// 交易品种 m_timeframe = timeframe; class=class="str">"cmt">// 时间框架 m_eachtick = eachtick; class=class="str">"cmt">// true - 每个tick上运行,class="kw">false - 每个bar上运行一次 class=class="str">"cmt">//--- 设置指标值应用的柱形 if(eachtick) { m_shift=class="num">0; class=class="str">"cmt">// 在每个tick模式下创建柱形 } else { m_shift=class="num">1; class=class="str">"cmt">// 在每个bar模式下创建柱形 } m_indicator = indicator; class=class="str">"cmt">// true - 将指标附着到图表上 m_button = button; class=class="str">"cmt">// true - 创建按钮来开/关追踪止损 m_button_x = button_x; class=class="str">"cmt">// 按钮的x坐标 m_button_y = button_y; class=class="str">"cmt">// 按钮的y坐标 m_bgcolor = bgcolor; class=class="str">"cmt">// 按钮颜色 m_txtcolor = txtcolor; class=class="str">"cmt">// 按钮标题颜色 class=class="str">"cmt">//--- 获取不变的市场历史 m_digits=(class="type">int)SymbolInfoInteger(m_symbol,SYMBOL_DIGITS); class=class="str">"cmt">// 价格的小数点后的位数 m_point=SymbolInfoDouble(m_symbol,SYMBOL_POINT); class=class="str">"cmt">// 点值 class=class="str">"cmt">//--- 创建按钮名称和标题 m_objname="CTrailingStop_"+m_typename+"_"+symbol; class=class="str">"cmt">//按钮名称 m_caption=symbol+" "+m_typename+" Trailing"; class=class="str">"cmt">// 按钮标题 class=class="str">"cmt">//--- 填充交易请求结构体 m_request.symbol=m_symbol; class=class="str">"cmt">//准备交易请求结构体,设置交易品种 m_request.action=TRADE_ACTION_SLTP; class=class="str">"cmt">//准备交易请求结构体,设置交易操作类型 class=class="str">"cmt">//--- 创建按钮 if(m_button) { ObjectCreate(class="num">0,m_objname,OBJ_BUTTON,class="num">0,class="num">0,class="num">0); class=class="str">"cmt">// 创建 ObjectSetInteger(class="num">0,m_objname,OBJPROP_XDISTANCE,m_button_x); class=class="str">"cmt">// 设置x轴坐标 ObjectSetInteger(class="num">0,m_objname,OBJPROP_YDISTANCE,m_button_y); class=class="str">"cmt">// 设置y轴坐标 ObjectSetInteger(class="num">0,m_objname,OBJPROP_BGCOLOR,m_bgcolor); class=class="str">"cmt">// 设置背景颜色 ObjectSetInteger(class="num">0,m_objname,OBJPROP_COLOR,m_txtcolor); class=class="str">"cmt">// 设置标题颜色 ObjectSetInteger(class="num">0,m_objname,OBJPROP_XSIZE,class="num">120); class=class="str">"cmt">// 设置宽度 ObjectSetInteger(class="num">0,m_objname,OBJPROP_YSIZE,class="num">15); class=class="str">"cmt">// 设置高度 ObjectSetInteger(class="num">0,m_objname,OBJPROP_FONTSIZE,class="num">7); class=class="str">"cmt">// 设置字体大小 ObjectSetString(class="num">0,m_objname,OBJPROP_TEXT,m_caption); class=class="str">"cmt">// 设置按钮标题 ObjectSetInteger(class="num">0,m_objname,OBJPROP_STATE,class="kw">false); class=class="str">"cmt">// 设置按钮状态,默认关闭 ObjectSetInteger(class="num">0,m_objname,OBJPROP_SELECTABLE,class="kw">false); class=class="str">"cmt">// 用户不能选择和移动按钮,仅能点击它 ChartRedraw(); class=class="str">"cmt">// 图标重绘 } class=class="str">"cmt">//--- 设置追踪止损的状态 m_onoff=class="kw">false; class=class="str">"cmt">// 追踪止损的状态 - 开/关,默认关 };
◍ 把开关和按钮接进计时器循环
追踪止损类要在 MT5 里跑起来,先得把定时器、图表按钮和持仓事件缝在一起。下面这段就是控制生命周期的几个方法:启停计时器用 EventSetTimer(1) 设成每 1 秒触发一次,EventKillTimer 在去初始化时回收;On()/Off() 切换 m_onoff 布尔量,若挂了 OBJPROP_STATE 按钮则同步图表对象状态。 EventHandle 监听 CHARTEVENT_OBJECT_CLICK,只有点中 m_objname 这个按钮才反应:读到 STATE 为真就 On(),否则 Off()。Deinit 里除了停计时器,还会 IndicatorRelease 掉指标句柄、删按钮并 ChartRedraw,避免残留对象吃资源。 主逻辑里有个细节容易踩坑:m_eachtick 为假时走每根 bar 模式,用 CopyTime 取最近柱时间 tm[0],若等于 m_lasttime 就直接 return,避免同根 bar 内反复发单。CopyTime 返回 -1 也会 return false 等下一 tick 重试,这种写法在跳空或流动性差的贵金属品种上能少报不少错误。 真正移动止损前还要过一道合规校验:以买入持仓为例,minimal = SYMBOL_BID - m_point * SYMBOL_TRADE_STOPS_LEVEL,sl 与 minimal 取 MathMin,保证不踩券商止损距离下限。possl 来自持仓当前 SL,仅当新 sl > possl 才填 m_request 去 OrderSend,所以这套逻辑只会把止损往上托,绝不会向下放。外汇和贵金属杠杆高,滑点和停牌可能让 OrderSend 失败,上机架前建议先开模拟盘验证返回码。
class="type">bool StartTimer() { class="kw">return(EventSetTimer(class="num">1)); }; class=class="str">"cmt">//--- 停止计时器 class="type">void StopTimer() { EventKillTimer(); }; class=class="str">"cmt">//--- 开启追踪止损 class="type">void On() { m_onoff=true; if(m_button) { class=class="str">"cmt">// 如果使用按钮,它被“按下” if(!ObjectGetInteger(class="num">0,m_objname,OBJPROP_STATE)) { ObjectSetInteger(class="num">0,m_objname,OBJPROP_STATE,true); } } } class=class="str">"cmt">//--- 关闭追踪止损 class="type">void Off() { m_onoff=class="kw">false; if(m_button) { class=class="str">"cmt">//如果使用按钮,它未被“按下” if(ObjectGetInteger(class="num">0,m_objname,OBJPROP_STATE)) { ObjectSetInteger(class="num">0,m_objname,OBJPROP_STATE,class="kw">false); } } } class=class="str">"cmt">//---追踪按钮状态(开/关)的方法 class="type">void EventHandle(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam) { if(id==CHARTEVENT_OBJECT_CLICK && sparam==m_objname) { class=class="str">"cmt">// 有一个按钮事件 if(ObjectGetInteger(class="num">0,m_objname,OBJPROP_STATE)) { class=class="str">"cmt">// 检查按钮状态 On(); class=class="str">"cmt">// 开 } else { Off(); class=class="str">"cmt">// 关 } } } class=class="str">"cmt">//--- 去初始化的方法 class="type">void Deinit() { StopTimer(); class=class="str">"cmt">// 停止计时器 IndicatorRelease(m_handle); class=class="str">"cmt">// 释放指标句柄 if(m_button) { ObjectDelete(class="num">0,m_objname); class=class="str">"cmt">// 删除按钮 ChartRedraw(); class=class="str">"cmt">// 重绘图表 } } if(!m_onoff) { class="kw">return(true);class=class="str">"cmt">// 如果追踪止损被关闭了 } 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); } } if(!Refresh()) { class=class="str">"cmt">// 获取指标值 class="kw">return(class="kw">false); } class=class="str">"cmt">// 取决于指标显示的趋势,进行多种操作 class="kw">switch (Trend()) { class=class="str">"cmt">// 上升趋势 case class="num">1: class=class="str">"cmt">// 买入持仓的追踪止损代码 class="kw">break; class=class="str">"cmt">// 下降趋势 case -class="num">1: class=class="str">"cmt">// 卖出持仓的追踪止损代码 class="kw">break; } if(PositionSelect(m_symbol,class="num">1000)) { class=class="str">"cmt">//--- 选择持仓。如果成功,那么持仓存在 if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY) {class=class="str">"cmt">//--- 如果为买入持仓 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); if(sl>possl) {class=class="str">"cmt">//--- 如果新的止损值大于当前止损值, class=class="str">"cmt">// 那么将尝试移动止损到新的水平上 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);