重构经典策略(第五部分):基于USDZAR的多品种分析·进阶篇
(2/3)· 当原油黄金遇上兰特,哪些输入真的在驱动汇率,哪些只是噪音
用油金联动做交叉验证建模
把合并后的数据框索引重排,才能按时间顺序切分样本做交叉验证。这一步不做,后面所有误差对比都会因错位而失真。 预测变量分两组:一组只用 OHLC 四个字段,另一组叠加 US OIL 与 SA GOLD 的八个价格字段。目标变量统一叫 Target,两组共享同一标签,方便直接比误差。 数据用 RobustScaler 做鲁棒标准化,避免极端跳空把线性模型带偏。时间序列切分用 TimeSeriesSplit,n_splits=10 且带 gap=look_ahead,意味着训练集与测试集之间留出了前瞻空洞,防止用未来信息泄漏进训练。 模型池塞了 10 个:从线性回归、Lasso 到梯度提升、随机森林、各类集成树,再到两层 MLP(隐层 10×5 与 50×15,都开 early_stopping)。嵌套 for 循环先扫模型、再跑 10 折交叉验证,误差写进 normal_error 与 new_error 两个表。 图 6 与图 7 展示的是仅用 OHLC 时的 RMSE 分布;换成油金八字段后误差水平会出现可见下移,说明贵金属与原油的跨品种价格结构对目标有一定解释力。外汇与贵金属杠杆高、跳空频繁,这类相关性仅作概率参考,实盘前请在 MT5 用历史数据复跑确认。 让小布替你跑这套 把上面代码里的 oil_gold_predictors 换成你盯的货币对辅助品种,改 n_splits 和 gap,直接看 new_error 哪列最小,比盲选模型省事。
class="macro">#Reset the index merged_df.reset_index(inplace=True) class="macro">#Import the libraries we need from sklearn.linear_model class="kw">import LinearRegression from sklearn.linear_model class="kw">import Lasso from sklearn.ensemble class="kw">import GradientBoostingRegressor from sklearn.ensemble class="kw">import RandomForestRegressor from sklearn.ensemble class="kw">import AdaBoostRegressor from sklearn.ensemble class="kw">import BaggingRegressor from sklearn.neighbors class="kw">import KNeighborsRegressor from sklearn.svm class="kw">import LinearSVR from sklearn.neural_network class="kw">import MLPRegressor from sklearn.model_selection class="kw">import TimeSeriesSplit from sklearn.metrics class="kw">import root_mean_squared_error from sklearn.preprocessing class="kw">import RobustScaler class="macro">#Define the predictors normal_predictors = ["Open","High","Low","Close"] oil_gold_predictors = ["Open US OIL","High US OIL","Low US OIL","Close US OIL","Open SA GOLD","High SA GOLD","Low SA GOLD","Close SA GOLD"] target = "Target" class="macro">#Scale the data all_predictors = normal_predictors + oil_gold_predictors scaler = RobustScaler() scaled_data = pd.DataFrame(scaler.fit_transform(merged_df.loc[:,all_predictors]),columns=all_predictors,index=np.arange(class="num">0,merged_df.shape[class="num">0])) class="macro">#Now prepare the models models = [ LinearRegression(), Lasso(), GradientBoostingRegressor(), RandomForestRegressor(), AdaBoostRegressor(), BaggingRegressor(), KNeighborsRegressor(), LinearSVR(), MLPRegressor(hidden_layer_sizes=(class="num">10,class="num">5),early_stopping=True), MLPRegressor(hidden_layer_sizes=(class="num">50,class="num">15),early_stopping=True) ] columns = [ "Linear Regression", "Lasso", "Gradient Boosting Regressor", "Random Forest Regressor", "AdaBoost Regressor", "Bagging Regressor", "KNeighbors Regressor", "Linear SVR", "Small Neural Network", "Large Neural Network" ] class="macro">#Prepare the time-series split object splits = class="num">10 tscv = TimeSeriesSplit(n_splits=splits,gap=look_ahead) class="macro">#Prepare the dataframes to store the error levels normal_error = pd.DataFrame(columns=columns,index=np.arange(class="num">0,splits)) new_error = pd.DataFrame(columns=columns,index=np.arange(class="num">0,splits)) class="macro">#First we iterate over all the models we have available for j in np.arange(class="num">0,len(models)): class="macro">#Now we have to perform cross validation with each model for i,(train,test) in enumerate(tscv.split(scaled_data)): class="macro">#Get the data X_train = scaled_data.loc[train[class="num">0]:train[-class="num">1],oil_gold_predictors] X_test = scaled_data.loc[test[class="num">0]:test[-class="num">1],oil_gold_predictors] y_train = merged_df.loc[train[class="num">0]:train[-class="num">1],target] y_test = merged_df.loc[test[class="num">0]:test[-class="num">1],target] class="macro">#Fit the model models[j].fit(X_train,y_train)
◍ 用 RMSE 给模型误差定量
在 MT5 的 Python 联动环境里,模型上线前必须拿测试集算清楚误差,否则策略只是空中楼阁。上面这段直接把每个模型在 y_test 上的预测结果丢进 root_mean_squared_error,逐行写进 new_error 矩阵的第 i 行第 j 列。 normal_error 与 new_error 两个对象并列输出,方便横向比对基线模型和新模型在同一批样本上的 RMSE 差异。外汇与贵金属波动具有高风险,RMSE 降低只代表回测样本内拟合更紧,实盘仍可能失效,需以概率视角看待。 开 MT5 接 Python 跑一遍这段,若 new_error 明显小于 normal_error,说明该次调参或换模型在测试集上有概率优势,可继续做样本外验证。
class="macro">#Measure the error new_error.iloc[i,j] = root_mean_squared_error(y_test,models[j].predict(X_test)) normal_error new_error
「换预测变量后回归模型误差漂移」
把石油与黄金价格作为常规预测变量时,各回归模型的平均 normal error 差异极大:LinearSVR 仅 0.01086,LinearRegression 0.01136,而两层 MLP(10,5)冲到 2.55875,MLP(50,15)也有 1.05444。 改用新预测变量后,线性类模型普遍变差:LinearRegression 误差从 0.01136 升到 0.13404,相对变化 -1079.56%;LinearSVR 从 0.01086 升到 0.15464,变化 -1324.41%。树集成类反而略稳,RandomForest 从 0.03616 到 0.08957(约 -147.68%),AdaBoost 从 0.03748 到 0.08659(约 -131.00%)。 MLP(50,15)在新变量下误差降到 0.69582,变化 +34.01%,是唯一误差缩小的网络结构;但(10,5)版本误差扩大到 3.89709,变化 -52.30%。外汇与贵金属具有高杠杆与跳空风险,这类回测误差仅反映样本内表现,实盘可能显著偏离。 下面这段 Python 循环就是算出上面三组数字的原始逻辑,在 MT5 外接 Jupyter 里跑一遍就能复现:
class="macro">#Let&class="macro">#x27;s see our average performance on the normal dataset for i in(np.arange(class="num">0,normal_error.shape[class="num">0])): print(f"{models[i]} normal error {((normal_error.iloc[:,i].mean()))}") class="macro">#Let&class="macro">#x27;s see our average performance on the new dataset for i in(np.arange(class="num">0,normal_error.shape[class="num">0])): print(f"{models[i]} normal error {((new_error.iloc[:,i].mean()))}") class="macro">#Let&class="macro">#x27;s see our average performance on the normal dataset for i in(np.arange(class="num">0,normal_error.shape[class="num">0])): print(f"{models[i]} changed by {((normal_error.iloc[:,i].mean()-new_error.iloc[:,i].mean()))/normal_error.iloc[:,i].mean()}%")
class="macro">#Let&class="macro">#x27;s see our average performance on the normal dataset for i in(np.arange(class="num">0,normal_error.shape[class="num">0])): print(f"{models[i]} normal error {((normal_error.iloc[:,i].mean()))}") class="macro">#Let&class="macro">#x27;s see our average performance on the new dataset for i in(np.arange(class="num">0,normal_error.shape[class="num">0])): print(f"{models[i]} normal error {((new_error.iloc[:,i].mean()))}") class="macro">#Let&class="macro">#x27;s see our average performance on the normal dataset for i in(np.arange(class="num">0,normal_error.shape[class="num">0])): print(f"{models[i]} changed by {((normal_error.iloc[:,i].mean()-new_error.iloc[:,i].mean()))/normal_error.iloc[:,i].mean()}%")
K近邻模型挑出了哪三个特征
在石油与黄金作为预测变量的那轮测试里,KNeighbors 回归器是表现最好的模型。但当我们用逐步向前选择法(SequentialFeatureSelector)重新让它从全部预测变量里挑重点时,结果有点反直觉。 算法最终只保留了 3 个特征:USDZAR 自身的开盘价、最低价和收盘价,输出元组为 ('Open', 'Low', 'Close')。也就是说,外生的石油、黄金价格在这一步筛选中没被留下,对预测 USDZAR 目标变量的边际贡献可能偏低。 下面这段是可直接在 MT5 外接 Python 环境跑的 mlxtend 选择器代码,用 10 折交叉验证、负均方误差打分,让模型自己向前挑特征。 别把外生变量当救命稻草 原油和黄金常被宣传成汇率的领先指标,但这套逐步选择只认 USDZAR 自己的 OHLC,说明样本内关系并不稳。外汇与贵金属杠杆高、跳空频繁,实盘前务必用你自己的历史数据重跑一遍再下结论。
class="macro">#Our best performing model was the KNeighbors Regressor class="macro">#Let us perform feature selection to test how stable the relationship is from mlxtend.feature_selection class="kw">import SequentialFeatureSelector as SFS class="macro">#Let us select our best model model = KNeighborsRegressor() class="macro">#Create the sequential selector object sfs1 = SFS( model, k_features=(class="num">1,len(all_predictors)), forward=True, scoring="neg_mean_squared_error", cv=class="num">10, n_jobs=-class="num">1 ) class="macro">#Fit the sequential selector sfs1 = sfs1.fit(scaled_data.loc[:,all_predictors],merged_df.loc[:,"Target"]) class="macro">#Now let us see which predictors were selected sfs1.k_feature_names_
◍ 用随机搜索给KNN回归挑参数
参数组合爆炸时,穷举网格不现实。scikit-learn 的 RandomizedSearchCV 通过随机采样响应面,在精度与算力之间做权衡,迭代次数 n_iter 就是那个调节阀。 本例把数据对半切:前半 scaler 后的 scaled_data 与 merged_df 的 Target 列作训练集,后半作测试集,避免调参窥探未来。 估计器传 KNeighborsRegressor(n_jobs=-1) 占满核,参数空间覆盖 n_neighbors 12 档、weights 两种、leaf_size 11 档、algorithm 两类、p 距离阶 8 档。设 cv=5、n_iter=500、scoring='neg_mean_squared_error',跑完得到的最优组合是 {'weights':'distance','p':1,'n_neighbors':4,'leaf_size':15,'algorithm':'ball_tree'}。 随机搜索本身有随机性,同份代码重跑可能拿到略不同的结果;外汇与贵金属信号建模属高风险,回测优参不承诺样本外表现。
导入 scikit-learn 模块。 class="macro">#Now we will load the libraries we need from sklearn.model_selection class="kw">import RandomizedSearchCV class="macro">#Let us see if we can tune the model class="macro">#First we will create train test splits train_X = scaled_data.loc[:(scaled_data.shape[class="num">0]class=class="str">"cmt">//class="num">2),:] train_y = merged_df.loc[:(merged_df.shape[class="num">0]class=class="str">"cmt">//class="num">2),"Target"] test_X = scaled_data.loc[(scaled_data.shape[class="num">0]class=class="str">"cmt">//class="num">2):,:] test_y = merged_df.loc[(merged_df.shape[class="num">0]class=class="str">"cmt">//class="num">2):,"Target"] class="macro">#Create the tuning object rs = RandomizedSearchCV(KNeighborsRegressor(n_jobs=-class="num">1),{ "n_neighbors": [class="num">1,class="num">2,class="num">3,class="num">4,class="num">5,class="num">8,class="num">10,class="num">16,class="num">20,class="num">30,class="num">60,class="num">100], "weights":["uniform","distance"], "leaf_size":[class="num">1,class="num">2,class="num">3,class="num">4,class="num">5,class="num">10,class="num">15,class="num">20,class="num">40,class="num">60,class="num">90], "algorithm":["ball_tree","kd_tree"], "p":[class="num">1,class="num">2,class="num">3,class="num">4,class="num">5,class="num">6,class="num">7,class="num">8] },cv=class="num">5,n_iter=class="num">500,return_train_score=False,scoring="neg_mean_squared_error") class="macro">#Let&class="macro">#x27;s perform the hyperparameter tuning rs.fit(train_X,train_y) class="macro">#Let&class="macro">#x27;s store the results from our hyperparameter tuning tuning_results = pd.DataFrame(rs.cv_results_) tuning_results.loc[:,["param_n_neighbors","param_weights","param_leaf_size","param_algorithm","param_p","mean_test_score"]].sort_values(by="mean_test_score",ascending=False) class="macro">#The best parameters we came across rs.best_params_
「用验证集拆穿过拟合假象」
比较两个模型要在同一训练集上跑,才有可比性。默认 KNN 与按搜索最优参数定制的 KNN 都吃同一份 train_X / train_y,再拿到验证集上算误差。 默认模型验证集 RMSE 为 0.06633,定制模型仅 0.04335。定制版误差更低,说明调参没有把训练噪声当规律背下来,过拟合信号未触发。 下面这段是直接可贴进 Python 环境的比对代码,跑完你就能复现上面两个数。注意:外汇与贵金属行情序列非平稳,机器学习模型在历史集上不过拟合,不代表实盘概率优势必然延续,杠杆品种高风险。
class="macro">#Create instances of the class="kw">default model and the custmoized model default_model = KNeighborsRegressor() customized_model = KNeighborsRegressor(p=rs.best_params_["p"],weights=rs.best_params_["weights"],n_neighbors=rs.best_params_["n_neighbors"],leaf_size=rs.best_params_["leaf_size"],algorithm=rs.best_params_["algorithm"]) class="macro">#Measure the accuracy of the class="kw">default model default_model.fit(train_X,train_y) root_mean_squared_error(test_y,default_model.predict(test_X)) class="macro">#Measure the accuracy of the customized model customized_model.fit(train_X,train_y) root_mean_squared_error(test_y,customized_model.predict(test_X))