Files
myquant/finance/models/backtest_integration.py
T
Simon 73d191b43a feat: 量化引擎加固 — 新增测试 + 数据/因子/回测层优化
- 新增 finance/tests/ 6 个测试套件(agents/backtest/dao_upsert/factors/features/fundamental_lookahead)
- 数据层: data_manager / dao 优化,新增 upsert 逻辑
- 因子层: 基本面因子抽象定位 _mapping、ROE/PE/PB 重构
- 回测层: vectorbt/engine 大改动(251 行),report 增强
- ML 层: features/backtest_integration 特征工程与回测优化
- CLI: agent_cli 重构
- config/settings 扩充配置项
2026-08-31 14:01:06 +08:00

137 lines
5.1 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.
"""
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 模型基准对比测试。
入参的 feature_engine 应已用模型训练集 fit 过(scaler/winsor/median 已缓存)。
run() 对 **样本外测试数据**(test_factor_df/test_price_df)用 fit=False 转换后
计算预测 IC,避免"同一时间段既训练又评估"的前视泄漏。
"""
def __init__(
self,
models: list[BaseModel],
feature_engine: FeatureEngine,
test_factor_df: pd.DataFrame,
test_price_df: pd.DataFrame,
bt_engine: VectorBTEngine | None = None,
):
self.models = models
self.feature_engine = feature_engine
self.test_factor_df = test_factor_df
self.test_price_df = test_price_df
self.bt_engine = bt_engine or VectorBTEngine()
def run(self) -> pd.DataFrame:
"""对比各模型在样本外测试集上的预测质量和回测表现。"""
rows = []
# 标签由规则(前视收益)决定,预测时可直接用同一 build_labels 构造,
# 避免依赖 build(fit=False) 不产标签的语义。
y_test = self.feature_engine.build_labels(self.test_price_df)
X_test, _ = self.feature_engine.build(self.test_factor_df, self.test_price_df, fit=False)
if X_test is None or X_test.empty or y_test is None or y_test.dropna().empty:
raise RuntimeError(
"MLBenchmark: 样本外测试集为空或 feature_engine 未在训练集上 fit。")
for model in self.models:
# OOS 回测:MLStrategy 用同一已 fit engine 对测试集生成信号
strategy = MLStrategy(model, self.feature_engine)
report = self.bt_engine.run(strategy, self.test_price_df, self.test_factor_df)
preds = model.predict(X_test)
common = X_test.index.intersection(y_test.dropna().index)
ic = preds.reindex(common).astype(float).corr(y_test.reindex(common).astype(float)) if len(common) > 1 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")