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

421 lines
22 KiB
Python

#!/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()