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

375 lines
16 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
"""运行多组回测场景并输出对比表(v4.2 真实资金收益率版)。
v3 基线 vs v4 优化 vs v4.2 延迟确认 全面对比。
核心改进: 用真实占用资金(而非总周转金额)计算收益率和年化。"""
import sys
import os
import argparse
from datetime import date, datetime
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from backtest_recommend import (
get_db_conn, get_codes_with_data, run_backtest, START_DATE,
MAX_POSITION_AMOUNT, MAX_CONCURRENT_POSITIONS, PRICE_MIN, PRICE_MAX,
SELL_COOLDOWN_DAYS, MAX_BUYS_PER_DAY,
)
def main():
parser = argparse.ArgumentParser(description='多场景回测对比(v4.2 真实资金收益率)')
parser.add_argument('--start', type=str, default=None, metavar='YYYY-MM-DD',
help='回测起始日(默认 2026-01-02')
parser.add_argument('-v', '--verbose', action='store_true', help='每个场景输出每日进度')
parser.add_argument('--quick', type=int, default=None, metavar='N',
help='仅运行前 N 个场景(快速验证)')
parser.add_argument('--v3-only', action='store_true', help='仅运行 v3 基线场景')
parser.add_argument('--v4-only', action='store_true', help='仅运行 v4/v4.2 优化场景')
args = parser.parse_args()
start_date = START_DATE
if args.start:
try:
start_date = datetime.strptime(args.start, '%Y-%m-%d').date()
except ValueError:
print("错误: --start 格式应为 YYYY-MM-DD")
return
conn = get_db_conn()
end = date.today()
try:
codes = get_codes_with_data(conn, end, min_days=30)
except Exception:
codes = []
if not codes:
print("错误: 无 stock_kline_daily 数据")
conn.close()
return
# ═══ v3 基线 ═══
v3_scenarios = [
("v3|仅信号", {}),
("v3|止盈10+损8", {"take_profit_pct": 10, "stop_loss_pct": 8}),
]
# ═══ v4 优化(上轮胜出) ═══
v4_scenarios = [
("v4|触发≥2+止盈10+损8", {
"min_buy_triggered": 2,
"take_profit_pct": 10, "stop_loss_pct": 8,
}),
("v4|触发≥2+跟踪6-3+损5", {
"min_buy_triggered": 2,
"trailing_start_pct": 6, "trailing_gap_pct": 3,
"stop_loss_pct": 5,
}),
]
# ═══ v4.1 忽略卖出信号(参考对照) ═══
v41_scenarios = [
("v4.1|忽略卖出+止盈10+损8", {
"ignore_sell_signal": True,
"take_profit_pct": 10, "stop_loss_pct": 8,
}),
("v4.1|忽略+跟踪8-3+损5+20天", {
"ignore_sell_signal": True,
"trailing_start_pct": 8, "trailing_gap_pct": 3,
"stop_loss_pct": 5, "max_hold_days": 20,
}),
]
# ═══ v4.2 延迟卖出确认(核心创新) ═══
v42_scenarios = [
# ── K: 延迟2天确认 + 各种组合 ──
("v4.2-K1|延迟2天+止盈10+损8", {
"sell_confirm_days": 2,
"take_profit_pct": 10, "stop_loss_pct": 8,
}),
("v4.2-K2|延迟2天+触发≥2+止盈10+损8", {
"sell_confirm_days": 2,
"min_buy_triggered": 2,
"take_profit_pct": 10, "stop_loss_pct": 8,
}),
("v4.2-K3|延迟2天+跟踪8-3+损5", {
"sell_confirm_days": 2,
"trailing_start_pct": 8, "trailing_gap_pct": 3,
"stop_loss_pct": 5,
}),
("v4.2-K4|延迟2天+跟踪6-3+损5", {
"sell_confirm_days": 2,
"trailing_start_pct": 6, "trailing_gap_pct": 3,
"stop_loss_pct": 5,
}),
("v4.2-K5|延迟2天+触发≥2+跟踪8-3+损5", {
"sell_confirm_days": 2,
"min_buy_triggered": 2,
"trailing_start_pct": 8, "trailing_gap_pct": 3,
"stop_loss_pct": 5,
}),
("v4.2-K6|延迟2天+触发≥2+跟踪6-3+损8", {
"sell_confirm_days": 2,
"min_buy_triggered": 2,
"trailing_start_pct": 6, "trailing_gap_pct": 3,
"stop_loss_pct": 8,
}),
# ── L: 延迟3天确认 ──
("v4.2-L1|延迟3天+止盈10+损8", {
"sell_confirm_days": 3,
"take_profit_pct": 10, "stop_loss_pct": 8,
}),
("v4.2-L2|延迟3天+触发≥2+止盈10+损8", {
"sell_confirm_days": 3,
"min_buy_triggered": 2,
"take_profit_pct": 10, "stop_loss_pct": 8,
}),
("v4.2-L3|延迟3天+跟踪8-3+损5", {
"sell_confirm_days": 3,
"trailing_start_pct": 8, "trailing_gap_pct": 3,
"stop_loss_pct": 5,
}),
("v4.2-L4|延迟3天+触发≥2+跟踪6-3+损5", {
"sell_confirm_days": 3,
"min_buy_triggered": 2,
"trailing_start_pct": 6, "trailing_gap_pct": 3,
"stop_loss_pct": 5,
}),
# ── M: 延迟确认 + 盈利保护 + 超时 ──
("v4.2-M1|延迟2天+盈保5%+止盈10+损8", {
"sell_confirm_days": 2,
"profit_protect_pct": 5,
"take_profit_pct": 10, "stop_loss_pct": 8,
}),
("v4.2-M2|延迟2天+盈保5%+触发≥2+止盈10+损8", {
"sell_confirm_days": 2,
"profit_protect_pct": 5,
"min_buy_triggered": 2,
"take_profit_pct": 10, "stop_loss_pct": 8,
}),
("v4.2-M3|延迟3天+跟踪8-3+损5+30天", {
"sell_confirm_days": 3,
"trailing_start_pct": 8, "trailing_gap_pct": 3,
"stop_loss_pct": 5, "max_hold_days": 30,
}),
("v4.2-M4|延迟2天+触发≥2+跟踪8-3+损5+30天", {
"sell_confirm_days": 2,
"min_buy_triggered": 2,
"trailing_start_pct": 8, "trailing_gap_pct": 3,
"stop_loss_pct": 5, "max_hold_days": 30,
}),
]
# 选择场景
if args.v3_only:
scenarios = v3_scenarios
elif args.v4_only:
scenarios = v4_scenarios + v41_scenarios + v42_scenarios
else:
scenarios = v3_scenarios + v4_scenarios + v41_scenarios + v42_scenarios
if args.quick is not None:
scenarios = scenarios[: args.quick]
total = len(scenarios)
v3_count = len(v3_scenarios) if not args.v4_only else 0
v4_count = len(v4_scenarios) if not args.v3_only else 0
v41_count = len(v41_scenarios) if not args.v3_only else 0
print("=" * 150)
print(" 多场景回测对比 v4.2(真实资金收益率 + 延迟卖出确认)")
print("=" * 150)
print(f" 回测区间 : {start_date} ~ {end}")
print(f" 场景数 : {total}")
print(f" 股价区间 : {PRICE_MIN}~{PRICE_MAX} 元 | 单只上限 : ¥{MAX_POSITION_AMOUNT:,}")
print(f" 每日买入 : 最多 {MAX_BUYS_PER_DAY} 只 | 最大持仓 : {MAX_CONCURRENT_POSITIONS}")
print(f" 冷却期 : {SELL_COOLDOWN_DAYS}")
print(f" ⚠️ 本版使用【真实资金收益率】= 盈亏 / 最大同时占用资金")
print("=" * 150)
print()
rows = []
for k, (name, kwargs) in enumerate(scenarios, 1):
print(f"[进度] 场景 {k}/{total}: {name}", flush=True)
result = run_backtest(
conn, start_date=start_date, end_date=end,
verbose=args.verbose, **kwargs
)
if not result or not result.get('stats'):
rows.append((name, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
continue
s = result['stats']
rows.append((
name,
s['profit'], # 1: 盈亏
s.get('capital_pct', 0), # 2: 真实收益率
s.get('capital_ann_pct', 0), # 3: 真实年化
s['win_rate'], # 4: 胜率
s['max_drawdown'], # 5: 最大回撤
s.get('max_drawdown_pct', 0), # 6: 回撤%
s['avg_hold_days'], # 7: 平均持仓
s['profit_factor'], # 8: 盈亏比
s['trade_count'], # 9: 交易数
s.get('max_capital', 0), # 10: 最大占用
s['profit_pct'], # 11: 周转收益率(参考)
s['annualized_pct'], # 12: 周转年化(参考)
))
mc = s.get('max_capital', 0)
cp = s.get('capital_pct', 0)
ca = s.get('capital_ann_pct', 0)
if not args.verbose:
print(f" → 盈亏 ¥{s['profit']:>+10,.0f} 资金占用 ¥{mc:>8,.0f} "
f"真实收益 {cp:>+6.1f}% 年化 {ca:>+6.1f}% "
f"胜率 {s['win_rate']:>5.1f}% 交易 {s['trade_count']}", flush=True)
conn.close()
# 找最优(基于真实收益率)
if rows:
best_profit_idx = max(range(len(rows)), key=lambda i: rows[i][1])
best_cap_idx = max(range(len(rows)), key=lambda i: rows[i][2])
best_ann_idx = max(range(len(rows)), key=lambda i: rows[i][3])
best_winrate_idx = max(range(len(rows)), key=lambda i: rows[i][4])
min_dd_idx = min(range(len(rows)), key=lambda i: rows[i][5])
best_pf_idx = max(range(len(rows)), key=lambda i: rows[i][8])
else:
best_profit_idx = best_cap_idx = best_ann_idx = best_winrate_idx = min_dd_idx = best_pf_idx = -1
# 写入对比表
out_path = os.path.join(os.path.dirname(__file__), "docs", "backtest_comparison.md")
os.makedirs(os.path.dirname(out_path), exist_ok=True)
with open(out_path, "w", encoding="utf-8") as f:
f.write("# 回测场景对比 v4.2(真实资金收益率版)\n\n")
f.write(f"回测区间: {start_date} ~ {end}\n\n")
f.write("> ⚠️ **真实收益率** = 盈亏 / 最大同时占用资金(非总周转金额)\n\n")
f.write("## 对比结果\n\n")
f.write("| 场景 | 盈亏(元) | 占用资金 | 真实收益 | 真实年化 | 胜率 | 回撤% | 持仓天 | 盈亏比 | 交易 | 标注 |\n")
f.write("|------|---------|---------|---------|---------|------|-------|--------|--------|------|------|\n")
for idx, row_data in enumerate(rows):
name = row_data[0]
profit, cap_pct, cap_ann = row_data[1], row_data[2], row_data[3]
wr, dd, dd_pct = row_data[4], row_data[5], row_data[6]
hold, pf, n, mc = row_data[7], row_data[8], row_data[9], row_data[10]
tags = []
if idx == best_profit_idx:
tags.append('🏆收益最高')
if idx == best_ann_idx and idx != best_profit_idx:
tags.append('📈年化最高')
if idx == best_cap_idx and idx != best_profit_idx and idx != best_ann_idx:
tags.append('💰资金效率')
if idx == best_winrate_idx:
tags.append('🎯胜率最高')
if idx == min_dd_idx:
tags.append('🛡️回撤最小')
if idx == best_pf_idx and idx != best_profit_idx:
tags.append('⚖️盈亏比最佳')
tag_str = ' '.join(tags)
f.write(f"| {name} | {profit:+,.0f} | ¥{mc:,.0f} | {cap_pct:+.1f}% | {cap_ann:+.1f}% | "
f"{wr:.1f}% | {dd_pct:.1f}% | {hold:.0f}天 | {pf:.2f} | {n} | {tag_str} |\n")
f.write("\n")
# 加说明
f.write("## 指标说明\n\n")
f.write("| 指标 | 说明 |\n")
f.write("|------|------|\n")
f.write("| 占用资金 | 回测期间最大同时持仓成本 |\n")
f.write("| 真实收益 | 盈亏 / 最大占用资金 × 100% |\n")
f.write("| 真实年化 | 按持续期折算年化(复利公式) |\n")
f.write("| 回撤% | 最大回撤 / 最大占用资金 × 100% |\n")
f.write("| 盈亏比 | 总盈利金额 / 总亏损金额 |\n")
f.write("| 延迟N天 | 连续N天推荐卖出才执行卖出 |\n")
f.write("\n")
# 控制台表格
print("\n" + "=" * 160)
print(" v4.2 整体对比表(★ 真实资金收益率 ★)")
print("=" * 160)
header = (f"{'场景':<42} {'盈亏(元)':>10} {'占用资金':>10} {'真实收益':>8} {'真实年化':>8} "
f"{'胜率':>6} {'回撤%':>7} {'持仓':>6} {'盈亏比':>6} {'交易':>5}")
print(header)
print("-" * 160)
v3_end_idx = v3_count
v4_end_idx = v3_count + v4_count
v41_end_idx = v4_end_idx + v41_count
for idx, row_data in enumerate(rows):
name = row_data[0]
profit, cap_pct, cap_ann = row_data[1], row_data[2], row_data[3]
wr, dd, dd_pct = row_data[4], row_data[5], row_data[6]
hold, pf, n, mc = row_data[7], row_data[8], row_data[9], row_data[10]
tags = []
if idx == best_profit_idx:
tags.append('🏆')
if idx == best_ann_idx and idx != best_profit_idx:
tags.append('📈')
if idx == best_cap_idx and idx != best_profit_idx and idx != best_ann_idx:
tags.append('💰')
if idx == best_winrate_idx:
tags.append('🎯')
if idx == min_dd_idx:
tags.append('🛡️')
if idx == best_pf_idx and idx != best_profit_idx:
tags.append('⚖️')
tag_str = ''.join(tags)
# 分隔线
if not args.v3_only and not args.v4_only:
if idx == v3_end_idx and v3_count > 0:
print("" * 160)
print(f" {'↑ v3 基线 ↓ v4 优化':^148}")
print("" * 160)
if idx == v4_end_idx and v4_count > 0:
print("" * 160)
print(f" {'↑ v4 优化 ↓ v4.1 忽略卖出(参考对照)':^148}")
print("" * 160)
if idx == v41_end_idx and v41_count > 0:
print("" * 160)
print(f" {'↑ v4.1 参考 ↓ v4.2 延迟卖出确认(核心创新)':^148}")
print("" * 160)
print(f"{name:<42} {profit:>+10,.0f} {'¥'+str(int(mc)):>10} {cap_pct:>+7.1f}% {cap_ann:>+7.1f}% "
f"{wr:>5.1f}% {dd_pct:>6.1f}% {hold:>5.0f}{pf:>6.2f} {n:>5} {tag_str}")
print("=" * 160)
# 总结
if rows and len(rows) > 1:
print("\n 📊 关键发现(★ 基于真实资金收益率 ★):")
if best_profit_idx >= 0:
r = rows[best_profit_idx]
print(f" 🏆 绝对收益最高: {r[0]} → ¥{r[1]:+,.0f} (真实{r[2]:+.1f}%, 年化{r[3]:+.1f}%)")
if best_ann_idx >= 0 and best_ann_idx != best_profit_idx:
r = rows[best_ann_idx]
print(f" 📈 年化最高: {r[0]} → 真实年化 {r[3]:+.1f}% (占用 ¥{r[10]:,.0f})")
if best_cap_idx >= 0 and best_cap_idx not in (best_profit_idx, best_ann_idx):
r = rows[best_cap_idx]
print(f" 💰 资金效率最高: {r[0]} → 真实收益 {r[2]:+.1f}% (占用 ¥{r[10]:,.0f})")
if best_winrate_idx >= 0:
r = rows[best_winrate_idx]
print(f" 🎯 胜率最高: {r[0]}{r[4]:.1f}%")
if best_pf_idx >= 0:
r = rows[best_pf_idx]
print(f" ⚖️ 盈亏比最佳: {r[0]}{r[8]:.2f}")
if min_dd_idx >= 0:
r = rows[min_dd_idx]
print(f" 🛡️ 回撤最小: {r[0]}{r[6]:.1f}%")
# 银行对比
print("\n 🏦 银行存款利率对比(年化2.5%:")
for idx, r in enumerate(rows):
ann = r[3]
if ann > 2.5:
icon = ''
else:
icon = ''
print(f" {icon} {r[0]:<42} 年化 {ann:>+6.1f}% {'超过银行' if ann > 2.5 else '低于银行'}")
print(f"\n场景对比已写入 {out_path}")
if __name__ == "__main__":
main()