在 MQL5 中构建自优化智能交易系统(第六部分):防止爆仓·综合运用
(3/3)· 均值回归策略下,为什么「放宽止损」反而让你更快亏光,以及 MQL5 里怎么落地硬规则
很多交易者明明看对了方向,却倒在回调扫损上,于是本能地把止损拉宽。这种「放宽止损」的习惯没有统一规则兜底,只会让每笔交易的风险敞口漂移。本篇用 EURUSD 小时图上的真实扫损场景,拆一套固定止损的均值回归解法。
模型信号与高低点突破的联立过滤
这段逻辑把 AI 模型预测和价格行为过滤绑在一起:先确认模型能出预测,否则直接在图表上提示并退出,不浪费后续计算。 若次级周期(TF_2)连续两根 K 线的最高价都低于昨日低点,且最近一根收盘站在均线 ma[0] 上方,才触发 check_buy() 做多条件评估;对称地,连续两根最低价高于昨日高点且收盘在均线下方,才进入 check_sell()。 check_buy 内部还要求模型预测值同时高于均线和当前现价,并且预测位在 bid - SL_SIZE 之上,才用 Trade.Buy 以 ask 进场、bid-SL_SIZE 挂损、bid+SL_SIZE 挂盈。check_sell 镜像处理。外汇与贵金属杠杆高,这种联立条件也只是降低噪音,信号失效概率依旧存在。 update_sl 函数负责持仓后移动止损:先用 PositionSelect 锁定当前符号持仓,读出 current_sl、current_tp、open_price,flag==0 表示仅在可行时前推止损与止盈。开 MT5 把这段挂到 EA 里,改 TF_2 和 SL_SIZE 就能看过滤松紧对信号数的影响。
if(!model_predict()) { Comment("Failed to get a forecast from our model"); class="kw">return; } if((iHigh(Symbol(),TF_2,class="num">1) < yesterday_low) && (iHigh(Symbol(),TF_2,class="num">2) < yesterday_low)) { if(iClose(Symbol(),TF_2,class="num">1) > ma[class="num">0]) { check_buy(); } } if((iLow(Symbol(),TF_2,class="num">1) > yesterday_high) && (iLow(Symbol(),TF_2,class="num">2) > yesterday_high)) { if(iClose(Symbol(),TF_2,class="num">1) < ma[class="num">0]) { check_sell(); } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check if we have a valid buy setup | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void check_buy(class="type">void) { if((model_forecast[class="num">0] > ma[class="num">0]) && (model_forecast[class="num">0] > iClose(Symbol(),TF_2,class="num">0))) { if(model_forecast[class="num">0] > (bid - (SL_SIZE))) Trade.Buy(VOL,Symbol(),ask,(bid - (SL_SIZE)),(bid + (SL_SIZE))); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check if we have a valid sell setup | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void check_sell(class="type">void) { if((model_forecast[class="num">0] < ma[class="num">0]) && (model_forecast[class="num">0] < iClose(Symbol(),TF_2,class="num">0))) { if(model_forecast[class="num">0] < (ask + (SL_SIZE))) Trade.Sell(VOL,Symbol(),bid,(ask + (SL_SIZE)),(ask - (SL_SIZE))); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Update our stop loss | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void update_sl(class="type">int flag) { class=class="str">"cmt">//--- First find our open position if(PositionSelect(Symbol())) { class="type">class="kw">double current_sl = PositionGetDouble(POSITION_SL); class="type">class="kw">double current_tp = PositionGetDouble(POSITION_TP); class="type">class="kw">double open_price = PositionGetDouble(POSITION_PRICE_OPEN); class=class="str">"cmt">//--- Flag class="num">0 means we just want to push the stop loss and take profit forward if its possible if(flag == class="num">0) {
「用模型预测值去挪止损的两种分支」
这段逻辑跑在 flag==1 的情境下:先调 model_predict() 拿到 model_forecast[0],再按持仓方向决定怎么改止损。外汇与贵金属杠杆高,模型只是概率工具,改损动作不代表方向判断必胜。 多头仓(current_tp > current_sl)里,若预测值低于当前止损,且同时高于均线 ma[0] 与昨低 yesterday_low,就把止损提到预测值,保留原止盈。若预测值还低于开仓价 open_price,则止损直接设为预测值的 1.5 倍——相当于往亏损方向再让出半段空间,倾向用于过滤假突破。 空头仓反过来:预测值高于当前止损、又低于 ma[0] 和昨高时,止损下移到预测值;若预测值高于开仓价,止损取预测值的 0.5 倍,把风险收紧一半。 普通追踪(flag!=1)那段更粗暴:买仓里只要 (bid - SL_SIZE) 比 current_sl 大就平移止损到 bid 上下各挂 SL_SIZE;卖仓对称用 ask。两类写法可以并存在同一个 EA 里,靠 flag 切换。
class=class="str">"cmt">//--- Buy Setup if(current_tp > current_sl) { if((bid - SL_SIZE) > current_sl) Trade.PositionModify(Symbol(),(bid - SL_SIZE),(bid + SL_SIZE)); } class=class="str">"cmt">//--- Sell setup if(current_tp < current_sl) { if((ask + SL_SIZE) < current_sl) Trade.PositionModify(Symbol(),(ask + SL_SIZE),(ask - SL_SIZE)); } } class=class="str">"cmt">//--- Flag class="num">1 means we want to check if the stop loss may be hit soon, and act accordingly if(flag == class="num">1) { model_predict(); class=class="str">"cmt">//--- Buy setup if(current_tp > current_sl) { if(model_forecast[class="num">0] < current_sl) { if((model_forecast[class="num">0] > ma[class="num">0]) && (model_forecast[class="num">0] > yesterday_low)) Trade.PositionModify(Symbol(),model_forecast[class="num">0],current_tp); } if(model_forecast[class="num">0] < open_price) Trade.PositionModify(Symbol(),model_forecast[class="num">0] * class="num">1.5,current_tp); } class=class="str">"cmt">//--- Sell setup if(current_tp < current_sl) { if(model_forecast[class="num">0] > current_sl) { if((model_forecast[class="num">0] < ma[class="num">0]) && (model_forecast[class="num">0] < yesterday_high)) Trade.PositionModify(Symbol(),model_forecast[class="num">0],current_tp); } if(model_forecast[class="num">0] > open_price) Trade.PositionModify(Symbol(),model_forecast[class="num">0] * class="num">0.5,current_tp); } } } } 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);
◍ 多周期钩子里怎么喂数据给模型
这段逻辑把日线前高前低、M30 的实时报价与均线、以及逐 tick 的止损更新拆进了不同作用域,核心是用时间截比对避免每 tick 重算。日线块里只在 time_stamp 变化时取 iHigh/iLow(Symbol(),TF_1,1) 的前一根高低,并删旧建新两条 OBJ_HLINE,等于把「昨天区间」钉在图上供价格行为观察。 M30 块用 static datetime 存上次 K 线时间,iTime(Symbol(),TF_2,0) 变了才刷新 bid/ask、CopyBuffer 取均线最新值;空仓调 find_setup() 找形态,有仓调 update_sl(1) 推止损。这里若 TF_2 设为 PERIOD_M30,回测里每根 30 分钟 K 线只进一次,能压住计算开销。 逐 tick 块只放 update_sl(0),原文特意提醒此作用域的函数调用会拖慢回测速度——外汇与贵金属点差跳变频繁,每 tick 跑重逻辑可能让 1 年回测从几分钟变几十分钟,概率上影响参数寻优效率。 model_predict() 把 M30 前一根 OHLC 加一根均线值塞进 vectorf 送 ONNX 推理,输入维度靠 ONNX_MODEL_INPUTS 约束;CopyBuffer(ma_handler,0,1,1,ma_input) 取的是偏移 1 的均线值,也就是「已完成那根」而非当前未闭合值,避免未来函数泄漏。
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 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">//--- Check for a setup if(PositionsTotal() > class="num">0) update_sl(class="num">1); } } class=class="str">"cmt">//--- Per tick procedures { class=class="str">"cmt">//--- These function calls can become expensive and may slow down the speed of your back tests class=class="str">"cmt">//--- Be thoughtful when placing any function calls in this scope update_sl(class="num">0); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Get a forecast from our deep neural network | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool model_predict(class="type">void) { class="type">class="kw">double ma_input[] = {class="num">0}; CopyBuffer(ma_handler,class="num">0,class="num">1,class="num">1,ma_input); vectorf model_inputs = { (class="type">float) iOpen(Symbol(),TF_2,class="num">1), (class="type">float) iHigh(Symbol(),TF_2,class="num">1), (class="type">float) iLow(Symbol(),TF_2,class="num">1), (class="type">float) iClose(Symbol(),TF_2,class="num">1), (class="type">float) ma_input[class="num">0] }; for(class="type">int i = class="num">0; i < ONNX_MODEL_INPUTS;i++)
标准化输入与 ONNX 推理的收尾动作
把特征喂给模型前,必须先做 Z-score 标准化:每个维度减去对应均值再除以标准差,转成 float 送入 model_inputs。跳过这步,ONNX 模型大概率输出漂移严重的预测值。 推理调用 OnnxRun 走 ONNX_DEFAULT 配置,失败就用 Comment 把 GetLastError 抛到图表,并直接 return(false) 阻断后续逻辑。成功则在图表打印 Expected MA Value,取 model_forecast[0] 作为均线预期。 跑完记得在 release 里 OnnxRelease 释放模型、IndicatorRelease 释放 MA 句柄,否则 MT5 反复加载 EA 会漏内存。外汇与贵金属行情受杠杆与跳空影响,模型输出只是概率倾向,实盘前务必在策略测试器跑至少 3 个月 tick 数据验证。
{
model_inputs[i] = (class="type">float)((model_inputs[i] - mean_values[i]) / std_values[i]);
}
if(!OnnxRun(onnx_model,ONNX_DEFAULT,model_inputs,model_forecast))
{
Comment("Failed to obtain forecast: ",GetLastError());
class="kw">return(false);
}
Comment(StringFormat("Expected MA Value: %f",model_forecast[class="num">0]));
class="kw">return(true);
}
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)
{
OnnxRelease(onnx_model);
IndicatorRelease(ma_handler);
}
class=class="str">"cmt">//+------------------------------------------------------------------+「别急着下结论」
把盈利单被止损扫掉这件事彻底消灭,在外汇和贵金属这种高杠杆市场里基本是幻想。每套量化框架补上一个漏洞,往往顺手开出另一个口子,EURUSD 这类品种尤其吃微观流动性,模型外推要谨慎。 附件里 Baseline_Model.mq5 仅 5.65 KB,而 Stop_Out_Prevention_Model.mq5 到了 12 KB,体量差背后是深度神经网络接管的止损逻辑;想验证就开 MT5 把两个 EA 在同一周期回测对照,看被扫损频次的差值是否值得那 6 KB 多的复杂度。 真要落地,先跑 Fetch_Data_MA.mq5 把行情拉进 EURUSD_Stop_Out_Moving_Average_Model.ipynb 做一遍分布检查,再决定 .onnx 那个 1.84 KB 的网络能不能上实盘。过滤掉无意义磨损单是概率层面的优化,不是账户保本的开关。