重塑经典策略(第六部分):多时间框架分析·进阶篇
◍ 把 EURUSD 行情翻成可训练序列
MT5 导出的 EURUSD 历史默认按时间倒序排列, newest 在前、oldest 在后。直接喂给模型会让时序逻辑反掉,所以第一步必须原地反转,让最老的数据排在第 0 行。
预测窗口设成 20 根 K 线,用 Close.shift(-20) 把 20 根之后的收盘价挪到当前行当标签。这样每一行样本对应的监督目标就是「20 根后 EURUSD 可能的收盘价位」,属于典型回归设定,不涉及方向断言。
反转后务必 reset_index,否则原索引错位会让后续切片踩坑。最后 dropna 干掉末尾因 shift 产生的空标签行——EURUSD 高频数据里这类缺失行一般不超 20 条,但留着必报维度错。外汇及贵金属杠杆高,样本标签仅反映历史统计关系,实盘仍可能触发极端滑点。
class="kw">import pandas as pd class="kw">import numpy as np class="macro">#Let&class="macro">#x27;s format the data so it starts with the oldest date market_data = market_data[::-class="num">1] market_data.reset_index(inplace=True) look_ahead = class="num">20 class="macro">#Let&class="macro">#x27;s label the data market_data["Target"] = market_data["Close"].shift(-look_ahead) class="macro">#Drop rows with missing values market_data.dropna(inplace=True)
跨周期相关性与互信息实测
先跑一遍基础相关性矩阵,覆盖数据集中除首尾列之外的所有行情字段。结果呈现的是中等偏弱的线性相关,但弱相关本身不证明变量间存在真实驱动关系,这一点在外汇与贵金属高频数据里尤其要警惕,避免过度拟合噪声。 互信息(MI)用来衡量某个特征对预测目标的解释潜力。以开盘价作基准,它对目标的 MI 得分为 1.4954735008645943,属于强解释力特征。 换到跨周期价格变化:M5 框架上的价格变化对 M1 未来价格的 MI 得分仅 0.16417018723996168,M15 框架上的得分是 0.17449824184274743。两个值都比开盘价低一个数量级,说明不同时间框架的价格水平之间大概率不存在可依赖的预测关系,或者我们揭示出的联系在统计上接近无意义。贵金属与外汇跨周期套利模型若依赖此类弱 MI 特征,实盘风险较高。 下面这段代码可直接在装了 sklearn 的 Python 环境里复现上述结论,先算相关再逐周期打 MI 分。
class="macro">#Let&class="macro">#x27;s see if there is any correlation market_data.iloc[:,class="num">2:-class="num">1].corr() from sklearn.feature_selection class="kw">import mutual_info_regression class="macro">#MI Score for the Open price print(f&class="macro">#x27;Open price has MI score: {mutual_info_regression(market_data.loc[:,["Open"]],market_data.loc[:,"Target"])[class="num">0]}&class="macro">#x27;) class="macro">#MI Score for the M5 change in price print(f&class="macro">#x27;M5 change in price has MI score: {mutual_info_regression(market_data.loc[:,["M5"]],market_data.loc[:,"Target"])[class="num">0]}&class="macro">#x27;) class="macro">#MI Score for the M15 change in price print(f&class="macro">#x27;M15 change in price has MI score: {mutual_info_regression(market_data.loc[:,["M15"]],market_data.loc[:,"Target"])[class="num">0]}&class="macro">#x27;)
「交叉验证里挑出能调参的回归器」
把预测变量和目标先定清楚:基础组只用 Open/High/Low/Close 四个字段,多周期组叠了 M5 到 D1 的五档时间框架,全量组则是两者相加,目标列统一叫 Target。代码里用 RobustScaler 对全量预测变量做稳健缩放,避免极端影线把模型带偏。 常规 OHLC 输入下,线性回归误差仅 0.00042256,明显优于 SGD 的 0.03243 和小型神经网络的 0.07741;但线性模型没有可供搜索的超参数空间。梯度提升回归器(GBR)在常规输入误差 0.00065887,排第二,且支持超参调优,因此被选作后续主攻对象。 切换成多周期输入后,线性回归误差升到 0.00191364,GBR 微升至 0.00191807,而 AdaBoost 纹丝不动停在 0.00071596。全量预测变量时,线性 SVR 误差 0.00064575 反超部分树模型,但 GBR 仍稳定在 0.00069440 附近。 三层交叉验证都用 TimeSeriesSplit 划 10 折、gap 设为 look_ahead 防止未来函数泄漏。外汇与贵金属价格序列非平稳,这类回测误差仅反映样本内概率倾向,实盘需以 MT5 导出数据重跑确认。
class="macro">#Let&class="macro">#x27;s define our predictors and our target ohlc_predictors = [ "Open", "High", "Low", "Close" ] time_frame_predictors = [ "M5", "M15", "M30", "H1", "D1" ] all_predictors = ohlc_predictors + time_frame_predictors target = "Target" class="macro">#Import the libraries we need from sklearn.linear_model class="kw">import LinearRegression from sklearn.linear_model class="kw">import SGDRegressor from sklearn.ensemble class="kw">import RandomForestRegressor from sklearn.ensemble class="kw">import BaggingRegressor from sklearn.ensemble class="kw">import GradientBoostingRegressor from sklearn.ensemble class="kw">import AdaBoostRegressor 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,RandomizedSearchCV from sklearn.metrics class="kw">import root_mean_squared_error from sklearn.preprocessing class="kw">import RobustScaler class="macro">#Define the time series split object gap = look_ahead splits = class="num">10 class="macro">#Store our models in a list models = [ LinearRegression(), SGDRegressor(), RandomForestRegressor(), BaggingRegressor(), GradientBoostingRegressor(), AdaBoostRegressor(), KNeighborsRegressor(), LinearSVR(), MLPRegressor(hidden_layer_sizes=(class="num">10,class="num">4),early_stopping=True), MLPRegressor(hidden_layer_sizes=(class="num">100,class="num">20),early_stopping=True) ] class="macro">#Create a list of column titles for each model columns = [ "Linear Regression", "SGD Regressor", "Random Forest Regressor", "Bagging Regressor", "Gradient Boosting Regressor", "AdaBoost Regressor", "K Neighbors Regressor", "Linear SVR", "Small Neural Network", "Large Neurla Network" ] class="macro">#Create data frames to store our accuracy ohlc_accuracy = pd.DataFrame(index=np.arange(class="num">0,class="num">10),columns=columns) multiple_time_frame_accuracy = pd.DataFrame(index=np.arange(class="num">0,class="num">10),columns=columns) all_accuracy = pd.DataFrame(index=np.arange(class="num">0,class="num">10),columns=columns) class="macro">#Preparing to perform cross validation current_predictors = all_predictors scaled_data = pd.DataFrame(RobustScaler().fit_transform(market_data.loc[:,all_predictors]),columns=all_predictors)
◍ 滚动窗口里给每个模型算误差
这段代码干的事很直接:用 TimeSeriesSplit 把按时间排好的数据切成多段训练/测试对,gap 参数控制两段之间留出的空白期,避免未来信息泄漏。n_splits 决定切几折,外汇和贵金属这种强时序品种绝不能随机打乱,否则回测结果会虚高。 外层循环遍历 models 列表里的每一个模型,内层循环按 tscv.split 给出的索引切出 train 和 test。注意 train_X 取自 scaled_data 的当前特征列,train_y 却来自未缩放的 market_data 目标列——特征归一、标签保持原始量纲,是常见处理方式。 每次切分都用 model.fit 训完,再用 root_mean_squared_error 算测试集预测误差,写进 all_accuracy 的第 j 行第 i 列。跑完之后,三段打印分别输出 ohlc_accuracy、multiple_time_frame_accuracy、all_accuracy 各列的平均误差,columns[i] 对应模型名。 在 MT5 里验证时,你可以把 scaled_data 换成自己导出的 XAUUSD 小时线特征矩阵,splits 设 5、gap 设 24,看 RMSE 是否随折数稳定。外汇与贵金属杠杆高、滑点大,回测误差低只代表历史样本内拟合倾向,实盘仍可能显著偏离。
class="macro">#Create the time series split object tscv = TimeSeriesSplit(gap=gap,n_splits=splits) class="macro">#First we will iterate over all the available models for i in np.arange(class="num">0,len(models)): class="macro">#First select the model model = models[i] class="macro">#Now we will cross validate this current model for j , (train,test) in enumerate(tscv.split(scaled_data)): class="macro">#First define the train and test data train_X = scaled_data.loc[train[class="num">0]:train[-class="num">1],current_predictors] train_y = market_data.loc[train[class="num">0]:train[-class="num">1],target] test_X = scaled_data.loc[test[class="num">0]:test[-class="num">1],current_predictors] test_y = market_data.loc[test[class="num">0]:test[-class="num">1],target] class="macro">#Now we will fit the model model.fit(train_X,train_y) class="macro">#And finally record the accuracy all_accuracy.iloc[j,i] = root_mean_squared_error(test_y,model.predict(test_X)) ohlc_accuracy for i in np.arange(class="num">0,ohlc_accuracy.shape[class="num">1]): print(f"{columns[i]} had error levels {ohlc_accuracy.iloc[:,i].mean()}") multiple_time_frame_accuracy for i in np.arange(class="num">0,ohlc_accuracy.shape[class="num">1]): print(f"{columns[i]} had error levels {multiple_time_frame_accuracy.iloc[:,i].mean()}") all_accuracy for i in np.arange(class="num">0,ohlc_accuracy.shape[class="num">1]): print(f"{columns[i]} had error levels {all_accuracy.iloc[:,i].mean()}")
后向筛选只留下最高价
用后向选择给梯度提升回归器做特征瘦身,起点是全部预测变量,每一步只删掉那些删了反而能降低误差的项。跑完算法,剩下的特征集合只有一项:最高价(High),其余开盘、收盘、最低、成交量类变量全被丢弃。 图里能直接读出一条规律:模型规模与误差水平成正比。特征越多,交叉验证下的 RMSE 反而往上走,说明堆变量在这里是负贡献,外汇与贵金属行情的高噪声环境里过拟合倾向很明显,杠杆品种高风险需自担。 下面这段 Python 用 mlxtend 的 SequentialFeatureSelector 复现了上述过程,10 折交叉、负均方根误差打分、反向搜索,换到 MT5 导出的 OHLC 序列上也能直接套。
class="macro">#Feature selection from mlxtend.feature_selection class="kw">import SequentialFeatureSelector as SFS class="macro">#We&class="macro">#x27;ll select the Gradient Boosting Regressor as our chosen model model = GradientBoostingRegressor() class="macro">#Let us prepare the Feature Selector Object sfs = SFS(model, k_features=(class="num">1,len(all_predictors)), forward=False, n_jobs=-class="num">1, scoring="neg_root_mean_squared_error", cv=class="num">10 ) class="macro">#Select the best feature sfs_results = sfs.fit(scaled_data.loc[:,all_predictors],market_data.loc[:,"Target"]) class="macro">#The best feature we found sfs_results.k_feature_names_ class="macro">#Prepare the plot fig1 = plot_sfs(sfs_results.get_metric_dict(),kind="std_dev") plt.title("Backward Selection on Gradient Boosting Regressor") plt.grid()
「GBR调参的切分与随机搜索落地」
把GBR模型扔进随机搜索前,先得把数据劈成两半:前半段喂给训练和优化,后半段只用来验证、揪过拟合。这套做法在外汇和贵金属样本上尤其必要,这类序列噪声大,不分held-out集很容易把随机波动当规律记进模型。 调参对象用RandomizedSearchCV包住GradientBoostingRegressor,参数网格覆盖了11个维度,从loss类型、学习率(10的0到-7次方)、树数量到叶子节点约束全摊开。迭代上限设1000次,实测整轮跑完耗时约2818秒,也就是快47分钟,MT5外接Python环境跑这种规模得留够机器空闲。 搜索吐出的最佳组合里,n_estimators=500、max_depth=3、learning_rate=0.01、loss='absolute_error'、criterion='friedman_mse',min_impurity_decrease还顶到了1。贵金属和外汇行情对绝对误差更不敏感极端值干扰,倾向用absolute_error而非平方误差。 代码里切分直接用形状整除定位,train取前半行、test取后半行,Target列单独抽走。随机搜索前用time.time()打点,能直观看到每次调参实验的成本,方便你判断要不要砍参数网格。
class="macro">#Let us try to tune our model from sklearn.model_selection class="kw">import RandomizedSearchCV class="macro">#Before we try to tune our model, let&class="macro">#x27;s first create a train and test set train_X = scaled_data.loc[:(scaled_data.shape[class="num">0]class=class="str">"cmt">//class="num">2),:] train_y = market_data.loc[:(market_data.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 = market_data.loc[(scaled_data.shape[class="num">0]class=class="str">"cmt">//class="num">2):,"Target"] class="macro">#Time the process class="kw">import time start_time = time.time() class="macro">#Prepare the tuning object tuner = RandomizedSearchCV(GradientBoostingRegressor(), { "loss": ["squared_error","absolute_error","huber"], "learning_rate": [class="num">0,(class="num">10.0 ** -class="num">1),(class="num">10.0 ** -class="num">2),(class="num">10.0 ** -class="num">3),(class="num">10.0 ** -class="num">4),(class="num">10.0 ** -class="num">5),(class="num">10.0 ** -class="num">6),(class="num">10.0 ** -class="num">7)], "n_estimators": [class="num">5,class="num">10,class="num">25,class="num">50,class="num">100,class="num">200,class="num">500,class="num">1000], "max_depth": [class="num">1,class="num">2,class="num">3,class="num">5,class="num">9,class="num">10], "min_samples_split":[class="num">0.1,class="num">0.2,class="num">0.3,class="num">0.4,class="num">0.5,class="num">0.6,class="num">0.7,class="num">0.8,class="num">0.9,class="num">1.0], "criterion":["friedman_mse","squared_error"], "min_samples_leaf":[class="num">0.1,class="num">0.2,class="num">0.3,class="num">0.4,class="num">0.5,class="num">0.6,class="num">0.7,class="num">0.8,class="num">0.9], "min_weight_fraction_leaf":[class="num">0.0,class="num">0.1,class="num">0.2,class="num">0.3,class="num">0.4,class="num">0.5], "max_features":[class="num">1,class="num">2,class="num">3,class="num">4,class="num">5,class="num">20],