Files
trade/engine/example/generate_report.py
T
Rekey 626acb20d3 feat: 全链路新增 type 字段支持 + exchange.ts 超时退出优化
- TS: exit 函数统一管理进程退出与 DB 连接关闭;10s 超时 + 异常路径 clearTimeout
- Python: PairType(spot/um/cm) 贯穿 Kline 模型、策略配置、数据查询
- 回测脚本升级: 9策略 × 4币种 × 6时间级别 × 2交易类型
- 新增 generate_report.py 回测报告生成工具
2026-06-17 10:01:52 +08:00

286 lines
14 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.
"""
从 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()