Files
stock/stock-html/run_timing_search.py
T
freedakgmail 9c7d7abdd4 Initial commit
2026-07-17 18:49:35 +08:00

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