374 lines
17 KiB
Python
374 lines
17 KiB
Python
#!/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()
|