Initial commit

This commit is contained in:
freedakgmail
2026-07-17 18:49:07 +08:00
commit 9c7d7abdd4
100 changed files with 41337 additions and 0 deletions
+374
View File
@@ -0,0 +1,374 @@
#!/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()