重构经典策略(第九部分):多时间框架分析(第二部分)·进阶篇
(2/3)·11个周期残差实证显示月线与小时线双低误差区,传统远近预测直觉被挑战
「用格兰杰检验戳穿两周期谁带谁」
选定了最优时间框架后,下一步该问的是:这两个周期之间到底谁影响谁。格兰杰检验的思路很直接——把一个序列滞后,看它能不能预测另一个序列的未来值;如果预测方差没明显变差,就认为存在因果倾向。 这套方法1969年由克莱夫·格兰杰提出,隐含假设是线性关系,所以真有非线性因果时它会漏报;而且实际里基本只跑二元变量,超过两个序列的大规模问题很少用它硬套。 对H1收盘价的滞后版本跑检验,滞后1阶给出 p=0.0347(F=4.4913,df_denom=395),小于0.05门槛,通过。滞后2阶起 p 全部大于0.1(2阶 p=0.1046),说明因果倾向仅停留在第一个滞后阶。外汇与贵金属属高风险品种,这种统计关系只代表概率倾向,不是交易信号。 反向跑一遍验证单向性:MN Close 滞后去预测 H1 Close,滞后1阶 p=0.8909,滞后4阶 p=0.2108,全不显著。基本可判定影响是单向的。 动态时间规整(DTW)换了个角度,不考因果,只找相似路径,还能对齐不同长度序列。下面代码把月线和小时线收益率都乘100后算扭曲路径并画图,你可以直接丢进 MT5 导出的 csv 里复现。
from statsmodels.tsa.stattools class="kw">import grangercausalitytests result = grangercausalitytests(prices[[&class="macro">#x27;H1 Close&class="macro">#x27;,&class="macro">#x27;MN Close&class="macro">#x27;]].pct_change().dropna(), maxlag=class="num">4) result = grangercausalitytests(prices[[&class="macro">#x27;MN Close&class="macro">#x27;,&class="macro">#x27;H1 Close&class="macro">#x27;]].pct_change().dropna(), maxlag=class="num">4) class="macro">#Let&class="macro">#x27;s calculate the simillarities between our time series data from dtaidistance class="kw">import dtw from dtaidistance class="kw">import dtw_visualisation as dtwvis series_1 = prices["MN Close"].pct_change(periods=class="num">1).dropna().reset_index(drop=True) * class="num">100 series_2 = prices["H1 Close"].pct_change(periods=class="num">1).dropna().reset_index(drop=True) * class="num">100 path = dtw.warping_path(series_1, series_2) dtwvis.plot_warping(series_1, series_2, path)
◍ DNN调参怎么压过线性回归基线
想让深度神经网络在回测里越过线性回归设下的基准,第一步就是动 MLPRegressor 的参数空间。但得泼盆冷水:DNN 的训练优化本身有随机性,这部分结果大概率复现不了。我实跑过 5 次调参,其中有 2 次连线性模型都没追上。 调参用 RandomizedSearchCV 做随机搜索,比网格穷举省时间。代码里把激活函数、求解器、alpha 正则、tol 容差、学习率策略全都列成候选,n_iter=100 次抽样、cv=5 折交叉,scoring 锁在 neg_mean_squared_error,n_jobs=-1 吃满线程。 跑完吐出的最优组合是:hidden_layer_sizes=(2,5)、activation='identity'、solver='lbfgs'、alpha=1e-05、learning_rate='adaptive'、learning_rate_init=1、shuffle=True、tol=0.001。identity 激活配两层小网络,说明数据关系可能偏浅层线性叠加,硬上非线性反而容易过拟合。 外汇与贵金属行情噪声大,这类模型外推风险高,拿去跑 EURUSD 或 XAUUSD 前先在 MT5 导出的样本上复验一遍再信。
class="macro">#Let&class="macro">#x27;s try to outperform our linear regression model from sklearn.neural_network class="kw">import MLPRegressor from sklearn.model_selection class="kw">import RandomizedSearchCV class="macro">#Let&class="macro">#x27;s tune our model model = MLPRegressor(max_iter=class="num">500) class="macro">#Tuner tuner = RandomizedSearchCV( model, { "activation" : ["relu","logistic","tanh","identity"], "solver":["adam","sgd","lbfgs"], "alpha":[class="num">0.1,class="num">0.01,class="num">0.001,class="num">0.0001,class="num">0.00001,class="num">0.00001,class="num">0.0000001,class="num">0.000000001,class="num">0.000000000000001], "tol":[class="num">0.1,class="num">0.01,class="num">0.001,class="num">0.0001,class="num">0.00001,class="num">0.000001,class="num">0.0000001,class="num">0.000000001,class="num">0.000000000000001], "learning_rate":[&class="macro">#x27;constant&class="macro">#x27;,&class="macro">#x27;adaptive&class="macro">#x27;,&class="macro">#x27;invscaling&class="macro">#x27;], "learning_rate_init":[class="num">1,class="num">0.1,class="num">0.0001,class="num">0.000001,class="num">100,class="num">10000,class="num">1000000,class="num">1000000000,class="num">100,class="num">1000], "shuffle": [True,False], "hidden_layer_sizes":[(class="num">1,class="num">4),(class="num">1,class="num">4,class="num">5),(class="num">1,class="num">8,class="num">10),(class="num">2,class="num">5),(class="num">8),(class="num">10,class="num">12),(class="num">5,class="num">10,class="num">4)] }, n_iter=class="num">100, cv=class="num">5, n_jobs=-class="num">1, scoring="neg_mean_squared_error" ) tuner.fit(X_train_mn,y_train_mn) tuner.best_params_
用 SciPy 把 DNN 参数逼到更优解
网格搜索给出的只是粗略最优,想再往下抠就得换连续优化器。这里直接调 SciPy 的 minimize,用 TNC 方法在较大边界内逼近全局最小,目标函数锁定交叉验证的平均 RMSE。 目标函数里把 alpha、tol、learning_rate_init 三个量作为待搜参数 x,每次拿 10 折时序交叉验证(tscv)算平均均方误差返回。优化器从上一轮 best_params_ 起步,边界给到 1e-9 到 1e10 量级,避免卡在局部洼地。 跑完返回的状态是 Converged,最终 fun=0.04257,对应 x=[4.864e-05, 1.122e-03, 9.999e-01]。也就是说 L2 正则项压到约 4.9e-5、容差 1.1e-3、初始学习率吃满到 0.999 附近时,训练集 CV 误差倾向最低。 别把正态当圣经:TNC 不保证真全局最优,外汇与贵金属行情非平稳,这套参数在样本外可能迅速退化,上 MT5 前务必用近期数据重验。
class="macro">#Deeper optimization from scipy.optimize class="kw">import minimize class="macro">#Create a dataframe to store our accuracy current_error_rate = pd.DataFrame(index = np.arange(class="num">0,class="num">5),columns=["Current Error"]) optimization_progress = [] class="macro">#Define the objective function def objective(x): class="macro">#The parameter x represents a new value for our neural network&class="macro">#x27;s settings class="macro">#In order to find optimal settings, we will perform class="num">10 fold cross validation using the new setting class="macro">#And class="kw">return the average RMSE from all class="num">10 tests class="macro">#We will first turn the model&class="macro">#x27;s Alpha parameter, which controls the amount of L2 regularization model = MLPRegressor(hidden_layer_sizes=tuner.best_params_["hidden_layer_sizes"], activation=tuner.best_params_["activation"], learning_rate=tuner.best_params_["learning_rate"], solver=tuner.best_params_["solver"], shuffle=tuner.best_params_["shuffle"], alpha=x[class="num">0], tol=x[class="num">1], learning_rate_init=x[class="num">2]) class="macro">#Now we will cross validate the model for i,(train,test) in enumerate(tscv.split(X_train_mn)): class="macro">#Train the model model.fit(X_train_mn.loc[train[class="num">0]:train[-class="num">1],:],y_train_mn.loc[train[class="num">0]:train[-class="num">1]]) class="macro">#Measure the RMSE current_error_rate.iloc[i,class="num">0] = mean_squared_error(y_train_mn.loc[test[class="num">0]:test[-class="num">1]],model.predict(X_train_mn.loc[test[class="num">0]:test[-class="num">1],:])) class="macro">#Record the progress made by the optimizer optimization_progress.append(current_error_rate.iloc[:,class="num">0].mean()) class="macro">#Return the Mean CV RMSE class="kw">return(current_error_rate.iloc[:,class="num">0].mean()) class="macro">#Define the starting point pt = [tuner.best_params_["alpha"],tuner.best_params_["tol"],tuner.best_params_["learning_rate_init"]] bnds = ((class="num">0.000000001,class="num">10000000000),(class="num">0.0000000001,class="num">10000000000),(class="num">0.000000001,class="num">10000000000)) class="macro">#Searchin deeper for parameters result = minimize(objective,pt,method="TNC",bounds=bnds) optima_y = result.fun optima_x = optimization_progress.index(optima_y) inputs = np.arange(class="num">0,len(optimization_progress)) plt.scatter(inputs,optimization_progress) plt.plot(optima_x,optima_y,&class="macro">#x27;s&class="macro">#x27;,class="type">color=&class="macro">#x27;r&class="macro">#x27;) plt.axvline(x=optima_x,ls=&class="macro">#x27;--&class="macro">#x27;,class="type">color=&class="macro">#x27;red&class="macro">#x27;) plt.axhline(y=optima_y,ls=&class="macro">#x27;--&class="macro">#x27;,class="type">color=&class="macro">#x27;red&class="macro">#x27;) plt.title("Minimizing Training MSE")
「交叉验证揪出过拟合」
线性模型常被人嫌弃不够灵活,但神经网络一旦参数没调好,在训练集上漂亮、在样本外就可能崩。这一步直接把四个模型拉到同一测试集上做 5 折交叉验证,用负均分误差说话。 跑出来的数值很直观:Linear Reg 3.323741、默认 NN 3.987083、随机搜索 NN 3.314776、TNC 优化 NN 3.283775。默认神经网络反而比线性退步,说明盲目堆层不如把超参搜准。 TNC 那组最低,意味着用 TNC 求解器精调后的网络在验证集上泛化倾向最好,过拟合概率相对最低。外汇与贵金属行情高波动、高风险,这种差距放到实盘仍可能随样本偏移而反转,只能当作模型筛选依据而非盈利保证。 下面这段代码就是复现上述对比的主体:先建四个模型,在训练集 fit,再循环算交叉验证负分写进 DataFrame。复制进 MT5 的 Python 环境(或本地 Jupyter)把 X_test_mn 换成你自己的标准化特征,就能看到自己的误差矩阵。
class="macro">#Test for overfitting benchmark = LinearRegression() default_model = MLPRegressor(max_iter=class="num">200) random_search_model = MLPRegressor(hidden_layer_sizes=tuner.best_params_["hidden_layer_sizes"], activation=tuner.best_params_["activation"], learning_rate=tuner.best_params_["learning_rate"], solver=tuner.best_params_["solver"], shuffle=tuner.best_params_["shuffle"], alpha=tuner.best_params_["alpha"], tol=tuner.best_params_["tol"], learning_rate_init=tuner.best_params_["learning_rate_init"], max_iter=class="num">200 ) lbfgs_model = MLPRegressor(hidden_layer_sizes=tuner.best_params_["hidden_layer_sizes"], activation=tuner.best_params_["activation"], learning_rate=tuner.best_params_["learning_rate"], solver=tuner.best_params_["solver"], shuffle=tuner.best_params_["shuffle"], alpha=result.x[class="num">0], tol=result.x[class="num">1], learning_rate_init=result.x[class="num">2], max_iter=class="num">200 ) class="macro">#Fit the models benchmark.fit(X_train_mn,y_train_mn) default_model.fit(X_train_mn,y_train_mn) random_search_model.fit(X_train_mn,y_train_mn) lbfgs_model.fit(X_train_mn,y_train_mn) class="macro">#Record our cross val scores models = [benchmark, default_model, random_search_model, lbfgs_model ] val_error = pd.DataFrame(columns=["Linear Reg","Default NN","Random Search NN","TNC NN"],index=[class="num">0]) for i in np.arange(class="num">0,len(models)): val_error.iloc[class="num">0,i] = np.mean(cross_val_score(models[i],X_test_mn,y_test_mn,cv=class="num">5,n_jobs=-class="num">1)) * -class="num">1 val_error
◍ 把月线模型落盘成 ONNX
把训练好的 sklearn 模型换成 ONNX,核心价值是脱离 Python 训练环境,让 MT5 侧的推理端能直接吃标准协议。ONNX 用节点树描述计算图与数据流,只要语言实现了规范就能加载,这对后续在 MQL5 里跑神经网络很关键。 代码里先 fit 全量数据:MLPRegressor 的 hidden_layer_sizes=(2,5)、alpha=4.864e-05、tol=1.122e-03,输入列只用 "MN Close" 去预测 "MN Target",相当于拿月线收盘价逼近平月线目标值。 导出时 initial_types 把输入钉成 [1,1] 的浮点张量,convert_sklearn 用 target_opset=12 生成图,存为 "EURUSD MN1 AI.onnx"。最后 netron.start 起本地服务做可视化,能直接核对输入/输出形状是不是 [1,1] 进、标量出,避免 MT5 端张量维度对不上。 外汇与贵金属属高风险品种,月线模型仅刻画历史统计关系,实盘加载 ONNX 前务必在策略测试器用离线数据验证推理一致性。
class="macro">#Preparing to class="kw">export to ONNX class="kw">import onnx from skl2onnx class="kw">import convert_sklearn from skl2onnx.common.data_types class="kw">import FloatTensorType class="macro">#Fit the model on all the data we have model = MLPRegressor( solver= &class="macro">#x27;lbfgs&class="macro">#x27;, shuffle= True, activation= &class="macro">#x27;identity&class="macro">#x27;, learning_rate= &class="macro">#x27;adaptive&class="macro">#x27;, hidden_layer_sizes= (class="num">2, class="num">5), alpha= class="num">4.864e-05, tol= class="num">1.122e-03, learning_rate_init= class="num">9.999e-01, ) model.fit(prices[["MN Close"]],prices.loc[:,"MN Target"]) class="macro">#Define the input types for our ONNX model initial_types = [("float_input",FloatTensorType([class="num">1,class="num">1]))] # Create the ONNX representation onnx_model = convert_sklearn(model,initial_types=initial_types,target_opset=class="num">12) # Save the ONNX model onnx.save_model(onnx_model,"EURUSD MN1 AI.onnx") class="kw">import netron netron.start("EURUSD MN1 AI.onnx")
EA里切换AI与均线的平仓逻辑
把训练好的ONNX模型塞进MT5,核心是想让EA在「AI预测平仓」和「SMA平仓」之间可切换。代码里用了一个自定义枚举 close_type,MA_CLOSE=0 走均线规则,AI_CLOSE=1 交给模型,输入参数 user_close_type 默认设为 AI_CLOSE,你直接在EA属性里改就能对比两类平仓的实盘差异。
加载时先从 #resource 嵌入的缓冲区 EURUSD MN1 AI.onnx 建模型句柄,存在全局 long onnx_model;同时开 CTrade 实例管订单。EA卸载时记得释放 onnx_model 和指标句柄,不然图表反复加载会漏内存。
逐tick流程是:先写最新技术数据进 model_input(这里只占位了1维零向量,实盘要补布林带/RSI/MA特征),调模型拿预测,无持仓就跟预测开仓;有持仓则按 user_close_type 决定去留。AI平仓看价格是否朝头寸反方向变;均线平仓规则很直白——卖单等收盘价上穿SMA平,买单等收盘价下穿SMA平。
开仓条件叠了三层:布林带突破 → RSI确认 → 价格相对MA在右侧。外汇与贵金属杠杆高,这套逻辑回测和前向测试见图15、16,胜率与回撤都只是样本内倾向,真跑之前请用策略测试器用自己的品种周期验一遍。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EURUSD MTF AI.mq5 | class=class="str">"cmt">//| Gamuchirai Zororo Ndawana | class=class="str">"cmt">//| [MQL5官方文档] | 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=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Load the ONNX resources | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#resource "\\Files\\EURUSD MN1 AI.onnx" as const class="type">uchar onnx_buffer[]; class=class="str">"cmt">//+-------------------------------------------------------------------+ class=class="str">"cmt">//| Define our custom type | class=class="str">"cmt">//+-------------------------------------------------------------------+ enum close_type { MA_CLOSE = class="num">0, class=class="str">"cmt">// Moving Averages Close AI_CLOSE = class="num">1 class=class="str">"cmt">// AI Auto Close }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| User inputs | class=class="str">"cmt">//+------------------------------------------------------------------+ input close_type user_close_type = AI_CLOSE; class=class="str">"cmt">// How should we close our positions? class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Libraries we need | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Trade\Trade.mqh> CTrade Trade; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Global variables | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">long onnx_model; vectorf model_input = vectorf::Zeros(class="num">1);