数据科学与机器学习(第24部分):使用常规AI模型进行外汇时间序列预测·进阶篇
📈

数据科学与机器学习(第24部分):使用常规AI模型进行外汇时间序列预测·进阶篇

(2/3)·从回归到分类,跳过平稳性陷阱的模型在实盘里多半撑不过一周

含代码示例 第 2/3 篇

不少交易者把历史K线直接丢进LightGBM就以为拿到了预测引擎,结果在策略测试器里秒崩。外汇序列里藏着趋势与季节扰动,不先问数据稳不稳,模型学到的只是噪声记忆。本篇接着基础概念,直接进训练与检验的实操层。

「用LightGBM回归给收盘价建模」

把整理好的特征矩阵按 7:3 切分,前 700 行喂训练、后 300 行留测试,总共 26 个特征列。LGBMRegressor 拟合后,在测试集上用 r2_score 打分,全量数据预测收盘价的 R² 达到 0.84,看起来能用,但别急着当圣杯。 树模型算特征重要性看的是分裂频次和分裂带来的增益,跟线性类方法不是一回事。从 gain 图的输出看,DAYOFWEEK、MONTH、DAYOFMONTH、DAYOFYEAR 这几个想抓季节性的字段贡献几乎垫底,可怪的是 ADF 检验里它们又都是平稳的——平稳却不重要,说明周期性噪声大概率盖过了真实信号。 想交叉验证重要性,可以上 SHAP 的 TreeExplainer 把每个样本的贡献拆开画 summary plot。外汇和贵金属这种高杠杆品种,模型回测漂亮不代表实盘能跑,样本外漂移随时可能发生,验证时务必留足滚动窗口。

MQL5 / C++
X = df.drop(columns=["TARGET_CLOSE"])
Y = df["TARGET_CLOSE"]
train_size = class="num">0.7 class="macro">#configure train size
train_size = round(train_size*df.shape[class="num">0])
x_train = X.iloc[:train_size,:]
x_test = X.iloc[train_size:, :]
y_train = Y.iloc[:train_size]
y_test = Y.iloc[train_size:]
print(f"x_train_size{x_train.shape}
x_test_size{x_test.shape}

y_train{y_train.shape}
y_test{y_test.shape}")
x_train_size(class="num">700, class="num">26)
x_test_size(class="num">300, class="num">26)
y_train(class="num">700,)
y_test(class="num">300,)
model = lgb.LGBMRegressor(**params)
model.fit(x_train, y_train)
from sklearn.metrics class="kw">import r2_score
test_pred = model.predict(x_test)
accuracy = r2_score(y_test, test_pred)
class="macro">#showing actual test values and predictions
plt.figure(figsize=(class="num">8, class="num">6))  
plt.plot(y_test, label=&class="macro">#x27;Actual Values&class="macro">#x27;)
plt.plot(test_pred, label=&class="macro">#x27;Predicted Values&class="macro">#x27;)
plt.xlabel(&class="macro">#x27;Index&class="macro">#x27;)
plt.ylabel(&class="macro">#x27;Values&class="macro">#x27;)
plt.title(&class="macro">#x27;Actual vs. Predicted Values&class="macro">#x27;)
plt.legend(loc="lower center")
# Add R-squared(accuracy) score in a corner
plt.text(class="num">0.05, class="num">0.95, f"LightGBM(Accuracy): {accuracy:.4f}", ha=&class="macro">#x27;left&class="macro">#x27;, va=&class="macro">#x27;top&class="macro">#x27;, transform=plt.gca().transAxes, fontsize=class="num">10, bbox=dict(boxstyle=&class="macro">#x27;round&class="macro">#x27;, facecolor=&class="macro">#x27;white&class="macro">#x27;, alpha=class="num">0.7))
plt.grid(True)
plt.savefig("LighGBM Test plot")
plt.show()
# Plot feature importance using Gain
lgb.plot_importance(model, importance_type="gain", figsize=(class="num">8,class="num">6), title="LightGBM Feature Importance(Gain)")
plt.tight_layout()
plt.savefig("LighGBM feature importance(Gain)")
plt.show()
explainer = shap.TreeExplainer(model)
shap_values = explainer(x_train)  
shap.summary_plot(shap_values, x_train, max_display=len(x_train.columns), show=False)  # Show all features
# Adjust layout and set figure size
plt.subplots_adjust(left=class="num">0.12, bottom=class="num">0.1, right=class="num">0.9, top=class="num">0.9)  
plt.gcf().set_size_inches(class="num">6, class="num">8) 
plt.tight_layout()
plt.savefig("SHAP_Feature_Importance_Summary_Plot.png")
plt.show()

用ADF筛掉会骗人的非平稳变量

ADF检验干一件事:判断时间序列是否平稳。平稳意味着均值、方差、自相关不随时间漂移,这是很多预测模型成立的前提。外汇和贵金属价格本身的OHLC多数非平稳,但派生出的差值类序列往往平稳——这点在做特征工程时直接决定你喂给模型的数据有没有用。 实测对27个候选变量跑全样本ADF,只有9个通过平稳性(显著性0.05):7DAY_STDDEV、DAYOFMONTH、DAYOFWEEK、DAYOFYEAR、MONTH、DIFF_LAG1_OPEN、DIFF_LAG1_HIGH、DIFF_LAG1_LOW、DIFF_LAG1_CLOSE。其余18个价格类原始字段大概率是漂移的,硬塞进回归或机器学习里会放大伪相关。 肉眼初筛比跑代码快:把变量分布图画出来,数据点紧密裹在均值周围、没有明显扩张或偏移趋势的,大概率平稳。但真要落地还是得看p值。下面这段Python可直接套用,注意它是pandas+statsmodels环境,不是MT5内置,但逻辑你搬到MQL5做预处理前先用Python探一遍最省事。 别把正态当圣经 平稳不等于正态,ADF过了只说明统计属性不随时间变,不保证分布形状好看。贵金属跳空造成的厚尾照样能让模型翻车,风控上外汇贵金属本身就是高杠杆高风险品种,平稳化只是降低过拟合概率,不是免死金牌。

MQL5 / C++
from statsmodels.tsa.stattools class="kw">import adfuller
def adf_test(series, signif=class="num">0.05):
  """
  Performs the ADF test on a pandas Series and interprets the results.
  Args:
      series: The pandas Series containing the time series data.
      signif: Significance level for the test(class="kw">default: class="num">0.05).
  Returns:
      A dictionary containing the test statistic, p-value, used lags,
      critical values, and interpretation of stationarity.
  """
  dftest = adfuller(series, autolag=&class="macro">#x27;AIC&class="macro">#x27;)
  adf_stat = dftest[class="num">0]  # Access test statistic
  pvalue = dftest[class="num">1]  # Access p-value
  usedlag = dftest[class="num">2]  # Access used lags
  critical_values = dftest[class="num">4]  # Access critical values
  interpretation = &class="macro">#x27;Stationary&class="macro">#x27; if pvalue < signif else &class="macro">#x27;Non-Stationary&class="macro">#x27;
  result = {&class="macro">#x27;Statistic&class="macro">#x27;: adf_stat, &class="macro">#x27;p-value&class="macro">#x27;: pvalue, &class="macro">#x27;Used Lags&class="macro">#x27;: usedlag,
            &class="macro">#x27;Critical Values&class="macro">#x27;: critical_values, &class="macro">#x27;Interpretation&class="macro">#x27;: interpretation}
  class="kw">return result
for col in df.columns:
  adf_results = adf_test(df[col], signif=class="num">0.05)
  print(f"ADF Results for column {col}:\n {adf_results}")

◍ 平稳性为何左右模型可信度

做时间序列分析时,大量统计方法默认数据是平稳的。一旦序列带趋势或结构突变,这类方法给出的结论往往会偏掉,甚至完全误导交易决策。 可以设想一只持续单边上行的股票价格:传统线性外推在这种环境里几乎捕捉不到内在节奏,预测误差会随 horizon 拉长而失控。 我们跑过的 LightGBM 确实能啃下 OHLC 之间的非线性,对平稳性的依赖比 ARIMA 那类模型低。特征重要性排序里,最靠前的恰恰是非平稳的开盘、最高、最低、收盘四个变量。 但这不代表星期几这种平稳特征没用。特征重要性只是切片,不是全貌;在 EURUSD 上,我认为日历效应仍会通过微观流动性渗透进价格,单纯因为它排名靠后就把列删掉并不明智。外汇与贵金属杠杆高、跳空频繁,这类隐性变量在高波动时段的影响概率会被放大,建议开 MT5 用特征消融自己验一遍。

「把收盘价做成一阶差分试试」

做时间序列预测时,目标变量平稳往往能提升不少机器学习模型的稳定性——它的均值、方差和自相关不随时间漂移。直接猜下一段行情往哪走很难,但猜下一段能跑出多少点,相对更可行;若能预测点数,就能反推止损和获利位置。 具体做法是对收盘价取一阶差分:用下一根收盘价减前一根收盘价,得到的新序列在统计上趋于平稳。下面这段 Python 只是思路示意,MQL5 里可用 iClose 自行错位相减。 用 ADF 检验验证,统计量落到 -23.37、p-value 为 0.0,远低于 1% 临界值 -3.437,结论明确是平稳。但把这平稳变量喂给 LightGBM 回归,实测性能反而很差;至少在这份数据上,LightGBM 处理差分目标并不讨好,后续仍沿用直接预测目标收盘价的回归模型。 不过连续收盘价的回归模型,实用价值不如直接出买卖信号的模型。要做信号过滤或触发,得另训一个分类模型,而不是只盯价格数值。

用LightGBM给K线方向打二分类标签

把行情转成机器学习能吃的样本,第一步是把目标变量压成 0/1:收盘高于开盘记 1(偏买入),否则记 0(偏卖出)。这套标签逻辑直接决定了分类器学的是什么,而不是去猜涨跌幅度。 下面这段 Python 先把 TARGET_OPEN 与 TARGET_CLOSE 逐根比对生成 Y,再按 train_size 切出训练集和测试集;随后用 Pipeline 把 StandardScaler 标准化和 LGBMClassifier 串起来,参数里 num_leaves=25、max_depth=5、learning_rate=0.05,属于浅树低学习率的保守配置。 [CODE] Y = [] target_open = df["TARGET_OPEN"] target_close = df["TARGET_CLOSE"] for i in range(len(target_open)): if target_close[i] > target_open[i]: # if the candle closed above where it opened thats a buy signal Y.append(1) else: #otherwise it is a sell signal Y.append(0) # split Y into irrespective training and testing samples y_train = Y[:train_size] y_test = Y[train_size:] from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler params = { 'boosting_type': 'gbdt', # Gradient Boosting Decision Tree 'objective': 'binary', # For binary classification (use 'regression' for regression tasks) 'metric': ['auc','binary_logloss'], # Evaluation metric 'num_leaves': 25, # Number of leaves in one tree 'n_estimators' : 100, # number of trees 'max_depth': 5, 'learning_rate': 0.05, # Learning rate 'feature_fraction': 0.9 # Fraction of features to be used for each boosting round } pipe = Pipeline([ ("scaler", StandardScaler()), ("lgbm", lgb.LGBMClassifier(**params)) ]) # Fit the pipeline to the training data pipe.fit(x_train, y_train) [/CODE] 回测切分后测试集共 300 根样本,模型总体准确率只有 0.53,基本贴近随机猜。分类报告里 0 类 recall 0.79 但 precision 0.49,1 类 precision 0.62 但 recall 仅 0.30——说明模型倾向把多数棒线判为卖出,买入信号漏抓严重。 别把 53% 当废案丢掉 外汇与贵金属属高杠杆高风险品种,这种浅层 LightGBM 单看准确率没用;recall 和 precision 的错位反而提示你:拿它做反向过滤或仓位触发,比直接跟单更稳。开 MT5 导出自家品种的分时 OHLC,重跑上面管道,先盯自己数据的 0/1 分布再谈调参。

MQL5 / C++
Y = []
target_open = df["TARGET_OPEN"]
target_close = df["TARGET_CLOSE"]
for i in range(len(target_open)):
    if target_close[i] > target_open[i]: # if the candle closed above where it opened thats a buy signal
        Y.append(class="num">1)
    else: class="macro">#otherwise it is a sell signal
        Y.append(class="num">0)
# split Y into irrespective training and testing samples 
y_train = Y[:train_size]
y_test = Y[train_size:]
from sklearn.pipeline class="kw">import Pipeline
from sklearn.preprocessing class="kw">import StandardScaler
params = {
    &class="macro">#x27;boosting_type&class="macro">#x27;: &class="macro">#x27;gbdt&class="macro">#x27;,   # Gradient Boosting Decision Tree
    &class="macro">#x27;objective&class="macro">#x27;: &class="macro">#x27;binary&class="macro">#x27;,     # For binary classification(use &class="macro">#x27;regression&class="macro">#x27; for regression tasks)
    &class="macro">#x27;metric&class="macro">#x27;: [&class="macro">#x27;auc&class="macro">#x27;,&class="macro">#x27;binary_logloss&class="macro">#x27;],   # Evaluation metric
    &class="macro">#x27;num_leaves&class="macro">#x27;: class="num">25,          # Number of leaves in one tree
    &class="macro">#x27;n_estimators&class="macro">#x27; : class="num">100, # number of trees
    &class="macro">#x27;max_depth&class="macro">#x27;: class="num">5,
    &class="macro">#x27;learning_rate&class="macro">#x27;: class="num">0.05,     # Learning rate
    &class="macro">#x27;feature_fraction&class="macro">#x27;: class="num">0.9    # Fraction of features to be used for each boosting round
}
pipe = Pipeline([
    ("scaler", StandardScaler()),
    ("lgbm", lgb.LGBMClassifier(**params))
])
# Fit the pipeline to the training data
pipe.fit(x_train, y_train)
把重复劳动交给小布
这些诊断与小布盯盘的 AIGC 已内置,打开对应品种页即可看到平稳性提示与特征快照,你只需专注调参和决策。

常见问题

回归适合预测具体价格变动幅度,分类适合预测涨跌方向;进阶做法是用平稳后的收益率做回归,再把符号转成分类标签。
目前小布内置的是通用诊断与特征视图,自定义ONNX需自行接EA;但品种页的AIGC面板可对照显示实时平稳性状态,辅助你判断模型是否失效。
倾向先做差分或对数收益处理再验,p值偏高说明存在单位根,直接拟合大概率捕获伪趋势,样本外表现可能急剧退化。
原始价含累计趋势与异方差,平稳序列均值回归特性强,模型更易学到可复用结构,外汇贵金属属高风险品种仍需严控样本外回撤。