#!/usr/bin/env python3 """ v7.0 交易时点网格搜索 — 寻找最优买入/卖出时间点 在48×48=2,304种时间点组合中搜索最佳买卖时机: 买入时间: 09:35, 09:40, ..., 11:30, 13:05, ..., 15:00 卖出时间: 09:35, 09:40, ..., 11:30, 13:05, ..., 15:00 使用Top 3历史最优算法 × 所有时间点组合,共 ~7,000 种回测。 """ import sys, os, time, argparse from datetime import date, datetime from multiprocessing import Pool, cpu_count from itertools import product sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from backtest_recommend import ( get_db_conn, preload_all_data, run_backtest ) # ─── A股5分钟K线时间点 (48个) ──────────────────── ALL_5MIN_SLOTS = [] # 上午: 09:35 ~ 11:30 for h in range(9, 12): for m in range(0, 60, 5): t = f"{h:02d}:{m:02d}" if t >= "09:35" and t <= "11:30": ALL_5MIN_SLOTS.append(t) # 下午: 13:05 ~ 15:00 for h in range(13, 16): for m in range(0, 60, 5): t = f"{h:02d}:{m:02d}" if t >= "13:05" and t <= "15:00": ALL_5MIN_SLOTS.append(t) # 时间点信息(在main中打印,避免worker进程重复输出) # ─── Top 3 算法 (来自 algo_search_results.md) ──────────── TOP_ALGORITHMS = [ ("🏆TP12|SL6|d3|h30|10%SW", { "take_profit_pct": 12, "stop_loss_pct": 6, "sell_confirm_days": 3, "max_hold_days": 30, "position_pct": 10, "signal_weight": True, "ignore_sell_signal": True, }), ("🥈TP12|SL6|d3|h30|15%SW", { "take_profit_pct": 12, "stop_loss_pct": 6, "sell_confirm_days": 3, "max_hold_days": 30, "position_pct": 15, "signal_weight": True, "ignore_sell_signal": True, }), ("🥉TP10|SL8|ign|15%SW", { "take_profit_pct": 10, "stop_loss_pct": 8, "ignore_sell_signal": True, "position_pct": 15, "signal_weight": True, }), ] # ─── 全局变量(multiprocessing共享)───────────────── _preloaded_data = None def init_worker(preloaded): """每个worker进程初始化时加载预加载数据""" global _preloaded_data _preloaded_data = preloaded def run_single(args): """运行单次回测(供multiprocessing调用)""" algo_name, algo_params, buy_time, sell_time, start, end, total_capital = args try: result = run_backtest( conn=None, start_date=start, end_date=end, preloaded=_preloaded_data, use_5min_prices=True, total_capital=total_capital, buy_time=buy_time, sell_time=sell_time, **algo_params, ) if result and result.get('stats'): s = result['stats'] return { 'algo': algo_name, 'buy_time': buy_time, 'sell_time': sell_time, 'profit': s.get('profit', 0), 'capital_pct': s.get('capital_pct', 0), 'capital_ann': s.get('capital_ann_pct', 0), 'win_rate': s.get('win_rate', 0), 'max_drawdown_pct': s.get('max_drawdown_pct', 0), 'profit_loss_ratio': s.get('profit_loss_ratio', 0), 'trade_count': s.get('trade_count', 0), 'coverage': s.get('5min_coverage', 0), } except Exception as e: pass return None def main(): parser = argparse.ArgumentParser(description="v7.0 交易时点网格搜索") parser.add_argument('--capital', type=float, default=200000, help='总本金 (默认200000)') parser.add_argument('--start', type=str, default=None, help='起始日 YYYY-MM-DD (默认=5min数据起始)') parser.add_argument('--end', type=str, default=None, help='结束日 YYYY-MM-DD') parser.add_argument('--fast', action='store_true', help='快速模式: 仅测试9个代表性时间点') parser.add_argument('--workers', type=int, default=0, help=f'并行进程数 (默认={cpu_count()})') args = parser.parse_args() total_capital = args.capital n_workers = args.workers or cpu_count() # ── 连接数据库 & 确定回测区间 ── conn = get_db_conn() cur = conn.cursor() # 5分钟数据的实际覆盖范围 cur.execute("SELECT MIN(dt::date), MAX(dt::date), COUNT(DISTINCT dt::date) FROM stock_kline_5min") r = cur.fetchone() min_5min_date, max_5min_date, n_5min_days = r print(f"\n[数据] 5分钟K线: {min_5min_date} ~ {max_5min_date} ({n_5min_days}个交易日)") start_date = datetime.strptime(args.start, '%Y-%m-%d').date() if args.start else min_5min_date end_date = datetime.strptime(args.end, '%Y-%m-%d').date() if args.end else date.today() print(f"[回测] 区间: {start_date} ~ {end_date}") print(f"[回测] 本金: ¥{total_capital:,.0f}") print(f"[回测] 算法: {len(TOP_ALGORITHMS)} 种") # ── 时间点选择 ── if args.fast: # 快速模式: 9个代表性时间点 time_slots = ['09:35', '09:45', '10:00', '10:30', '11:00', '13:05', '13:30', '14:00', '14:30', '15:00'] time_slots = [t for t in time_slots if t in ALL_5MIN_SLOTS] else: time_slots = ALL_5MIN_SLOTS n_combos = len(time_slots) ** 2 n_total = n_combos * len(TOP_ALGORITHMS) print(f"[搜索] 时间点: {len(time_slots)} 个 → {n_combos:,} 种组合 × {len(TOP_ALGORITHMS)} 算法 = {n_total:,} 次回测") print(f"[搜索] 并行进程: {n_workers}") # ── 预加载全部数据(含全部48个5分钟时间点)── print(f"\n{'='*60}") print(" 预加载数据...") print(f"{'='*60}") preloaded = preload_all_data(conn, start_date, end_date, use_5min=True, full_5min=True) conn.close() # ── 构建任务列表 ── tasks = [] for algo_name, algo_params in TOP_ALGORITHMS: for buy_t in time_slots: for sell_t in time_slots: tasks.append((algo_name, algo_params, buy_t, sell_t, start_date, end_date, total_capital)) # ── 并行执行 ── print(f"\n开始搜索 ({n_total:,} 次回测)...") t0 = time.time() results = [] with Pool(n_workers, initializer=init_worker, initargs=(preloaded,)) as pool: for i, r in enumerate(pool.imap_unordered(run_single, tasks, chunksize=50)): if r: results.append(r) if (i + 1) % 500 == 0: elapsed = time.time() - t0 speed = (i + 1) / elapsed eta = (n_total - i - 1) / speed print(f" 进度: {i+1}/{n_total} ({(i+1)/n_total*100:.1f}%) | " f"速度: {speed:.0f}/s | ETA: {eta:.0f}s | " f"有效结果: {len(results)}", flush=True) elapsed = time.time() - t0 print(f"\n搜索完成! {len(results):,} 个有效结果, 耗时 {elapsed:.1f}s ({len(results)/elapsed:.1f}次/s)") if not results: print("⚠️ 没有有效结果!") return # ── 分析结果 ── print(f"\n{'='*100}") print(" 📊 分析结果") print(f"{'='*100}") # 1. 按盈利排序 - 全局Top 20 results.sort(key=lambda x: -x['profit']) print(f"\n## 🏆 全局 Top 20 (按绝对盈利)") print(f"{'排名':<4} {'算法':<25} {'买入时间':<8} {'卖出时间':<8} {'盈亏':>10} {'收益%':>7} {'年化%':>7} {'胜率':>6} {'回撤%':>6} {'交易':>5} {'5min%':>5}") print("-" * 100) for i, r in enumerate(results[:20]): print(f"{'🏆' if i==0 else '🥈' if i==1 else '🥉' if i==2 else f'#{i+1}':<4} " f"{r['algo']:<25} {r['buy_time']:<8} {r['sell_time']:<8} " f"¥{r['profit']:>+9,.0f} {r['capital_pct']:>+6.1f}% {r['capital_ann']:>+6.1f}% " f"{r['win_rate']:>5.1f}% {r['max_drawdown_pct']:>5.1f}% {r['trade_count']:>5} {r['coverage']:>4.0f}%") # 2. 按算法分组 - 每个算法的最优时间点 print(f"\n## 📊 每个算法的最优时间点") for algo_name, _ in TOP_ALGORITHMS: algo_results = [r for r in results if r['algo'] == algo_name] if not algo_results: continue algo_results.sort(key=lambda x: -x['profit']) best = algo_results[0] worst = algo_results[-1] default = next((r for r in algo_results if r['buy_time'] == '10:00' and r['sell_time'] == '15:00'), None) print(f"\n {algo_name}:") print(f" 最优: 买@{best['buy_time']} 卖@{best['sell_time']} → ¥{best['profit']:>+,.0f} ({best['capital_pct']:>+.1f}%)") if default: diff = best['profit'] - default['profit'] print(f" 默认: 买@10:00 卖@15:00 → ¥{default['profit']:>+,.0f} ({default['capital_pct']:>+.1f}%)") print(f" 提升: ¥{diff:>+,.0f} ({diff/max(abs(default['profit']),1)*100:>+.1f}%)") print(f" 最差: 买@{worst['buy_time']} 卖@{worst['sell_time']} → ¥{worst['profit']:>+,.0f} ({worst['capital_pct']:>+.1f}%)") print(f" 差距: ¥{best['profit'] - worst['profit']:>,.0f}") # 3. 买入时间热力图 (每个buy_time的平均盈利) print(f"\n## 📈 买入时间热力图 (固定卖出@15:00)") buy_time_profits = {} for r in results: if r['sell_time'] == '15:00': bt = r['buy_time'] if bt not in buy_time_profits: buy_time_profits[bt] = [] buy_time_profits[bt].append(r['profit']) if buy_time_profits: sorted_buy = sorted(buy_time_profits.items(), key=lambda x: -sum(x[1])/len(x[1])) print(f" {'时间':<8} {'平均盈利':>10} {'最高盈利':>10} {'最低盈利':>10}") print(f" {'-'*45}") for bt, profits in sorted_buy: avg = sum(profits) / len(profits) print(f" {bt:<8} ¥{avg:>+9,.0f} ¥{max(profits):>+9,.0f} ¥{min(profits):>+9,.0f}") # 4. 卖出时间热力图 (每个sell_time的平均盈利) print(f"\n## 📉 卖出时间热力图 (固定买入@10:00)") sell_time_profits = {} for r in results: if r['buy_time'] == '10:00': st = r['sell_time'] if st not in sell_time_profits: sell_time_profits[st] = [] sell_time_profits[st].append(r['profit']) if sell_time_profits: sorted_sell = sorted(sell_time_profits.items(), key=lambda x: -sum(x[1])/len(x[1])) print(f" {'时间':<8} {'平均盈利':>10} {'最高盈利':>10} {'最低盈利':>10}") print(f" {'-'*45}") for st, profits in sorted_sell: avg = sum(profits) / len(profits) print(f" {st:<8} ¥{avg:>+9,.0f} ¥{max(profits):>+9,.0f} ¥{min(profits):>+9,.0f}") # 5. 买卖时间交叉分析 (平均盈利矩阵的摘要) print(f"\n## 🔥 最优买卖时间组合 Top 10 (所有算法平均)") combo_profits = {} for r in results: key = (r['buy_time'], r['sell_time']) if key not in combo_profits: combo_profits[key] = [] combo_profits[key].append(r['profit']) sorted_combos = sorted(combo_profits.items(), key=lambda x: -sum(x[1])/len(x[1])) print(f" {'排名':<4} {'买入':<8} {'卖出':<8} {'平均盈利':>10} {'组合数':>6}") print(f" {'-'*42}") for i, (combo, profits) in enumerate(sorted_combos[:10]): avg = sum(profits) / len(profits) print(f" {'🏆' if i==0 else f'#{i+1}':<4} {combo[0]:<8} {combo[1]:<8} ¥{avg:>+9,.0f} {len(profits):>6}") print(f"\n 最差组合:") for i, (combo, profits) in enumerate(sorted_combos[-5:]): avg = sum(profits) / len(profits) print(f" {'#'+str(len(sorted_combos)-4+i):<4} {combo[0]:<8} {combo[1]:<8} ¥{avg:>+9,.0f} {len(profits):>6}") # ── 生成Markdown报告 ── md_path = os.path.join(os.path.dirname(__file__), 'docs', 'timing_search_results.md') os.makedirs(os.path.dirname(md_path), exist_ok=True) with open(md_path, 'w') as f: f.write(f"# ⏰ v7.0 交易时点网格搜索结果\n\n") f.write(f"> 生成时间: {datetime.now():%Y-%m-%d %H:%M}\n\n") f.write(f"## 搜索配置\n\n") f.write(f"| 项目 | 值 |\n|------|----|") f.write(f"\n| 本金 | ¥{total_capital:,.0f} |") f.write(f"\n| 回测区间 | {start_date} ~ {end_date} |") f.write(f"\n| 5分钟数据 | {min_5min_date} ~ {max_5min_date} ({n_5min_days}天) |") f.write(f"\n| 时间点 | {len(time_slots)} 个 |") f.write(f"\n| 组合数 | {n_combos:,} × {len(TOP_ALGORITHMS)} 算法 = {n_total:,} |") f.write(f"\n| 耗时 | {elapsed:.1f}s ({len(results)/elapsed:.1f}次/s) |") f.write(f"\n| 有效结果 | {len(results):,} |") f.write(f"\n\n") # Top 20 f.write(f"## 🏆 全局 Top 20\n\n") f.write(f"| 排名 | 算法 | 买入 | 卖出 | 盈亏 | 收益% | 年化% | 胜率 | 回撤% | 交易 | 5min% |\n") f.write(f"|------|------|------|------|------|-------|-------|------|-------|------|-------|\n") for i, r in enumerate(results[:20]): rank = '🏆' if i==0 else '🥈' if i==1 else '🥉' if i==2 else f'#{i+1}' f.write(f"| {rank} | {r['algo']} | {r['buy_time']} | {r['sell_time']} | " f"¥{r['profit']:>+,.0f} | {r['capital_pct']:>+.1f}% | {r['capital_ann']:>+.1f}% | " f"{r['win_rate']:.1f}% | {r['max_drawdown_pct']:.1f}% | {r['trade_count']} | {r['coverage']:.0f}% |\n") # 每算法最优 f.write(f"\n## 📊 每算法最优时间点\n\n") f.write(f"| 算法 | 最优买入 | 最优卖出 | 最优盈利 | 默认盈利(10:00/15:00) | 提升 |\n") f.write(f"|------|---------|---------|---------|---------------------|------|\n") for algo_name, _ in TOP_ALGORITHMS: algo_res = sorted([r for r in results if r['algo'] == algo_name], key=lambda x: -x['profit']) if not algo_res: continue best = algo_res[0] default = next((r for r in algo_res if r['buy_time'] == '10:00' and r['sell_time'] == '15:00'), None) default_profit = default['profit'] if default else 0 diff = best['profit'] - default_profit f.write(f"| {algo_name} | {best['buy_time']} | {best['sell_time']} | " f"¥{best['profit']:>+,.0f} | ¥{default_profit:>+,.0f} | ¥{diff:>+,.0f} |\n") # 买入时间排名 (卖出固定15:00) f.write(f"\n## 📈 买入时间排名 (卖出固定@15:00)\n\n") f.write(f"| 排名 | 买入时间 | 平均盈利 | 最高盈利 | 最低盈利 |\n") f.write(f"|------|---------|---------|---------|----------|\n") if buy_time_profits: for i, (bt, profits) in enumerate(sorted_buy): avg = sum(profits) / len(profits) rank = '🏆' if i==0 else f'#{i+1}' f.write(f"| {rank} | {bt} | ¥{avg:>+,.0f} | ¥{max(profits):>+,.0f} | ¥{min(profits):>+,.0f} |\n") # 卖出时间排名 (买入固定10:00) f.write(f"\n## 📉 卖出时间排名 (买入固定@10:00)\n\n") f.write(f"| 排名 | 卖出时间 | 平均盈利 | 最高盈利 | 最低盈利 |\n") f.write(f"|------|---------|---------|---------|----------|\n") if sell_time_profits: for i, (st, profits) in enumerate(sorted_sell): avg = sum(profits) / len(profits) rank = '🏆' if i==0 else f'#{i+1}' f.write(f"| {rank} | {st} | ¥{avg:>+,.0f} | ¥{max(profits):>+,.0f} | ¥{min(profits):>+,.0f} |\n") # 最优组合 Top 10 f.write(f"\n## 🔥 最优买卖时间组合 Top 10\n\n") f.write(f"| 排名 | 买入 | 卖出 | 平均盈利 |\n") f.write(f"|------|------|------|----------|\n") for i, (combo, profits) in enumerate(sorted_combos[:10]): avg = sum(profits) / len(profits) rank = '🏆' if i==0 else f'#{i+1}' f.write(f"| {rank} | {combo[0]} | {combo[1]} | ¥{avg:>+,.0f} |\n") # 结论 f.write(f"\n## 💡 结论\n\n") if results: best_overall = results[0] default_results = [r for r in results if r['buy_time'] == '10:00' and r['sell_time'] == '15:00'] default_avg = sum(r['profit'] for r in default_results) / len(default_results) if default_results else 0 best_avg_combo = sorted_combos[0] if sorted_combos else None f.write(f"1. **全局最优**: {best_overall['algo']} 买@{best_overall['buy_time']} 卖@{best_overall['sell_time']} → ¥{best_overall['profit']:>+,.0f}\n") f.write(f"2. **默认(10:00/15:00)平均盈利**: ¥{default_avg:>+,.0f}\n") if best_avg_combo: avg = sum(best_avg_combo[1]) / len(best_avg_combo[1]) f.write(f"3. **最优时间组合(跨算法平均)**: 买@{best_avg_combo[0][0]} 卖@{best_avg_combo[0][1]} → 平均¥{avg:>+,.0f}\n") f.write(f"4. **时点优化潜在提升**: ¥{avg - default_avg:>+,.0f}\n") print(f"\n📄 报告已保存: {md_path}") print("完成!") if __name__ == '__main__': main()