Add stock prediction experiments: AAPL forecast + 4-version backtest comparison
Python package build / build (push) Has been cancelled
Python package build / build (push) Has been cancelled
- stock_forecast.py: Basic AAPL price forecast - stock_backtest.py: V1 original price prediction backtest - stock_backtest_optimized.py: V2 log-return + ensemble optimization - stock_backtest_xreg.py: V3 XReg covariates (volume/RSI/SPY) - stock_backtest_trick.py: V4 SPY trend guidance + volatility adjustment - Visualization charts for all versions
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import yfinance as yf
|
||||
import numpy as np
|
||||
import torch
|
||||
import timesfm
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# 1. 获取 AAPL 数据(到今天为止)
|
||||
print("下载 AAPL 股票数据...", flush=True)
|
||||
end = datetime.now()
|
||||
start = end - timedelta(days=365)
|
||||
df = yf.download("AAPL", start=start.strftime("%Y-%m-%d"), end=end.strftime("%Y-%m-%d"), progress=False)
|
||||
close = df["Close"].values.flatten().astype(np.float32)
|
||||
dates = df.index
|
||||
|
||||
# 2. 分割:6/15 之前为训练集,6/16 之后为实际值
|
||||
split_date = "2026-06-15"
|
||||
split_idx = None
|
||||
for i, d in enumerate(dates):
|
||||
if str(d.date()) <= split_date:
|
||||
split_idx = i
|
||||
# split_idx 是 <= 6/15 的最后一个索引
|
||||
train_data = close[: split_idx + 1]
|
||||
train_dates = dates[: split_idx + 1]
|
||||
actual_data = close[split_idx + 1 :]
|
||||
actual_dates = dates[split_idx + 1 :]
|
||||
|
||||
HORIZON = len(actual_data)
|
||||
print(f"训练数据: {len(train_data)} 天(截至 {train_dates[-1].date()})", flush=True)
|
||||
print(f"实际数据: {HORIZON} 天({actual_dates[0].date()} ~ {actual_dates[-1].date()})", flush=True)
|
||||
print(f"分割日收盘价: {train_data[-1]:.2f}", flush=True)
|
||||
|
||||
# 3. 加载模型
|
||||
print()
|
||||
print("加载 TimesFM 模型...", flush=True)
|
||||
torch.set_float32_matmul_precision("high")
|
||||
model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
|
||||
"google/timesfm-2.5-200m-pytorch", torch_compile=False
|
||||
)
|
||||
print("模型加载完成", flush=True)
|
||||
|
||||
# 4. 编译并预测
|
||||
model.compile(
|
||||
timesfm.ForecastConfig(
|
||||
max_context=512,
|
||||
max_horizon=128,
|
||||
normalize_inputs=True,
|
||||
use_continuous_quantile_head=True,
|
||||
force_flip_invariance=False,
|
||||
infer_is_positive=True,
|
||||
fix_quantile_crossing=True,
|
||||
)
|
||||
)
|
||||
print(f"预测未来 {HORIZON} 个交易日...", flush=True)
|
||||
pf, qf = model.forecast(horizon=HORIZON, inputs=[train_data])
|
||||
print("预测完成!", flush=True)
|
||||
|
||||
# 5. 计算误差指标
|
||||
actual = actual_data
|
||||
pred = pf[0][:HORIZON]
|
||||
mae = np.mean(np.abs(actual - pred))
|
||||
rmse = np.sqrt(np.mean((actual - pred) ** 2))
|
||||
mape = np.mean(np.abs((actual - pred) / actual)) * 100
|
||||
|
||||
# 方向准确率
|
||||
actual_dir = np.diff(actual)
|
||||
pred_dir = np.diff(pred)
|
||||
dir_acc = np.mean(actual_dir * pred_dir > 0) * 100
|
||||
|
||||
# 置信区间覆盖率
|
||||
in_80 = np.mean((actual >= qf[0, :HORIZON, 1]) & (actual <= qf[0, :HORIZON, 9])) * 100
|
||||
in_40 = np.mean((actual >= qf[0, :HORIZON, 3]) & (actual <= qf[0, :HORIZON, 7])) * 100
|
||||
|
||||
print()
|
||||
print("=== 回测结果:6/16 ~ 6/30 预测 vs 实际 ===", flush=True)
|
||||
print()
|
||||
print(" 日期 实际价 预测价 误差 误差%", flush=True)
|
||||
for i in range(HORIZON):
|
||||
err = pred[i] - actual[i]
|
||||
err_pct = err / actual[i] * 100
|
||||
print(
|
||||
f" {actual_dates[i].date()} {actual[i]:7.2f} {pred[i]:7.2f} {err:+7.2f} {err_pct:+6.2f}%",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
print()
|
||||
print("=== 误差指标 ===", flush=True)
|
||||
print(f" MAE (平均绝对误差): ${mae:.2f}", flush=True)
|
||||
print(f" RMSE (均方根误差): ${rmse:.2f}", flush=True)
|
||||
print(f" MAPE (平均绝对百分比误差): {mape:.2f}%", flush=True)
|
||||
print(f" 方向准确率: {dir_acc:.1f}%", flush=True)
|
||||
print(f" 80%置信区间覆盖率: {in_80:.1f}%", flush=True)
|
||||
print(f" 40%置信区间覆盖率: {in_40:.1f}%", flush=True)
|
||||
|
||||
# 6. 画图
|
||||
fig, ax = plt.subplots(figsize=(14, 6))
|
||||
show_n = min(60, len(train_data))
|
||||
ax.plot(range(show_n), train_data[-show_n:], label="Historical Close", color="steelblue", linewidth=1.5)
|
||||
x_actual = range(show_n, show_n + HORIZON)
|
||||
ax.plot(x_actual, actual, label="Actual", color="forestgreen", linewidth=2, marker="o", markersize=3)
|
||||
ax.plot(x_actual, pred, label="Forecast", color="tomato", linewidth=2, linestyle="--")
|
||||
ax.fill_between(x_actual, qf[0, :HORIZON, 1], qf[0, :HORIZON, 9], alpha=0.15, color="tomato", label="80% CI")
|
||||
ax.fill_between(x_actual, qf[0, :HORIZON, 3], qf[0, :HORIZON, 7], alpha=0.3, color="tomato", label="40% CI")
|
||||
ax.axvline(x=show_n - 1, color="gray", linestyle="--", alpha=0.5, label="Forecast Start")
|
||||
ax.set_title(f"AAPL Backtest: Forecast vs Actual (MAPE={mape:.2f}%, Dir.Acc={dir_acc:.0f}%)", fontsize=14)
|
||||
ax.set_xlabel("Trading Days")
|
||||
ax.set_ylabel("Price (USD)")
|
||||
ax.legend(loc="upper left")
|
||||
plt.tight_layout()
|
||||
plt.savefig("aapl_backtest.png", dpi=150)
|
||||
print()
|
||||
print("Chart saved: aapl_backtest.png", flush=True)
|
||||
print("Done!", flush=True)
|
||||
Reference in New Issue
Block a user