Files
timesfm/stock_backtest.py
T
freedakgmail 6041e3ff27
Python package build / build (push) Has been cancelled
Add stock prediction experiments: AAPL forecast + 4-version backtest comparison
- 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
2026-07-01 21:01:59 +08:00

116 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)