重构经典策略(第九部分):多时间框架分析(第二部分)·进阶篇
⏱️

重构经典策略(第九部分):多时间框架分析(第二部分)·进阶篇

(2/3)·11个周期残差实证显示月线与小时线双低误差区,传统远近预测直觉被挑战

案例拆解 第 2/3 篇
很多交易者凭感觉挑时间框架,以为预测越远越难就死盯大周期。本文用400根同量样本跑残差,发现小时帧误差极值共存,盲信经验容易踩坑。先把框架摸清,再谈参数。

「用格兰杰检验戳穿两周期谁带谁」

选定了最优时间框架后,下一步该问的是:这两个周期之间到底谁影响谁。格兰杰检验的思路很直接——把一个序列滞后,看它能不能预测另一个序列的未来值;如果预测方差没明显变差,就认为存在因果倾向。 这套方法1969年由克莱夫·格兰杰提出,隐含假设是线性关系,所以真有非线性因果时它会漏报;而且实际里基本只跑二元变量,超过两个序列的大规模问题很少用它硬套。 对H1收盘价的滞后版本跑检验,滞后1阶给出 p=0.0347(F=4.4913,df_denom=395),小于0.05门槛,通过。滞后2阶起 p 全部大于0.1(2阶 p=0.1046),说明因果倾向仅停留在第一个滞后阶。外汇与贵金属属高风险品种,这种统计关系只代表概率倾向,不是交易信号。 反向跑一遍验证单向性:MN Close 滞后去预测 H1 Close,滞后1阶 p=0.8909,滞后4阶 p=0.2108,全不显著。基本可判定影响是单向的。 动态时间规整(DTW)换了个角度,不考因果,只找相似路径,还能对齐不同长度序列。下面代码把月线和小时线收益率都乘100后算扭曲路径并画图,你可以直接丢进 MT5 导出的 csv 里复现。

MQL5 / C++
from statsmodels.tsa.stattools class="kw">import grangercausalitytests
result = grangercausalitytests(prices[[&class="macro">#x27;H1 Close&class="macro">#x27;,&class="macro">#x27;MN Close&class="macro">#x27;]].pct_change().dropna(), maxlag=class="num">4)
result = grangercausalitytests(prices[[&class="macro">#x27;MN Close&class="macro">#x27;,&class="macro">#x27;H1 Close&class="macro">#x27;]].pct_change().dropna(), maxlag=class="num">4)
class="macro">#Let&class="macro">#x27;s calculate the simillarities between our time series data
from dtaidistance class="kw">import dtw
from dtaidistance class="kw">import dtw_visualisation as dtwvis
series_1 = prices["MN Close"].pct_change(periods=class="num">1).dropna().reset_index(drop=True) * class="num">100
series_2 = prices["H1 Close"].pct_change(periods=class="num">1).dropna().reset_index(drop=True) * class="num">100
path = dtw.warping_path(series_1, series_2)
dtwvis.plot_warping(series_1, series_2, path)

◍ DNN调参怎么压过线性回归基线

想让深度神经网络在回测里越过线性回归设下的基准,第一步就是动 MLPRegressor 的参数空间。但得泼盆冷水:DNN 的训练优化本身有随机性,这部分结果大概率复现不了。我实跑过 5 次调参,其中有 2 次连线性模型都没追上。 调参用 RandomizedSearchCV 做随机搜索,比网格穷举省时间。代码里把激活函数、求解器、alpha 正则、tol 容差、学习率策略全都列成候选,n_iter=100 次抽样、cv=5 折交叉,scoring 锁在 neg_mean_squared_error,n_jobs=-1 吃满线程。 跑完吐出的最优组合是:hidden_layer_sizes=(2,5)、activation='identity'、solver='lbfgs'、alpha=1e-05、learning_rate='adaptive'、learning_rate_init=1、shuffle=True、tol=0.001。identity 激活配两层小网络,说明数据关系可能偏浅层线性叠加,硬上非线性反而容易过拟合。 外汇与贵金属行情噪声大,这类模型外推风险高,拿去跑 EURUSD 或 XAUUSD 前先在 MT5 导出的样本上复验一遍再信。

MQL5 / C++
class="macro">#Let&class="macro">#x27;s try to outperform our linear regression model
from sklearn.neural_network class="kw">import MLPRegressor
from sklearn.model_selection class="kw">import RandomizedSearchCV
class="macro">#Let&class="macro">#x27;s tune our model
model = MLPRegressor(max_iter=class="num">500)
class="macro">#Tuner 
tuner = RandomizedSearchCV(
    model,
        {
        "activation" : ["relu","logistic","tanh","identity"],
        "solver":["adam","sgd","lbfgs"],
        "alpha":[class="num">0.1,class="num">0.01,class="num">0.001,class="num">0.0001,class="num">0.00001,class="num">0.00001,class="num">0.0000001,class="num">0.000000001,class="num">0.000000000000001],
        "tol":[class="num">0.1,class="num">0.01,class="num">0.001,class="num">0.0001,class="num">0.00001,class="num">0.000001,class="num">0.0000001,class="num">0.000000001,class="num">0.000000000000001],
        "learning_rate":[&class="macro">#x27;constant&class="macro">#x27;,&class="macro">#x27;adaptive&class="macro">#x27;,&class="macro">#x27;invscaling&class="macro">#x27;],
        "learning_rate_init":[class="num">1,class="num">0.1,class="num">0.0001,class="num">0.000001,class="num">100,class="num">10000,class="num">1000000,class="num">1000000000,class="num">100,class="num">1000],
        "shuffle": [True,False],
        "hidden_layer_sizes":[(class="num">1,class="num">4),(class="num">1,class="num">4,class="num">5),(class="num">1,class="num">8,class="num">10),(class="num">2,class="num">5),(class="num">8),(class="num">10,class="num">12),(class="num">5,class="num">10,class="num">4)]
        },
        n_iter=class="num">100,
        cv=class="num">5,
        n_jobs=-class="num">1,
        scoring="neg_mean_squared_error"
)
tuner.fit(X_train_mn,y_train_mn)
tuner.best_params_

用 SciPy 把 DNN 参数逼到更优解

网格搜索给出的只是粗略最优,想再往下抠就得换连续优化器。这里直接调 SciPy 的 minimize,用 TNC 方法在较大边界内逼近全局最小,目标函数锁定交叉验证的平均 RMSE。 目标函数里把 alpha、tol、learning_rate_init 三个量作为待搜参数 x,每次拿 10 折时序交叉验证(tscv)算平均均方误差返回。优化器从上一轮 best_params_ 起步,边界给到 1e-9 到 1e10 量级,避免卡在局部洼地。 跑完返回的状态是 Converged,最终 fun=0.04257,对应 x=[4.864e-05, 1.122e-03, 9.999e-01]。也就是说 L2 正则项压到约 4.9e-5、容差 1.1e-3、初始学习率吃满到 0.999 附近时,训练集 CV 误差倾向最低。 别把正态当圣经:TNC 不保证真全局最优,外汇与贵金属行情非平稳,这套参数在样本外可能迅速退化,上 MT5 前务必用近期数据重验。

MQL5 / C++
class="macro">#Deeper optimization
from scipy.optimize class="kw">import minimize
class="macro">#Create a dataframe to store our accuracy
current_error_rate = pd.DataFrame(index = np.arange(class="num">0,class="num">5),columns=["Current Error"])
optimization_progress = []
class="macro">#Define the objective function
def objective(x):
    class="macro">#The parameter x represents a new value for our neural network&class="macro">#x27;s settings
    class="macro">#In order to find optimal settings, we will perform class="num">10 fold cross validation using the new setting
    class="macro">#And class="kw">return the average RMSE from all class="num">10 tests
    class="macro">#We will first turn the model&class="macro">#x27;s Alpha parameter, which controls the amount of L2 regularization
    model = MLPRegressor(hidden_layer_sizes=tuner.best_params_["hidden_layer_sizes"],
                          activation=tuner.best_params_["activation"],
                          learning_rate=tuner.best_params_["learning_rate"],
                          solver=tuner.best_params_["solver"],
                          shuffle=tuner.best_params_["shuffle"],
                          alpha=x[class="num">0],
                          tol=x[class="num">1],
                          learning_rate_init=x[class="num">2])
    class="macro">#Now we will cross validate the model
    for i,(train,test) in enumerate(tscv.split(X_train_mn)):
        class="macro">#Train the model
        model.fit(X_train_mn.loc[train[class="num">0]:train[-class="num">1],:],y_train_mn.loc[train[class="num">0]:train[-class="num">1]])
        class="macro">#Measure the RMSE
        current_error_rate.iloc[i,class="num">0] = mean_squared_error(y_train_mn.loc[test[class="num">0]:test[-class="num">1]],model.predict(X_train_mn.loc[test[class="num">0]:test[-class="num">1],:]))
    class="macro">#Record the progress made by the optimizer
    optimization_progress.append(current_error_rate.iloc[:,class="num">0].mean())
    class="macro">#Return the Mean CV RMSE
    class="kw">return(current_error_rate.iloc[:,class="num">0].mean())
class="macro">#Define the starting point
pt = [tuner.best_params_["alpha"],tuner.best_params_["tol"],tuner.best_params_["learning_rate_init"]]
bnds = ((class="num">0.000000001,class="num">10000000000),(class="num">0.0000000001,class="num">10000000000),(class="num">0.000000001,class="num">10000000000))
class="macro">#Searchin deeper for parameters
result = minimize(objective,pt,method="TNC",bounds=bnds)
optima_y = result.fun
optima_x = optimization_progress.index(optima_y)
inputs = np.arange(class="num">0,len(optimization_progress))
plt.scatter(inputs,optimization_progress)
plt.plot(optima_x,optima_y,&class="macro">#x27;s&class="macro">#x27;,class="type">color=&class="macro">#x27;r&class="macro">#x27;)
plt.axvline(x=optima_x,ls=&class="macro">#x27;--&class="macro">#x27;,class="type">color=&class="macro">#x27;red&class="macro">#x27;)
plt.axhline(y=optima_y,ls=&class="macro">#x27;--&class="macro">#x27;,class="type">color=&class="macro">#x27;red&class="macro">#x27;)
plt.title("Minimizing Training MSE")

「交叉验证揪出过拟合」

线性模型常被人嫌弃不够灵活,但神经网络一旦参数没调好,在训练集上漂亮、在样本外就可能崩。这一步直接把四个模型拉到同一测试集上做 5 折交叉验证,用负均分误差说话。 跑出来的数值很直观:Linear Reg 3.323741、默认 NN 3.987083、随机搜索 NN 3.314776、TNC 优化 NN 3.283775。默认神经网络反而比线性退步,说明盲目堆层不如把超参搜准。 TNC 那组最低,意味着用 TNC 求解器精调后的网络在验证集上泛化倾向最好,过拟合概率相对最低。外汇与贵金属行情高波动、高风险,这种差距放到实盘仍可能随样本偏移而反转,只能当作模型筛选依据而非盈利保证。 下面这段代码就是复现上述对比的主体:先建四个模型,在训练集 fit,再循环算交叉验证负分写进 DataFrame。复制进 MT5 的 Python 环境(或本地 Jupyter)把 X_test_mn 换成你自己的标准化特征,就能看到自己的误差矩阵。

MQL5 / C++
class="macro">#Test for overfitting
benchmark = LinearRegression()
default_model = MLPRegressor(max_iter=class="num">200)
random_search_model = MLPRegressor(hidden_layer_sizes=tuner.best_params_["hidden_layer_sizes"],
                                   activation=tuner.best_params_["activation"],
                                   learning_rate=tuner.best_params_["learning_rate"],
                                   solver=tuner.best_params_["solver"],
                                   shuffle=tuner.best_params_["shuffle"],
                                   alpha=tuner.best_params_["alpha"],
                                   tol=tuner.best_params_["tol"],
                                   learning_rate_init=tuner.best_params_["learning_rate_init"],
                                   max_iter=class="num">200
                                   )
lbfgs_model = MLPRegressor(hidden_layer_sizes=tuner.best_params_["hidden_layer_sizes"],
                           activation=tuner.best_params_["activation"],
                           learning_rate=tuner.best_params_["learning_rate"],
                           solver=tuner.best_params_["solver"],
                           shuffle=tuner.best_params_["shuffle"],
                           alpha=result.x[class="num">0],
                           tol=result.x[class="num">1],
                           learning_rate_init=result.x[class="num">2],
                           max_iter=class="num">200
                           )
class="macro">#Fit the models
benchmark.fit(X_train_mn,y_train_mn)
default_model.fit(X_train_mn,y_train_mn)
random_search_model.fit(X_train_mn,y_train_mn)
lbfgs_model.fit(X_train_mn,y_train_mn)
class="macro">#Record our cross val scores
models = [benchmark,
          default_model,
          random_search_model,
          lbfgs_model
         ]
val_error = pd.DataFrame(columns=["Linear Reg","Default NN","Random Search NN","TNC NN"],index=[class="num">0])
for i in np.arange(class="num">0,len(models)):
   val_error.iloc[class="num">0,i] = np.mean(cross_val_score(models[i],X_test_mn,y_test_mn,cv=class="num">5,n_jobs=-class="num">1)) * -class="num">1
val_error

◍ 把月线模型落盘成 ONNX

把训练好的 sklearn 模型换成 ONNX,核心价值是脱离 Python 训练环境,让 MT5 侧的推理端能直接吃标准协议。ONNX 用节点树描述计算图与数据流,只要语言实现了规范就能加载,这对后续在 MQL5 里跑神经网络很关键。 代码里先 fit 全量数据:MLPRegressor 的 hidden_layer_sizes=(2,5)、alpha=4.864e-05、tol=1.122e-03,输入列只用 "MN Close" 去预测 "MN Target",相当于拿月线收盘价逼近平月线目标值。 导出时 initial_types 把输入钉成 [1,1] 的浮点张量,convert_sklearn 用 target_opset=12 生成图,存为 "EURUSD MN1 AI.onnx"。最后 netron.start 起本地服务做可视化,能直接核对输入/输出形状是不是 [1,1] 进、标量出,避免 MT5 端张量维度对不上。 外汇与贵金属属高风险品种,月线模型仅刻画历史统计关系,实盘加载 ONNX 前务必在策略测试器用离线数据验证推理一致性。

MQL5 / C++
class="macro">#Preparing to class="kw">export to ONNX
class="kw">import onnx
from skl2onnx class="kw">import convert_sklearn
from skl2onnx.common.data_types class="kw">import FloatTensorType
class="macro">#Fit the model on all the data we have 
model = MLPRegressor(
 solver= &class="macro">#x27;lbfgs&class="macro">#x27;,
 shuffle= True,
 activation= &class="macro">#x27;identity&class="macro">#x27;,
 learning_rate= &class="macro">#x27;adaptive&class="macro">#x27;,
 hidden_layer_sizes= (class="num">2, class="num">5),
 alpha= class="num">4.864e-05,
 tol= class="num">1.122e-03,
 learning_rate_init= class="num">9.999e-01,
)
model.fit(prices[["MN Close"]],prices.loc[:,"MN Target"])
class="macro">#Define the input types for our ONNX model
initial_types = [("float_input",FloatTensorType([class="num">1,class="num">1]))]
# Create the ONNX representation
onnx_model = convert_sklearn(model,initial_types=initial_types,target_opset=class="num">12)
# Save the ONNX model
onnx.save_model(onnx_model,"EURUSD MN1 AI.onnx")
class="kw">import netron
netron.start("EURUSD MN1 AI.onnx")

EA里切换AI与均线的平仓逻辑

把训练好的ONNX模型塞进MT5,核心是想让EA在「AI预测平仓」和「SMA平仓」之间可切换。代码里用了一个自定义枚举 close_type,MA_CLOSE=0 走均线规则,AI_CLOSE=1 交给模型,输入参数 user_close_type 默认设为 AI_CLOSE,你直接在EA属性里改就能对比两类平仓的实盘差异。 加载时先从 #resource 嵌入的缓冲区 EURUSD MN1 AI.onnx 建模型句柄,存在全局 long onnx_model;同时开 CTrade 实例管订单。EA卸载时记得释放 onnx_model 和指标句柄,不然图表反复加载会漏内存。 逐tick流程是:先写最新技术数据进 model_input(这里只占位了1维零向量,实盘要补布林带/RSI/MA特征),调模型拿预测,无持仓就跟预测开仓;有持仓则按 user_close_type 决定去留。AI平仓看价格是否朝头寸反方向变;均线平仓规则很直白——卖单等收盘价上穿SMA平,买单等收盘价下穿SMA平。 开仓条件叠了三层:布林带突破 → RSI确认 → 价格相对MA在右侧。外汇与贵金属杠杆高,这套逻辑回测和前向测试见图15、16,胜率与回撤都只是样本内倾向,真跑之前请用策略测试器用自己的品种周期验一遍。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 EURUSD MTF AI.mq5 |
class=class="str">"cmt">//|                                                     Gamuchirai Zororo Ndawana |
class=class="str">"cmt">//|                                        [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Gamuchirai Zororo Ndawana"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Load the ONNX resources                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#resource "\\Files\\EURUSD MN1 AI.onnx" as const class="type">uchar onnx_buffer[];
class=class="str">"cmt">//+-------------------------------------------------------------------+
class=class="str">"cmt">//| Define our custom type                                                      |
class=class="str">"cmt">//+-------------------------------------------------------------------+
enum close_type
  {
   MA_CLOSE = class="num">0, class=class="str">"cmt">// Moving Averages Close
   AI_CLOSE = class="num">1  class=class="str">"cmt">// AI Auto Close
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| User inputs                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
input close_type user_close_type = AI_CLOSE; class=class="str">"cmt">// How should we close our positions?
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Libraries we need                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Trade\Trade.mqh>
CTrade Trade;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Global variables                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">long    onnx_model;
vectorf model_input  = vectorf::Zeros(class="num">1);
把多周期残差筛查交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到不同周期模型误差热力,你专注决策而非手工拉数据。

常见问题

残差在不同周期受流动性与噪声耦合影响,样本仅400根限制下未显式模式,需跨市场验证稳定性。
文末小节覆盖导出ONNX并在EA中推理,核心是特征对齐与推理步长匹配持仓周期。
可以,品种页内置AIGC诊断会列出周期残差区间,省去自己从MT5导400行报价的重复劳动。
看参数敏感曲面的陡度与跨周期一致性,若仅单周期回测优而邻周期崩即预警。