#!/usr/bin/env python3 """运行多组回测场景并输出对比表(v4.2 真实资金收益率版)。 v3 基线 vs v4 优化 vs v4.2 延迟确认 全面对比。 核心改进: 用真实占用资金(而非总周转金额)计算收益率和年化。""" import sys import os import argparse from datetime import date, datetime sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from backtest_recommend import ( get_db_conn, get_codes_with_data, run_backtest, START_DATE, MAX_POSITION_AMOUNT, MAX_CONCURRENT_POSITIONS, PRICE_MIN, PRICE_MAX, SELL_COOLDOWN_DAYS, MAX_BUYS_PER_DAY, ) def main(): parser = argparse.ArgumentParser(description='多场景回测对比(v4.2 真实资金收益率)') parser.add_argument('--start', type=str, default=None, metavar='YYYY-MM-DD', help='回测起始日(默认 2026-01-02)') parser.add_argument('-v', '--verbose', action='store_true', help='每个场景输出每日进度') parser.add_argument('--quick', type=int, default=None, metavar='N', help='仅运行前 N 个场景(快速验证)') parser.add_argument('--v3-only', action='store_true', help='仅运行 v3 基线场景') parser.add_argument('--v4-only', action='store_true', help='仅运行 v4/v4.2 优化场景') args = parser.parse_args() start_date = START_DATE if args.start: try: start_date = datetime.strptime(args.start, '%Y-%m-%d').date() except ValueError: print("错误: --start 格式应为 YYYY-MM-DD") return conn = get_db_conn() end = date.today() try: codes = get_codes_with_data(conn, end, min_days=30) except Exception: codes = [] if not codes: print("错误: 无 stock_kline_daily 数据") conn.close() return # ═══ v3 基线 ═══ v3_scenarios = [ ("v3|仅信号", {}), ("v3|止盈10+损8", {"take_profit_pct": 10, "stop_loss_pct": 8}), ] # ═══ v4 优化(上轮胜出) ═══ v4_scenarios = [ ("v4|触发≥2+止盈10+损8", { "min_buy_triggered": 2, "take_profit_pct": 10, "stop_loss_pct": 8, }), ("v4|触发≥2+跟踪6-3+损5", { "min_buy_triggered": 2, "trailing_start_pct": 6, "trailing_gap_pct": 3, "stop_loss_pct": 5, }), ] # ═══ v4.1 忽略卖出信号(参考对照) ═══ v41_scenarios = [ ("v4.1|忽略卖出+止盈10+损8", { "ignore_sell_signal": True, "take_profit_pct": 10, "stop_loss_pct": 8, }), ("v4.1|忽略+跟踪8-3+损5+20天", { "ignore_sell_signal": True, "trailing_start_pct": 8, "trailing_gap_pct": 3, "stop_loss_pct": 5, "max_hold_days": 20, }), ] # ═══ v4.2 延迟卖出确认(核心创新) ═══ v42_scenarios = [ # ── K: 延迟2天确认 + 各种组合 ── ("v4.2-K1|延迟2天+止盈10+损8", { "sell_confirm_days": 2, "take_profit_pct": 10, "stop_loss_pct": 8, }), ("v4.2-K2|延迟2天+触发≥2+止盈10+损8", { "sell_confirm_days": 2, "min_buy_triggered": 2, "take_profit_pct": 10, "stop_loss_pct": 8, }), ("v4.2-K3|延迟2天+跟踪8-3+损5", { "sell_confirm_days": 2, "trailing_start_pct": 8, "trailing_gap_pct": 3, "stop_loss_pct": 5, }), ("v4.2-K4|延迟2天+跟踪6-3+损5", { "sell_confirm_days": 2, "trailing_start_pct": 6, "trailing_gap_pct": 3, "stop_loss_pct": 5, }), ("v4.2-K5|延迟2天+触发≥2+跟踪8-3+损5", { "sell_confirm_days": 2, "min_buy_triggered": 2, "trailing_start_pct": 8, "trailing_gap_pct": 3, "stop_loss_pct": 5, }), ("v4.2-K6|延迟2天+触发≥2+跟踪6-3+损8", { "sell_confirm_days": 2, "min_buy_triggered": 2, "trailing_start_pct": 6, "trailing_gap_pct": 3, "stop_loss_pct": 8, }), # ── L: 延迟3天确认 ── ("v4.2-L1|延迟3天+止盈10+损8", { "sell_confirm_days": 3, "take_profit_pct": 10, "stop_loss_pct": 8, }), ("v4.2-L2|延迟3天+触发≥2+止盈10+损8", { "sell_confirm_days": 3, "min_buy_triggered": 2, "take_profit_pct": 10, "stop_loss_pct": 8, }), ("v4.2-L3|延迟3天+跟踪8-3+损5", { "sell_confirm_days": 3, "trailing_start_pct": 8, "trailing_gap_pct": 3, "stop_loss_pct": 5, }), ("v4.2-L4|延迟3天+触发≥2+跟踪6-3+损5", { "sell_confirm_days": 3, "min_buy_triggered": 2, "trailing_start_pct": 6, "trailing_gap_pct": 3, "stop_loss_pct": 5, }), # ── M: 延迟确认 + 盈利保护 + 超时 ── ("v4.2-M1|延迟2天+盈保5%+止盈10+损8", { "sell_confirm_days": 2, "profit_protect_pct": 5, "take_profit_pct": 10, "stop_loss_pct": 8, }), ("v4.2-M2|延迟2天+盈保5%+触发≥2+止盈10+损8", { "sell_confirm_days": 2, "profit_protect_pct": 5, "min_buy_triggered": 2, "take_profit_pct": 10, "stop_loss_pct": 8, }), ("v4.2-M3|延迟3天+跟踪8-3+损5+30天", { "sell_confirm_days": 3, "trailing_start_pct": 8, "trailing_gap_pct": 3, "stop_loss_pct": 5, "max_hold_days": 30, }), ("v4.2-M4|延迟2天+触发≥2+跟踪8-3+损5+30天", { "sell_confirm_days": 2, "min_buy_triggered": 2, "trailing_start_pct": 8, "trailing_gap_pct": 3, "stop_loss_pct": 5, "max_hold_days": 30, }), ] # 选择场景 if args.v3_only: scenarios = v3_scenarios elif args.v4_only: scenarios = v4_scenarios + v41_scenarios + v42_scenarios else: scenarios = v3_scenarios + v4_scenarios + v41_scenarios + v42_scenarios if args.quick is not None: scenarios = scenarios[: args.quick] total = len(scenarios) v3_count = len(v3_scenarios) if not args.v4_only else 0 v4_count = len(v4_scenarios) if not args.v3_only else 0 v41_count = len(v41_scenarios) if not args.v3_only else 0 print("=" * 150) print(" 多场景回测对比 v4.2(真实资金收益率 + 延迟卖出确认)") print("=" * 150) print(f" 回测区间 : {start_date} ~ {end}") print(f" 场景数 : {total}") print(f" 股价区间 : {PRICE_MIN}~{PRICE_MAX} 元 | 单只上限 : ¥{MAX_POSITION_AMOUNT:,}") print(f" 每日买入 : 最多 {MAX_BUYS_PER_DAY} 只 | 最大持仓 : {MAX_CONCURRENT_POSITIONS} 只") print(f" 冷却期 : {SELL_COOLDOWN_DAYS} 天") print(f" ⚠️ 本版使用【真实资金收益率】= 盈亏 / 最大同时占用资金") print("=" * 150) print() rows = [] for k, (name, kwargs) in enumerate(scenarios, 1): print(f"[进度] 场景 {k}/{total}: {name}", flush=True) result = run_backtest( conn, start_date=start_date, end_date=end, verbose=args.verbose, **kwargs ) if not result or not result.get('stats'): rows.append((name, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) continue s = result['stats'] rows.append(( name, s['profit'], # 1: 盈亏 s.get('capital_pct', 0), # 2: 真实收益率 s.get('capital_ann_pct', 0), # 3: 真实年化 s['win_rate'], # 4: 胜率 s['max_drawdown'], # 5: 最大回撤 s.get('max_drawdown_pct', 0), # 6: 回撤% s['avg_hold_days'], # 7: 平均持仓 s['profit_factor'], # 8: 盈亏比 s['trade_count'], # 9: 交易数 s.get('max_capital', 0), # 10: 最大占用 s['profit_pct'], # 11: 周转收益率(参考) s['annualized_pct'], # 12: 周转年化(参考) )) mc = s.get('max_capital', 0) cp = s.get('capital_pct', 0) ca = s.get('capital_ann_pct', 0) if not args.verbose: print(f" → 盈亏 ¥{s['profit']:>+10,.0f} 资金占用 ¥{mc:>8,.0f} " f"真实收益 {cp:>+6.1f}% 年化 {ca:>+6.1f}% " f"胜率 {s['win_rate']:>5.1f}% 交易 {s['trade_count']} 笔", flush=True) conn.close() # 找最优(基于真实收益率) if rows: best_profit_idx = max(range(len(rows)), key=lambda i: rows[i][1]) best_cap_idx = max(range(len(rows)), key=lambda i: rows[i][2]) best_ann_idx = max(range(len(rows)), key=lambda i: rows[i][3]) best_winrate_idx = max(range(len(rows)), key=lambda i: rows[i][4]) min_dd_idx = min(range(len(rows)), key=lambda i: rows[i][5]) best_pf_idx = max(range(len(rows)), key=lambda i: rows[i][8]) else: best_profit_idx = best_cap_idx = best_ann_idx = best_winrate_idx = min_dd_idx = best_pf_idx = -1 # 写入对比表 out_path = os.path.join(os.path.dirname(__file__), "docs", "backtest_comparison.md") os.makedirs(os.path.dirname(out_path), exist_ok=True) with open(out_path, "w", encoding="utf-8") as f: f.write("# 回测场景对比 v4.2(真实资金收益率版)\n\n") f.write(f"回测区间: {start_date} ~ {end}\n\n") f.write("> ⚠️ **真实收益率** = 盈亏 / 最大同时占用资金(非总周转金额)\n\n") f.write("## 对比结果\n\n") f.write("| 场景 | 盈亏(元) | 占用资金 | 真实收益 | 真实年化 | 胜率 | 回撤% | 持仓天 | 盈亏比 | 交易 | 标注 |\n") f.write("|------|---------|---------|---------|---------|------|-------|--------|--------|------|------|\n") for idx, row_data in enumerate(rows): name = row_data[0] profit, cap_pct, cap_ann = row_data[1], row_data[2], row_data[3] wr, dd, dd_pct = row_data[4], row_data[5], row_data[6] hold, pf, n, mc = row_data[7], row_data[8], row_data[9], row_data[10] tags = [] if idx == best_profit_idx: tags.append('🏆收益最高') if idx == best_ann_idx and idx != best_profit_idx: tags.append('📈年化最高') if idx == best_cap_idx and idx != best_profit_idx and idx != best_ann_idx: tags.append('💰资金效率') if idx == best_winrate_idx: tags.append('🎯胜率最高') if idx == min_dd_idx: tags.append('🛡️回撤最小') if idx == best_pf_idx and idx != best_profit_idx: tags.append('⚖️盈亏比最佳') tag_str = ' '.join(tags) f.write(f"| {name} | {profit:+,.0f} | ¥{mc:,.0f} | {cap_pct:+.1f}% | {cap_ann:+.1f}% | " f"{wr:.1f}% | {dd_pct:.1f}% | {hold:.0f}天 | {pf:.2f} | {n} | {tag_str} |\n") f.write("\n") # 加说明 f.write("## 指标说明\n\n") f.write("| 指标 | 说明 |\n") f.write("|------|------|\n") f.write("| 占用资金 | 回测期间最大同时持仓成本 |\n") f.write("| 真实收益 | 盈亏 / 最大占用资金 × 100% |\n") f.write("| 真实年化 | 按持续期折算年化(复利公式) |\n") f.write("| 回撤% | 最大回撤 / 最大占用资金 × 100% |\n") f.write("| 盈亏比 | 总盈利金额 / 总亏损金额 |\n") f.write("| 延迟N天 | 连续N天推荐卖出才执行卖出 |\n") f.write("\n") # 控制台表格 print("\n" + "=" * 160) print(" v4.2 整体对比表(★ 真实资金收益率 ★)") print("=" * 160) header = (f"{'场景':<42} {'盈亏(元)':>10} {'占用资金':>10} {'真实收益':>8} {'真实年化':>8} " f"{'胜率':>6} {'回撤%':>7} {'持仓':>6} {'盈亏比':>6} {'交易':>5}") print(header) print("-" * 160) v3_end_idx = v3_count v4_end_idx = v3_count + v4_count v41_end_idx = v4_end_idx + v41_count for idx, row_data in enumerate(rows): name = row_data[0] profit, cap_pct, cap_ann = row_data[1], row_data[2], row_data[3] wr, dd, dd_pct = row_data[4], row_data[5], row_data[6] hold, pf, n, mc = row_data[7], row_data[8], row_data[9], row_data[10] tags = [] if idx == best_profit_idx: tags.append('🏆') if idx == best_ann_idx and idx != best_profit_idx: tags.append('📈') if idx == best_cap_idx and idx != best_profit_idx and idx != best_ann_idx: tags.append('💰') if idx == best_winrate_idx: tags.append('🎯') if idx == min_dd_idx: tags.append('🛡️') if idx == best_pf_idx and idx != best_profit_idx: tags.append('⚖️') tag_str = ''.join(tags) # 分隔线 if not args.v3_only and not args.v4_only: if idx == v3_end_idx and v3_count > 0: print("─" * 160) print(f" {'↑ v3 基线 ↓ v4 优化':^148}") print("─" * 160) if idx == v4_end_idx and v4_count > 0: print("─" * 160) print(f" {'↑ v4 优化 ↓ v4.1 忽略卖出(参考对照)':^148}") print("─" * 160) if idx == v41_end_idx and v41_count > 0: print("─" * 160) print(f" {'↑ v4.1 参考 ↓ v4.2 延迟卖出确认(核心创新)':^148}") print("─" * 160) print(f"{name:<42} {profit:>+10,.0f} {'¥'+str(int(mc)):>10} {cap_pct:>+7.1f}% {cap_ann:>+7.1f}% " f"{wr:>5.1f}% {dd_pct:>6.1f}% {hold:>5.0f}天 {pf:>6.2f} {n:>5} {tag_str}") print("=" * 160) # 总结 if rows and len(rows) > 1: print("\n 📊 关键发现(★ 基于真实资金收益率 ★):") if best_profit_idx >= 0: r = rows[best_profit_idx] print(f" 🏆 绝对收益最高: {r[0]} → ¥{r[1]:+,.0f} (真实{r[2]:+.1f}%, 年化{r[3]:+.1f}%)") if best_ann_idx >= 0 and best_ann_idx != best_profit_idx: r = rows[best_ann_idx] print(f" 📈 年化最高: {r[0]} → 真实年化 {r[3]:+.1f}% (占用 ¥{r[10]:,.0f})") if best_cap_idx >= 0 and best_cap_idx not in (best_profit_idx, best_ann_idx): r = rows[best_cap_idx] print(f" 💰 资金效率最高: {r[0]} → 真实收益 {r[2]:+.1f}% (占用 ¥{r[10]:,.0f})") if best_winrate_idx >= 0: r = rows[best_winrate_idx] print(f" 🎯 胜率最高: {r[0]} → {r[4]:.1f}%") if best_pf_idx >= 0: r = rows[best_pf_idx] print(f" ⚖️ 盈亏比最佳: {r[0]} → {r[8]:.2f}") if min_dd_idx >= 0: r = rows[min_dd_idx] print(f" 🛡️ 回撤最小: {r[0]} → {r[6]:.1f}%") # 银行对比 print("\n 🏦 银行存款利率对比(年化2.5%):") for idx, r in enumerate(rows): ann = r[3] if ann > 2.5: icon = '✅' else: icon = '❌' print(f" {icon} {r[0]:<42} 年化 {ann:>+6.1f}% {'超过银行' if ann > 2.5 else '低于银行'}") print(f"\n场景对比已写入 {out_path}") if __name__ == "__main__": main()