基于MQL5和Python的自优化EA(第六部分):利用深度双重下降算法(基础篇)
「过拟合的尽头可能是双重下降」
机器学习里过拟合最常见的形态,是模型把训练数据里的噪声也当规律学进去,结果一碰到样本外行情就失灵。对刚接触 AI 的交易者来说,传统正则化、 dropout 那些抑制过拟合的手段,落地门槛不低。
- 年 6 月 25 日发布的这组研究中提到,哈佛背景团队的一篇论文指出:在特定任务上,随着模型容量继续放大,测试误差会先升后降,出现“双重下降”现象,过拟合反而被容量本身消化掉。
这对做 MT5 自优化 EA 的人是个提醒——外汇和贵金属杠杆高、跳空频繁,样本外失效风险始终存在;与其死磕小模型防过拟合,不如验证下大模型在扩大参数规模后是否进入下降区间。
把过拟合图表从单谷重新看成双谷
做 AI 模型时,老办法是盯着训练误差和测试误差两条曲线:开头一起往下走算健康,继续训下去训练误差还降、测试误差却抬头,就判定过拟合,于是用提前停止在验证误差恶化前砍掉训练、回滚最佳权重。
- 年那篇《深度双重下降》把这套常识砸了。它没给解释,只描述了现象:某些任务上测试误差先降、再升、然后又二次陡降创出新低,最终才收敛,形成双谷而不是单谷。
现象可写成两个变量的函数——模型参数量、最大训练迭代次数。同数据集上不断换更大的模型,测试误差走的就是先降后升、再二次探底。但算力不允许无限堆参数,所以我们这次只把它当“最大迭代次数”的函数来玩:允许模型训更久,验证误差总会先冲高、再掉到新低,冲高到回落的时长取决于数据噪声和模型类型。 关键反直觉点在插值阈值。简单网络欠拟合,加复杂度能降本;等复杂到完美拟合数据,训练误差贴近 0 不再降——这和“更复杂必更低”的传统说法第一次冲突。过了阈值还加复杂度,测试精度会明显掉,但再往后训下去误差又能落进新低并稳住。 提前停止这类防过拟合手段,恰恰总在二次下降前就叫停,等于把双谷后半截掐了。下面我们用 MT5 环境自己复现一次,独立看清楚这条曲线。
◍ 用网格搜索给线性模型找最佳预测窗口
从 MT5 拽数据只是前戏。真正要干的,是给 GBPUSD 日线 OHLC 定两个旋钮:返回值按什么周期算、往前预测多少步。默认大家爱用「周期1、预测1步」,但这未必是最优解,让数据自己说话。 我们对这两个参数做网格搜索,x 轴 y 轴拉平后生成二维网格,再写函数按每组参数清洗数据、交叉验证、把误差写回同形数组。简单线性回归跑完,等高线图里最深的蓝区白点就是最佳设置——图表显示,线性模型在日线周期与预测范围都落在 20 到 40 之间时误差最低。 把误差摊成 3D 曲面能看见:随预测 horizon 拉远,误差先降后升,有个明确最优谷。也就是说,这个线性 AI 模型在 20~40 日窗口上的表现,可能轻松盖过市场上那些死守「周期1预测1步」的经典价格模型。外汇与贵金属属高风险品种,回测优越不等于实盘稳赢。 换深度神经网络进同一参数空间,曲面图乱得多:蓝区夹着突兀红区,相邻组合误差天差地别。这源于神经网络优化算法的随机性——重训一次,最佳点就跑位。传统视角会判线性模型胜出(误差图平滑=有技能),但按双重下降理论,现在说神经网络过拟合还太早,它更可能是没收敛。 下面这段脚本负责从当前品种拉最多 10 万根数据落盘成 CSV,是后面所有实验的取数底座。
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 "Gamuchirai Zororo Ndawana" 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">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script Inputs | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">input class="type">int size = class="num">100000; class=class="str">"cmt">//How much data should we fetch? class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Global variables | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| On start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- File name class="type">class="kw">string file_name = "Market Data " + Symbol()+ ".csv"; 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">0;i--) { if(i == size) {
「把MT5导出的K线落盘并喂给Python建模」
MT5里写文件要先区分表头与数据行:首次建文件时写列名,之后每个Bar用 iTime / iOpen / iHigh / iLow / iClose 按当前品种与周期抓取,循环结束务必 FileClose,否则句柄泄漏会让后续写盘失败。
FileWrite(file_handle,"Time","Open","High","Low","Close"); } else { FileWrite(file_handle,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) ); } } class=class="str">"cmt">//--- Close the file FileClose(file_handle);
FileWrite(file_handle,"Time","Open","High","Low","Close"); } else { FileWrite(file_handle,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) ); } } class=class="str">"cmt">//--- Close the file FileClose(file_handle);
用差分周期和预测步长给英镑美元日线建模
这段处理把原始 K 线字段重命名后直接丢掉日期与成交量类列,只留 OHLC 四列做特征。目标变量用 Close 向前平移 look_ahead 根 K 线得到,也就是用当下差分后的开高低收去猜若干根之后的收盘价,属于典型的多步向前回归设定。 差分环节对 Open/High/Low/Close 统一做 period 阶滞后相减,再把含空值的首行删掉、索引重置。这样序列从价格水平变成价格变动,线性模型才不容易被趋势带偏。 评估函数里用了 TimeSeriesSplit(n_splits=5, gap=look_ahead),5 折且折间隔离预测步长,避免未来信息泄漏。cross_val_score 对线性回归取平均 R² 作为该 (look_ahead, period) 组合的得分,objective 再双重循环铺满参数网格。 图上白点标的是绝对值误差最小的组合:x 轴是 Forecast Horizon(预测步长),y 轴是 Differencing Period(差分周期)。英镑美元日线在这个网格下,线性与神经网络两张等高线图都指向相近的低谷区,说明差分周期选错可能比模型选错更伤精度。外汇与贵金属为高杠杆品种,此类回测仅反映历史样本,实盘信号失效概率不低。 把这段代码丢进本地 Python,换 x/y 网格密度重跑一次,你能直接看到自己机器上 GBPUSD 日线的最优差分周期落在哪。
temp.rename(columns={&class="macro">#x27;<OPEN>&class="macro">#x27;:&class="macro">#x27;Open&class="macro">#x27;,&class="macro">#x27;<HIGH>&class="macro">#x27;:&class="macro">#x27;High&class="macro">#x27;,&class="macro">#x27;<LOW>&class="macro">#x27;:&class="macro">#x27;Low&class="macro">#x27;,&class="macro">#x27;<CLOSE>&class="macro">#x27;:&class="macro">#x27;Close&class="macro">#x27;},inplace=True) temp = temp.drop([&class="macro">#x27;<DATE>&class="macro">#x27;,&class="macro">#x27;<VOL>&class="macro">#x27;,&class="macro">#x27;<SPREAD>&class="macro">#x27;,&class="macro">#x27;<TICKVOL>&class="macro">#x27;],axis=class="num">1) class="macro">#Define our target temp["Target"] = temp["Close"].shift(-look_ahead) class="macro">#Apply the differencing temp["Close"] = temp["Close"].diff(period) temp["Open"] = temp["Open"].diff(period) temp["High"] = temp["High"].diff(period) temp["Low"] = temp["Low"].diff(period) temp = temp.dropna() temp = temp.reset_index(drop=True) class="kw">return(temp) class="macro">#Evaluate the objective function def evaluate(look_ahead,period): class="macro">#Define the model model = LinearRegression() class="macro">#Define our time series split tscv = TimeSeriesSplit(n_splits=class="num">5,gap=look_ahead) temp = clean_data(look_ahead,period) score = np.mean(cross_val_score(model,temp.loc[:,["Open","High","Low","Close"]],temp["Target"],cv=tscv)) class="kw">return(score) class="macro">#Define the objective def objective(x,y): class="macro">#Define the output matrix results = np.zeros([x.shape[class="num">0],y.shape[class="num">0]]) class="macro">#Fill in the output matrix for i in np.arange(class="num">0,x.shape[class="num">0]): class="macro">#Select the rows look_ahead = x[i] period = y[i] for j in np.arange(class="num">0,y.shape[class="num">0]): results[i,j] = evaluate(look_ahead[j],period[j]) class="kw">return(results) linear_reg_res = objective(x,y) linear_reg_res = np.abs(linear_reg_res) plt.contourf(x,y,linear_reg_res,class="num">100,cmap="jet") plt.plot(x_axis[linear_reg_res.min(axis=class="num">0).argmin()],y_axis[linear_reg_res.min(axis=class="num">1).argmin()],&class="macro">#x27;.&class="macro">#x27;,class="type">color=&class="macro">#x27;white&class="macro">#x27;) plt.ylabel("Differencing Period") plt.xlabel("Forecast Horizon") plt.title("Linear Regression Accuracy Forecasting GBPUSD Daily Close") class="macro">#Create a surface plot fig , ax = plt.subplots(subplot_kw={"projection":"3d"}) fig.set_size_inches(class="num">8,class="num">8) ax.plot_surface(x,y,linear_reg_res,cmap="jet") res = objective(x,y) res = np.abs(res) plt.contourf(x,y,res,class="num">100,cmap="jet") plt.plot(x_axis[res.min(axis=class="num">0).argmin()],y_axis[res.min(axis=class="num">1).argmin()],&class="macro">#x27;.&class="macro">#x27;,class="type">color=&class="macro">#x27;white&class="macro">#x27;) plt.ylabel("Differencing Period") plt.xlabel("Forecast Horizon") plt.title("Neural Network Accuracy Forecasting GBPUSD Daily Close") class="macro">#Create a surface plot fig , ax = plt.subplots(subplot_kw={"projection":"3d"}) fig.set_size_inches(class="num">8,class="num">8) ax.plot_surface(x,y,res,cmap="jet")