在 MQL5 中构建自优化智能交易系统(第六部分):防止爆仓(基础篇)
◍ 盈利单为何总死在止损上
EURUSD 小时图上常出现一种尴尬:方向看对了,单子却先被扫损。图 1 里白虚线框出交易时段,红色高亮区是价格上涨刺穿止损的位置——交易者判跌没错,但价格在下行前先大幅拉升,止损若落在红区,亏损平仓后行情才真正掉头。 被止损出局的根源是价格不按直线走,波动中途的逆向刺探会清洗掉正确头寸。31 十月 2025 发布的示例中指出,这类现象直接拉低交易时段盈利能力,还让风险管理变混乱。 圈子里两种老办法都不管用:一是「放宽止损」,波动大日子就把止损拉远,这等于同一类交易承担不同风险,却没有明确规则约束;二是「进場前等确认」,只是把被扫的时间推后,刺探一来照样出局。 我们的思路反着来:止损规模固定,不随波动随便放宽,用明确规则压低盈利单被误扫的概率。外汇与贵金属杠杆高,逆向刺探频繁,固定止损也需配合仓位上限,风险仍不可小视。
用前日高低点做均值回归入场
这套逻辑本质是均值回归:把前一天的 High、Low 当成关键价位,等当天价格先突破再去赌它回吐。突破不是信号,回归动作才是。 具体落地时,以前一日高点为界:若当天创出更高价并收在前高上方,不追多,而是等价格再站上移动平均线后,空头触发条件才算清晰。也就是说,破高+收均线上,是做空的观察窗口,不是破高即空。 外汇与贵金属波动受消息驱动,假突破概率不低,用这套规则须接受多次止损的高风险现实,任何信号都只是概率倾向。 图 2 展示了该策略在K线上的实际标记方式,开 MT5 把前日高低画成水平线,叠加一条 MA,就能肉眼核对信号出现的位置。
「固定回测窗口怎么设」
要验证策略改动能不能跑赢原版,先得锁死一段可比的行情区间。这里直接定死:2022.01.01 到 2025.01.01,月线图上 EURUSD 这段已经被标出来,后面所有改动都只在这块历史里比。 实盘级回测放在 M30 跑,品种锁 EURUSD(你换自己惯用的也行,但别中途换)。账户设置里把建模模式选成“基于真实 tick 的每个 tick”,这样是用经纪商实际 tick 重演市场,误差最小;代价是拉数据慢,网络差的时候等得久。 这两组参数(时间窗 + M30 + 真实 tick)全文不再动,你开 MT5 跟着复现的话,先照这个把测试器配置好,不然后面数字对不上。
◍ 搭一个突破基线EA先测水温
做外汇或贵金属策略验证,第一步不是优化,而是先跑通一个最朴素的基线。这里用 MQL5 写了一个只做前日高低突破的 EA:日线取昨日高低,M30 周期监突破,辅以 14 周期 EMA 确认。 系统常量把行为锁死——MA_PERIOD=14、MA_TYPE=MODE_EMA、TF_1=PERIOD_D1、TF_2=PERIOD_M30、VOL=0.1、SL_SIZE=1000*_Point。全局变量里 yesterday_high / yesterday_low 跟踪前日极值,ma_handler 管指标句柄,逻辑分离靠两个时间框架常量天然切分:日线任务一天一次,持仓扫描三十分钟一次。 回测 3 年,这版裸策略亏掉超 1000 美元,平均亏损和最大亏损均大于平均盈利和最大盈利。资金曲线单调向下,没有任何随时间的增长倾向。外汇贵金属本身高杠杆高风险,这种基线绝不能直接上实盘,但它是后续加过滤器的对照锚。 下一步思路很直接:开仓规则不动,尝试滤掉那些大概率触止损的单。先有烂基线,才有比较基准。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//| Baseline Model.mq5 | class=class="str">"cmt">//| Gamuchirai Ndawana | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Gamuchirai Ndawana" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Libraries | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Trade/Trade.mqh> CTrade Trade; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| System constants | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define MA_PERIOD class="num">14 class=class="str">"cmt">//--- Moving Average Period class="macro">#define MA_TYPE MODE_EMA class=class="str">"cmt">//--- Type of moving average we have class="macro">#define MA_PRICE PRICE_CLOSE class=class="str">"cmt">//---- Applied Price of Moving Average class="macro">#define TF_1 PERIOD_D1 class=class="str">"cmt">//--- Our time frame for technical analysis class="macro">#define TF_2 PERIOD_M30 class=class="str">"cmt">//--- Our time frame for managing positions class="macro">#define VOL class="num">0.1 class=class="str">"cmt">//--- Our trading volume class="macro">#define SL_SIZE class="num">1e3 * _Point class=class="str">"cmt">//--- The size of our stop loss class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Our global variables | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int ma_handler,system_state; class="type">class="kw">double ma[]; class="type">class="kw">double bid,ask,yesterday_high,yesterday_low;
用静态时间戳切分日线与M30的更新节奏
EA 骨架里最容易被忽略的是更新频率控制。上面这段把日线(TF_1)和 M30(TF_2)的例程拆进两个独立代码块,各自用 static datetime time_stamp 记住上一根 K 线的开盘时间,只有 iTime 返回的值变了才重算,避免每个 tick 都刷指标。 日线块里,一旦检测到新日线,就把前一根(shift=1)的 iHigh / iLow 读进 yesterday_high / yesterday_low,然后删掉旧的 LAST_HIGH / LAST_LOW 水平线再重画 OBJ_HLINE。这意味着图表上始终只留昨日高低点参考,不会叠一堆垃圾线。 M30 块逻辑类似,但只更新 bid / ask 和后续技术指标,不碰日线水平线。实盘里这种分离能明显降低 CPU 占用——尤其同时挂多个品种时,tick 级重复计算会拖慢 MT5 执行。 开 MT5 把这段粘进专家顾问,把 TF_1 设成 PERIOD_D1、TF_2 设成 PERIOD_M30,切到 EURUSD 看昨日高低线是否每天 0 点重绘;外汇和贵金属杠杆高,回测前先确认点差和滑点设定,否则画线对了信号也可能是假的。
class="kw">const class="type">class="kw">string last_high = "LAST_HIGH"; class="kw">const class="type">class="kw">string last_low = "LAST_LOW"; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- setup(); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- release(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- update(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Perform our update routines | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void update() { class=class="str">"cmt">//--- Daily procedures { class="kw">static class="type">class="kw">datetime time_stamp; class="type">class="kw">datetime current_time = iTime(Symbol(),TF_1,class="num">0); if(time_stamp != current_time) { yesterday_high = iHigh(Symbol(),TF_1,class="num">1); yesterday_low = iLow(Symbol(),TF_1,class="num">1); class=class="str">"cmt">//--- Mark yesterday&class="macro">#x27;s levels ObjectDelete(class="num">0,last_high); ObjectDelete(class="num">0,last_low); ObjectCreate(class="num">0,last_high,OBJ_HLINE,class="num">0,class="num">0,yesterday_high); ObjectCreate(class="num">0,last_low,OBJ_HLINE,class="num">0,class="num">0,yesterday_low); } } class=class="str">"cmt">//--- M30 procedures { class="kw">static class="type">class="kw">datetime time_stamp; class="type">class="kw">datetime current_time = iTime(Symbol(),TF_2,class="num">0); if(time_stamp != current_time) { time_stamp = current_time; class=class="str">"cmt">//--- Get updated prices bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); class=class="str">"cmt">//--- Update our technical indicators
「把均线和昨日区间塞进开仓判定」
这段自定义函数把前面攒下的指标句柄和价格边界真正用起来。setup() 只在初始化阶段跑一次,给二级周期挂上一条 MA_PERIOD 参数的均线,句柄存进 ma_handler,后续不再重复申请。 find_setup() 是裸信号过滤器:先比昨低,若二级周期前一根 K 线最高价低于 yesterday_low,且收盘价落在 ma[0] 下方,才用 Trade.Buy 以 ask 入场,止损止盈对称放在 bid ± SL_SIZE。反向逻辑镜像处理——前低高于昨高、收在 ma[0] 上方则 Trade.Sell。 release() 在 EA 卸载时 IndicatorRelease 掉均线句柄,末尾一串 #undef 把 TF_1、TF_2、VOL、SL_SIZE、MA_PERIOD、MA_PRICE、MA_TYPE 全部取消宏定义,避免与其他文件撞名。外汇与贵金属杠杆高,这类对称止损若 SL_SIZE 设太小,毛刺就可能频繁扫损。 开 MT5 把 MA_PERIOD 从默认改到 34 或 55,回看同一段行情,你会看到穿越 yesterday_low 后被 ma[0] 拦掉的单子明显变多,这是调参能直接验证的现象。
CopyBuffer(ma_handler,class="num">0,class="num">0,class="num">1,ma); class=class="str">"cmt">//--- Check for a setup if(PositionsTotal()==class="num">0) find_setup(); } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Custom functions | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void setup(class="type">void) { ma_handler = iMA(Symbol(),TF_2,MA_PERIOD,class="num">0,MA_TYPE,MA_PRICE); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check if we have any trading setups | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void find_setup(class="type">void) { if(iHigh(Symbol(),TF_2,class="num">1) < yesterday_low) { if(iClose(Symbol(),TF_2,class="num">1) < ma[class="num">0]) { Trade.Buy(VOL,Symbol(),ask,(bid - (SL_SIZE)),(bid + (SL_SIZE))); } } if(iLow(Symbol(),TF_2,class="num">1) > yesterday_high) { if(iClose(Symbol(),TF_2,class="num">1) > ma[class="num">0]) { Trade.Sell(VOL,Symbol(),bid,(ask + (SL_SIZE)),(ask - (SL_SIZE))); } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Free resources we are no longer class="kw">using up | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void release(class="type">void) { IndicatorRelease(ma_handler); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Undefine the system constants we created | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#undef TF_1 class="macro">#undef TF_2 class="macro">#undef VOL class="macro">#undef SL_SIZE class="macro">#undef MA_PERIOD class="macro">#undef MA_PRICE class="macro">#undef MA_TYPE