在任何市场中获得优势(第五部分):联邦储备经济数据库(FRED)欧元兑美元( EURUSD)可替代数据·进阶篇
(2/3)·当广义美元指数与欧美汇率呈现近完美负相关,你的策略还只看K线吗?
切分样本与建好准确率容器
做价格行为建模,第一步不是调参,而是把输入特征 X 和目标 y 从合并数据里单独抽出来。这里 y 直接取 Target 列,X 取事先圈定的 predictors 集合,逻辑上和 MT5 导出的 OHLC 特征、宏观 FRED 特征完全解耦。 样本切分用了 train_test_split,但 shuffle=False,意味着按时间顺序前 50% 做训练、后 50% 做测试。外汇与贵金属属高波动品种,乱序洗牌会引入未来函数,这种时序直切更贴近实盘回测。三组切分分别覆盖纯 MT5 行情、纯宏观、以及全特征,test_size 统一设为 0.5。 代码末尾建了一个 validation_error 的空 DataFrame,列名是 MT5 Data / FRED Data / ALL Data,索引 0 到 4 共 5 行。这就是后面存交叉验证准确率的地方,你开 MT5 把数据拉齐后,照这个结构跑,能直接比对三类特征的样本外表现。
class="macro">#Let&class="macro">#x27;s define our set of predictors X = merged_data.loc[:,predictors] y = merged_data.loc[:,"Target"] class="macro">#Import the libraries we need from sklearn.model_selection class="kw">import train_test_split class="macro">#Partition the data ohlc_train_X,ohlc_test_X,train_y,test_y = train_test_split(X.loc[:,ohlc_predictors],y,test_size=class="num">0.5,shuffle=False) fred_train_X,fred_test_X,_,_ = train_test_split(X.loc[:,fred_predictors],y,test_size=class="num">0.5,shuffle=False) train_X,test_X,_,_ = train_test_split(X.loc[:,predictors],y,test_size=class="num">0.5,shuffle=False) class="macro">#Prepare the dataframe to store our validation error validation_error = pd.DataFrame(columns=["MT5 Data","FRED Data","ALL Data"],index=np.arange(class="num">0,class="num">5))
◍ 普通行情数据反而最稳
用三个结构相同的 MLPRegressor(隐藏层 10/20/40,最大迭代 500)分别吃 MT5 行情、FRED 宏观替代量、以及两者合并的全量数据,做 5 折交叉验证,评分指标取负 RMSE。 逐折误差里就能看出分歧:MT5 那列五折得分在 -0.095 到 -1.234 之间波动,而 FRED 和全量数据都出现超过 -2 的折,全量甚至在某一折掉到 -4.817。外汇与贵金属市场高风险,这种外部数据混入后误差放大,说明噪声可能压过了有效信号。 平均下来差距更直观:MT5 平均 -0.359,FRED -1.450,全量 -2.050。箱线图里 MT5 的箱体几乎压扁,代表各折表现集中、模型具备稳定技能;其余两组箱体拉长,意味着回测结果对数据切分敏感。 下面这段 Python 可直接在本地重跑验证,把 MT5 导出的 OHLC 训练集塞进 ohlc_train_X 即可比对你的样本。
class="macro">#Let&class="macro">#x27;s cross validate our models from sklearn.neural_network class="kw">import MLPRegressor from sklearn.model_selection class="kw">import cross_val_score class="macro">#Define the neural networks ohlc_nn = MLPRegressor(hidden_layer_sizes=(class="num">10,class="num">20,class="num">40),max_iter=class="num">500) fred_nn = MLPRegressor(hidden_layer_sizes=(class="num">10,class="num">20,class="num">40),max_iter=class="num">500) all_nn = MLPRegressor(hidden_layer_sizes=(class="num">10,class="num">20,class="num">40),max_iter=class="num">500) class="macro">#Let&class="macro">#x27;s obtain our cv score ohlc_score = cross_val_score(ohlc_nn,ohlc_train_X,train_y,scoring=&class="macro">#x27;neg_root_mean_squared_error&class="macro">#x27;,cv=class="num">5,n_jobs=-class="num">1) fred_score = cross_val_score(fred_nn,fred_train_X,train_y,scoring=&class="macro">#x27;neg_root_mean_squared_error&class="macro">#x27;,cv=class="num">5,n_jobs=-class="num">1) all_score = cross_val_score(all_nn,train_X,train_y,scoring=&class="macro">#x27;neg_root_mean_squared_error&class="macro">#x27;,cv=class="num">5,n_jobs=-class="num">1) for i in np.arange(class="num">0,class="num">5): validation_error.iloc[i,class="num">0] = ohlc_score[i] validation_error.iloc[i,class="num">1] = fred_score[i] validation_error.iloc[i,class="num">2] = all_score[i] class="macro">#Our validation error validation_error class="macro">#Our mean performane across all groups validation_error.mean() class="macro">#Plotting our performance validation_error.plot() class="macro">#Creating box-plots of our performance sns.boxplot(validation_error)
「MT5行情自身就够解释目标了」
用累积局部效应(ALE)图看 DNN 每个输入对目标的影响,比部分依赖图更适合强相关的外汇数据。若 ALE 是水平线,说明该特征对模型几乎无贡献;偏离线性越远,模型学到的关系越复杂。图12左上角欧元兑美元开盘价的 ALE 随开盘价上升而上升,收盘价 ALE 反向走动,仅这两个 MT5 市场字段就可能解释目标的大部分方差。 跑正向选择(SequentialFeatureSelector,forward=True,5折 CV,负均方误差评分)从空模型逐步加特征。结果算法最终只留下 ('open','high','low'),没有任何外部宏观时间序列被选入,且图13显示随预测变量增多模型性能反而下降。 对外汇、贵金属这类高杠杆品种,模型解释力高度依赖平台自带行情,外部替代数据未必带来增量信息,实盘前应在 MT5 用历史数据复算确认。
class="macro">#Feature importance from alibi.explainers class="kw">import ALE, plot_ale class="macro">#Explaining our deep neural network model = MLPRegressor(hidden_layer_sizes=(class="num">10,class="num">20,class="num">40),max_iter=class="num">500) model.fit(train_X,train_y) dnn_ale = ALE(model.predict,feature_names=predictors,target_names=["Target"]) class="macro">#Obtaining the explanation ale_X = X.to_numpy() dnn_explanations = dnn_ale.explain(ale_X) class="macro">#Plotting feature importance plot_ale(dnn_explanations,n_cols=class="num">3,fig_kw={&class="macro">#x27;figwidth&class="macro">#x27;:class="num">8,&class="macro">#x27;figheight&class="macro">#x27;:class="num">8},sharey=None) class="macro">#Forward selection from mlxtend.feature_selection class="kw">import SequentialFeatureSelector as SFS from mlxtend.plotting class="kw">import plot_sequential_feature_selection as plot_sfs class="macro">#Reinitialize the model all_nn = MLPRegressor(hidden_layer_sizes=(class="num">10,class="num">20,class="num">40),max_iter=class="num">500) class="macro">#Define the feature selector sfs1 = SFS(all_nn, k_features=(class="num">1,X.shape[class="num">1]), forward=True, scoring=&class="macro">#x27;neg_mean_squared_error&class="macro">#x27;, cv=class="num">5, n_jobs=-class="num">1 ) class="macro">#Best features we identified sfs1.k_feature_names_ class="macro">#Fit the forward selection algorithm fig1 = plot_sfs(sfs1.get_metric_dict(), kind=&class="macro">#x27;std_dev&class="macro">#x27;)
用随机搜索给DNN调参
在 MT5 的 Python 环境里做深度神经网络回归,最费时的不是写模型,而是把隐藏层结构和学习率这些超参跑出一组能用的组合。手动试太低效,直接上 RandomizedSearchCV 做随机搜索,能在有限迭代里覆盖大量参数空间。 上面这段代码先把 MLPRegressor 以 max_iter=500 重新初始化,再挂一个随机搜索调优器:激活函数扫 relu/logistic/tanh/identity 四种,求解器覆盖 adam/sgd/lbfgs,alpha 与 tol 都按 10 倍步长从 0.1 一路下探到 1e-7,隐藏层尺寸给了 7 种拓扑。n_iter=500、cv=5、n_jobs=-1 意味着用满 CPU 做 500 次随机采样、每次 5 折交叉,以负均方误差为评分。 跑完取 best_params_,实测一组较优解是 hidden_layer_sizes=(10,20,40,80)、solver='lbfgs'、activation='relu'、alpha=0.1、learning_rate='invscaling'、learning_rate_init=0.01、early_stopping=True。外汇与贵金属行情噪声大,这套参数只是某次训练窗下的偏好,换周期或品种大概率要重搜,杠杆交易高风险,别直接当固定模板。
class="macro">#Reinitialize the model model = MLPRegressor(max_iter=class="num">500) class="macro">#Define the 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], "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], "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">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], "hidden_layer_sizes":[(class="num">10,class="num">20,class="num">40),(class="num">10,class="num">20,class="num">40,class="num">80),(class="num">5,class="num">10,class="num">20,class="num">100),(class="num">100,class="num">50,class="num">10),(class="num">20,class="num">20,class="num">10),(class="num">1,class="num">5,class="num">10,class="num">20),(class="num">20,class="num">10,class="num">5,class="num">1)], "early_stopping":[True,False], "warm_start":[True,False], "shuffle": [True,False] }, n_iter=class="num">500, cv=class="num">5, n_jobs=-class="num">1, scoring="neg_mean_squared_error" ) class="macro">#Fit the tuner tuner.fit(train_X,train_y) class="macro">#The best parameters we found tuner.best_params_
◍ 用 TNC 在交叉验证里挖参数
把参数寻优看成捉迷藏:能让模型在未见数据上误差更小的理想权重,藏在连续参数的无限取值空间里。这里用 SciPy 的 minimize 配合 TNC(截断牛顿约束)算法,在保持 DNN 其他结构不变的前提下,只调 alpha、tol、learning_rate_init 三个连续量。 目标函数取 5 折时间序列交叉验证的均方误差均值作为最小化对象,TimeSeriesSplit 设 n_splits=5 且带 look_ahead 间隔,避免未来信息泄漏。边界只要求参数为正,下限放到 10^-100、上限 10^100,相当于不收紧。 实际跑出来并不顺:终止消息显示「线性搜索失败」,status=4,nit=0,fun=0.00191,x 被压到 [1e-100, 1e-100, 1e-100],jac 里首量梯度高达 2.689e+06。说明初始点附近曲面太陡或数值下溢,优化器一步没走就退出了。 别把正态当圣经 这种结果不是模型没用,而是 TNC 对极小边界和病态梯度敏感。真要落地,先把 alpha 等参数做 log 变换再寻优,或在 MT5 外接 Python 时用 L-BFGS-B 对比一遍,看 x 是否还黏在边界上。外汇与贵金属波动有跳空风险,样本外误差可能突然放大,参数最优也只是概率上的较优。
class="macro">#Deeper optimization from scipy.optimize class="kw">import minimize from sklearn.metrics class="kw">import mean_squared_error from sklearn.model_selection class="kw">import TimeSeriesSplit class="macro">#Define the time series split object tscv = TimeSeriesSplit(n_splits=class="num">5,gap=look_ahead) 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"]) algorithm_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 model = MLPRegressor(hidden_layer_sizes=tuner.best_params_["hidden_layer_sizes"], early_stopping=tuner.best_params_["early_stopping"], warm_start=tuner.best_params_["warm_start"], max_iter=class="num">500, 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(train_X)): class="macro">#Train the model model.fit(train_X.loc[train[class="num">0]:train[-class="num">1],:],train_y.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(train_y.loc[test[class="num">0]:test[-class="num">1]],model.predict(train_X.loc[test[class="num">0]:test[-class="num">1],:])) class="macro">#Store the algorithm&class="macro">#x27;s progress algorithm_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">10.00 ** -class="num">100,class="num">10.00 ** class="num">100), (class="num">10.00 ** -class="num">100,class="num">10.00 ** class="num">100), (class="num">10.00 ** -class="num">100,class="num">10.00 ** class="num">100)) class="macro">#Searching deeper for parameters result = minimize(objective,pt,method="TNC",bounds=bnds) class="macro">#The result of our optimization result class="macro">#Store the optimal coefficients optimal_weights = result.x
「把优化轨迹画出来找最低点」
跑完优化循环后,algorithm_progress 里存的是每一轮的训练均方误差(MSE),数值越小代表那一轮参数越接近局部最优。用 min() 取出最小 MSE 作为 optima_y,再用 index() 定位它出现在第几轮,得到 optima_x,这两个值就是图上要标红的点。 下面这段 Python 把迭代次数当横轴、MSE 当纵轴散点画出,并在最优处打红点、拉红色虚线。虽是 Python 而非 MQL5,但思路可直接搬到 MT5:把每轮回测的某指标误差写进数组,用类似的绘图逻辑在脚本里输出最优参数代次。 外汇与贵金属市场高风险,最优 MSE 仅代表样本内拟合较好,实盘迁移效果可能衰减,须用 Out-of-Sample 数据复核。
optima_y = min(algorithm_progress) optima_x = algorithm_progress.index(optima_y) inputs = np.arange(class="num">0,len(algorithm_progress)) class="macro">#Plot the performance of our optimization procedure plt.scatter(inputs,algorithm_progress) plt.plot(optima_x,optima_y,&class="macro">#x27;ro&class="macro">#x27;,class="type">class="kw">color=&class="macro">#x27;r&class="macro">#x27;) plt.axvline(x=optima_x,ls=&class="macro">#x27;--&class="macro">#x27;,class="type">class="kw">color=&class="macro">#x27;red&class="macro">#x27;) plt.axhline(y=optima_y,ls=&class="macro">#x27;--&class="macro">#x27;,class="type">class="kw">color=&class="macro">#x27;red&class="macro">#x27;) plt.xlabel("Iterations") plt.ylabel("Training MSE") plt.title("Minimizing Training Error")