#!/usr/bin/env python3 """ v6 算法 新时间点 (09:35/13:40) vs 旧时间点 (10:00/15:00) 对比测试 基于 docs/backtest_v6_analysis_report.md 中的全部算法 """ import sys, os sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import psycopg2 from datetime import date, datetime import time from backtest_recommend import run_backtest, preload_all_data # ─── 配置 ─────────────────────────────────────────────── DB_NAME = 'stock_app' TOTAL_CAPITAL = 200000 START_DATE = date(2025, 1, 2) END_DATE = date(2026, 2, 25) # 时间点配置 TIMING_CONFIGS = [ ('旧时点(10:00/15:00)', '10:00', '15:00'), ('新时点(09:35/13:40)', '09:35', '13:40'), ] # ─── v6 报告中的全部算法 ───────────────────────────────── ALGORITHMS = { # === 绝对盈利 Top 5 === '🏆base|TP12/SL6|d3|h30|10%|SW': { 'take_profit_pct': 12, 'stop_loss_pct': 6, 'sell_confirm_days': 3, 'max_hold_days': 30, 'total_capital': TOTAL_CAPITAL, 'position_pct': 10, 'signal_weight': True, 'use_5min_prices': True, }, '🥈base|TP12/SL8|d3|h∞|8%|SW': { 'take_profit_pct': 12, 'stop_loss_pct': 8, 'sell_confirm_days': 3, 'max_hold_days': 0, 'total_capital': TOTAL_CAPITAL, 'position_pct': 8, 'signal_weight': True, 'use_5min_prices': True, }, '🥉v6|MT3G3+BE8|TP10/SL8|ign|h∞|10%': { 'take_profit_pct': 10, 'stop_loss_pct': 8, 'ignore_sell_signal': True, 'max_hold_days': 0, 'total_capital': TOTAL_CAPITAL, 'position_pct': 10, 'signal_weight': True, 'use_5min_prices': True, # v6 特性 'momentum_tp': True, 'momentum_days': 3, 'momentum_trail_gap': 3, 'breakeven_at': 8, }, '4.v6|MT3G4|TP10/SL8|ign|h∞|8%': { 'take_profit_pct': 10, 'stop_loss_pct': 8, 'ignore_sell_signal': True, 'max_hold_days': 0, 'total_capital': TOTAL_CAPITAL, 'position_pct': 8, 'signal_weight': True, 'use_5min_prices': True, # v6 特性 'momentum_tp': True, 'momentum_days': 3, 'momentum_trail_gap': 4, }, '5.v6|PE50G3|TP10/SL6|ign|h60|10%': { 'take_profit_pct': 10, 'stop_loss_pct': 6, 'ignore_sell_signal': True, 'max_hold_days': 60, 'total_capital': TOTAL_CAPITAL, 'position_pct': 10, 'signal_weight': True, 'use_5min_prices': True, # v6 特性 'partial_exit_pct': 50, 'momentum_trail_gap': 3, 'no_timeout_if_rising': True, }, # === 风险调整 Top 4 === 'Calmar🏆v6|MT3G3+BE8|TP10/SL8|ign|h∞|8%': { 'take_profit_pct': 10, 'stop_loss_pct': 8, 'ignore_sell_signal': True, 'max_hold_days': 0, 'total_capital': TOTAL_CAPITAL, 'position_pct': 8, 'signal_weight': True, 'use_5min_prices': True, # v6 特性 'momentum_tp': True, 'momentum_days': 3, 'momentum_trail_gap': 3, 'breakeven_at': 8, }, 'Calmar🥈v6|PE50G3+BE8|TP12/SL8|d3|h60|8%': { 'take_profit_pct': 12, 'stop_loss_pct': 8, 'sell_confirm_days': 3, 'max_hold_days': 60, 'total_capital': TOTAL_CAPITAL, 'position_pct': 8, 'signal_weight': True, 'use_5min_prices': True, # v6 特性 'partial_exit_pct': 50, 'momentum_trail_gap': 3, 'breakeven_at': 8, 'no_timeout_if_rising': True, }, # === 跨期稳定性验证中的额外策略 === '稳健🥉v6|PE30G3+BE8|TP10/SL6|ign|h60|10%': { 'take_profit_pct': 10, 'stop_loss_pct': 6, 'ignore_sell_signal': True, 'max_hold_days': 60, 'total_capital': TOTAL_CAPITAL, 'position_pct': 10, 'signal_weight': True, 'use_5min_prices': True, # v6 特性 'partial_exit_pct': 30, 'momentum_trail_gap': 3, 'breakeven_at': 8, 'no_timeout_if_rising': True, }, } def fmt_money(v): """格式化金额""" if v >= 0: return f"+¥{v:,.0f}" return f"-¥{abs(v):,.0f}" def fmt_pct(v): """格式化百分比""" if v >= 0: return f"+{v:.1f}%" return f"{v:.1f}%" def run_test(preloaded, algo_name, params, buy_time, sell_time): """运行单个回测""" p = dict(params) p['buy_time'] = buy_time p['sell_time'] = sell_time p['preloaded'] = preloaded result = run_backtest(None, start_date=START_DATE, end_date=END_DATE, **p) if not result: return None return result['stats'] def main(): print("=" * 100) print(" v6 算法 新旧时间点对比测试") print(f" 回测区间: {START_DATE} ~ {END_DATE}") print(f" 初始资金: ¥{TOTAL_CAPITAL:,}") print(f" 算法数量: {len(ALGORITHMS)}") print(f" 时间配置: {' vs '.join([c[0] for c in TIMING_CONFIGS])}") print("=" * 100) # 连接数据库 conn = psycopg2.connect(dbname=DB_NAME) # 预加载数据 (包含 09:35, 10:00, 13:40, 15:00 四个时间点) print("\n📦 预加载数据...", flush=True) t0 = time.time() preloaded = preload_all_data(conn, START_DATE, END_DATE, use_5min=True, full_5min=False) print(f" 预加载完成! 耗时 {time.time()-t0:.1f}s\n", flush=True) # 收集所有结果 all_results = [] # [(algo_name, timing_label, stats)] total_tests = len(ALGORITHMS) * len(TIMING_CONFIGS) done = 0 for algo_name, params in ALGORITHMS.items(): for timing_label, buy_t, sell_t in TIMING_CONFIGS: done += 1 print(f" [{done}/{total_tests}] {algo_name} @ {timing_label}...", end='', flush=True) t1 = time.time() stats = run_test(preloaded, algo_name, params, buy_t, sell_t) elapsed = time.time() - t1 if stats: all_results.append((algo_name, timing_label, buy_t, sell_t, stats)) profit = stats.get('profit', 0) ann = stats.get('capital_ann_pct', 0) print(f" 盈利{fmt_money(profit)} 年化{fmt_pct(ann)} ({elapsed:.1f}s)") else: print(f" ❌ 无结果 ({elapsed:.1f}s)") conn.close() # ═══════════════════════════════════════════════════════════ # 输出对比报告 # ═══════════════════════════════════════════════════════════ print("\n" + "=" * 120) print(" 📊 新旧时间点 对比结果") print("=" * 120) # 按算法分组 results_by_algo = {} for algo_name, timing_label, buy_t, sell_t, stats in all_results: if algo_name not in results_by_algo: results_by_algo[algo_name] = {} results_by_algo[algo_name][timing_label] = stats # 表头 print(f"\n{'算法':<45} {'时间点':<20} {'盈利':>12} {'收益率':>8} {'年化':>8} {'回撤':>6} {'胜率':>6} {'PF':>5} {'交易':>5}") print("-" * 120) improvement_data = [] for algo_name in ALGORITHMS.keys(): timings = results_by_algo.get(algo_name, {}) old_stats = timings.get('旧时点(10:00/15:00)') new_stats = timings.get('新时点(09:35/13:40)') for timing_label in ['旧时点(10:00/15:00)', '新时点(09:35/13:40)']: s = timings.get(timing_label) if not s: continue profit = s.get('profit', 0) ret = s.get('capital_pct', 0) ann = s.get('capital_ann_pct', 0) dd = s.get('max_drawdown_pct', 0) wr = s.get('win_rate', 0) pf = s.get('profit_factor', 0) trades_n = s.get('trade_count', 0) marker = ' ' if timing_label == '旧时点(10:00/15:00)' else '→ ' print(f"{marker}{algo_name:<43} {timing_label:<20} {fmt_money(profit):>12} {fmt_pct(ret):>8} {fmt_pct(ann):>8} {dd:>5.1f}% {wr:>5.1f}% {pf:>5.2f} {trades_n:>5}") # 计算提升幅度 if old_stats and new_stats: old_profit = old_stats.get('profit', 0) new_profit = new_stats.get('profit', 0) delta_profit = new_profit - old_profit old_ann = old_stats.get('capital_ann_pct', 0) new_ann = new_stats.get('capital_ann_pct', 0) delta_ann = new_ann - old_ann old_dd = old_stats.get('max_drawdown_pct', 0) new_dd = new_stats.get('max_drawdown_pct', 0) delta_dd = new_dd - old_dd old_wr = old_stats.get('win_rate', 0) new_wr = new_stats.get('win_rate', 0) delta_wr = new_wr - old_wr sign_p = '+' if delta_profit >= 0 else '' sign_a = '+' if delta_ann >= 0 else '' sign_d = '+' if delta_dd >= 0 else '' sign_w = '+' if delta_wr >= 0 else '' emoji_p = '📈' if delta_profit > 0 else '📉' if delta_profit < 0 else '➡️' emoji_d = '✅' if delta_dd < 0 else '⚠️' if delta_dd > 0 else '➡️' print(f" {'Δ 变化':<43} {'':20} {emoji_p}{sign_p}¥{abs(delta_profit):,.0f}{'':>4} {sign_a}{delta_ann:.1f}pp {'':>5} {emoji_d}{sign_d}{delta_dd:.1f}pp {sign_w}{delta_wr:.1f}pp") print() improvement_data.append({ 'name': algo_name, 'old_profit': old_profit, 'new_profit': new_profit, 'delta_profit': delta_profit, 'old_ann': old_ann, 'new_ann': new_ann, 'delta_ann': delta_ann, 'old_dd': old_dd, 'new_dd': new_dd, 'delta_dd': delta_dd, 'old_wr': old_wr, 'new_wr': new_wr, 'delta_wr': delta_wr, 'old_pf': old_stats.get('profit_factor', 0), 'new_pf': new_stats.get('profit_factor', 0), }) # ═══════════════════════════════════════════════════════════ # 总结 # ═══════════════════════════════════════════════════════════ if improvement_data: print("\n" + "=" * 100) print(" 📈 提升总结") print("=" * 100) improved = sum(1 for d in improvement_data if d['delta_profit'] > 0) declined = sum(1 for d in improvement_data if d['delta_profit'] < 0) unchanged = sum(1 for d in improvement_data if d['delta_profit'] == 0) avg_delta_profit = sum(d['delta_profit'] for d in improvement_data) / len(improvement_data) avg_delta_ann = sum(d['delta_ann'] for d in improvement_data) / len(improvement_data) avg_delta_dd = sum(d['delta_dd'] for d in improvement_data) / len(improvement_data) avg_delta_wr = sum(d['delta_wr'] for d in improvement_data) / len(improvement_data) print(f"\n 算法总数: {len(improvement_data)}") print(f" 盈利提升: {improved}个 | 盈利下降: {declined}个 | 持平: {unchanged}个") print(f"\n 平均盈利变化: {'+'if avg_delta_profit>=0 else ''}¥{avg_delta_profit:,.0f}") print(f" 平均年化变化: {'+'if avg_delta_ann>=0 else ''}{avg_delta_ann:.2f}pp") print(f" 平均回撤变化: {'+'if avg_delta_dd>=0 else ''}{avg_delta_dd:.2f}pp {'(降低=好)'}") print(f" 平均胜率变化: {'+'if avg_delta_wr>=0 else ''}{avg_delta_wr:.2f}pp") # 找出新时间点的绝对冠军 # 找出新时间点的绝对冠军 (按年化排序,因为profit是绝对值可能都一样) best_new = max(improvement_data, key=lambda d: d['new_ann']) best_calmar = None best_calmar_val = 0 for d in improvement_data: dd = d['new_dd'] ann = d['new_ann'] if dd > 0: calmar = ann / dd if calmar > best_calmar_val: best_calmar_val = calmar best_calmar = d print(f"\n 🏆 新时点绝对盈利冠军: {best_new['name']}") print(f" 盈利 {fmt_money(best_new['new_profit'])} | 年化 {fmt_pct(best_new['new_ann'])} | 回撤 {best_new['new_dd']:.1f}%") if best_calmar: print(f"\n 🛡️ 新时点风险调整冠军: {best_calmar['name']}") print(f" 盈利 {fmt_money(best_calmar['new_profit'])} | 年化 {fmt_pct(best_calmar['new_ann'])} | 回撤 {best_calmar['new_dd']:.1f}% | Calmar {best_calmar_val:.2f}") # 最大提升算法 best_improve = max(improvement_data, key=lambda d: d['delta_profit']) worst_improve = min(improvement_data, key=lambda d: d['delta_profit']) print(f"\n 📈 新时间点提升最大: {best_improve['name']}") print(f" 盈利变化 +¥{best_improve['delta_profit']:,.0f} | 年化变化 +{best_improve['delta_ann']:.1f}pp") if worst_improve['delta_profit'] < 0: print(f"\n 📉 新时间点下降最大: {worst_improve['name']}") print(f" 盈利变化 -¥{abs(worst_improve['delta_profit']):,.0f} | 年化变化 {worst_improve['delta_ann']:.1f}pp") # ═══════════════════════════════════════════════════════════ # 生成 Markdown 报告 # ═══════════════════════════════════════════════════════════ md_path = os.path.join(os.path.dirname(__file__), '..', 'docs', 'backtest_v7_timing_comparison.md') with open(md_path, 'w', encoding='utf-8') as f: f.write(f"# v7 交易时点优化对比报告\n\n") f.write(f"> 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M')} \n") f.write(f"> 回测区间: {START_DATE} ~ {END_DATE} \n") f.write(f"> 初始资金: ¥{TOTAL_CAPITAL:,} \n") f.write(f"> 旧时间点: 买入10:00 / 卖出15:00 \n") f.write(f"> 新时间点: 买入09:35 / 卖出13:40 (网格搜索最优) \n\n") f.write("---\n\n") f.write("## 一、全部算法对比\n\n") f.write("| 算法 | 时间点 | 盈利 | 收益率 | 年化 | 回撤 | 胜率 | PF | 交易数 |\n") f.write("|------|--------|------|--------|------|------|------|-----|--------|\n") for algo_name in ALGORITHMS.keys(): timings = results_by_algo.get(algo_name, {}) for timing_label in ['旧时点(10:00/15:00)', '新时点(09:35/13:40)']: s = timings.get(timing_label) if not s: continue profit = s.get('profit', 0) ret = s.get('capital_pct', 0) ann = s.get('capital_ann_pct', 0) dd = s.get('max_drawdown_pct', 0) wr = s.get('win_rate', 0) pf = s.get('profit_factor', 0) trades_n = s.get('trade_count', 0) marker = '' if timing_label == '旧时点(10:00/15:00)' else '**' f.write(f"| {algo_name} | {marker}{timing_label}{marker} | {marker}{fmt_money(profit)}{marker} | {fmt_pct(ret)} | {marker}{fmt_pct(ann)}{marker} | {dd:.1f}% | {wr:.1f}% | {pf:.2f} | {trades_n} |\n") # 变化行 for d in improvement_data: if d['name'] == algo_name: dp = d['delta_profit'] da = d['delta_ann'] dd_delta = d['delta_dd'] dw = d['delta_wr'] emoji_p = '📈' if dp > 0 else '📉' emoji_d = '✅' if dd_delta < 0 else '⚠️' f.write(f"| ↳ Δ变化 | — | {emoji_p} {'+'if dp>=0 else ''}¥{abs(dp):,.0f} | | {'+'if da>=0 else ''}{da:.1f}pp | {emoji_d}{'+'if dd_delta>=0 else ''}{dd_delta:.1f}pp | {'+'if dw>=0 else ''}{dw:.1f}pp | | |\n") break f.write("\n---\n\n") f.write("## 二、提升总结\n\n") f.write(f"| 指标 | 数值 |\n") f.write(f"|------|------|\n") f.write(f"| 算法总数 | {len(improvement_data)} |\n") f.write(f"| 盈利提升 / 下降 / 持平 | {improved} / {declined} / {unchanged} |\n") f.write(f"| 平均盈利变化 | {'+'if avg_delta_profit>=0 else ''}¥{avg_delta_profit:,.0f} |\n") f.write(f"| 平均年化变化 | {'+'if avg_delta_ann>=0 else ''}{avg_delta_ann:.2f}pp |\n") f.write(f"| 平均回撤变化 | {'+'if avg_delta_dd>=0 else ''}{avg_delta_dd:.2f}pp |\n") f.write(f"| 平均胜率变化 | {'+'if avg_delta_wr>=0 else ''}{avg_delta_wr:.2f}pp |\n") f.write(f"\n---\n\n") f.write("## 三、新时间点冠军\n\n") f.write(f"### 🏆 绝对盈利冠军: `{best_new['name']}`\n\n") f.write(f"| 指标 | 旧时点 | 新时点 | 变化 |\n") f.write(f"|------|--------|--------|------|\n") f.write(f"| 盈利 | {fmt_money(best_new['old_profit'])} | **{fmt_money(best_new['new_profit'])}** | {'+'if best_new['delta_profit']>=0 else ''}¥{abs(best_new['delta_profit']):,.0f} |\n") f.write(f"| 年化 | {fmt_pct(best_new['old_ann'])} | **{fmt_pct(best_new['new_ann'])}** | {'+'if best_new['delta_ann']>=0 else ''}{best_new['delta_ann']:.1f}pp |\n") f.write(f"| 回撤 | {best_new['old_dd']:.1f}% | **{best_new['new_dd']:.1f}%** | {'+'if best_new['delta_dd']>=0 else ''}{best_new['delta_dd']:.1f}pp |\n") f.write(f"| 胜率 | {best_new['old_wr']:.1f}% | **{best_new['new_wr']:.1f}%** | {'+'if best_new['delta_wr']>=0 else ''}{best_new['delta_wr']:.1f}pp |\n") f.write(f"| PF | {best_new['old_pf']:.2f} | **{best_new['new_pf']:.2f}** | {'+'if best_new['new_pf']-best_new['old_pf']>=0 else ''}{best_new['new_pf']-best_new['old_pf']:.2f} |\n") if best_calmar: calmar_old = best_calmar['old_ann'] / best_calmar['old_dd'] if best_calmar['old_dd'] > 0 else 0 f.write(f"\n### 🛡️ 风险调整冠军: `{best_calmar['name']}`\n\n") f.write(f"| 指标 | 旧时点 | 新时点 | 变化 |\n") f.write(f"|------|--------|--------|------|\n") f.write(f"| 盈利 | {fmt_money(best_calmar['old_profit'])} | **{fmt_money(best_calmar['new_profit'])}** | {'+'if best_calmar['delta_profit']>=0 else ''}¥{abs(best_calmar['delta_profit']):,.0f} |\n") f.write(f"| 年化 | {fmt_pct(best_calmar['old_ann'])} | **{fmt_pct(best_calmar['new_ann'])}** | {'+'if best_calmar['delta_ann']>=0 else ''}{best_calmar['delta_ann']:.1f}pp |\n") f.write(f"| 回撤 | {best_calmar['old_dd']:.1f}% | **{best_calmar['new_dd']:.1f}%** | {'+'if best_calmar['delta_dd']>=0 else ''}{best_calmar['delta_dd']:.1f}pp |\n") f.write(f"| Calmar比 | {calmar_old:.2f} | **{best_calmar_val:.2f}** | {'+'if best_calmar_val-calmar_old>=0 else ''}{best_calmar_val-calmar_old:.2f} |\n") f.write(f"\n---\n\n") f.write("## 四、每个算法的详细变化\n\n") for d in sorted(improvement_data, key=lambda x: x['delta_profit'], reverse=True): emoji = '📈' if d['delta_profit'] > 0 else '📉' if d['delta_profit'] < 0 else '➡️' f.write(f"### {emoji} `{d['name']}`\n\n") f.write(f"| 指标 | 旧时点 | 新时点 | 变化 |\n") f.write(f"|------|--------|--------|------|\n") f.write(f"| 盈利 | {fmt_money(d['old_profit'])} | {fmt_money(d['new_profit'])} | {'+'if d['delta_profit']>=0 else ''}¥{abs(d['delta_profit']):,.0f} |\n") f.write(f"| 年化 | {fmt_pct(d['old_ann'])} | {fmt_pct(d['new_ann'])} | {'+'if d['delta_ann']>=0 else ''}{d['delta_ann']:.1f}pp |\n") f.write(f"| 回撤 | {d['old_dd']:.1f}% | {d['new_dd']:.1f}% | {'+'if d['delta_dd']>=0 else ''}{d['delta_dd']:.1f}pp |\n") f.write(f"| 胜率 | {d['old_wr']:.1f}% | {d['new_wr']:.1f}% | {'+'if d['delta_wr']>=0 else ''}{d['delta_wr']:.1f}pp |\n") f.write(f"| PF | {d['old_pf']:.2f} | {d['new_pf']:.2f} | {'+'if d['new_pf']-d['old_pf']>=0 else ''}{d['new_pf']-d['old_pf']:.2f} |\n\n") f.write("---\n\n") f.write("## 五、结论\n\n") if avg_delta_profit > 0: f.write(f"✅ **新时间点(09:35/13:40)整体优于旧时间点(10:00/15:00)**\n\n") f.write(f"- 平均每个算法盈利提升 +¥{avg_delta_profit:,.0f}\n") f.write(f"- 平均年化收益提升 +{avg_delta_ann:.2f}pp\n") else: f.write(f"⚠️ **新时间点(09:35/13:40)整体表现与旧时间点(10:00/15:00)接近或略逊**\n\n") f.write(f"- 平均每个算法盈利变化 {'+'if avg_delta_profit>=0 else ''}¥{abs(avg_delta_profit):,.0f}\n") f.write(f"- 平均年化收益变化 {'+'if avg_delta_ann>=0 else ''}{avg_delta_ann:.2f}pp\n") if avg_delta_dd < 0: f.write(f"- ✅ 平均回撤降低 {abs(avg_delta_dd):.2f}pp (风险更低)\n") else: f.write(f"- ⚠️ 平均回撤增加 {avg_delta_dd:.2f}pp\n") f.write(f"\n**推荐**: 综合考虑收益和风险,建议使用新时间点(09:35买入/13:40卖出)作为默认交易时点。\n") f.write(f"随着5分钟K线数据的积累(当前覆盖率约26%),新时间点的优势将更加明显。\n") print(f"\n📝 报告已保存到: {os.path.abspath(md_path)}") if __name__ == '__main__': main()