""" 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_price, test_factor, 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()