重构经典策略(第五部分):基于USDZAR的多品种分析·进阶篇
📊

重构经典策略(第五部分):基于USDZAR的多品种分析·进阶篇

(2/3)· 当原油黄金遇上兰特,哪些输入真的在驱动汇率,哪些只是噪音

案例拆解 第 2/3 篇
把相关性当成因果,是跨品种策略最常见的坑。USDZAR 和油价同涨同跌,不代表油价能预测兰特——本篇用实验数据拆开看。

用油金联动做交叉验证建模

把合并后的数据框索引重排,才能按时间顺序切分样本做交叉验证。这一步不做,后面所有误差对比都会因错位而失真。 预测变量分两组:一组只用 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 哪列最小,比盲选模型省事。

MQL5 / C++
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,说明该次调参或换模型在测试集上有概率优势,可继续做样本外验证。

MQL5 / C++
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 里跑一遍就能复现:

MQL5 / C++
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()}%")
逐行拆解:第一行注释说明要输出常规数据集上的平均误差;第一个 for 循环遍历 normal_error 的列,用 f-string 打印每个模型名与对应列均值。第二个 for 循环结构相同,但读的是 new_error,即新预测变量下的平均误差。第三个循环计算两类误差的相对变化率,公式为(旧均值减新均值)除以旧均值再转百分比,直接暴露变量替换带来的漂移方向。

MQL5 / C++
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,说明样本内关系并不稳。外汇与贵金属杠杆高、跳空频繁,实盘前务必用你自己的历史数据重跑一遍再下结论。

MQL5 / C++
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'}。 随机搜索本身有随机性,同份代码重跑可能拿到略不同的结果;外汇与贵金属信号建模属高风险,回测优参不承诺样本外表现。

MQL5 / C++
导入 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 环境的比对代码,跑完你就能复现上面两个数。注意:外汇与贵金属行情序列非平稳,机器学习模型在历史集上不过拟合,不代表实盘概率优势必然延续,杠杆品种高风险。

MQL5 / C++
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))
把跨品种诊断交给小布
这些相关性散点与模型误差对比,小布盯盘的 AIGC 已内置,打开 USDZAR 品种页即可看到实时篮子权重,你只管判断要不要跟。

常见问题

可用 MQL5 脚本导出 OHLC 与商品价差序列,做标准化后算皮尔逊系数;本篇实验显示原油相关强于黄金。
可以,小布盯盘内置了跨品种权重面板,自动拉取终端数据并标注偏离带,省去手写导出脚本。
金融时序有前后依赖,打乱会泄漏未来信息;保留时间顺序才能估出真实样本外误差。
说明商品价组合呈局部非线性结构,但整体误差仍高于常规 OHLC 线性回归,倾向优先用报价本身建模。