- 新增 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 扩充配置项
176 lines
8.7 KiB
Python
176 lines
8.7 KiB
Python
"""
|
||
Sprint 4 验证脚本 — ML 模型训练与回测。
|
||
|
||
用法:
|
||
python cli/demo_ml.py
|
||
python cli/demo_ml.py --ts_code 600519.SH
|
||
python cli/demo_ml.py --ts_code 000001.SZ --lookahead 10
|
||
"""
|
||
|
||
import sys, os, argparse
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
|
||
import numpy as np
|
||
|
||
from data.data_manager import DataManager
|
||
from factors.engine import FactorEngine
|
||
from factors.registry import get_factor
|
||
from models.features import FeatureEngine
|
||
from models.lightgbm.model import LightGBMModel
|
||
from models.catboost.model import CatBoostModel
|
||
from models.backtest_integration import MLStrategy, MLBenchmark
|
||
from backtest.vectorbt.engine import VectorBTEngine
|
||
|
||
|
||
def main():
|
||
p = argparse.ArgumentParser(description="Sprint 4 — ML 模型训练与回测验证")
|
||
p.add_argument("--ts_code", default="000001.SZ", help="测试股票代码")
|
||
p.add_argument("--lookahead", type=int, default=5, help="预测未来 N 日(默认: 5)")
|
||
args = p.parse_args()
|
||
|
||
print("=" * 60)
|
||
print("Sprint 4 — ML 模型训练与回测验证")
|
||
print("=" * 60)
|
||
|
||
print("\n[1/6] 准备数据...")
|
||
dm = DataManager(); dm.init_db()
|
||
engine_fe = FactorEngine(dm)
|
||
price_df = dm.get_daily(args.ts_code).set_index("trade_date").sort_index()
|
||
|
||
tech_names = ["momentum_5", "momentum_10", "momentum_20", "momentum_60", "rsi_7", "rsi_14", "macd",
|
||
"vol_ratio_5", "vol_ratio_20", "vol_chg_5", "boll", "boll_width", "atr_14", "atr_ratio_14",
|
||
"ma_cross_5_20", "ma_cross_10_60", "ma_dev_20", "ma_dev_60", "volatility_20", "volatility_60",
|
||
"down_vol_20", "turnover_5", "turnover_chg_5", "amplitude_5", "amplitude_20"]
|
||
factor_df = engine_fe.compute(args.ts_code, [get_factor(n) for n in tech_names])
|
||
print(" 日线: {} 条, 因子: {} 个".format(len(price_df), factor_df.shape[1]))
|
||
|
||
print("\n[2/6] 特征工程 (lookahead={}, regression)...".format(args.lookahead))
|
||
fe = FeatureEngine(lookahead=args.lookahead, label_type="regression")
|
||
X, y = fe.build(factor_df, price_df, fit=True)
|
||
print(" 特征矩阵: {} x {}".format(X.shape[0], X.shape[1]))
|
||
print(" 标签: mean={:.2f}% std={:.2f}% min={:.1f}% max={:.1f}%".format(y.mean(), y.std(), y.min(), y.max()))
|
||
|
||
print("\n[3/6] 训练集/测试集划分 (前70%训练)...")
|
||
n = len(X); split = int(n * 0.7)
|
||
X_train, X_test = X.iloc[:split], X.iloc[split:]
|
||
y_train, y_test = y.iloc[:split], y.iloc[split:]
|
||
print(" 训练集: {} 行 ({} ~ {})".format(len(X_train), X_train.index[0], X_train.index[-1]))
|
||
print(" 测试集: {} 行 ({} ~ {})".format(len(X_test), X_test.index[0], X_test.index[-1]))
|
||
|
||
print("\n[4/6] LightGBM 训练...")
|
||
lgb_model = LightGBMModel(eval_ratio=0.0, early_stopping=100)
|
||
lgb_model.fit(X_train, y_train)
|
||
ic_lgb = lgb_model.predict(X_test).corr(y_test)
|
||
print(" 测试集 IC: {:.4f} trees: {}".format(ic_lgb, lgb_model.n_estimators_used))
|
||
imp = lgb_model.get_feature_importance()
|
||
print(" Top 5 特征:")
|
||
for _, row in imp.head(5).iterrows():
|
||
print(" {:25s} {:5.1f}%".format(row["feature"], row["importance_pct"]))
|
||
# 解读
|
||
abs_ic = abs(ic_lgb)
|
||
if abs_ic < 0.03:
|
||
print(" > 解读: IC 接近 0,单股票预测信号极弱(正常现象)。多股票截面预测效果更好。")
|
||
elif abs_ic < 0.08:
|
||
print(" > 解读: IC {:.3f} 有微弱预测能力,可用于因子组合。".format(ic_lgb))
|
||
else:
|
||
print(" > 解读: IC {:.3f} 有显著预测能力,特征工程有效。".format(ic_lgb))
|
||
top_feat = imp.iloc[0]
|
||
print(" > 最重要特征 '{}' 占比 {:.1f}%,说明该类因子对短期收益影响最大。".format(top_feat["feature"], top_feat["importance_pct"]))
|
||
|
||
print("\n[5/6] CatBoost 训练...")
|
||
cb_model = CatBoostModel(eval_ratio=0.0, early_stopping=100)
|
||
cb_model.fit(X_train, y_train)
|
||
ic_cb = cb_model.predict(X_test).corr(y_test)
|
||
print(" 测试集 IC: {:.4f} trees: {}".format(ic_cb, cb_model.n_estimators_used))
|
||
if abs(ic_cb) < 0.03:
|
||
print(" > 解读: CatBoost IC 同样接近 0,两者结论一致:单股票短期收益很难预测。")
|
||
|
||
print("\n[6/6] ML 策略回测对比...")
|
||
test_price = price_df.loc[X_test.index]
|
||
test_factor = factor_df.loc[X_test.index]
|
||
bt_engine = VectorBTEngine()
|
||
benchmark = MLBenchmark([lgb_model, cb_model], fe, test_factor, test_price, bt_engine)
|
||
result = benchmark.run()
|
||
print(result.round(2).to_string())
|
||
print(" > 解读: 回测结果反映 ML 策略在测试集上的实盘表现。")
|
||
print(" > 正收益+高夏普=模型有效;负收益=需更多特征或换截面预测。")
|
||
print(" > 单股票 ML 策略通常不如多因子规则策略稳定,这是正常现象。")
|
||
|
||
try:
|
||
from reports.storage import save_report
|
||
|
||
# 组装完整报告
|
||
report_lines = []
|
||
report_lines.append("# ML 模型训练报告 — {}".format(args.ts_code))
|
||
report_lines.append("")
|
||
report_lines.append("## 数据概况")
|
||
report_lines.append("- 日线: {} 条 ({} ~ {})".format(len(price_df), price_df.index[0], price_df.index[-1]))
|
||
report_lines.append("- 因子: {} 个".format(factor_df.shape[1]))
|
||
report_lines.append("- 特征矩阵: {} × {}".format(X.shape[0], X.shape[1]))
|
||
report_lines.append("- 标签 (未来{}日收益): mean={:.2f}% std={:.2f}%".format(args.lookahead, y.mean(), y.std()))
|
||
report_lines.append("- 训练集: {} 行 | 测试集: {} 行".format(len(X_train), len(X_test)))
|
||
report_lines.append("")
|
||
|
||
report_lines.append("## LightGBM")
|
||
report_lines.append("- 测试集 IC: {:.4f} | 树数: {}".format(ic_lgb, lgb_model.n_estimators_used))
|
||
report_lines.append("- 解读: {}".format(
|
||
"IC 接近 0,单股票预测信号极弱(正常现象)" if abs(ic_lgb) < 0.03
|
||
else "IC {:.3f} 有微弱预测能力".format(ic_lgb) if abs(ic_lgb) < 0.08
|
||
else "IC {:.3f} 有显著预测能力".format(ic_lgb)))
|
||
report_lines.append("")
|
||
report_lines.append("### 特征重要性 (Top 10)")
|
||
report_lines.append("| 特征 | 重要性 |")
|
||
report_lines.append("|------|--------|")
|
||
for _, row in imp.head(10).iterrows():
|
||
report_lines.append("| {} | {:.1f}% |".format(row["feature"], row["importance_pct"]))
|
||
report_lines.append("")
|
||
|
||
report_lines.append("## CatBoost")
|
||
report_lines.append("- 测试集 IC: {:.4f} | 树数: {}".format(ic_cb, cb_model.n_estimators_used))
|
||
report_lines.append("- 解读: {}".format(
|
||
"IC 接近 0,两者结论一致:单股票短期收益很难预测" if abs(ic_cb) < 0.03
|
||
else "IC {:.3f}".format(ic_cb)))
|
||
cb_imp = cb_model.get_feature_importance()
|
||
report_lines.append("")
|
||
report_lines.append("### 特征重要性 (Top 10)")
|
||
report_lines.append("| 特征 | 重要性 |")
|
||
report_lines.append("|------|--------|")
|
||
for _, row in cb_imp.head(10).iterrows():
|
||
report_lines.append("| {} | {:.1f}% |".format(row["feature"], row["importance_pct"]))
|
||
report_lines.append("")
|
||
|
||
report_lines.append("## 回测对比")
|
||
report_lines.append("")
|
||
|
||
# 将 DataFrame 转为 MD 管道表格
|
||
if result is not None and not result.empty:
|
||
cols = result.columns.tolist()
|
||
report_lines.append("| model | " + " | ".join(cols) + " |")
|
||
report_lines.append("|" + "|".join(["------"] * (len(cols) + 1)) + "|")
|
||
for idx, row in result.iterrows():
|
||
vals = []
|
||
for c in cols:
|
||
v = row[c]
|
||
vals.append("{:.2f}".format(v) if isinstance(v, (int, float)) and not np.isnan(v) else str(v) if not (isinstance(v, float) and np.isnan(v)) else "-")
|
||
report_lines.append("| " + str(idx) + " | " + " | ".join(vals) + " |")
|
||
report_lines.append("")
|
||
report_lines.append("> 解读: 正收益+高夏普=模型有效;负收益=需更多特征或换截面预测。单股票 ML 策略通常不如多因子规则策略稳定。")
|
||
else:
|
||
report_lines.append("无回测数据")
|
||
report_lines.append("")
|
||
report_lines.append("> 解读: 正收益+高夏普=模型有效;负收益=需更多特征或换截面预测。单股票 ML 策略通常不如多因子规则策略稳定,这是正常现象。")
|
||
|
||
save_report("\n".join(report_lines), "ML 模型训练报告",
|
||
subject_type="stock", subject_code=args.ts_code)
|
||
print("\n 报告已存入 DB")
|
||
except Exception as e:
|
||
print("\n [WARN] 报告入库失败: {}".format(e))
|
||
|
||
print("\n" + "=" * 60)
|
||
print("Sprint 4 验证完成")
|
||
print("=" * 60)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|