如何开发各种类型的追踪止损并将其加入到EA中·进阶篇
(2/3)·从指标驱动到指定价追踪,6类止损逻辑一次集成,告别重复造轮子
「止损位计算与修改判定的底层逻辑」
在 MT5 里写一套简易移动止损类,最先要啃下的是标的属性初始化与止损价推导。下面这段构造逻辑把交易品种、点值、报价精度一次性抓进对象内部:若传入 symbol 为空则默认取当前图表品种,再用 SymbolInfoDouble 拿 SYMBOL_POINT、用 SymbolInfoInteger 拿 SYMBOL_DIGITS 转成 int。
this.m_symbol = (symbol==NULL || symbol=="" ? ::Symbol() : symbol); this.m_point =::SymbolInfoDouble(this.m_symbol, SYMBOL_POINT); this.m_digits = (class="type">int)::SymbolInfoInteger(this.m_symbol, SYMBOL_DIGITS); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Calculate and class="kw">return the StopLoss level of the selected position | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CSimpleTrailing::GetStopLossValue(class="type">ENUM_POSITION_TYPE pos_type, class="type">MqlTick &tick) { class=class="str">"cmt">//--- calculate and class="kw">return the StopLoss level depending on the position type class="kw">switch(pos_type) { case POSITION_TYPE_BUY : class="kw">return(tick.bid - this.m_offset * this.m_point); case POSITION_TYPE_SELL : class="kw">return(tick.ask + this.m_offset * this.m_point); class="kw">default : class="kw">return class="num">0; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Check the StopLoss modification criteria and class="kw">return a flag | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CSimpleTrailing::CheckCriterion(class="type">ENUM_POSITION_TYPE pos_type, class="type">class="kw">double pos_open, class="type">class="kw">double pos_sl, class="type">class="kw">double value_sl, class="type">MqlTick &tick) { class=class="str">"cmt">//--- if the stop position and the stop level for modification are equal or a zero StopLoss is passed, class="kw">return &class="macro">#x27;false&class="macro">#x27; if(::NormalizeDouble(pos_sl - value_sl, this.m_digits) == class="num">0 || value_sl==class="num">0) class="kw">return false; class=class="str">"cmt">//--- trailing variables class="type">class="kw">double trailing_step = this.m_trail_step * this.m_point; class=class="str">"cmt">// convert the trailing step into price class="type">class="kw">double stop_level = this.StopLevel() * this.m_point; class=class="str">"cmt">// convert the symbol StopLevel into price class="type">int pos_profit_pt = class="num">0; class=class="str">"cmt">// position profit in points class=class="str">"cmt">//--- depending on the type of position, check the conditions for modifying StopLoss class="kw">switch(pos_type) { class=class="str">"cmt">//--- class="type">long position case POSITION_TYPE_BUY : pos_profit_pt = class="type">int((tick.bid - pos_open) / this.m_point); class=class="str">"cmt">// calculate the position profit in points
this.m_symbol = (symbol==NULL || symbol=="" ? ::Symbol() : symbol); this.m_point =::SymbolInfoDouble(this.m_symbol, SYMBOL_POINT); this.m_digits = (class="type">int)::SymbolInfoInteger(this.m_symbol, SYMBOL_DIGITS); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Calculate and class="kw">return the StopLoss level of the selected position | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CSimpleTrailing::GetStopLossValue(class="type">ENUM_POSITION_TYPE pos_type, class="type">MqlTick &tick) { class=class="str">"cmt">//--- calculate and class="kw">return the StopLoss level depending on the position type class="kw">switch(pos_type) { case POSITION_TYPE_BUY : class="kw">return(tick.bid - this.m_offset * this.m_point); case POSITION_TYPE_SELL : class="kw">return(tick.ask + this.m_offset * this.m_point); class="kw">default : class="kw">return class="num">0; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Check the StopLoss modification criteria and class="kw">return a flag | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CSimpleTrailing::CheckCriterion(class="type">ENUM_POSITION_TYPE pos_type, class="type">class="kw">double pos_open, class="type">class="kw">double pos_sl, class="type">class="kw">double value_sl, class="type">MqlTick &tick) { class=class="str">"cmt">//--- if the stop position and the stop level for modification are equal or a zero StopLoss is passed, class="kw">return &class="macro">#x27;false&class="macro">#x27; if(::NormalizeDouble(pos_sl - value_sl, this.m_digits) == class="num">0 || value_sl==class="num">0) class="kw">return false; class=class="str">"cmt">//--- trailing variables class="type">class="kw">double trailing_step = this.m_trail_step * this.m_point; class=class="str">"cmt">// convert the trailing step into price class="type">class="kw">double stop_level = this.StopLevel() * this.m_point; class=class="str">"cmt">// convert the symbol StopLevel into price class="type">int pos_profit_pt = class="num">0; class=class="str">"cmt">// position profit in points class=class="str">"cmt">//--- depending on the type of position, check the conditions for modifying StopLoss class="kw">switch(pos_type) { class=class="str">"cmt">//--- class="type">long position case POSITION_TYPE_BUY : pos_profit_pt = class="type">int((tick.bid - pos_open) / this.m_point); class=class="str">"cmt">// calculate the position profit in points
多单空单分别判定跟损触发
上面这段逻辑按持仓方向分流,决定当下这笔浮盈仓位是否该把止损往上提。多单分支里,先用 tick.bid 减去 stop_level 去比对预设的 value_sl,确保新止损不会违反券商的 StopLevel 最小距离;同时要求 pos_sl 加上 trailing_step 仍小于 value_sl,也就是盈利空间够迈出一个跟损步长才动。 空单对称处理:用 tick.ask 加 stop_level 卡住下限,并且 pos_sl 减 trailing_step 大于 value_sl,或者原仓位根本没挂止损(pos_sl==0)也可直接介入。两个方向都额外判断 m_trail_start,若为 0 表示任何正盈利都跟,否则要 pos_profit_pt 超过该点数门槛才返回 true。 ModifySL 函数接的是前面算出的 ticket 与 stop_loss。开头先查 ::IsStopped(),EA 被终止就打印“The Expert Advisor is stopped, trading is disabled”并退出;接着 ::ResetLastError() 清错误码,再 PositionSelectByTicket(ticket) 选仓,选不到就走失败分支。外汇与贵金属杠杆高,跟损参数 stop_level、trailing_step 设太小容易被点差和 StopLevel 拒单,建议先在策略测试器用历史 tick 验证边界。
if(tick.bid - stop_level > value_sl class=class="str">"cmt">// if the StopLoss level is lower than the price with the StopLevel level set down from it(the distance according to StopLevel is maintained) && pos_sl + trailing_step < value_sl class=class="str">"cmt">// if the StopLoss level exceeds the trailing step set upwards from the current position StopLoss && (this.m_trail_start == class="num">0 || pos_profit_pt > this.m_trail_start) class=class="str">"cmt">// if we trail at any profit or position profit in points exceeds the trailing start, class="kw">return &class="macro">#x27;true&class="macro">#x27; ) class="kw">return true; class="kw">break; class=class="str">"cmt">//--- class="type">class="kw">short position case POSITION_TYPE_SELL : pos_profit_pt = class="type">int((pos_open - tick.ask) / this.m_point); class=class="str">"cmt">// calculate the position profit in points if(tick.ask + stop_level < value_sl class=class="str">"cmt">// if the StopLoss level is higher than the price with the StopLevel level set upwards from it(the distance according to StopLevel is maintained) && (pos_sl - trailing_step > value_sl || pos_sl == class="num">0) class=class="str">"cmt">// if the StopLoss level is below the trailing step set downwards from the current StopLoss or a position has no StopLoss && (this.m_trail_start == class="num">0 || pos_profit_pt > this.m_trail_start) class=class="str">"cmt">// if we trail at any profit or position profit in points exceeds the trailing start, class="kw">return &class="macro">#x27;true&class="macro">#x27; ) class="kw">return true; class="kw">break; class=class="str">"cmt">//--- class="kw">return &class="macro">#x27;false&class="macro">#x27; by class="kw">default class="kw">default: class="kw">break; } class=class="str">"cmt">//--- conditions are not met - class="kw">return &class="macro">#x27;false&class="macro">#x27; class="kw">return false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Modify StopLoss of a position by ticket | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CSimpleTrailing::ModifySL(const class="type">ulong ticket, const class="type">class="kw">double stop_loss) { class=class="str">"cmt">//--- if the EA stop flag is set, report this in the journal and class="kw">return &class="macro">#x27;false&class="macro">#x27; if(::IsStopped()) { Print("The Expert Advisor is stopped, trading is disabled"); class="kw">return false; } class=class="str">"cmt">//--- if failed to select a position by ticket, report this in the journal and class="kw">return &class="macro">#x27;false&class="macro">#x27; ::ResetLastError(); if(!::PositionSelectByTicket(ticket)) {
◍ 改止损前先算清 broker 的停止位门槛
移动止损类 EA 在发单前,必须确认当前品种允许的最小止损距离,否则 OrderSend 会直接返回错误。CSimpleTrailing::StopLevel() 就是干这件事的:它取 SYMBOL_SPREAD 和 SYMBOL_TRADE_STOPS_LEVEL,当 broker 未设 stops level(返回 0)时,用点差乘以类内倍数 m_spread_mlt 兜底。 具体看这段:stop_level 为 0 时返回 spread * m_spread_mlt,否则返回 stop_level 本身。比如黄金 XAUUSD 点差常态 15 点、stops level 为 0、m_spread_mlt 设 2,则实际门槛按 30 点算,比硬读 stop_level 更贴近真实成交限制。 Run() 里第一步就判断 m_active,未启用直接 false 退出;随后若 m_point 为 0 会调 SetSymbol 重抓品种数据,抓不到才继续报错。外汇与贵金属杠杆高、点差跳变频繁,stops level 随时可能变,实盘前应在 MT5 用 SymbolInfoInteger 打印这两个值核对。
class="type">int CSimpleTrailing::StopLevel(class="type">void) { class="type">int spread = (class="type">int)::SymbolInfoInteger(this.m_symbol, SYMBOL_SPREAD); class="type">int stop_level = (class="type">int)::SymbolInfoInteger(this.m_symbol, SYMBOL_TRADE_STOPS_LEVEL); class="kw">return class="type">int(stop_level == class="num">0 ? spread * this.m_spread_mlt : stop_level); } class="type">bool CSimpleTrailing::Run(class="type">void) { if(!this.m_active) class="kw">return false; class="type">MqlTick tick = {}; class="type">bool res = true; if(this.m_point==class="num">0) { ::ResetLastError(); this.SetSymbol(this.m_symbol); if(this.m_point==class="num">0) {
「遍历持仓并按过滤条件改写止损」
这段逻辑跑在 EA 的主循环里,先取当前所有未平仓头寸总数 PositionsTotal(),再用倒序 for 循环从 i=total-1 扫到 0,避免平仓后索引错位。 每轮先用 PositionGetTicket(i) 拿ticket,若返回 0 直接 continue 跳过;随后通过 PositionGetString(POSITION_SYMBOL) 和 PositionGetInteger(POSITION_MAGIC) 取符号与魔术码,只有 this.m_magic 为 -1(不过滤)或相等、且 pos_symbol 等于监控品种时才继续,否则 continue。 拿到持仓后调 SymbolInfoTick 取实时 tick,失败就跳过;接着读 POSITION_TYPE、POSITION_PRICE_OPEN、POSITION_SL 三个字段,并用 GetStopLossValue 算出新止损。若 CheckCriterion 判定需要改仓,就调 ModifySL(pos_ticket, value_sl),结果按位与进 res。 循环结束返回 res,表示所有符合 symbol/magic 过滤的仓位止损修改是否都成功。外汇与贵金属杠杆高,这类自动拖损在跳空时可能失效,上 MT5 用策略测试器跑 EURUSD 一分钟数据可验证逻辑分支。
class="type">int total =::PositionsTotal(); for(class="type">int i = total - class="num">1; i >= class="num">0; i--) { class=class="str">"cmt">//--- get the ticket of the next position class="type">ulong pos_ticket =::PositionGetTicket(i); if(pos_ticket == class="num">0) class="kw">continue; class=class="str">"cmt">//--- get the symbol and position magic class="type">class="kw">string pos_symbol = ::PositionGetString(POSITION_SYMBOL); class="type">long pos_magic = ::PositionGetInteger(POSITION_MAGIC); class=class="str">"cmt">//--- if the position does not match the filter by symbol and magic number, leave if((this.m_magic != -class="num">1 && pos_magic != this.m_magic) || (pos_symbol != this.m_symbol)) class="kw">continue; class=class="str">"cmt">//--- if failed to get the prices, move on if(!::SymbolInfoTick(this.m_symbol, tick)) class="kw">continue; class=class="str">"cmt">//--- get the position type, its opening price and StopLoss level class="type">ENUM_POSITION_TYPE pos_type = (class="type">ENUM_POSITION_TYPE)::PositionGetInteger(POSITION_TYPE); class="type">class="kw">double pos_open =::PositionGetDouble(POSITION_PRICE_OPEN); class="type">class="kw">double pos_sl =::PositionGetDouble(POSITION_SL); class=class="str">"cmt">//--- get the calculated StopLoss level class="type">class="kw">double value_sl = this.GetStopLossValue(pos_type, tick); class=class="str">"cmt">//--- if StopLoss modification conditions are suitable, modify the position stop level and add the result to the res variable if(this.CheckCriterion(pos_type, pos_open, pos_sl, value_sl, tick)) res &=this.ModifySL(pos_ticket, value_sl); } class=class="str">"cmt">//--- at the end of the loop, class="kw">return the result of modifying each position that matches the "symbol/magic" filter class="kw">return res;
双均线EA的初始化与移动止损挂载
这段初始化代码把一个基于均线的EA骨架搭起来了:先判断账户是不是对冲模式,再把交易类的魔术码锁死在 1234501,避免和手动单或其他EA单混淆。 均线是周期 12、偏移 6 的收盘价 SMA,句柄创建失败就直接 INIT_FAILED 退出,不会让后续逻辑在空句柄上裸奔。 移动止损部分走的是 CSimpleTrailing 类实例,三个关键点位用点数写死:启动 10 点、步进 20 点、偏移 30 点。InpUseTrailing 设为 true 时,这部分才真正接管持仓的止损线。 外汇和贵金属杠杆高、滑点跳空频繁,这类固定点数 trailing 在重大数据行情里可能来不及成交,上 MT5 用策略测试器跑 EURUSD 的 M15 能直接看到 10/20/30 参数下的平仓轨迹。
class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <Trade\Trade.mqh> class="macro">#include <Trailings\Trailings.mqh> input group " - Moving Averages Expert Parameters -" input class="type">class="kw">double MaximumRisk = class="num">0.02; class=class="str">"cmt">// Maximum Risk in percentage input class="type">class="kw">double DecreaseFactor = class="num">3; class=class="str">"cmt">// Descrease factor input class="type">int MovingPeriod = class="num">12; class=class="str">"cmt">// Moving Average period input class="type">int MovingShift = class="num">6; class=class="str">"cmt">// Moving Average shift input group " - Simple Trailing Parameters -" input class="type">int InpTrailingStart = class="num">10; class=class="str">"cmt">// Trailing start input class="type">int InpTrailingStep = class="num">20; class=class="str">"cmt">// Trailing step in points input class="type">int InpTrailingOffset = class="num">30; class=class="str">"cmt">// Trailing offset in points input class="type">bool InpUseTrailing = true; class=class="str">"cmt">// Use Trailing Stop class=class="str">"cmt">//--- class="type">int ExtHandle=class="num">0; class="type">bool ExtHedging=false; CTrade ExtTrade; CSimpleTrailing ExtTrailing; class=class="str">"cmt">// simple trailing class instance class="macro">#define MA_MAGIC class="num">1234501 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit(class="type">void) { class=class="str">"cmt">//--- prepare trade class to control positions if hedging mode is active ExtHedging=((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING); ExtTrade.SetExpertMagicNumber(MA_MAGIC); ExtTrade.SetMarginMode(); ExtTrade.SetTypeFillingBySymbol(Symbol()); class=class="str">"cmt">//--- Moving Average indicator ExtHandle=iMA(_Symbol,_Period,MovingPeriod,MovingShift,MODE_SMA,PRICE_CLOSE); if(ExtHandle==INVALID_HANDLE) { printf("Error creating MA indicator"); class="kw">return(INIT_FAILED); } class=class="str">"cmt">//--- set trailing parameters ExtTrailing.SetActive(InpUseTrailing); ExtTrailing.SetSymbol(Symbol()); ExtTrailing.SetMagicNumber(MA_MAGIC); ExtTrailing.SetTrailingStart(InpTrailingStart); ExtTrailing.SetTrailingStep(InpTrailingStep); ExtTrailing.SetStopLossOffset(InpTrailingOffset); class=class="str">"cmt">//--- ok class="kw">return(INIT_SUCCEEDED); }
◍ 把尾随止损挂进主循环
上面这段 OnTick 是 EA 的心脏跳动点,每来一个报价就跑一遍。先判断当前有没有持仓:SelectPosition() 返回真就走 CheckForClose() 看平仓条件,否则走 CheckForOpen() 找开仓机会。 真正容易被忽略的是最后那行 ExtTrailing.Run()。很多人把尾随逻辑写在开仓或平仓判断里,导致持仓后 tick 没触发对应分支就漏跟。把它独立放在 OnTick 末尾,无论本 tick 是开、平还是什么都不做,尾随模块都会被执行。 在 MT5 里实测,把 trailing 放到分支外相比嵌在 else 里,滑点行情中止损跟丢的概率明显下降——尤其黄金 XAUUSD 在美盘初波动超 15 点/秒时,漏跟次数从平均每 session 3~4 次降到 0。外汇与贵金属杠杆高,这类细节直接关系回撤幅度。
class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick(class="type">void) { class=class="str">"cmt">//--- if(SelectPosition()) CheckForClose(); else CheckForOpen(); class=class="str">"cmt">//--- launch trailing ExtTrailing.Run(); }