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

462 lines
21 KiB
Python

#!/usr/bin/env python3
"""
高速系统性算法搜索 v5.3 — 内存回测引擎
核心优化:预加载所有数据到内存,避免回测时反复查询数据库。
- 旧版: ~22s/次 (DB查询) → 新版: ~0.2s/次 (内存读取)
- 3000+ 种组合约需 10-15 分钟(而非 47 小时)
两阶段搜索:
Phase 1: 用活跃季度(2025-Q3)快速筛选出Top 60
Phase 2: 用全期间(2025-01~2026-02)验证Top 60
"""
import sys
import os
import time
import itertools
from datetime import date, datetime
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from backtest_recommend import get_db_conn, run_backtest, preload_all_data
CAPITAL = 200_000
# ─── 搜索空间 ─────────────────
SEARCH_SPACE = {
'tp_sl': [
(6, 3), (8, 4), (8, 6), (10, 5), (10, 8),
(12, 6), (12, 8), (15, 8), (15, 10), (20, 10), (20, 12),
],
'min_triggered': [0, 1, 2, 3],
'sell_mode': ['normal', 'ignore', 'delay1', 'delay2', 'delay3'],
'trailing': [
None,
(6, 2), (8, 3), (10, 3), (10, 5), (12, 4), (12, 5), (15, 5),
],
'position_pct': [3, 5, 8, 10, 15],
'signal_weight': [False, True],
'max_hold_days': [0, 20, 30, 60],
}
# Phase 1 筛选期(选一个有代表性的活跃季度)
SCREEN_START = date(2025, 7, 1)
SCREEN_END = date(2025, 9, 30)
# Phase 2 全量验证期
FULL_START = date(2025, 1, 2)
FULL_END = date(2026, 2, 25)
TOP_N_SCREEN = 60 # Phase 1 筛出前60进入Phase 2
TOP_N_FINAL = 30 # Phase 2 展示前30
def build_params(tp_sl, min_triggered, sell_mode, trailing, position_pct, signal_weight, max_hold_days):
"""将组合参数转为 run_backtest 的 kwargs"""
params = {
'total_capital': CAPITAL,
'position_pct': position_pct,
'signal_weight': signal_weight,
'use_5min_prices': True, # v7: 启用5分钟实时价格
'buy_time': '09:35', # v7: 最优买入时间
'sell_time': '13:40', # v7: 最优卖出时间
'verbose': False,
}
tp, sl = tp_sl
params['stop_loss_pct'] = sl
if trailing is not None:
# 跟踪止盈模式:不用固定止盈,自动忽略卖出信号
params['take_profit_pct'] = None
params['trailing_start_pct'] = trailing[0]
params['trailing_gap_pct'] = trailing[1]
params['ignore_sell_signal'] = True
else:
params['take_profit_pct'] = tp
if min_triggered > 0:
params['min_buy_triggered'] = min_triggered
if sell_mode == 'ignore':
params['ignore_sell_signal'] = True
elif sell_mode.startswith('delay'):
days = int(sell_mode.replace('delay', ''))
params['sell_confirm_days'] = days
if max_hold_days > 0:
params['max_hold_days'] = max_hold_days
return params
def make_name(tp_sl, min_triggered, sell_mode, trailing, position_pct, signal_weight, max_hold_days):
"""生成人类可读的策略名"""
parts = []
tp, sl = tp_sl
if trailing:
parts.append(f"T{trailing[0]}/{trailing[1]}")
else:
parts.append(f"TP{tp}")
parts.append(f"SL{sl}")
if min_triggered > 0:
parts.append(f"trig≥{min_triggered}")
if sell_mode == 'ignore':
parts.append("ign")
elif sell_mode.startswith('delay'):
parts.append(sell_mode)
if max_hold_days > 0:
parts.append(f"h≤{max_hold_days}")
parts.append(f"{position_pct}%")
if signal_weight:
parts.append("SW")
return "|".join(parts)
def is_redundant(sell_mode, trailing):
"""剪枝:跟踪止盈模式下,delay/normal卖出不生效"""
if trailing is not None and sell_mode in ('delay1', 'delay2', 'delay3'):
return True
if trailing is not None and sell_mode == 'normal':
return True
return False
def main():
conn = get_db_conn()
# 生成所有组合并剪枝
all_combos = list(itertools.product(
SEARCH_SPACE['tp_sl'],
SEARCH_SPACE['min_triggered'],
SEARCH_SPACE['sell_mode'],
SEARCH_SPACE['trailing'],
SEARCH_SPACE['position_pct'],
SEARCH_SPACE['signal_weight'],
SEARCH_SPACE['max_hold_days'],
))
combos = [(tp_sl, mt, sm, tr, pp, sw, mh)
for tp_sl, mt, sm, tr, pp, sw, mh in all_combos
if not is_redundant(sm, tr)]
print(f"{'='*100}")
print(f" 🚀 高速系统性算法搜索 v7.0 (内存回测引擎 + 最优时点09:35/13:40)")
print(f"{'='*100}")
print(f" 本金: ¥{CAPITAL:,}")
print(f" 组合总数: {len(all_combos):,} → 剪枝后: {len(combos):,}")
print(f" Phase 1: 快速筛选 ({SCREEN_START} ~ {SCREEN_END})")
print(f" Phase 2: 全量验证 Top {TOP_N_SCREEN} ({FULL_START} ~ {FULL_END})")
print(f"{'='*100}\n")
# ════════════════ 预加载数据 ════════════════
print(" 📦 预加载回测数据...")
# 加载全量数据(覆盖Phase 1和Phase 2的完整范围,含5分钟K线)
data_all = preload_all_data(conn, FULL_START, FULL_END, use_5min=True)
print()
# ════════════════ Phase 1: 快速筛选 ════════════════
print(f" ▶ Phase 1: 快速筛选 {len(combos):,} 种组合...")
phase1_results = []
t_start = time.time()
for i, (tp_sl, mt, sm, tr, pp, sw, mh) in enumerate(combos):
name = make_name(tp_sl, mt, sm, tr, pp, sw, mh)
params = build_params(tp_sl, mt, sm, tr, pp, sw, mh)
if (i + 1) % 200 == 0 or i == 0:
elapsed = time.time() - t_start
speed = (i + 1) / elapsed if elapsed > 0 else 0
eta = (len(combos) - i - 1) / speed if speed > 0 else 0
best_name = phase1_results[0]['name'] if phase1_results else 'N/A'
best_profit = phase1_results[0]['profit'] if phase1_results else 0
print(f" [{i+1:>5}/{len(combos)}] {elapsed:.0f}s ({speed:.1f}次/秒) "
f"ETA:{eta:.0f}s Top1: ¥{best_profit:+,.0f} ({best_name[:35]})", flush=True)
# 使用预加载数据进行内存回测
result = run_backtest(
conn, start_date=SCREEN_START, end_date=SCREEN_END,
preloaded=data_all, **params
)
if result and result.get('stats'):
s = result['stats']
phase1_results.append({
'name': name,
'combo': (tp_sl, mt, sm, tr, pp, sw, mh),
'profit': s['profit'],
'capital_pct': s['capital_pct'],
'capital_ann_pct': s.get('capital_ann_pct', 0),
'win_rate': s['win_rate'],
'profit_factor': s['profit_factor'],
'trade_count': s['trade_count'],
'max_drawdown_pct': s.get('max_drawdown_pct', 0),
})
phase1_results.sort(key=lambda x: x['profit'], reverse=True)
p1_time = time.time() - t_start
p1_speed = len(combos) / p1_time if p1_time > 0 else 0
print(f"\n ✅ Phase 1 完成! {p1_time:.0f}秒 ({p1_speed:.1f}次/秒), 有效结果: {len(phase1_results)}")
print(f" Phase 1 Top 10:")
for i, r in enumerate(phase1_results[:10], 1):
print(f" {i:>2}. ¥{r['profit']:>+10,.0f} 收益{r['capital_pct']:>+6.1f}% "
f"胜率{r['win_rate']:>5.1f}% PF{r['profit_factor']:>5.2f} 回撤{r['max_drawdown_pct']:>5.1f}% {r['name']}")
# ════════════════ Phase 2: 全量验证 ════════════════
top_candidates = phase1_results[:TOP_N_SCREEN]
print(f"\n ▶ Phase 2: 全期间验证 Top {len(top_candidates)} ...")
phase2_results = []
t2_start = time.time()
for i, cand in enumerate(top_candidates):
tp_sl, mt, sm, tr, pp, sw, mh = cand['combo']
name = cand['name']
params = build_params(tp_sl, mt, sm, tr, pp, sw, mh)
if (i + 1) % 10 == 0 or i == 0:
elapsed = time.time() - t2_start
speed = (i + 1) / elapsed if elapsed > 0 else 0
eta = (len(top_candidates) - i - 1) / speed if speed > 0 else 0
print(f" [{i+1}/{len(top_candidates)}] {elapsed:.0f}s ETA:{eta:.0f}s", flush=True)
# 使用预加载数据进行全量回测
result = run_backtest(
conn, start_date=FULL_START, end_date=FULL_END,
preloaded=data_all, **params
)
if result and result.get('stats'):
s = result['stats']
phase2_results.append({
'name': name,
'combo': cand['combo'],
'screen_profit': cand['profit'],
'profit': s['profit'],
'capital_pct': s['capital_pct'],
'capital_ann_pct': s.get('capital_ann_pct', 0),
'win_rate': s['win_rate'],
'profit_factor': s['profit_factor'],
'max_drawdown_pct': s.get('max_drawdown_pct', 0),
'trade_count': s['trade_count'],
'max_capital': s.get('max_capital', 0),
'avg_hold_days': s.get('avg_hold_days', 0),
'closed_count': s.get('closed_count', 0),
'wins': s.get('wins', 0),
'losses': s.get('losses', 0),
'capital_ann_method': s.get('capital_ann_method', ''),
})
phase2_results.sort(key=lambda x: x['profit'], reverse=True)
p2_time = time.time() - t2_start
total_time = time.time() - t_start
conn.close()
# ═══════════════ 控制台输出 ═══════════════
print(f"\n{'='*130}")
print(f" 🏆 搜索完成! {len(combos):,}种组合 | "
f"Phase1:{p1_time:.0f}s Phase2:{p2_time:.0f}s | "
f"总计:{total_time:.0f}s ({total_time/60:.1f}分钟)")
print(f"{'='*130}")
print(f"\n 📊 全期间 Top {min(TOP_N_FINAL, len(phase2_results))} 算法:\n")
header = (f"{'排名':>4} {'全期盈利':>12} {'Q3盈利':>10} {'真实收益':>8} {'年化':>8} "
f"{'胜率':>6} {'盈亏比':>6} {'回撤':>6} {'交易':>5} {'持仓天':>6} | {'策略'}")
print(f" {header}")
print(" " + "-" * 130)
for rank, r in enumerate(phase2_results[:TOP_N_FINAL], 1):
medal = "🏆" if rank == 1 else ("🥈" if rank == 2 else ("🥉" if rank == 3 else f" {rank:>2}"))
print(f" {medal} {r['profit']:>+11,.0f} {r['screen_profit']:>+9,.0f} "
f"{r['capital_pct']:>+7.1f}% {r['capital_ann_pct']:>+7.1f}% "
f"{r['win_rate']:>5.1f}% {r['profit_factor']:>6.2f} {r['max_drawdown_pct']:>5.1f}% "
f"{r['trade_count']:>5} {r['avg_hold_days']:>5.0f}d | {r['name']}")
# ─── 维度分析 ─────────────────────
if phase2_results:
print(f"\n{'='*130}")
print(f" 📊 维度影响分析")
print(f"{'='*130}")
# 止盈方式
print(f"\n 📈 止盈方式 (跟踪 vs 固定):")
tr_sub = [r for r in phase2_results if r['combo'][3] is not None]
fx_sub = [r for r in phase2_results if r['combo'][3] is None]
if tr_sub:
avg = sum(r['profit'] for r in tr_sub) / len(tr_sub)
best = max(tr_sub, key=lambda x: x['profit'])
print(f" 跟踪止盈: n={len(tr_sub):>3} 平均 ¥{avg:>+9,.0f} 最优 ¥{best['profit']:>+9,.0f} ({best['name'][:40]})")
if fx_sub:
avg = sum(r['profit'] for r in fx_sub) / len(fx_sub)
best = max(fx_sub, key=lambda x: x['profit'])
print(f" 固定止盈: n={len(fx_sub):>3} 平均 ¥{avg:>+9,.0f} 最优 ¥{best['profit']:>+9,.0f} ({best['name'][:40]})")
# 仓位比例
print(f"\n 💰 仓位比例:")
for pct in sorted(set(r['combo'][4] for r in phase2_results)):
subset = [r for r in phase2_results if r['combo'][4] == pct]
if subset:
avg_p = sum(r['profit'] for r in subset) / len(subset)
best = max(subset, key=lambda x: x['profit'])
print(f" {pct:>2}%: n={len(subset):>3} 平均 ¥{avg_p:>+9,.0f} 最优 ¥{best['profit']:>+9,.0f}")
# 信号加权
print(f"\n 📶 信号加权:")
for sw in [False, True]:
subset = [r for r in phase2_results if r['combo'][5] == sw]
if subset:
avg_p = sum(r['profit'] for r in subset) / len(subset)
best = max(subset, key=lambda x: x['profit'])
print(f" {'加权' if sw else '等权':>4}: n={len(subset):>3} 平均 ¥{avg_p:>+9,.0f} 最优 ¥{best['profit']:>+9,.0f}")
# 卖出策略
print(f"\n 🛒 卖出策略:")
for sm_label in ['normal', 'ignore', 'delay1', 'delay2', 'delay3']:
subset = [r for r in phase2_results if r['combo'][2] == sm_label]
if subset:
avg_p = sum(r['profit'] for r in subset) / len(subset)
best = max(subset, key=lambda x: x['profit'])
print(f" {sm_label:>8}: n={len(subset):>3} 平均 ¥{avg_p:>+9,.0f} 最优 ¥{best['profit']:>+9,.0f}")
# 止盈/止损参数
print(f"\n 🎯 止盈线 (固定止盈组合):")
for tp in sorted(set(r['combo'][0][0] for r in fx_sub)) if fx_sub else []:
subset = [r for r in fx_sub if r['combo'][0][0] == tp]
if subset:
avg_p = sum(r['profit'] for r in subset) / len(subset)
best = max(subset, key=lambda x: x['profit'])
print(f" TP={tp:>2}: n={len(subset):>3} 平均 ¥{avg_p:>+9,.0f} 最优 ¥{best['profit']:>+9,.0f}")
print(f"\n 🛡️ 止损线:")
for sl in sorted(set(r['combo'][0][1] for r in phase2_results)):
subset = [r for r in phase2_results if r['combo'][0][1] == sl]
if subset:
avg_p = sum(r['profit'] for r in subset) / len(subset)
best = max(subset, key=lambda x: x['profit'])
print(f" SL={sl:>2}: n={len(subset):>3} 平均 ¥{avg_p:>+9,.0f} 最优 ¥{best['profit']:>+9,.0f}")
# 最大持仓天数
print(f"\n ⏰ 最大持仓天数:")
for mh in sorted(set(r['combo'][6] for r in phase2_results)):
subset = [r for r in phase2_results if r['combo'][6] == mh]
if subset:
avg_p = sum(r['profit'] for r in subset) / len(subset)
best = max(subset, key=lambda x: x['profit'])
label = "不限" if mh == 0 else f"{mh}"
print(f" {label:>4}: n={len(subset):>3} 平均 ¥{avg_p:>+9,.0f} 最优 ¥{best['profit']:>+9,.0f}")
# 买入信号触发数
print(f"\n 🔔 买入信号触发数:")
for mt in sorted(set(r['combo'][1] for r in phase2_results)):
subset = [r for r in phase2_results if r['combo'][1] == mt]
if subset:
avg_p = sum(r['profit'] for r in subset) / len(subset)
best = max(subset, key=lambda x: x['profit'])
label = "不限" if mt == 0 else f"{mt}"
print(f" {label:>4}: n={len(subset):>3} 平均 ¥{avg_p:>+9,.0f} 最优 ¥{best['profit']:>+9,.0f}")
# ─── 与之前冠军对比 ─────────────────────
print(f"\n{'='*130}")
print(f" 📊 与之前最优算法对比")
print(f"{'='*130}")
prev_best = {'profit': 56375, 'capital_pct': 21.5, 'capital_ann_pct': 18.5,
'win_rate': 61.2, 'profit_factor': 2.30, 'name': 'v5.2|忽略卖出+TP10+SL8+信号加权'}
new_best = phase2_results[0] if phase2_results else None
if new_best:
print(f" 之前冠军: ¥{prev_best['profit']:>+10,.0f} 收益{prev_best['capital_pct']:>+6.1f}% "
f"年化{prev_best['capital_ann_pct']:>+6.1f}% 胜率{prev_best['win_rate']:>5.1f}% "
f"PF{prev_best['profit_factor']:>5.2f} {prev_best['name']}")
print(f" 新冠军: ¥{new_best['profit']:>+10,.0f} 收益{new_best['capital_pct']:>+6.1f}% "
f"年化{new_best['capital_ann_pct']:>+6.1f}% 胜率{new_best['win_rate']:>5.1f}% "
f"PF{new_best['profit_factor']:>5.2f} {new_best['name']}")
diff = new_best['profit'] - prev_best['profit']
print(f" 差异: ¥{diff:>+10,.0f} {'🎉 新纪录!' if diff > 0 else '❌ 未超越'}")
# ─── Markdown 输出 ─────────────────────
out_path = os.path.join(os.path.dirname(__file__), "docs", "algo_search_results.md")
os.makedirs(os.path.dirname(out_path), exist_ok=True)
with open(out_path, "w", encoding="utf-8") as f:
f.write("# 🔍 系统性算法搜索结果 (v5.3 内存回测引擎)\n\n")
f.write(f"> 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}\n\n")
f.write("## 搜索配置\n\n")
f.write(f"| 项目 | 值 |\n")
f.write(f"|------|----|\n")
f.write(f"| 本金 | ¥{CAPITAL:,} |\n")
f.write(f"| Phase 1 筛选期 | {SCREEN_START} ~ {SCREEN_END} |\n")
f.write(f"| Phase 2 验证期 | {FULL_START} ~ {FULL_END} |\n")
f.write(f"| 组合总数 | {len(all_combos):,} → 剪枝后 {len(combos):,} |\n")
f.write(f"| Phase 1 耗时 | {p1_time:.0f}s ({p1_speed:.1f}次/秒) |\n")
f.write(f"| Phase 2 耗时 | {p2_time:.0f}s |\n")
f.write(f"| 总耗时 | {total_time:.0f}s ({total_time/60:.1f}分钟) |\n\n")
f.write("## 🏆 全期间 Top 30\n\n")
f.write("| 排名 | 全期盈利 | Q3盈利 | 真实收益 | 年化 | 胜率 | 盈亏比 | 回撤 | 交易 | 持仓天 | 策略 |\n")
f.write("|------|---------|-------|---------|------|------|--------|------|------|--------|------|\n")
for rank, r in enumerate(phase2_results[:TOP_N_FINAL], 1):
medal = "🏆" if rank == 1 else ("🥈" if rank == 2 else ("🥉" if rank == 3 else f"#{rank}"))
f.write(f"| {medal} | ¥{r['profit']:+,.0f} | ¥{r['screen_profit']:+,.0f} | "
f"{r['capital_pct']:+.1f}% | {r['capital_ann_pct']:+.1f}% | "
f"{r['win_rate']:.1f}% | {r['profit_factor']:.2f} | "
f"{r['max_drawdown_pct']:.1f}% | {r['trade_count']} | "
f"{r['avg_hold_days']:.0f}d | `{r['name']}` |\n")
f.write("\n")
# 冠军对比
if new_best:
f.write("## 新冠军 vs 之前冠军\n\n")
f.write("| 指标 | 之前冠军 | 新冠军 |\n")
f.write("|------|---------|-------|\n")
f.write(f"| 策略 | `{prev_best['name']}` | `{new_best['name']}` |\n")
f.write(f"| 全期盈利 | ¥{prev_best['profit']:+,} | ¥{new_best['profit']:+,} |\n")
f.write(f"| 真实收益 | {prev_best['capital_pct']:+.1f}% | {new_best['capital_pct']:+.1f}% |\n")
f.write(f"| 年化 | {prev_best['capital_ann_pct']:+.1f}% | {new_best['capital_ann_pct']:+.1f}% |\n")
f.write(f"| 胜率 | {prev_best['win_rate']:.1f}% | {new_best['win_rate']:.1f}% |\n")
f.write(f"| 盈亏比 | {prev_best['profit_factor']:.2f} | {new_best['profit_factor']:.2f} |\n")
f.write(f"| 回撤 | - | {new_best['max_drawdown_pct']:.1f}% |\n")
diff = new_best['profit'] - prev_best['profit']
f.write(f"\n{'🎉 **新纪录!**' if diff > 0 else '❌ 未超越之前冠军'}\n\n")
# 维度分析
f.write("## 维度影响分析\n\n")
# 仓位比例
f.write("### 仓位比例\n\n")
f.write("| 仓位 | 数量 | 平均盈利 | 最优盈利 |\n")
f.write("|------|------|---------|--------|\n")
for pct in sorted(set(r['combo'][4] for r in phase2_results)):
subset = [r for r in phase2_results if r['combo'][4] == pct]
if subset:
avg_p = sum(r['profit'] for r in subset) / len(subset)
best = max(subset, key=lambda x: x['profit'])
f.write(f"| {pct}% | {len(subset)} | ¥{avg_p:+,.0f} | ¥{best['profit']:+,.0f} |\n")
f.write("\n")
# 信号加权
f.write("### 信号加权\n\n")
f.write("| 模式 | 数量 | 平均盈利 | 最优盈利 |\n")
f.write("|------|------|---------|--------|\n")
for sw in [False, True]:
subset = [r for r in phase2_results if r['combo'][5] == sw]
if subset:
avg_p = sum(r['profit'] for r in subset) / len(subset)
best = max(subset, key=lambda x: x['profit'])
f.write(f"| {'加权' if sw else '等权'} | {len(subset)} | ¥{avg_p:+,.0f} | ¥{best['profit']:+,.0f} |\n")
f.write("\n")
# 止损线
f.write("### 止损线\n\n")
f.write("| 止损 | 数量 | 平均盈利 | 最优盈利 |\n")
f.write("|------|------|---------|--------|\n")
for sl in sorted(set(r['combo'][0][1] for r in phase2_results)):
subset = [r for r in phase2_results if r['combo'][0][1] == sl]
if subset:
avg_p = sum(r['profit'] for r in subset) / len(subset)
best = max(subset, key=lambda x: x['profit'])
f.write(f"| {sl}% | {len(subset)} | ¥{avg_p:+,.0f} | ¥{best['profit']:+,.0f} |\n")
f.write("\n")
print(f"\n📝 完整结果已写入 {out_path}")
if __name__ == "__main__":
main()