数据科学与机器学习(第四十二部分):使用Python中的ARIMA模型进行外汇时间序列预测  您需要了解的一切·综合运用
📘

数据科学与机器学习(第四十二部分):使用Python中的ARIMA模型进行外汇时间序列预测 您需要了解的一切·综合运用

第 3/3 篇

◍ SARIMAX 拟合里的尖峰与协方差告警

对 4007 根收盘价做 SARIMAX(0,1,0)x(0,1,0,5) 拟合,双尾 Prob(H) 落到 0.00,峰度读到 6.11。峰度明显大于 3,说明序列残差比正态假设更尖,极端跳空的概率被低估,外汇与贵金属这种高波动品种尤其要当心。

拟合时打了一条警告:协方差矩阵用梯度的外积(complex-step)估算,而非标准 Hessian。这意味着系数标准差只是近似,z 值和 P>z只能作参考,不能直接当成显著性铁证。

下面这段 Python 是当时跑模型的骨架,MT5 侧虽不能直接跑,但你能照着把 train 换成自己的 H1 收盘价序列、把季节周期 5 改成品种真实周期去复算。 from statsmodels.tsa.statespace.sarimax import SARIMAX model = SARIMAX( train, order=auto_model.order, # 非季节 (p,d,q) seasonal_order=auto_model.order+(5,), # 季节 (P,D,Q,S) enforce_stationarity=False ) results = model.fit() print(results.summary()) Log Likelihood 12613.829、AIC -25225.658、BIC -25219.364,样本区间 0 到 4007。这几个数你开 MT5 导出同样长度 EURUSD 的 H1 收盘,用 statsmodels 复一遍,偏差若超过 1% 就该查数据对齐了。

MQL5 / C++
from statsmodels.tsa.statespace.sarimax class="kw">import SARIMAX
model = SARIMAX(
    train,
    order=auto_model.order,                # Non-seasonal(p,d,q)
    seasonal_order=auto_model.order+(class="num">5,),  # Seasonal(P,D,Q,S)
    enforce_stationarity=False
)
results = model.fit()
print(results.summary())

残差诊断与滚动预测精度实测

SARIMAX 拟合后的诊断输出里,sigma2 估到 0.0001,z 值 63.423,p 值直接为 0,说明方差项显著非零。Ljung-Box Q 统计量 3.42、Prob(Q)=0.06,在 5% 水平上未能拒绝无自相关原假设;但 Jarque-Bera 达 676.61、Prob(JB)=0.00,峰度 5.01、偏度 -0.01,残差明显厚尾而非正态。 Heteroskedasticity 检验 H=0.48、Prob(H)=0.00,提示存在异方差结构,建模时不能把波动平稳当默认前提。外汇与贵金属价格受事件驱动跳变多,这类非正态残差在实盘里可能放大尾部风险,仓位管理须留缓冲。 滚动预测用 append(refit=False) 逐根吞入 test 数据,不重估参数只推下一步。砍掉前 5 根与末位预测后,forecast_accuracy 给出 corr=0.993、r2_score=0.986、rmse=0.0071、mape=0.0049——样本内跟随度很高,但这是历史回测,实盘漂移概率不低。 接 MT5 实时行情那段靠 mt5.copy_rates_from_pos 取最新 1 根,失败则睡 60 秒重试;全局 results 与 forecasts 在函数外维护,便于定时任务连续追加。开 MT5 跑通这套循环,先验证 copy_rates_from_pos 返回字段与你 pd.DataFrame 列的映射是否一致。

MQL5 / C++
predicted = results.predict(start=class="num">1, end=len(train))
clean_train = train[class="num">5:]
clean_predicted = predicted[class="num">5:]
plt.figure(figsize=(class="num">7,class="num">4))
plt.plot(clean_train.index[class="num">5:], clean_train[class="num">5:], label=&class="macro">#x27;Actual&class="macro">#x27;)
plt.plot(clean_train.index[class="num">5:], clean_predicted[class="num">5:], label=&class="macro">#x27;Forecasted mean&class="macro">#x27;, linestyle=&class="macro">#x27;--&class="macro">#x27;)
plt.title(&class="macro">#x27;Actual vs Forecast&class="macro">#x27;)
plt.legend()
plt.savefig("sarimax train actual&forecast plot.png")
plt.show()
# Initialize forecasts
forecasts = [results.forecast(steps=class="num">1).iloc[class="num">0]]  # First forecast
# Update with test data iteratively
for i in range(len(test)):
    # Append new observation without refitting
    results = results.append(test.iloc[i:i+class="num">1], refit=False)
    
    # Forecast next step
    forecasts.append(results.forecast(steps=class="num">1).iloc[class="num">0])
clean_test = test[class="num">5:]
forecasts = forecasts[class="num">5:-class="num">1] # remove the last element which is the predicted next value and the first class="num">5 items
forecast_accuracy(forecasts, clean_test)
{&class="macro">#x27;mape&class="macro">#x27;: class="num">0.004900183060803821,
&class="macro">#x27;me&class="macro">#x27;: -class="num">6.94082142749275e-06,
&class="macro">#x27;mae&class="macro">#x27;: class="num">0.005432456867698095,
&class="macro">#x27;mpe&class="macro">#x27;: -class="num">7.226495372320155e-06,
&class="macro">#x27;rmse&class="macro">#x27;: class="num">0.007127465498996785,
&class="macro">#x27;corr&class="macro">#x27;: class="num">0.9931778828074744,
&class="macro">#x27;minmax&class="macro">#x27;: class="num">0.004880027322298863,
&class="macro">#x27;acf1&class="macro">#x27;: class="num">0.10724254539104018,
&class="macro">#x27;r2_score&class="macro">#x27;: class="num">0.9864021833085908}
class="kw">import schedule
# Make realtime predictions based on the recent data from MetaTrader5
def predict_close():
    
    rates = mt5.copy_rates_from_pos(symbol, timeframe, class="num">0, class="num">1)
    if not rates:
        print(f"Failed to get recent OHLC values, error = {mt5.last_error}")
        time.sleep(class="num">60)
    
    rates_df = pd.DataFrame(rates)    
    
    global results # Get the variable globally, outside the function
    global forecasts

「在线追加观测避免重拟合断点」

SARIMAX 类模型在 MT5 收盘序列上做滚动预测时,最容易被忽视的是索引连续性。若新观测的 index 不从训练集 endog 的 shape[0] 接着排,append 后下一步 forecast 会错位,等于拿错时间窗在估明天收盘。 下面这段把最新一根 close 接进既有 results,并显式 refit=False,只做一步外推。对外汇、贵金属这类高波动品种,不重拟合能省下每根 K 线重跑模型的开销,但代价是模型对结构突变的捕捉会滞后,实战中需自行设定重拟合触发条件。

MQL5 / C++
# Append new observation to the model without refitting

new_obs_value = rates_df["close"].iloc[-class="num">1]
new_obs_index = results.data.endog.shape[class="num">0]  # class="kw">continue integer index

new_obs = pd.Series([new_obs_value], index=[new_obs_index]) # Its very important to class="kw">continue making predictions where we ended on the training data
results = results.append(new_obs, refit=False)

# Forecast next step
forecasts.append(results.forecast(steps=class="num">1).iloc[class="num">0])
print(f"Current Close Price: {new_obs_value} Forecasted next day Close Price: {forecasts[-class="num">1]}")

schedule.every(class="num">1).days.do(predict_close) # call the predict function after a given time
while True:

    schedule.run_pending()    
    time.sleep(class="num">60)
mt5.shutdown()
Current Close Price: class="num">1.1374900000000001 Forecasted next day Close Price: class="num">1.1337899981049262
Current Close Price: class="num">1.1372200000000001 Forecasted next day Close Price: class="num">1.1447100065656721
逐行拆:第 4 行取 DataFrame 末行 close 作为新观测值;第 5 行用训练集内生变量行数当新索引,保证连续;第 7–8 行包成单元素 Series 并 append,关键在 refit=False 不重算参数;第 11–12 行做单步预测并打印,回测样例中 EURUSD 当前 1.13749 给出次日 1.13379,下一刻 1.13722 给出 1.14471,误差约 0.004 级别。 调度部分用 schedule 每 1 天跑一次 predict_close,while 里 60 秒轮询;mt5.shutdown() 放循环外仅作收尾。实盘接这逻辑时,把 sleep(60) 换成行情 tick 触发更合理,外汇和贵金属杠杆高、跳空频繁,盲目信单步预测可能放大回撤。

MQL5 / C++
# Append new observation to the model without refitting

new_obs_value = rates_df["close"].iloc[-class="num">1]
new_obs_index = results.data.endog.shape[class="num">0]  # class="kw">continue integer index

new_obs = pd.Series([new_obs_value], index=[new_obs_index]) # Its very important to class="kw">continue making predictions where we ended on the training data
results = results.append(new_obs, refit=False)

# Forecast next step
forecasts.append(results.forecast(steps=class="num">1).iloc[class="num">0])
print(f"Current Close Price: {new_obs_value} Forecasted next day Close Price: {forecasts[-class="num">1]}")

schedule.every(class="num">1).days.do(predict_close) # call the predict function after a given time
while True:

    schedule.run_pending()    
    time.sleep(class="num">60)
mt5.shutdown()
Current Close Price: class="num">1.1374900000000001 Forecasted next day Close Price: class="num">1.1337899981049262
Current Close Price: class="num">1.1372200000000001 Forecasted next day Close Price: class="num">1.1447100065656721

◍ 模型够用就好,别神话线性外推

ARIMA 与 SARIMA 在平稳单变量序列上仍有实用价值,但落到外汇和贵金属盘面,三条硬伤先记牢:差分会削掉你关心的原始趋势结构;线性假设在非线性主导的行情里经常失效;单变量视角天然漏掉多维度信息,即便上 SARIMAX 加外生变量也补不全。 高风险提示:外汇与贵金属杠杆高、跳空频繁,这类传统模型直接挂实盘极易被黑天鹅打穿,仅适合做辅助参考而非下单依据。 话说回来,参数对、问题匹配、数据干净时,简单 ARIMA 在部分时间序列任务上跑赢 RNN 并不是稀罕事——复杂不等于有效,能解释、能快速回测的轻量模型反而更经得起你自己在 MT5 里复验。

常见问题

尖峰加协方差告警说明数值不稳定,先降阶或加差分;若告警仍在,放弃该组参数,别硬用。
重点看残差是否近似白噪声、滚动预测 MAPE 是否稳定在可接受区间;偏太大就别拿来预测。
小布可自动跑残差诊断并展示滚动预测误差,打开对应品种页即可看,不用自己写脚本。
可以,用追加更新代替全量重拟合能保住历史状态;但数据机制变了仍要重训。
不是,线性外推对突变很弱,模型够用就行,别神话长程预测,贵金属外汇高风险需谨慎。