7 Sprints 全部完成: Sprint 0: 基础设施 (DataManager + MariaDB) Sprint 1: 因子引擎 (34因子/12分类) Sprint 2: VectorBT 回测 (5策略+截面) Sprint 3: Optuna 优化 (+Walk-Forward) Sprint 4: ML 模型 (LightGBM+CatBoost) Sprint 5: Qwen 情绪因子 (三源新闻+日期对齐) Sprint 6: Agent 系统 (4Agent+日报.md/.html) 生产加固 (15项): Tushare双源fallback, SSH自动恢复, pool_pre_ping, save_daily先删后插, load_dotenv绝对路径, 日报5d/20d修复, RiskAgent改上证指数, 昨日对比+数据截止, mac_report utf8mb4, CLAUDE-*.md 9条已知Bug, demo全参数化, djapi数据源归一化, indexDatas API修正 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
125 lines
4.1 KiB
Python
125 lines
4.1 KiB
Python
"""
|
|
ML 模型回测集成。
|
|
|
|
MLStrategy: 将 ML 预测值作为交易信号接入回测引擎。
|
|
MLBenchmark: 多模型基准对比。
|
|
"""
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
from backtest.base import BaseStrategy
|
|
from backtest.vectorbt.engine import VectorBTEngine
|
|
from models.base import BaseModel
|
|
from models.features import FeatureEngine
|
|
|
|
|
|
class MLStrategy(BaseStrategy):
|
|
"""
|
|
ML 预测 → 交易信号。
|
|
|
|
用模型预测未来 N 日收益,按预测值分位数生成信号:
|
|
- 预测值 > buy_quantile → 买入
|
|
- 预测值 < sell_quantile → 平仓
|
|
|
|
参数:
|
|
model: 已训练的 BaseModel
|
|
feature_engine: 已 fit 的 FeatureEngine
|
|
buy_quantile: 买入分位阈值(0.7 = 预测值最高的30%买入)
|
|
sell_quantile: 卖出分位阈值(0.3 = 预测值最低的30%平仓)
|
|
rebalance_freq: 调仓间隔(交易日)
|
|
"""
|
|
|
|
category = "ml"
|
|
|
|
def __init__(
|
|
self,
|
|
model: BaseModel,
|
|
feature_engine: FeatureEngine,
|
|
buy_quantile: float = 0.7,
|
|
sell_quantile: float = 0.3,
|
|
rebalance_freq: int = 5,
|
|
):
|
|
self.model = model
|
|
self.feature_engine = feature_engine
|
|
self.buy_quantile = buy_quantile
|
|
self.sell_quantile = sell_quantile
|
|
self.rebalance_freq = rebalance_freq
|
|
self.name = f"ml_{model.name}"
|
|
|
|
def generate_signals(self, factor_df: pd.DataFrame) -> pd.Series:
|
|
X, _ = self.feature_engine.build(factor_df, factor_df, fit=False)
|
|
if X.empty:
|
|
return pd.Series(-1, index=factor_df.index)
|
|
|
|
preds = self.model.predict(X)
|
|
# 用预测值本身的分布作为阈值(相对排序,避免模型偏差影响)
|
|
buy_threshold = preds.quantile(self.buy_quantile)
|
|
sell_threshold = preds.quantile(self.sell_quantile)
|
|
|
|
signals = pd.Series(-1, index=factor_df.index)
|
|
common = signals.index.intersection(preds.index)
|
|
buy_mask = preds.loc[common] > buy_threshold
|
|
sell_mask = preds.loc[common] < sell_threshold
|
|
signals.loc[buy_mask[buy_mask].index] = 1
|
|
signals.loc[sell_mask[sell_mask].index] = 0
|
|
|
|
signals = self._filter_rebalance(signals)
|
|
return signals
|
|
|
|
def _filter_rebalance(self, signals: pd.Series) -> pd.Series:
|
|
"""每隔 rebalance_freq 个交易日保留第一个非持有信号。"""
|
|
result = signals.copy()
|
|
last_active = -self.rebalance_freq - 1
|
|
for i in range(len(result)):
|
|
sig = result.iloc[i]
|
|
if sig in (0, 1):
|
|
if i - last_active >= self.rebalance_freq:
|
|
last_active = i
|
|
else:
|
|
result.iloc[i] = -1
|
|
return result
|
|
|
|
|
|
class MLBenchmark:
|
|
"""ML 模型基准对比测试。"""
|
|
|
|
def __init__(
|
|
self,
|
|
models: list[BaseModel],
|
|
feature_engine: FeatureEngine,
|
|
price_df: pd.DataFrame,
|
|
factor_df: pd.DataFrame,
|
|
bt_engine: VectorBTEngine | None = None,
|
|
):
|
|
self.models = models
|
|
self.feature_engine = feature_engine
|
|
self.price_df = price_df
|
|
self.factor_df = factor_df
|
|
self.bt_engine = bt_engine or VectorBTEngine()
|
|
|
|
def run(self) -> pd.DataFrame:
|
|
"""对比各模型的预测质量和回测表现。"""
|
|
rows = []
|
|
for model in self.models:
|
|
strategy = MLStrategy(model, self.feature_engine)
|
|
report = self.bt_engine.run(strategy, self.price_df, self.factor_df)
|
|
|
|
# OOS 预测 vs 真实值
|
|
X, y_true = self.feature_engine.build(self.factor_df, self.price_df, fit=True)
|
|
y_pred = model.predict(X)
|
|
|
|
ic = y_pred.corr(y_true) if len(y_pred) > 0 else 0
|
|
|
|
rows.append({
|
|
"model": model.name,
|
|
"ic": round(ic, 4),
|
|
"total_return": report.total_return,
|
|
"cagr": report.cagr,
|
|
"max_dd": report.max_drawdown,
|
|
"sharpe": report.sharpe_ratio,
|
|
"win_rate": report.win_rate,
|
|
"trades": report.total_trades,
|
|
})
|
|
return pd.DataFrame(rows).set_index("model")
|