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