重思经典策略(第八部分):基于美元兑加元(USDCAD)探讨外汇市场与贵金属·进阶篇
贵金属与美加的关联在图里散成一锅
把黄金、钯金和美元兑加元(USDCAD)拉到一张相关性热力图上,能看出两种贵金属跟 USDCAD 存在显著强相关,这和两国 GDP 里金属权重的基本面直觉对得上。但真拿它去预测 USDCAD 收盘价,表现并没有变好,说明相关不等于可交易。 接着做了分类图:左列是金或钯上涨日、右列是下跌日,点按 USDCAD 涨(橙)跌(蓝)着色。两列里橙蓝混在一起,暗示 USDCAD 的波动跟这两类金属的单边运动可能相互独立。散点图换钯金对 USDCAD、再换黄金对 USDCAD,都没有出现能用的清晰边界;金对钯的散点用 USDCAD 变化标色,同样无改进。 怀疑是变量看得太少把关系藏住了,于是堆了个 3D 散点:X轴 USDCAD、Y轴 XAUUSD、Z轴 XPDUSD,取前 1100 行数据。图里有些簇状聚集,但分离度很差,基本只是把前面已知结论重说一遍。外汇与贵金属波动剧烈,这类可视化结论仅作探索,实盘前须在 MT5 用真实 tick 复核。
class="macro">#Correlation heatmap fig , ax = plt.subplots(figsize=(class="num">7,class="num">7)) sns.heatmap(merged_data.loc[:,predictors].corr(),annot=True,ax=ax) class="macro">#Let&class="macro">#x27;s create categorical plots sns.catplot(data=merged_data,x="XAU Target",y="Close",hue="Binary Target") class="macro">#Let&class="macro">#x27;s create categorical plots sns.catplot(data=merged_data,x="XPD Target",y="Close",hue="Binary Target") class="macro">#Let&class="macro">#x27;s visualize scatter plots sns.scatterplot(data=merged_data,x="Close XPD",y="Close",hue="Binary Target") class="macro">#Let&class="macro">#x27;s visualize scatter plots sns.scatterplot(data=merged_data,x="Close XAU",y="Close",hue="Binary Target") class="macro">#Let&class="macro">#x27;s visualize scatter plots sns.scatterplot(data=merged_data,x="Close XPD",y="Close XAU",hue="Binary Target") class="macro">#Visualizing 3D data fig = plt.figure(figsize=(class="num">7,class="num">7)) ax = fig.add_subplot(class="num">111,projection=&class="macro">#x27;3d&class="macro">#x27;) colors = [&class="macro">#x27;blue&class="macro">#x27; if movement == class="num">0 else &class="macro">#x27;orange&class="macro">#x27; for movement in merged_data.loc[class="num">0:class="num">1100,"Binary Target"]] ax.scatter(merged_data.loc[class="num">0:class="num">1100,"Close"],merged_data.loc[class="num">0:class="num">1100,"Close XAU"],merged_data.loc[class="num">0:class="num">1100,"Close XPD"],c=colors) class="macro">#Set labels ax.set_xlabel(&class="macro">#x27;USDCAD&class="macro">#x27;) ax.set_ylabel(&class="macro">#x27;XAUUSD&class="macro">#x27;) ax.set_zlabel(&class="macro">#x27;XPDUSD&class="macro">#x27;)
「用 sklearn 跑一遍多模型交叉验证」
先把标准库导进来,确认环境是 Sklearn 1.4.1.post1。外汇与贵金属预测属于高风险场景,模型输出只能作为概率参考,不能直接当入场信号。 数据进模型前必须做缩放,这里用 RobustScaler 处理 merged_data 里的预测列,避免量纲差异干扰训练。随后按时间序列切分:train_test_split 设 shuffle=False、test_size=0.5,前一半训练后一半验证,防止未来信息泄漏。 建模环节用 TimeSeriesSplit(gap=look_ahead, n_splits=5) 做 5 折时序交叉验证,模型塞进列表迭代:线性回归、随机森林、梯度提升、Bagging、LinearSVR、K近邻、MLP 神经网络(隐藏层 100×10)。三个 DataFrame 分别存 OHLC、加贵金属、全量数据的误差。 误差均值很说明问题——线性回归 0.000523,LinearSVR 0.000653,梯度提升 0.001227,随机森林 0.001333,Bagging 0.001343,K近邻 0.001837,神经网络直接飙到 0.114188。简单线性结构在美元兑加元预测上明显占优,神经网络在这种小样本金融序列上容易崩。 开 MT5 把历史数据按同样预测列导出,复跑这段脚本,重点看自己样本里 LinearSVR 能不能靠调 C 和 epsilon 逼近线性回归。贵金属加入后线性优势会收窄,这是数据异构带来的正常现象。
class="macro">#Modelling the data class="kw">import sklearn from sklearn.model_selection class="kw">import train_test_split from sklearn.preprocessing class="kw">import RobustScaler class="macro">#Print library version print(f"Sklearn version {sklearn.__version__}") class="macro">#Scale the data scaled_data = pd.DataFrame(RobustScaler().fit_transform(merged_data.loc[:,predictors]),columns=predictors) class="macro">#Split the data train_X,test_X,train_y,test_y = train_test_split(scaled_data,merged_data.loc[:,"Target"],shuffle=False,test_size=class="num">0.5) class="macro">#Preparing to model the data from sklearn.model_selection class="kw">import TimeSeriesSplit from sklearn.linear_model class="kw">import LinearRegression from sklearn.ensemble class="kw">import RandomForestRegressor , GradientBoostingRegressor , BaggingRegressor from sklearn.svm class="kw">import LinearSVR from sklearn.neighbors class="kw">import KNeighborsRegressor from sklearn.neural_network class="kw">import MLPRegressor from sklearn.metrics class="kw">import root_mean_squared_error class="macro">#Create the time series split object tscv = TimeSeriesSplit(gap=look_ahead,n_splits=class="num">5) class="macro">#Create a list of models models = [ LinearRegression(), RandomForestRegressor(), GradientBoostingRegressor(), BaggingRegressor(), LinearSVR(), KNeighborsRegressor(), MLPRegressor(hidden_layer_sizes=(class="num">100,class="num">10)) ] class="macro">#List of models columns = [ "Linear Regression", "Random Forest", "Gradient Boost", "Bagging", "Linear SVR", "K-Neighbors", "Neural Network" ] class="macro">#Create a dataframe to store our error metrics ohlc_error = pd.DataFrame(columns=columns,index=np.arange(class="num">0,class="num">5)) new_error = pd.DataFrame(columns=columns,index=np.arange(class="num">0,class="num">5)) all_error = pd.DataFrame(columns=columns,index=np.arange(class="num">0,class="num">5)) class="macro">#Setting the current predictors current_predictors = predictors class="macro">#Perform cross validation for j in np.arange(class="num">0,len(models)): model = models[j] for i,(train,test) in enumerate(tscv.split(train_X)): model.fit(train_X.loc[train[class="num">0]:train[-class="num">1],current_predictors],train_y.loc[train[class="num">0]:train[-class="num">1]]) all_error.iloc[i,j] = root_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],current_predictors])) ohlc_error ohlc_error.plot() fig = plt.figure(figsize=(class="num">5,class="num">5)) plt.boxplot(ohlc_error) new_error new_error.plot() fig = plt.figure(figsize=(class="num">5,class="num">5)) plt.boxplot(new_error) all_error all_error.plot() fig = plt.figure(figsize=(class="num">5,class="num">5)) plt.boxplot(all_error) all_error.mean()
◍ 互信息和SHAP在贵金属特征上的分歧
动手优化模型前,先搞清楚手里哪些特征真有信息量。互信息(MI)用对数尺度衡量已知一个预测因子能挤出多少关于目标值的确定性,实战里 MI 分数超过 2 极少见,多数特征落在 0~1 区间。 跑一遍 mutual_info_regression 后画出的条形图显示,美元兑加元报价的相关特征,信息量可能压过了贵金属市场自身的所有数据;交叉验证里只喂美加报价的线性模型,误差最低,这一点和 MI 结论对得上。外汇与贵金属杠杆高、跳空频繁,单看这类分数不能直接下单,只能当特征筛选的参考。 换 SHAP 解释器再算一遍全局重要性,结果和 MI 评估不一致——我们在前文聊过这种背离,根源是黑箱解释方法对共线特征和尺度敏感。两种口径虽打架,但都指向一个事实:贵金属序列里确实掺着有用信号,只是占比和优先级不像直觉那么高。 下面这段是可直接丢进 Jupyter 跑的 sklearn 代码,注意 train_X 需是 12 列预测因子的 DataFrame,predictors 为对应列名列表。
class="macro">#Mutual information score from sklearn.feature_selection class="kw">import mutual_info_regression class="macro">#Prepare the data for plotting mi = mutual_info_regression(train_X,train_y) mi = mi.reshape(class="num">1,class="num">12) mi_scores = pd.DataFrame(mi,columns=predictors) class="macro">#Prepare the data for plotting mi = mutual_info_regression(train_X,train_y) mi = mi.reshape(class="num">1,class="num">12) mi_scores = pd.DataFrame(mi,columns=predictors) class="macro">#Plot the scores mi_scores.plot.bar() class="macro">#The Linear SVR appears to be performing second best class="kw">import shap class="macro">#Initialize the model model = LinearSVR() model.fit(train_X,train_y) class="macro">#Compute SHAP values explainer = shap.Explainer(model,train_X) explanations = explainer(train_X) class="macro">#Plot SHAP values shap.plots.violin(explanations)
用随机搜索给线性SVR找最优超参
默认 LinearSVR 在 MT5 导出的行情特征上往往不是最优解,手动调参耗时且容易过拟合。用 RandomizedSearchCV 在训练集上做随机采样搜索,能在有限算力内摸到泛化更好的参数组合。 这里把 epsilon、tol、C、loss 四个维度各给了一组候选值,搜索总迭代次数设成 1000 次,交叉验证折数 cv=5,评分用负均方误差(neg_mean_squared_error),也就是验证误差越低的分越高。n_jobs=-1 会占满所有 CPU 核心并行跑,笔记本上能明显缩短等待时间。 实际跑完返回的最佳参数是 {'tol': 1e-05, 'loss': 'squared_epsilon_insensitive', 'epsilon': 0, 'C': 10000}。C 拉到上限说明模型倾向低偏差、对训练误差容忍度极低,配合 squared_epsilon_insensitive 损失,在外汇小时线回归预测里可能比默认配置更跟得上价格突变。 外汇与贵金属波动受突发消息影响大,任何超参优化都只是提升历史样本上的拟合效率,实盘仍属高风险,参数不能直接当方向信号。
class="macro">#Parameter tuning from sklearn.model_selection class="kw">import RandomizedSearchCV class="macro">#Reinitialize the model model = LinearSVR() class="macro">#Define the tuner tuner = RandomizedSearchCV( model, { "epsilon" : [class="num">0,class="num">10,class="num">100,class="num">1000], "tol":[class="num">0.01,class="num">0.001,class="num">0.0001,class="num">0.00001,class="num">0.0000001], "C":[class="num">1,class="num">10,class="num">100,class="num">1000,class="num">10000], "loss":[&class="macro">#x27;epsilon_insensitive&class="macro">#x27;, &class="macro">#x27;squared_epsilon_insensitive&class="macro">#x27;] }, n_iter=class="num">1000, cv=class="num">5, n_jobs=-class="num">1, scoring="neg_mean_squared_error" ) class="macro">#Let&class="macro">#x27;s fit the tuner tuner_results = tuner.fit(train_X,train_y) class="macro">#Let&class="macro">#x27;s see the best parameters we found tuner_results.best_params_
「用五折交叉验证揪出过拟合」
验证模型是不是死记硬背了样本,最直白的做法是重置索引后做交叉验证,而不是只看训练集表现。把测试集拆成 5 折,每折轮换当验证集,记录三种模型的均方根误差,才有可比性。 跑出来的误差矩阵里,线性回归五折误差分别为 0.000598、0.000472、0.000318、0.000341、0.00043,LSVR 为 0.000542、0.000573、0.000451、0.000366、0.000839,定制 LSVR 为 0.000743、0.000722、0.000333、0.000499、0.00043。取全折平均,线性回归 0.000432 最低,LSVR 0.000554,定制 LSVR 0.000545,线性模型不仅没被更复杂的对手反超,还压过了默认设定。 外汇与贵金属行情具有高杠杆高风险,这类回测误差仅反映样本内与交叉验证表现,实盘换时段可能漂移,结论只能当作概率倾向而非保证。下面这段脚本就是上述流程的落地版,复制进 MT5 关联的 Python 环境就能复算。
class="macro">#Testing for overfitting benchmark = LinearRegression() default_lsvr = LinearSVR() customized_lsvr = LinearSVR(tol=class="num">1e-05,loss=&class="macro">#x27;squared_epsilon_insensitive&class="macro">#x27;,epsilon=class="num">0,C=class="num">10000) class="macro">#Reset the indexes test_y = test_y.reset_index() test_X = test_X.reset_index() class="macro">#Format the data test_y = test_y.loc[:,"Target"] test_X = test_X.loc[:,predictors] class="macro">#Create dataframes to store our error levels test_error = pd.DataFrame(columns=["Linear Regression","LSVR","Customized LSVR"],index=[class="num">0,class="num">1,class="num">2,class="num">3,class="num">4]) class="macro">#Fit the models on the training set benchmark.fit(train_X,train_y) default_lsvr.fit(train_X,train_y) customized_lsvr.fit(train_X,train_y) models = [benchmark,default_lsvr,customized_lsvr] for j in np.arange(class="num">0,len(models)): model = models[j] for i,(train,test) in enumerate(tscv.split(test_X)): model.fit(test_X.loc[train[class="num">0]:train[-class="num">1],:],test_y.loc[train[class="num">0]:train[-class="num">1]]) test_error.iloc[i,j] = root_mean_squared_error(test_y.loc[test[class="num">0]:test[-class="num">1]],model.predict(test_X.loc[test[class="num">0]:test[-class="num">1],:])) test_error class="macro">#Let&class="macro">#x27;s calculate our mean performances test_error.mean() class="macro">#Let&class="macro">#x27;s visualize our error test_error.plot() class="macro">#Create a boxplot of the error sns.boxplot(data=test_error)
◍ 把缩放因子落盘供 MT5 复用
导出 ONNX 之前,先把特征做标准化:减均值、除标准差。这一步不是可选项,因为推理端 MQL5 必须能用同一组参数还原训练时的数值分布,否则模型输入错位,信号会偏得离谱。 做法很直接——建一个只有两行(mean、standard deviation)的数据帧,列名对应每个预测变量。循环里把每列的均值和标准差写进去,顺手完成标准化变换,最后把整张表存成 CSV,路径放到 MT5 的 MQL5/Files 下,EA 加载时直接读。 注意路径里是 wine 映射的盘符,实盘机若是原生 Windows 要改成对应 Program Files 下的 MQL5/Files。外汇与贵金属波动剧烈,标准化只是预处理,不预示任何方向,仓位仍需按高风险品种控。
class="macro">#Let&class="macro">#x27;s scale our data scaling_factors = pd.DataFrame(columns=predictors,index=[&class="macro">#x27;mean&class="macro">#x27;,&class="macro">#x27;standard deviation&class="macro">#x27;]) X = merged_data.loc[:,predictors] y = merged_data.loc[:,"Target"] class="macro">#Let&class="macro">#x27;s fill each column for i in np.arange(class="num">0,len(predictors)): scaling_factors.iloc[class="num">0,i] = X.iloc[:,i].mean() scaling_factors.iloc[class="num">1,i] = X.iloc[:,i].std() X.iloc[:,i] = ( ( X.iloc[:,i] - scaling_factors.iloc[class="num">0,i] ) / scaling_factors.iloc[class="num">1,i]) class="macro">#Save the scaling factors as a CSV scaling_factors.to_csv("/home/volatily/.wine/drive_c/Program Files/MetaTrader class="num">5/MQL5/Files/usd_cad_xau_xpd_scaling_factors.csv")