数据科学与机器学习(第四十二部分):使用Python中的ARIMA模型进行外汇时间序列预测 您需要了解的一切·综合运用
◍ 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% 就该查数据对齐了。
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 列的映射是否一致。
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 线重跑模型的开销,但代价是模型对结构突变的捕捉会滞后,实战中需自行设定重拟合触发条件。
# 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
# 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 里复验。