626acb20d3
- TS: exit 函数统一管理进程退出与 DB 连接关闭;10s 超时 + 异常路径 clearTimeout - Python: PairType(spot/um/cm) 贯穿 Kline 模型、策略配置、数据查询 - 回测脚本升级: 9策略 × 4币种 × 6时间级别 × 2交易类型 - 新增 generate_report.py 回测报告生成工具
286 lines
14 KiB
Python
286 lines
14 KiB
Python
"""
|
||
从 comparison_2h_6h_result.json 生成可读 Markdown 报告
|
||
用法:source .venv/bin/activate && python example/generate_report.py
|
||
"""
|
||
import json
|
||
from datetime import datetime, timezone
|
||
from pathlib import Path
|
||
from collections import defaultdict
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
JSON_PATH = ROOT / "example" / "comparison_2h_6h_result.json"
|
||
OUT_PATH = ROOT / "example" / "comparison_2h_6h_report.md"
|
||
|
||
STRATEGY_ORDER = [
|
||
"1.海龟交易", "2.超级趋势", "3.MACD金叉死叉", "4.布林收缩爆发",
|
||
"5.三均线排列", "6.RSI均值回归", "7.ATR波动率突破", "8.EMA双均线多空", "9.牛熊自适应",
|
||
]
|
||
SYMBOL_ORDER = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
|
||
TF_ORDER = ["1h", "2h", "4h", "6h", "8h", "1d"]
|
||
TYPE_ORDER = ["spot", "um"]
|
||
PERIOD_ORDER = ["全量", "近两年", "近一年", "近半年"]
|
||
|
||
|
||
def fmt(val, suffix="", decimals=1):
|
||
if val is None:
|
||
return "—"
|
||
return f"{val:+,.{decimals}f}{suffix}"
|
||
|
||
|
||
def safe_get(d, key, default=0):
|
||
v = d.get(key, default)
|
||
return default if v is None else v
|
||
|
||
|
||
def generate():
|
||
with open(JSON_PATH) as f:
|
||
data = json.load(f)
|
||
cfg = data["config"]
|
||
results = data["results"]
|
||
|
||
now_str = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
||
|
||
lines = []
|
||
def w(s=""):
|
||
lines.append(s)
|
||
|
||
# ── 标题 ──
|
||
w(f"# 1h-1d 策略全维度对比回测报告(Spot + 合约)")
|
||
w()
|
||
w(f"> **生成时间**:{now_str}")
|
||
w(f"> **总耗时**:{cfg['elapsed_seconds']:.1f} 秒")
|
||
w(f"> **测试维度**:9 策略 × 4 币种 × 2 时间级别 × 2 交易类型 × 4 数据量 = {cfg['total_tasks']} 次回测")
|
||
w(f"> **初始资金**:${cfg['initial_capital']:,.0f} | **预热 Bar**:{cfg['warmup_bars']}")
|
||
w(f"> **错误数**:{cfg['total_errors']}")
|
||
w()
|
||
|
||
# ── 一、策略概览 ──
|
||
w("## 一、策略概览")
|
||
w()
|
||
w("| # | 策略名称 | 类型 | 参数 | 描述 |")
|
||
w("|---|----------|------|------|------|")
|
||
all_full = [r for r in results if r["数据量"] == "全量"]
|
||
for sn in STRATEGY_ORDER:
|
||
sample = next((r for r in all_full if r["策略名"] == sn), None)
|
||
if sample:
|
||
w(f"| {sn[:2]} | **{sn[2:]}** | {sample['策略类型']} | `{sample['策略参数']}` | {sample['策略描述']} |")
|
||
w()
|
||
|
||
# ── 二、全量数据 TOP 20(混合 spot/um,按夏普排名)──
|
||
w("## 二、全量数据 TOP 20(按夏普比率排名)")
|
||
w()
|
||
w("| 排名 | 策略 | 币种 | TF | 类型 | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 交易数 | 卡尔玛 |")
|
||
w("|------|------|------|----|------|---------|-------|------|-------|-------|--------|--------|--------|")
|
||
full_sorted = sorted(all_full, key=lambda x: safe_get(x, "夏普比率"), reverse=True)
|
||
medals = ["🥇", "🥈", "🥉"] + [" " + str(i) for i in range(4, 21)]
|
||
for i, r in enumerate(full_sorted[:20]):
|
||
m = medals[i]
|
||
w(f"| {m} | {r['策略名'][2:]} | {r['币种']} | {r['时间级别']} | {r['类型']} | "
|
||
f"{safe_get(r,'总收益%'):+.1f}% | {safe_get(r,'年化收益%'):+.1f}% | "
|
||
f"**{safe_get(r,'夏普比率'):.2f}** | {safe_get(r,'最大回撤%'):.1f}% | "
|
||
f"{safe_get(r,'胜率%'):.1f}% | {safe_get(r,'盈亏比'):.2f} | {safe_get(r,'交易次数')} | "
|
||
f"{safe_get(r,'卡尔玛比率'):.2f} |")
|
||
w()
|
||
|
||
# ── 三、各策略全量详细(spot vs um)──
|
||
w("## 三、各策略全量数据详细表现(2h vs 6h × 4 币种 × spot/um)")
|
||
w()
|
||
for sn in STRATEGY_ORDER:
|
||
strat_full = [r for r in all_full if r["策略名"] == sn]
|
||
if not strat_full:
|
||
continue
|
||
sample = strat_full[0]
|
||
w(f"### {sn}")
|
||
w(f"> **类型**:{sample['策略类型']} | **参数**:`{sample['策略参数']}`")
|
||
w(f"> {sample['策略描述']}")
|
||
w()
|
||
w("| 币种 | TF | 类型 | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 交易数 | 卡尔玛 |")
|
||
w("|------|----|------|---------|-------|------|-------|-------|--------|--------|--------|")
|
||
strat_full.sort(key=lambda x: (SYMBOL_ORDER.index(x["币种"]), TF_ORDER.index(x["时间级别"]), TYPE_ORDER.index(x["类型"])))
|
||
for r in strat_full:
|
||
w(f"| {r['币种']} | {r['时间级别']} | {r['类型']} | "
|
||
f"{safe_get(r,'总收益%'):+.1f}% | {safe_get(r,'年化收益%'):+.1f}% | "
|
||
f"**{safe_get(r,'夏普比率'):.2f}** | {safe_get(r,'最大回撤%'):.1f}% | "
|
||
f"{safe_get(r,'胜率%'):.1f}% | {safe_get(r,'盈亏比'):.2f} | "
|
||
f"{safe_get(r,'交易次数')} | {safe_get(r,'卡尔玛比率'):.2f} |")
|
||
best = max(strat_full, key=lambda x: safe_get(x, "夏普比率"))
|
||
w()
|
||
w(f"> 🏆 **{sn[2:]} 最优**:{best['币种']} {best['时间级别']} {best['类型']},"
|
||
f"夏普 **{safe_get(best,'夏普比率'):.2f}**,"
|
||
f"总收益 **{safe_get(best,'总收益%'):+.1f}%**,"
|
||
f"年化 **{safe_get(best,'年化收益%'):+.1f}%**,"
|
||
f"交易 {safe_get(best,'交易次数')} 次")
|
||
w()
|
||
|
||
# ── 四、各币种全量 — 策略横向对比 ──
|
||
w("## 四、各币种全量数据 — 策略横向对比")
|
||
w()
|
||
for symbol in SYMBOL_ORDER:
|
||
w(f"### {symbol}")
|
||
w()
|
||
sym_full = [r for r in all_full if r["币种"] == symbol]
|
||
sym_full.sort(key=lambda x: safe_get(x, "夏普比率"), reverse=True)
|
||
w("| 排名 | 策略 | TF | 类型 | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 交易数 | 卡尔玛 |")
|
||
w("|------|------|----|------|---------|-------|------|-------|-------|--------|--------|--------|")
|
||
for i, r in enumerate(sym_full):
|
||
m = medals[i] if i < 3 else f" {i+1}"
|
||
w(f"| {m} | {r['策略名'][2:]} | {r['时间级别']} | {r['类型']} | "
|
||
f"{safe_get(r,'总收益%'):+.1f}% | {safe_get(r,'年化收益%'):+.1f}% | "
|
||
f"**{safe_get(r,'夏普比率'):.2f}** | {safe_get(r,'最大回撤%'):.1f}% | "
|
||
f"{safe_get(r,'胜率%'):.1f}% | {safe_get(r,'盈亏比'):.2f} | "
|
||
f"{safe_get(r,'交易次数')} | {safe_get(r,'卡尔玛比率'):.2f} |")
|
||
best_sym = max(sym_full, key=lambda x: safe_get(x, "夏普比率"))
|
||
w()
|
||
w(f"> 🏆 **{symbol} 最优**:{best_sym['策略名'][2:]} {best_sym['时间级别']} {best_sym['类型']},"
|
||
f"夏普 **{safe_get(best_sym,'夏普比率'):.2f}**,"
|
||
f"总收益 **{safe_get(best_sym,'总收益%'):+.1f}%**")
|
||
w()
|
||
|
||
# ── 五、spot vs um 对比 ──
|
||
w("## 五、Spot vs 合约(UM)对比 — 全量夏普差异")
|
||
w()
|
||
w("| 策略 | 币种 | TF | Spot 夏普 | UM 夏普 | 差异 | 更优 |")
|
||
w("|------|------|----|-----------|---------|------|------|")
|
||
diffs = []
|
||
for sn in STRATEGY_ORDER:
|
||
for symbol in SYMBOL_ORDER:
|
||
for tf in TF_ORDER:
|
||
spot_r = next((r for r in all_full if r["策略名"]==sn and r["币种"]==symbol and r["时间级别"]==tf and r["类型"]=="spot"), None)
|
||
um_r = next((r for r in all_full if r["策略名"]==sn and r["币种"]==symbol and r["时间级别"]==tf and r["类型"]=="um"), None)
|
||
if spot_r and um_r:
|
||
s_sharpe = safe_get(spot_r, "夏普比率")
|
||
u_sharpe = safe_get(um_r, "夏普比率")
|
||
diff = u_sharpe - s_sharpe
|
||
if abs(diff) > 0.15:
|
||
better = "UM ✅" if diff > 0 else "Spot ✅"
|
||
winner = "um" if diff > 0 else "spot"
|
||
diffs.append((sn, symbol, tf, s_sharpe, u_sharpe, diff, winner))
|
||
diffs.sort(key=lambda x: abs(x[5]), reverse=True)
|
||
for sn, symbol, tf, s_s, u_s, diff, winner in diffs:
|
||
w(f"| {sn[2:]} | {symbol} | {tf} | {s_s:.2f} | {u_s:.2f} | {diff:+.2f} | {winner} |")
|
||
w()
|
||
if diffs:
|
||
um_wins = sum(1 for d in diffs if d[6] == "um")
|
||
spot_wins = sum(1 for d in diffs if d[6] == "spot")
|
||
w(f"> 显著差异(|Δ夏普|>0.15)共 {len(diffs)} 组:UM 更优 {um_wins} 组,Spot 更优 {spot_wins} 组")
|
||
w()
|
||
|
||
# ── 六、周期对比(按 type 分组)──
|
||
w("## 六、2h vs 6h — 周期对比(按类型分组)")
|
||
w()
|
||
for pair_type in TYPE_ORDER:
|
||
type_full = [r for r in all_full if r["类型"] == pair_type]
|
||
w(f"### {pair_type.upper()}")
|
||
w()
|
||
for symbol in SYMBOL_ORDER:
|
||
w(f"#### {symbol}")
|
||
w()
|
||
w("| 策略 | 2h 夏普 | 2h 收益% | 6h 夏普 | 6h 收益% | 更优周期 |")
|
||
w("|------|---------|----------|---------|----------|----------|")
|
||
for sn in STRATEGY_ORDER:
|
||
r2 = next((r for r in type_full if r["策略名"]==sn and r["币种"]==symbol and r["时间级别"]=="2h"), None)
|
||
r6 = next((r for r in type_full if r["策略名"]==sn and r["币种"]==symbol and r["时间级别"]=="6h"), None)
|
||
if r2 and r6:
|
||
s2, s6 = safe_get(r2, "夏普比率"), safe_get(r6, "夏普比率")
|
||
better = "2h ✅" if s2 > s6 else "6h ✅"
|
||
w(f"| {sn[2:]} | {s2:.2f} | {safe_get(r2,'总收益%'):+.1f}% | {s6:.2f} | {safe_get(r6,'总收益%'):+.1f}% | {better} |")
|
||
w()
|
||
|
||
# ── 七、综合评分 TOP 20 ──
|
||
w("## 七、全量数据 综合评分 TOP 20")
|
||
w()
|
||
w("> 综合评分 = 夏普比率×0.4 + 年化收益归一化×0.3 + 卡尔玛归一化×0.2 - 回撤归一化×0.1")
|
||
w()
|
||
# normalize(回撤和卡尔玛取绝对值,因存储为负数)
|
||
all_annual = [safe_get(r, "年化收益%") for r in all_full]
|
||
all_calmar = [abs(safe_get(r, "卡尔玛比率")) for r in all_full]
|
||
all_dd = [abs(safe_get(r, "最大回撤%")) for r in all_full]
|
||
max_annual = max(all_annual) if all_annual else 1
|
||
max_calmar = max(all_calmar) if all_calmar else 1
|
||
max_dd = max(all_dd) if all_dd else 1
|
||
|
||
for r in all_full:
|
||
score = (
|
||
safe_get(r, "夏普比率") * 0.4
|
||
+ (safe_get(r, "年化收益%") / max(1, max_annual)) * 0.3
|
||
+ (safe_get(r, "卡尔玛比率") / max(0.01, max_calmar)) * 0.2
|
||
- (abs(safe_get(r, "最大回撤%")) / max(1, max_dd)) * 0.1
|
||
)
|
||
r["_score"] = score
|
||
|
||
scored = sorted(all_full, key=lambda x: x["_score"], reverse=True)
|
||
w("| 排名 | 策略 | 币种 | TF | 类型 | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 综合评分 |")
|
||
w("|------|------|------|----|------|---------|-------|------|-------|-------|--------|----------|")
|
||
for i, r in enumerate(scored[:20]):
|
||
m = medals[i]
|
||
w(f"| {m} | {r['策略名'][2:]} | {r['币种']} | {r['时间级别']} | {r['类型']} | "
|
||
f"{safe_get(r,'总收益%'):+.1f}% | {safe_get(r,'年化收益%'):+.1f}% | "
|
||
f"**{safe_get(r,'夏普比率'):.2f}** | {safe_get(r,'最大回撤%'):.1f}% | "
|
||
f"{safe_get(r,'胜率%'):.1f}% | {safe_get(r,'盈亏比'):.2f} | "
|
||
f"**{r['_score']:.3f}** |")
|
||
w()
|
||
|
||
# ── 八、策略类型平均表现 ──
|
||
w("## 八、策略类型平均表现(全量,4币种×2TF×2类型 平均)")
|
||
w()
|
||
w("| 类型 | 策略 | 平均夏普 | 平均收益% | 平均回撤% |")
|
||
w("|------|------|----------|-----------|-----------|")
|
||
for sn in STRATEGY_ORDER:
|
||
subset = [r for r in all_full if r["策略名"] == sn]
|
||
if subset:
|
||
avg_s = sum(safe_get(r, "夏普比率") for r in subset) / len(subset)
|
||
avg_ret = sum(safe_get(r, "总收益%") for r in subset) / len(subset)
|
||
avg_dd = sum(safe_get(r, "最大回撤%") for r in subset) / len(subset)
|
||
stype = subset[0]["策略类型"]
|
||
w(f"| {stype} | {sn[2:]} | {avg_s:.2f} | {avg_ret:+.1f}% | {avg_dd:.1f}% |")
|
||
w()
|
||
|
||
# ── 九、关键发现 ──
|
||
w("## 九、关键发现")
|
||
w()
|
||
# 最优夏普
|
||
best_all = max(all_full, key=lambda x: safe_get(x, "夏普比率"))
|
||
w(f"### 🏆 综合最优策略")
|
||
w(f"- **{best_all['策略名'][2:]}** — {best_all['币种']} {best_all['时间级别']} {best_all['类型']}")
|
||
w(f" - 夏普比率:**{safe_get(best_all,'夏普比率'):.2f}**")
|
||
w(f" - 总收益:**{safe_get(best_all,'总收益%'):+.1f}%**")
|
||
w(f" - 年化收益:**{safe_get(best_all,'年化收益%'):+.1f}%**")
|
||
w(f" - 最大回撤:{safe_get(best_all,'最大回撤%'):.1f}%")
|
||
w(f" - 交易次数:{safe_get(best_all,'交易次数')}")
|
||
w()
|
||
|
||
# 各币种最优
|
||
w("### 📊 各币种最优策略(全量,按夏普)")
|
||
w()
|
||
w("| 币种 | 最优策略 | TF | 类型 | 总收益% | 年化% | 夏普 | 回撤% | 交易数 |")
|
||
w("|------|----------|----|------|---------|-------|------|-------|--------|")
|
||
for symbol in SYMBOL_ORDER:
|
||
sym_full = [r for r in all_full if r["币种"] == symbol]
|
||
best = max(sym_full, key=lambda x: safe_get(x, "夏普比率"))
|
||
w(f"| {symbol} | **{best['策略名'][2:]}** | {best['时间级别']} | {best['类型']} | "
|
||
f"{safe_get(best,'总收益%'):+.1f}% | {safe_get(best,'年化收益%'):+.1f}% | "
|
||
f"**{safe_get(best,'夏普比率'):.2f}** | {safe_get(best,'最大回撤%'):.1f}% | "
|
||
f"{safe_get(best,'交易次数')} |")
|
||
w()
|
||
|
||
# spot vs um
|
||
w("### 🔄 Spot vs 合约(UM)")
|
||
w()
|
||
spot_full = [r for r in all_full if r["类型"] == "spot"]
|
||
um_full = [r for r in all_full if r["类型"] == "um"]
|
||
spot_avg_s = sum(safe_get(r, "夏普比率") for r in spot_full) / len(spot_full)
|
||
um_avg_s = sum(safe_get(r, "夏普比率") for r in um_full) / len(um_full)
|
||
w(f"- Spot 平均夏普:**{spot_avg_s:.2f}**")
|
||
w(f"- UM(合约)平均夏普:**{um_avg_s:.2f}**")
|
||
w(f"- 差距:{um_avg_s - spot_avg_s:+.2f}")
|
||
w()
|
||
|
||
# 保存
|
||
with open(OUT_PATH, "w", encoding="utf-8") as f:
|
||
f.write("\n".join(lines))
|
||
print(f"报告已生成:{OUT_PATH} ({len(lines)} 行)")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
generate()
|