在 MQL5 中构建自优化智能交易系统(第六部分):防止爆仓·进阶篇
用均线预判替代直接猜价格
在 MT5 终端测过 200 多个交易品种后会发现一个现象:移动平均线的走势比直接预测价格本身更容易把握。这给防爆仓思路提供了基础——与其赌价格不碰止损,不如先算均线未来值会不会越过止损线。 具体做法是让程序先推演均线走向:如果模型预期均线会触到止损位,就禁止开仓;更严格些,还可以要求程序必须预期均线能越过止盈位才放行交易。没理由相信止盈能成交的单子,本来就不该下。 落地第一步是用 MQL5 脚本把市场数据拉出来。下面这段脚本在 M30 周期取 14 期 EMA(收盘价应用),抓取最近 3000 根 K 线的 OHLC 与均线值,写进以品种名命名的 CSV,方便后续做预判训练。外汇与贵金属杠杆高,均线过滤只降概率不灭风险。 别把正态当圣经 脚本里 MA_PERIOD 写死 14、size 写死 3000,只是起手参数。换品种时先手动跑一遍,看 CSV 里 MA 14 列和 Close 的偏离程度,再决定要不要改周期。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ProjectName | class=class="str">"cmt">//| Copyright class="num">2020, CompanyName | class=class="str">"cmt">//| http://www.companyname.net | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property script_show_inputs class=class="str">"cmt">//--- Define our moving average indicator 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=class="str">"cmt">//--- Our handlers for our indicators class="type">int ma_handle; class=class="str">"cmt">//--- Data structures to store the readings from our indicators class="type">class="kw">double ma_reading[]; class=class="str">"cmt">//--- File name class="type">class="kw">string file_name = Symbol() + " Stop Out Prevention Market Data.csv"; class=class="str">"cmt">//--- Amount of data requested input class="type">int size = class="num">3000; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Our script execution | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//---Setup our technical indicators ma_handle = iMA(_Symbol,PERIOD_M30,MA_PERIOD,class="num">0,MA_TYPE,MA_PRICE); class=class="str">"cmt">//---Set the values as series CopyBuffer(ma_handle,class="num">0,class="num">0,size,ma_reading); ArraySetAsSeries(ma_reading,true); class=class="str">"cmt">//---Write to file class="type">int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,","); for(class="type">int i=size;i>=class="num">1;i--) { if(i == size) { FileWrite(file_handle,"Time","Open","High","Low","Close","MA class="num">14"); } else { FileWrite(file_handle,
「逐根 K 线落地到 CSV 的写法」
把当前图表每一根 K 线的时间、开高低收和均线读数写进文件,核心就是循环里连续调用 iTime / iOpen / iHigh / iLow / iClose 再补一个数组元素。下面这段片段假设 ma_reading[] 已经算好,i 从 0 走到 bars-1,就能把历史数据原样导出。 外汇与贵金属行情跳空频繁,导出的 CSV 在跨周末或重大数据夜可能缺行,用前先按时间轴核对连续性。开 MT5 按 F4 把这段塞进你自己的 EA,改 PERIOD_CURRENT 为指定周期,就能复现本地数据集。 文件句柄用完必须 FileClose,否则下次打开同路径会拿不到句柄而写失败。这个细节在回测和实盘里都一样,属于低级但高频的坑。
iTime(_Symbol,PERIOD_CURRENT,i), iOpen(_Symbol,PERIOD_CURRENT,i), iHigh(_Symbol,PERIOD_CURRENT,i), iLow(_Symbol,PERIOD_CURRENT,i), iClose(_Symbol,PERIOD_CURRENT,i), ma_reading[i]); } } class=class="str">"cmt">//--- Close the file FileClose(file_handle); } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 用 Python 给均线预测网络找最优迭代次数
把 MT5 导出的 EURUSD 市场数据读进 pandas 后,先按 LOOK_AHEAD=48 根 K 线平移 MA 14 生成预测目标,再直接砍掉回测重叠的最近约两年数据,避免前视偏差。外汇与贵金属属高风险品种,这类离线建模只能降低亏损交易的概率,不保证实盘规避。 代码里用 TimeSeriesSplit(n_splits=5, gap=48) 做时间序列交叉验证,训练测试各占一半且不打乱顺序;输入按训练集均值标准差做 z-score 归一化,测试集复用同一组参数。 对隐藏层 (5,10,4,2)、adam+relu 的 MLPRegressor,以 2 的幂遍历 max_iter 从 2^0 到 2^14 做线性搜索。测试集 5 折 RMSE 从 13171 降到 882.7(迭代 2^14),说明更大迭代倾向更优;但受算力限制没搜过 2^14,可能漏掉更好解。 模型定稿后在整个非测试数据上重拟合,转成 ONNX 交给 MT5 端调用,绕开 Python 实时依赖。
class="kw">import pandas as pd class="kw">import numpy as np class="kw">import seaborn as sns class="kw">import matplotlib.pyplot as plt data = pd.read_csv("EURUSD Stop Out Prevention Market Data.csv") data LOOK_AHEAD = class="num">48 data[&class="macro">#x27;Target&class="macro">#x27;] = data[&class="macro">#x27;MA class="num">14&class="macro">#x27;].shift(-LOOK_AHEAD) data.dropna(inplace=True) data.reset_index(drop=True,inplace=True) class="macro">#Let&class="macro">#x27;s entirely drop off the last class="num">2 years of data data.iloc[-((class="num">48 * class="num">365 * class="num">2) + (class="num">48 * class="num">31 * class="num">2) + (class="num">48 * class="num">14) - (class="num">3)):,:] class="macro">#Let&class="macro">#x27;s entirely drop off the last class="num">2 years of data _ = data.iloc[-((class="num">48 * class="num">365 * class="num">2) + (class="num">48 * class="num">31 * class="num">2) + (class="num">48 * class="num">14) - (class="num">3)):,:] data = data.iloc[:-((class="num">48 * class="num">365 * class="num">2) + (class="num">48 * class="num">31 * class="num">2) + (class="num">48 * class="num">14) - (class="num">3))],:] data from sklearn.neural_network class="kw">import MLPRegressor from sklearn.model_selection class="kw">import train_test_split,TimeSeriesSplit,cross_val_score tscv = TimeSeriesSplit(n_splits=class="num">5,gap=LOOK_AHEAD) X = data.columns[class="num">1:-class="num">1] y = data.columns[-class="num">1:] train , test = train_test_split(data,test_size=class="num">0.5,shuffle=False) train_X = train.loc[:,X] train_y = train.loc[:,y] test_X = test.loc[:,X] test_y = test.loc[:,y] mean_scores = train_X.mean() std_scores = train_X.std() train_X = ((train_X - mean_scores) / std_scores) test_X = ((test_X - mean_scores) / std_scores) MAX_POWER = class="num">15 results = pd.DataFrame(index=["Train","Test"],columns=[np.arange(class="num">0,MAX_POWER)]) class="macro">#Classical Inputs for i in np.arange(class="num">0,MAX_POWER): print(i) model = MLPRegressor(hidden_layer_sizes=(class="num">5,class="num">10,class="num">4,class="num">2),solver="adam",activation="relu",max_iter=(class="num">2**i),early_stopping=False) results.iloc[class="num">0,i] = np.mean(np.abs(cross_val_score(model,train_X.loc[:,:],train_y.values.ravel(),cv=tscv))) results.iloc[class="num">1,i] = np.mean(np.abs(cross_val_score(model,test_X.loc[:,:],test_y.values.ravel(),cv=tscv))) results plt.title("Neural Network RMSE Forecasting class="num">14 Period MA") plt.ylabel("class="num">5 CV RMSE") plt.xlabel("Training Iterations As Powers of class="num">2") plt.grid() sns.lineplot(np.array(results.iloc[class="num">1,:]).transpose()) plt.axhline(results.min(class="num">1)[class="num">1],linestyle=&class="macro">#x27;--&class="macro">#x27;,class="type">class="kw">color=&class="macro">#x27;red&class="macro">#x27;) plt.axvline(class="num">14,linestyle=&class="macro">#x27;--&class="macro">#x27;,class="type">class="kw">color=&class="macro">#x27;red&class="macro">#x27;) class="kw">import onnx class="kw">import skl2onnx from skl2onnx.common.data_types class="kw">import FloatTensorType
把止损预测模型固化成 ONNX
上面这段 Python 把多层感知机直接压成 ONNX 文件,核心目的是让 MT5 之外的训练结果能脱离 sklearn 环境被调用。网络结构是 (5,10,4,2) 四隐层,激活函数 relu,求解器 adam,迭代上限 2**14 即 16384 步,没开 early_stopping,属于跑满才停的设定。 输入特征 X 先做标准化:用训练集均值和标准差做 (x-mean)/std,均值和标准差分别落盘到 EURUSD StopOut Mean.csv 与 EURUSD StopOut Std.csv。这两个 csv 是后续 MT5 侧实时归一化的基准,换品种或换周期必须重算,直接套用会偏移。 模型拟合后通过 skl2onnx 转成 FloatTensorType([1,5]) 的图,target_opset=12,最终存为 EURUSD StopOut Prevention Model.onnx。外汇与贵金属杠杆高、滑点跳空频繁,该模型仅降低止损被扫的概率,不保证避免亏损,实盘前请在 MT5 用历史 tick 回放验证输入维度是否严格为 5。
model = MLPRegressor(hidden_layer_sizes=(class="num">5,class="num">10,class="num">4,class="num">2),solver="adam",activation="relu",max_iter=(class="num">2**class="num">14),early_stopping=False) mean_scores = data.loc[:,X].mean() std_scores = data.loc[:,X].std() mean_scores.to_csv("EURUSD StopOut Mean.csv") std_scores.to_csv("EURUSD StopOut Std.csv") data[X] = ((data.loc[:,X] - mean_scores) / std_scores) model.fit(data.loc[:,X],data.loc[:,&class="macro">#x27;Target&class="macro">#x27;].values.ravel()) initial_types = [("float_input",FloatTensorType([class="num">1,class="num">5]))] model_proto = skl2onnx.convert_sklearn(model,initial_types=initial_types,target_opset=class="num">12) onnx.save(model_proto,"EURUSD StopOut Prevention Model.onnx")
「给止损预防策略接上 ONNX 预测头」
把上一节训好的 ONNX 模型加载进 EA 后,先声明一批系统常量并读入全局 z-score 数组,模型才能正确初始化。开仓逻辑改动不大:先取模型预测,再叠加原有均线条件,同时要求模型预测的均线值大于当前指标读数、且未来预期均线高于现价的实时读数,计算机才倾向认为趋势会延续。 多空条件对称,只是方向相反。更新止损函数加了一个标志位:传 0 只做常规推损;传 1 会先重新取预测,若预期均线将越过当前止损但仍有盈利空间,就把止损顶到模型峰值位,反之预期会跌破开仓价就提前砍仓控风险。 回测对比很直接:初版净值曲线斜率为负、亏约 1000 美元,夏普 -0.39;改版后赚略超 1000 美元,夏普 0.79,平均盈利从 98 美元抬到 130 美元,平均亏损从 102 美元降到 63 美元。约 60% 平仓仍是亏损单,说明大模型不亏的过滤有效,但止损出局难题没根除。外汇与贵金属杠杆高,这套改进仅降低尾部风险,实盘前请在 MT5 策略测试器用历史数据复跑验证。
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">//| Resources | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#resource "\\Files\\EURUSD StopOut Prevention Model.onnx" as class="type">uchar onnx_model_buffer[]; 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
◍ 把 ONNX 模型与均线句柄接进 EA
这段代码是 EA 启动阶段把推理模型和指标句柄一次性接好的关键。先看全局区:SL_ADJUSTMENT 定义为 1e-5 倍的 _Point,用作追踪止损的最小步长;ONNX_MODEL_INPUTS 写死为 5,对应后面归一化数组的长度。 mean_values 与 std_values 各含 5 个双精度数,例如均值首项是 1.157641086508574、标准差首项是 0.04070388112283021,这是离线统计得到的归一化参数,推理前必须把原始特征按这两组数标准化。 setup() 里先用 OnnxCreateFromBuffer 从内存缓冲区建模型,失败就 Comment 报错并返回 false;随后用 ulong 数组声明输入输出形状,输入为 {1,5}、输出为 {1,1},再调 OnnxSetInputShape / OnnxSetOutputShape 绑定。任何一步返回 false,EA 都不会继续初始化。 最后 ma_handler = iMA(Symbol(),TF_2,MA_PERIOD,0,MA_TYPE,MA_PRICE) 加载均线句柄,句柄无效同样中止。外汇与贵金属杠杆高、滑点跳空频繁,跑这套前务必在策略测试器用真实点差回测,模型推理失败可能让仓位失控。
class="macro">#define SL_ADJUSTMENT class="num">1e-5 * _Point class=class="str">"cmt">//--- The step size for our trailing stop class="macro">#define ONNX_MODEL_INPUTS class="num">5 class=class="str">"cmt">//---- Total model inputs for our ONNX model 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 mean_values[ONNX_MODEL_INPUTS] = {class="num">1.157641086508574,class="num">1.1581085911361018,class="num">1.1571729541088953,class="num">1.1576420747040126,class="num">1.157640521193191}; class="type">class="kw">double std_values[ONNX_MODEL_INPUTS] = {class="num">0.04070388112283021,class="num">0.040730761156963606,class="num">0.04067819202368064,class="num">0.040703752648947544,class="num">0.040684857239172416}; class="type">class="kw">double bid,ask,yesterday_high,yesterday_low; 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="type">long onnx_model; vectorf model_forecast = vectorf::Zeros(class="num">1); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Prepare the resources our EA requires | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool setup(class="type">void) { onnx_model = OnnxCreateFromBuffer(onnx_model_buffer,ONNX_DEFAULT); if(onnx_model == INVALID_HANDLE) { Comment("Failed to create ONNX model: ",GetLastError()); class="kw">return(false); } class="type">ulong input_shape[] = {class="num">1,ONNX_MODEL_INPUTS}; class="type">ulong output_shape[] = {class="num">1,class="num">1}; if(!OnnxSetInputShape(onnx_model,class="num">0,input_shape)) { Comment("Failed to set ONNX model input shape: ",GetLastError()); class="kw">return(false); } if(!OnnxSetOutputShape(onnx_model,class="num">0,output_shape)) { Comment("Failed to set ONNX model output shape: ",GetLastError()); class="kw">return(false); } ma_handler = iMA(Symbol(),TF_2,MA_PERIOD,class="num">0,MA_TYPE,MA_PRICE); if(ma_handler == INVALID_HANDLE) { Comment("Failed to load technical indicator: ",GetLastError()); class="kw">return(false); } class="kw">return(true); }; 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) {