Files
simonandClaude Opus 4.7 271a9343a5 Initial commit: cc-cursor 全链路量化研究平台
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>
2026-06-07 15:59:05 +08:00

176 lines
8.7 KiB
Python
Raw Permalink 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.
"""
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()