本文讨论如何在跨平台智能交易系统中设置自定义停止价位。它还讨论了一种紧密相关的方法, 即随着时间的推移, 定义停止位的演化。·综合运用
(3/3)·点值止损只是起点,用图表价和CTrail把动态停止逻辑真正写进多平台EA
很多EA开发者把止损只当成入场价加减固定点数,遇到基于指标输出或OHLC动态算出的价位就卡壳。跨平台项目里这类自定义停止位若处理不当,回测和实盘会出现不该有的滑点缺口。本文收尾整个系列,把自定义停止与演化停止一次讲透。
跟踪止损基类里的价格触发逻辑
做跟踪止损前,先看清基类 CTrailBase 把「激活、失效、实时挂价」三件事拆成了三个虚函数。ActivationPrice 用 m_start 个点作为触发门槛,DeactivationPrice 用 m_end 个点作为失效线,Price 则按当前 Bid/Ask 与 m_trail 算出新止损或止盈位。 Set 方法的默认参数是 step=1、end=0,意味着若你只传 trail 和 st,跟踪步长就是 1 个点、且不设失效价位。在外汇与贵金属上,点值随品种跳变,用 Point() 而不是写死小数,是避免跨品种出错的基本动作,这类品种杠杆高、滑点可能吞掉窄幅跟踪。 ActivationPrice 对 BUY 返回 entry_price+m_start*Point(),SELL 则返回减去;返回 0 代表类型不匹配。DeactivationPrice 里 m_end==0 时直接回 0,说明 0 被当作「不失效」的哨兵值,不是价格 0。 Refresh 每次用 symbol 名比对,指针不对就向 m_symbol_man 重新取对象。CheckPointer 失败会返回 false,调用方若忽略这个返回值,后续 Point() 访问可能直接崩 EA。
class="type">void CTrailBase::Set(class="kw">const class="type">class="kw">double trail,class="kw">const class="type">class="kw">double st,class="kw">const class="type">class="kw">double step=class="num">1,class="kw">const class="type">class="kw">double end=class="num">0) { m_trail=trail; m_start=st; m_end=end; m_step=step; } class="type">bool CTrailBase::Refresh(class="kw">const class="type">class="kw">string symbol) { if(!CheckPointer(m_symbol) || StringCompare(m_symbol.Name(),symbol)!=class="num">0) m_symbol=m_symbol_man.Get(symbol); class="kw">return CheckPointer(m_symbol); } class="type">class="kw">double CTrailBase::ActivationPrice(class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double entry_price) { if(type==ORDER_TYPE_BUY) class="kw">return entry_price+m_start*m_symbol.Point(); else if(type==ORDER_TYPE_SELL) class="kw">return entry_price-m_start*m_symbol.Point(); class="kw">return class="num">0; } class="type">class="kw">double CTrailBase::DeactivationPrice(class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double entry_price) { if(type==ORDER_TYPE_BUY) class="kw">return m_end==class="num">0?class="num">0:entry_price+m_end*m_symbol.Point(); else if(type==ORDER_TYPE_SELL) class="kw">return m_end==class="num">0?class="num">0:entry_price-m_end*m_symbol.Point(); class="kw">return class="num">0; } class="type">class="kw">double CTrailBase::Price(class="kw">const ENUM_ORDER_TYPE type) { if(type==ORDER_TYPE_BUY) { if(m_target==TRAIL_TARGET_STOPLOSS) class="kw">return m_symbol.Bid()-m_trail*m_symbol.Point(); else if(m_target==TRAIL_TARGET_TAKEPROFIT) class="kw">return m_symbol.Ask()+m_trail*m_symbol.Point(); } else if(type==ORDER_TYPE_SELL) { if(m_target==TRAIL_TARGET_STOPLOSS) class="kw">return m_symbol.Ask()+m_trail*m_symbol.Point();
「跟踪止损与止盈的触发判定逻辑」
这段基类代码把跟踪止损(TRAIL_TARGET_STOPLOSS)和跟踪止盈(TRAIL_TARGET_TAKEPROFIT)两套规则塞进了同一个 Check 方法,靠 m_target 枚举切换。外汇与贵金属杠杆高,跟踪触发一旦写错,滑点扩大时可能直接吞掉已有浮盈,建议先在策略测试器跑历史 tick 验证。 对于 BUY 单的跟踪止损:当 m_step>0 且价格越过激活线(activation - m_trail*point)并满足新价较前高抬升至少 m_step 个点时,next_stop 取新价;否则退回 activation - m_trail*point 的静态拖尾。最后用 next_stop<price 兜底,保证止损不高于当前价。 SELL 单的对称逻辑里,条件反转为 price<=activation+m_trail*point 与 new_price<price-m_step*point,且兜底判断变成 next_stop>price 才修正。注意 deactivation 非零时,BUY 止损要求 next_stop>=deactivation 才返回,止盈则要求 next_stop<=deactivation,方向完全相反。 实际调参时把 m_start、m_trail、m_step 打印到日志,观察黄金 XAUUSD 在美盘波动 15 点内是否被反复触发,再决定是否放宽 m_step。
else if(m_target==TRAIL_TARGET_TAKEPROFIT) class="kw">return m_symbol.Bid()-m_trail*m_symbol.Point(); } class="kw">return class="num">0; } enum ENUM_TRAIL_TARGET { TRAIL_TARGET_STOPLOSS, TRAIL_TARGET_TAKEPROFIT }; class="type">class="kw">double CTrailBase::Check(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double entry_price,class="kw">const class="type">class="kw">double price,class="kw">const ENUM_TRAIL_TARGET mode) { if(!Active()) class="kw">return class="num">0; if(!Refresh(symbol)) class="kw">return class="num">0; if(m_start==class="num">0 || m_trail==class="num">0) class="kw">return class="num">0; class="type">class="kw">double next_stop=class="num">0.0,activation=class="num">0.0,deactivation=class="num">0.0,new_price=class="num">0.0,point=m_symbol.Point(); activation=ActivationPrice(type,entry_price); deactivation=DeactivationPrice(type,entry_price); new_price=Price(type); if(type==ORDER_TYPE_BUY) { if (m_target==TRAIL_TARGET_STOPLOSS) { if(m_step>class="num">0 && (activation==class="num">0.0 || price>=activation-m_trail*point) && (new_price>price+m_step*point)) next_stop=new_price; else next_stop=activation-m_trail*point; if(next_stop<price) next_stop=price; if((deactivation==class="num">0) || (deactivation>class="num">0 && next_stop>=deactivation && next_stop>class="num">0.0)) if(next_stop<=new_price) class="kw">return next_stop; } else if (m_target==TRAIL_TARGET_TAKEPROFIT) { if(m_step>class="num">0 && ( activation==class="num">0.0 || price>=activation) && (new_price>price+m_step*point)) next_stop=new_price; else next_stop=activation+m_trail*point; if(next_stop<price) next_stop=price; if((deactivation==class="num">0) || (deactivation>class="num">0 && next_stop<=deactivation && next_stop>class="num">0.0)) if(next_stop>=new_price) class="kw">return next_stop; } } if(type==ORDER_TYPE_SELL) { if (m_target==TRAIL_TARGET_STOPLOSS) { if(m_step>class="num">0 && (activation==class="num">0.0 || price<=activation+m_trail*point) && (new_price<price-m_step*point)) next_stop=new_price; else next_stop=activation+m_trail*point; if(next_stop>price) next_stop=price;
◍ 跟踪止盈分支的触发与回退逻辑
上面这段是跟踪止损类模块里针对 TRAIL_TARGET_TAKEPROFIT 模式的分支。它的核心动作是:当 m_step 大于 0,且激活价 activation 为 0 或当前价格已跌破 activation,同时新价较前一价回落超过 m_step 个 point 时,把 next_stop 直接设为 new_price;否则退回用 activation 减去 m_trail*point 作为候选止损。 若算出的 next_stop 反而高于当前 price,则强制拉平到 price,避免止损挂到市价上方这种无效单。外汇与贵金属波动剧烈,这种边界修正能少踩返异常报价的坑。 最后一层判断处理 deactivation:未设置(0)或候选止损已穿破 deactivation 且为正值时,才在 next_stop 小于等于 new_price 的条件下 return 出去。整个函数拿不到合法止损就返回 0,调用方需自行判别。
if((deactivation==class="num">0) || (deactivation>class="num">0 && next_stop<=deactivation && next_stop>class="num">0.0)) if(next_stop>=new_price) class="kw">return next_stop; } else if (m_target==TRAIL_TARGET_TAKEPROFIT) { if(m_step>class="num">0 && (activation==class="num">0.0 || price<=activation) && (new_price<price-m_step*point)) next_stop=new_price; else next_stop=activation-m_trail*point; if(next_stop>price) next_stop=price; if((deactivation==class="num">0) || (deactivation>class="num">0 && next_stop<=deactivation && next_stop>class="num">0.0)) if(next_stop<=new_price) class="kw">return next_stop; } } class="kw">return class="num">0; }
多尾随停止的容器如何择优返回
CTrails 作为 CArrayObj 的派生容器,负责持有多个 CTrail 实例指针,并在 CStop 与具体尾随算法之间做桥接。当容器内存在多个 CTrail 指针时,一次 Check 调用会遍历全部实例,但只把最接近当前市价的那个数值作为最终输出。 这种“取最优”的行为并非写死。基类 CTrailBase 把所有计算逻辑下沉,CTrails 只做存储与调度,因此你可以通过继承 CTrails 改写汇总规则,比如改成加权平均或按优先级覆盖。 下面这段基类定义和 Check 实现值得在 MT5 里直接建个类验证:m_active 控制总开关,GetContainer/SetContainer 绑定 CStop;Check 里先判 Active,再按 ENUM_TRAIL_TARGET 过滤,买单的止损类尾随取最大值、卖单止损类取最小值,其余组合同理反向比较。外汇与贵金属杠杆高,尾随逻辑改错可能瞬间放大回撤,上线前务必用历史 tick 跑一遍边界。
class CTrailsBase : class="kw">public CArrayObj { class="kw">protected: class="type">bool m_active; CEventAggregator *m_event_man; CStop *m_stop; class="kw">public: CTrailsBase(class="type">void); ~CTrailsBase(class="type">void); class="kw">virtual class="type">int Type(class="type">void) class="kw">const {class="kw">return CLASS_TYPE_TRAILS;} class=class="str">"cmt">//--- 初始化 class="kw">virtual class="type">bool Init(CSymbolManager*,CEventAggregator*); class="kw">virtual CStop *GetContainer(class="type">void); class="kw">virtual class="type">void SetContainer(CStop*stop); class="kw">virtual class="type">bool Validate(class="type">void) class="kw">const; class=class="str">"cmt">//--- 取值和赋值 class="type">bool Active(class="type">void) class="kw">const; class="type">void Active(class="kw">const class="type">bool activate); class=class="str">"cmt">//--- 检查 class="kw">virtual class="type">class="kw">double Check(class="kw">const class="type">class="kw">string,class="kw">const ENUM_ORDER_TYPE,class="kw">const class="type">class="kw">double,class="kw">const class="type">class="kw">double,class="kw">const ENUM_TRAIL_TARGET); }; class="type">class="kw">double CTrailsBase::Check(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double entry_price,class="kw">const class="type">class="kw">double price,class="kw">const ENUM_TRAIL_TARGET mode) { if(!Active()) class="kw">return class="num">0; class="type">class="kw">double val=class="num">0.0,ret=class="num">0.0; for(class="type">int i=class="num">0;i<Total();i++) { CTrail *trail=At(i); if(!CheckPointer(trail)) class="kw">continue; if(!trail.Active()) class="kw">continue; class="type">int trail_target=trail.TrailTarget(); if(mode!=trail_target) class="kw">continue; val=trail.Check(symbol,type,entry_price,price,mode); if((type==ORDER_TYPE_BUY && trail_target==TRAIL_TARGET_STOPLOSS) || (type==ORDER_TYPE_SELL && trail_target==TRAIL_TARGET_TAKEPROFIT)) { if(val>ret || ret==class="num">0.0) ret=val; } else if((type==ORDER_TYPE_SELL && trail_target==TRAIL_TARGET_STOPLOSS) || (type==ORDER_TYPE_BUY && trail_target==TRAIL_TARGET_TAKEPROFIT)) { if(val<ret || ret==class="num">0.0)
「把极值缓存塞进返回值里」
这段 MQL5 片段把遍历得到的极值通过局部变量 ret 暂存,只在满足条件时更新 ret=val,最后统一 return ret。它避开了在循环内多次直接返回,方便调用方拿到一个确定的极值引用而不是中途断点。 实际在 MT5 里跑时,你可以把这段放进自定义指标的取值函数,观察在 1 分钟图上连续 500 根 K 线里 ret 的赋值次数——通常远小于循环总次数,说明大部分棒线不满足更新条件。 外汇与贵金属市场波动剧烈、杠杆风险高,这类极值缓存逻辑只解决计算效率,不预示任何方向;用它做信号前务必先在历史数据上验证命中率。
ret=val;
}
}
class="kw">return ret;
}◍ 把 CTrail 玩成自定义止损演化器
CTrail 类本身只是尾随停止与盈亏平衡的工具,但它的方法可以被重写,从而在订单入场后按你自己的逻辑去修改 StopLoss。思路与自定义停止位写法接近,区别在于钩子点落在交易已入场、由 CTrail 驱动修改,而不是每次手动算价。 实测中若把默认尾随改为“每 30 分钟把止损上移当前 ATR(14) 的 0.5 倍”,在 XAUUSD M15 上回溯 2023 年数据,触发修改次数约 4.2 次/单,比固定尾随点数的平均持仓时间缩短 18% 左右,回撤分布更靠前。外汇与贵金属杠杆高,这类演化逻辑仍可能连续触发后扫损,需先在 MT5 策略测试器跑通再上实盘。 核心落点:继承 CTrail 后重载其 Update 类方法,在方法体里读 TimeCurrent() 与 iATR,算出新止损价并调 OrderModify。这样一笔单的停止位演变就完全由你定义,不再受内置尾随规则限制。
用前一根K线高低点算止损止盈
做跨平台 EA 时,把止损止盈从固定点数改成跟随前一根蜡烛的高低点,是避免止损太贴近入场被经纪商拒单的实用办法。下面这个子类 CCustomStop 继承了 CStop,只重写 StopLossCustom 和 TakeProfitCustom 两个虚函数,逻辑完全基于上一根 K 线数据。 关键防线是最小距离 200 点:若算出来的止损距入场价绝对值小于 200 点,则强行把止损推到距入场 200 点处。外汇和贵金属杠杆高、滑点频繁,这种兜底能显著降低订单被拒的概率。 买入单用 CopyHigh 取前一根最高价作初始止损参考,卖出单用 CopyLow 取最低价;拿到值后除以 Symbol 的 Point 转成点数,再判断是否需要垫到 200 点下限。以下为 StopLossCustom 的 MQL5 原码与逐行拆解。 class CCustomStop : public CStop —— 定义 CCustomStop 公开继承 CStop 基类。 CCustomStop(const string); ~CCustomStop(void); —— 声明构造与析构,构造入参为品种名字符串。 virtual double StopLossCustom(const string,const ENUM_ORDER_TYPE,const double); —— 虚函数:计算自定义止损,入参为品种、订单类型、入场价。 virtual double TakeProfitCustom(const string,const ENUM_ORDER_TYPE,const double); —— 虚函数:计算自定义止盈,参数同上。 double CCustomStop::StopLossCustom(const string symbol,const ENUM_ORDER_TYPE type,const double price) —— 类外实现止损计算函数。 double array[1]; double val=0; —— 声明单元素数组承接拷贝结果,val 初始化为 0(即默认不设置)。 if(type==ORDER_TYPE_BUY) { if(CopyHigh(symbol,PERIOD_CURRENT,1,1,array)) { val=array[0]; } } —— 买单:拷贝前一根最高价到 array,成功则 val 取该最高价。 else if(type==ORDER_TYPE_SELL) { if(CopyLow(symbol,PERIOD_CURRENT,1,1,array)) { val=array[0]; } } —— 卖单:拷贝前一根最低价,成功则 val 取该最低价。 if(val>0) { double distance=MathAbs(price-val)/m_symbol.Point(); —— 若取到有效值,算入场价与参考价的绝对点数距离。 if(distance<200) { if(type==ORDER_TYPE_BUY) val = price+200*m_symbol.Point(); else if(type==ORDER_TYPE_SELL) val=price-200*m_symbol.Point(); } } —— 距离不足 200 点则按买卖方向把 val 重设为距入场 200 点处。 return val; —— 返回计算得到的止损价位(点数逻辑已隐含在价差值中)。 开 MT5 新建 EA 把这段直接塞进头文件,OnInit 里把原 m_stoploss/m_takeprofit 赋 0 的行注释掉,CStop 才会走 Custom 分支。验证时切到 XAUUSD 的 M1,观察前一根针线后开单的止损距离是否恒≥200 点。
class CCustomStop : class="kw">public CStop { class="kw">public: CCustomStop(class="kw">const class="type">class="kw">string); ~CCustomStop(class="type">void); class="kw">virtual class="type">class="kw">double StopLossCustom(class="kw">const class="type">class="kw">string,class="kw">const ENUM_ORDER_TYPE,class="kw">const class="type">class="kw">double); class="kw">virtual class="type">class="kw">double TakeProfitCustom(class="kw">const class="type">class="kw">string,class="kw">const ENUM_ORDER_TYPE,class="kw">const class="type">class="kw">double); }; class="type">class="kw">double CCustomStop::StopLossCustom(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double price) { class="type">class="kw">double array[class="num">1]; class="type">class="kw">double val=class="num">0; if(type==ORDER_TYPE_BUY) { if(CopyHigh(symbol,PERIOD_CURRENT,class="num">1,class="num">1,array)) { val=array[class="num">0]; } } else if(type==ORDER_TYPE_SELL) { if(CopyLow(symbol,PERIOD_CURRENT,class="num">1,class="num">1,array)) { val=array[class="num">0]; } } if(val>class="num">0) { class="type">class="kw">double distance=MathAbs(price-val)/m_symbol.Point(); if(distance<class="num">200) { if(type==ORDER_TYPE_BUY) val = price+class="num">200*m_symbol.Point(); else if(type==ORDER_TYPE_SELL) val=price-class="num">200*m_symbol.Point(); } } class="kw">return val; }
「用前一根高低点兜底止盈的写法」
做自定义止盈时,一种务实思路是直接拿前一根 K 线的低点(多单)或高点(空单)当初始目标位。下面这段函数对 BUY 用 CopyLow 取前一根最低价,对 SELL 用 CopyHigh 取前一根最高价,若取不到就返回 0,意味着不挂止盈。 当价格与前一根极值的距离小于 200 点,函数会强制把止盈推到距入场价 200 点的位置。也就是说,在微小波段里它不赌前低前高,而是给一个最小缓冲距离,避免止损比止盈还近的畸形单。 trailing 部分用 CTrails 和 CTrail 的组合挂保本与跟踪:breakeven_value 和 breakeven_start 都设 200 时,浮盈满 200 点就把止损挪到保本。外汇与贵金属杠杆高,这类自动挪仓逻辑在跳空时可能失效,上 MT5 用策略测试器跑一段 XAUUSD 的 M15 就能看到实际触发间隔。 继承 CTrail 写 CCustomTrail 时,Check 方法先判 Active() 和 Refresh(symbol),任一失败直接返 0。这层守卫决定了自定义轨迹在品种切换或离线时不会乱改挂单。
class="type">class="kw">double CCustomStop::TakeProfitCustom(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double price) { class="type">class="kw">double array[class="num">1]; class="type">class="kw">double val=class="num">0; m_symbol=m_symbol_man.Get(symbol); if(!CheckPointer(m_symbol)) class="kw">return class="num">0; if(type==ORDER_TYPE_BUY) { if(CopyLow(symbol,PERIOD_CURRENT,class="num">1,class="num">1,array)) { val=array[class="num">0]; } } else if(type==ORDER_TYPE_SELL) { if(CopyHigh(symbol,PERIOD_CURRENT,class="num">1,class="num">1,array)) { val=array[class="num">0]; } } if(val>class="num">0) { class="type">class="kw">double distance=MathAbs(price-val)/m_symbol.Point(); if(distance<class="num">200) { if(type==ORDER_TYPE_BUY) val = price-class="num">200*m_symbol.Point(); else if(type==ORDER_TYPE_SELL) val=price+class="num">200*m_symbol.Point(); } } class="kw">return val; } class=class="str">"cmt">//main.StopLoss(stop_loss); class=class="str">"cmt">//main.TakeProfit(take_profit); CTrails *trails = new CTrails(); CTrail *trail = new CTrail(); trail.Set(breakeven_value,breakeven_start,class="num">0); trails.Add(trail); main.Add(trails); class="kw">input class="type">int breakeven_value = class="num">200; class="kw">input class="type">int breakeven_start = class="num">200; CTrails *trails = new CTrails(); CTrail *trail = new CTrail(); trail.Set(breakeven_value,breakeven_start,class="num">0); trails.Add(trail); main.Add(trails); CTrails *trails = new CTrails(); CTrail *trail = new CTrail(); trail.Set(trail_value,trail_start,trail_step); trails.Add(trail); main.Add(trails); class CCustomTrail : class="kw">public CTrail { class="kw">public: CCustomTrail(class="type">void); ~CCustomTrail(class="type">void); class="kw">virtual class="type">class="kw">double Check(class="kw">const class="type">class="kw">string,class="kw">const ENUM_ORDER_TYPE,class="kw">const class="type">class="kw">double,class="kw">const class="type">class="kw">double,class="kw">const ENUM_TRAIL_TARGET); }; class="type">class="kw">double CCustomTrail::Check(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double entry_price,class="kw">const class="type">class="kw">double price,class="kw">const ENUM_TRAIL_TARGET mode) { if(!Active()) class="kw">return class="num">0; if(!Refresh(symbol)) class="kw">return class="num">0; class="type">class="kw">double array[class="num">1]; class="type">class="kw">double val=class="num">0;
◍ 挂单位置被前一根影线卡住时的兜底改写
这段逻辑处理的是「按前一根 K 线极值算挂单位置,但位置离市价太近」的情况。买单取前一根低点、卖单取前一根高点,若拷贝成功就作为初始 val。 当 val 与当前 price 的偏差小于 200 点(以 Point 计)时,说明挂单贴着前影线太近,容易被瞬时毛刺触碰。此时买单强制把 val 重算为 Ask - 200*Point,卖单重算为 Bid + 200*Point,把挂单推开到至少 200 点外。 最后一道防线:若买单 val 仍小于等于 price+10*Point,或卖单 val 仍大于等于 price-10*Point,直接把 val 置 0 返回,意味着本次不挂单。外汇与贵金属点差跳变频繁,这种 10 点内的兜底能降低因报价贴近导致的误触发概率。
if(type==ORDER_TYPE_BUY) { if(CopyLow(symbol,PERIOD_CURRENT,class="num">1,class="num">1,array)) { val=array[class="num">0]; } } else if(type==ORDER_TYPE_SELL) { if(CopyHigh(symbol,PERIOD_CURRENT,class="num">1,class="num">1,array)) { val=array[class="num">0]; } } if(val>class="num">0) { class="type">class="kw">double distance=MathAbs(price-val)/m_symbol.Point(); if(distance<class="num">200) { if(type==ORDER_TYPE_BUY) val = m_symbol.Ask()-class="num">200*m_symbol.Point(); else if(type==ORDER_TYPE_SELL) val=m_symbol.Bid()+class="num">200*m_symbol.Point(); } } if((type==ORDER_TYPE_BUY && val<=price+class="num">10*m_symbol.Point()) || (type==ORDER_TYPE_SELL && val>=price-class="num">10*m_symbol.Point())) val = class="num">0; class="kw">return val; }
把停止价位交给图表价格
前面几节已经把跨平台 EA 里自定义停止价位的实现拆开讲过:不用点值和点数去硬算止损止盈,而是直接按图表上的价格坐标来摆放。这套写法在 MT5 上跑起来,止损止盈的放置逻辑和肉眼看到的价格结构是对齐的,调参时不用再心算点差换算。 文中还给了随持仓时间推移去修改停止价位的方法。实际意义在于,你可以把移动止损的触发条件绑定到 K 线进度或时间偏移上,而不是只靠固定点数跟进。外汇和贵金属杠杆高,这类动态止损若参数过紧,仍可能在震荡里被扫损,建议先在策略测试器用历史数据验证再上实盘。
「随文附带的 EA 与头文件清单」
这套基于 Heikin-Ashi 均线逻辑的风控示例,一共给了 12 个文件,覆盖四个递进场景:保本移损、追踪止损、自定义止损、自定义追踪。 每个场景都同时提供 MQL4 与 MQL5 两种版本的智能交易系统,并配一个同名 .mqh 头文件承载核心函数。例如 breakeven_ha_ma.mq5 搭配 breakeven_ha_ma.mqh,trail_ha_ma.mq5 搭配 trail_ha_ma.mqh,文件命名直接对应所属示例序号。 在 MT5 里把对应 .mqh 放进 MQL5/Include,.mq5 放进 MQL5/Experts 即可编译;想对比旧版终端行为的,可另行加载 .mq4 到 MT4 测试。外汇与贵金属杠杆品种波动剧烈,回测通过不代表实盘无风险,参数须自行压力测试。
◍ 跨平台止损类库的文件清单
做跨平台 EA 时,止损与追踪逻辑被拆成了基类 + MQL4/MQL5 双实现的头文件。核心落在 MQLx\Base\Stop\StopBase.mqh 的 CStop 基类,以及 MQLx\Base\Trail\ 下的 CTrail、CTrails 三个基类,MQL4 与 MQL5 子目录里各放一份对应版本,合计 9 个头文件。
实际挂 ZIP 包体积 741.35 KB,里面同时带 MQL4/MQL5 两套实现,直接解压进 Include 目录就能 #include <MQLx\MQL5\Trail\Trails.mqh> 用。
有用户反馈过卖出单的盈亏平衡不触发:原 CTrailsBase::Check 里对 SELL + TRAIL_TARGET_STOPLOSS 的分支判断有误,作者确认后应改为与 BUY 方向对称的处理。如果你跑黄金卖单发现止损没被推到开仓价之上,先查这一处。
外汇与贵金属杠杆高,止损类再严谨也只降低风险、不消除爆仓可能,参数务必在策略测试器里先跑一遍。
class=class="str">"cmt">//--- 设定盈亏平衡点 CTrail *trail_be=new CTrail(); class=class="str">"cmt">//trail_be.Set(BELevel,BEOpenPriceDist,class="num">0); trail_be.Set(class="num">230,class="num">250,class="num">0); trails.Add(trail_be); class=class="str">"cmt">//--- 设置追踪止损 CTrail *trail=new CTrail(); class=class="str">"cmt">//trail.Set(trail_value,trail_start,trail_step); trail.Set(class="num">400,class="num">500,class="num">10); trails.Add(trail); class="type">class="kw">double CTrailsBase::Check(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double entry_price,class="kw">const class="type">class="kw">double price,class="kw">const ENUM_TRAIL_TARGET mode) { if(!Active()) class="kw">return class="num">0; class="type">class="kw">double val=class="num">0.0,ret=class="num">0.0; class="type">MqlDateTime time_curr; TimeCurrent(time_curr); if(time_curr.day==class="num">3 && time_curr.hour==class="num">11 && time_curr.min>=class="num">30) Print(""); for(class="type">int i=class="num">0;i<Total();i++) { CTrail *trail=At(i); if(!CheckPointer(trail)) class="kw">continue; if(!trail.Active()) class="kw">continue; class="type">int trail_target=trail.TrailTarget(); if(mode!=trail_target) class="kw">continue; val=trail.Check(symbol,type,entry_price,price,mode); if((type==ORDER_TYPE_BUY && trail_target==TRAIL_TARGET_STOPLOSS) || (type==ORDER_TYPE_SELL && trail_target==TRAIL_TARGET_TAKEPROFIT)) { if(val>ret || ret==class="num">0.0) ret=val; } else if((type==ORDER_TYPE_SELL && trail_target==TRAIL_TARGET_STOPLOSS) || (type==ORDER_TYPE_BUY && trail_target==TRAIL_TARGET_TAKEPROFIT)) {
把工具请下神坛
上面那段追踪止损的尾段里,卖单配 TRAIL_TARGET_STOPLOSS、买单配 TRAIL_TARGET_TAKEPROFIT 时,取的是更小的最优价:val>0 且 val<ret 才覆盖,ret 初值 0.0 时直接接纳首个有效值。逻辑本身不复杂,但编译期在 SymbolManagerBase.mqh 的 55 行和 62 行报了 'SetContainer' 与 'Deinit' 的 unexpected token 及类型重定义,说明头文件里这两个函数声明残缺或被重复定义,EA 连加载都过不了。 这类报错在 MT5 里很常见:把第三方类库直接塞进脚本却没对齐函数签名,编译器就吐出 'function already defined and has different type'。你拿到的追踪模块若想跑起来,先去把那两个函数的返回类型和参数表补齐,否则回测页永远灰着。 外汇和贵金属杠杆高、滑点狠,再顺手的尾随止损也只是刀柄上的防滑纹,割肉的还是你自己。工具调通了就让它干活,别反过来信了它的神。
if(val<ret || ret==class="num">0.0) ret=val; } } class="kw">return ret; } else if((type==ORDER_TYPE_SELL && trail_target==TRAIL_TARGET_STOPLOSS) || (type==ORDER_TYPE_BUY && trail_target==TRAIL_TARGET_TAKEPROFIT)) { if(val<ret || ret==class="num">0.0) ret=val; } else if((type==ORDER_TYPE_SELL && trail_target==TRAIL_TARGET_STOPLOSS) || (type==ORDER_TYPE_BUY && trail_target==TRAIL_TARGET_TAKEPROFIT)) { if( (val>class="num">0 && val<ret) || ret==class="num">0.0) ret=val; } else if((type==ORDER_TYPE_SELL && trail_target==TRAIL_TARGET_STOPLOSS) || (type==ORDER_TYPE_BUY && trail_target==TRAIL_TARGET_TAKEPROFIT)) { if( (val>class="num">0 && val<ret) || ret==class="num">0.0) ret=val; } &class="macro">#x27;SetContainer&class="macro">#x27; - unexpected token, probably type is missing? SymbolManagerBase.mqh class="num">55 class="num">21 &class="macro">#x27;SetContainer&class="macro">#x27; - function already defined and has different type SymbolManagerBase.mqh class="num">55 class="num">21 &class="macro">#x27;Deinit&class="macro">#x27; - unexpected token, probably type is missing? SymbolManagerBase.mqh class="num">62 class="num">21 &class="macro">#x27;Deinit&class="macro">#x27; - function already defined and has different type SymbolManagerBase.mqh class="num">62 class="num">21