重思经典策略(第八部分):基于美元兑加元(USDCAD)探讨外汇市场与贵金属·进阶篇
📘

重思经典策略(第八部分):基于美元兑加元(USDCAD)探讨外汇市场与贵金属·进阶篇

第 2/3 篇

贵金属与美加的关联在图里散成一锅

把黄金、钯金和美元兑加元(USDCAD)拉到一张相关性热力图上,能看出两种贵金属跟 USDCAD 存在显著强相关,这和两国 GDP 里金属权重的基本面直觉对得上。但真拿它去预测 USDCAD 收盘价,表现并没有变好,说明相关不等于可交易。 接着做了分类图:左列是金或钯上涨日、右列是下跌日,点按 USDCAD 涨(橙)跌(蓝)着色。两列里橙蓝混在一起,暗示 USDCAD 的波动跟这两类金属的单边运动可能相互独立。散点图换钯金对 USDCAD、再换黄金对 USDCAD,都没有出现能用的清晰边界;金对钯的散点用 USDCAD 变化标色,同样无改进。 怀疑是变量看得太少把关系藏住了,于是堆了个 3D 散点:X轴 USDCAD、Y轴 XAUUSD、Z轴 XPDUSD,取前 1100 行数据。图里有些簇状聚集,但分离度很差,基本只是把前面已知结论重说一遍。外汇与贵金属波动剧烈,这类可视化结论仅作探索,实盘前须在 MT5 用真实 tick 复核。

MQL5 / C++
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 逼近线性回归。贵金属加入后线性优势会收窄,这是数据异构带来的正常现象。

MQL5 / C++
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 为对应列名列表。

MQL5 / C++
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 损失,在外汇小时线回归预测里可能比默认配置更跟得上价格突变。 外汇与贵金属波动受突发消息影响大,任何超参优化都只是提升历史样本上的拟合效率,实盘仍属高风险,参数不能直接当方向信号。

MQL5 / C++
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 环境就能复算。

MQL5 / C++
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。外汇与贵金属波动剧烈,标准化只是预处理,不预示任何方向,仓位仍需按高风险品种控。

MQL5 / C++
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")

常见问题

两者在原始价格尺度上噪声大、相位常错开,直接叠加看图容易误判。建议先做平稳化处理(如取对数收益)再比相关性,别裸图硬凑。
用 sklearn 的 Pipeline 配 KFold,把线性SVR、随机森林等塞进循环统一评分。重点看各模型在验证集上的稳定度,而非单次准确率。
小布可在品种页直接跑互信息与SHAP类诊断,标出分歧大的特征供你人工复核。把重复计算交给它,你只看结论冲突点。
互信息抓非线性依赖,SHAP偏局部边际效应,分歧常因特征共线或样本区间短。用五折交叉验证拆开看稳定性再定去留。
把拟合后的 scaler 参数(均值、缩放比)落盘为本地文件,下游加载时直接套用。避免每次重算导致信号偏移,保证复盘一致。