feat: 全链路新增 type 字段支持 + exchange.ts 超时退出优化

- TS: exit 函数统一管理进程退出与 DB 连接关闭;10s 超时 + 异常路径 clearTimeout
- Python: PairType(spot/um/cm) 贯穿 Kline 模型、策略配置、数据查询
- 回测脚本升级: 9策略 × 4币种 × 6时间级别 × 2交易类型
- 新增 generate_report.py 回测报告生成工具
This commit is contained in:
Rekey
2026-06-17 10:01:52 +08:00
parent ebaef5042e
commit 626acb20d3
13 changed files with 42394 additions and 5129 deletions
+67 -59
View File
@@ -1,8 +1,8 @@
"""
2h/6h 策略全维度对比回测 — 9策略 × 4币种 × 2时间级别 × 4数据量
1h-1d 策略全维度对比回测 — 9策略 × 4币种 × 6时间级别 × 2交易类型 × 4数据量
8个网络知名策略 + 牛熊自适应策略(RegimeDetector3投票)
2h 和 6h 两个新时间级别上的表现对比。
1h/2h/4h/6h/8h/1d 六个时间级别上的表现对比。
用法:
source .venv/bin/activate && python example/comparison_2h_6h.py
@@ -31,7 +31,8 @@ from engine.example.long_short import LongShortEngine
# ── 全局常量 ──
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
TIMEFRAMES = ["2h", "6h"]
TIMEFRAMES = ["1h", "2h", "4h", "6h", "8h", "1d"]
PAIR_TYPES = ["spot", "um"]
INITIAL = 10_000.0
WARMUP = 150
MAX_CONCURRENCY = 6
@@ -820,12 +821,13 @@ STRATEGY_PARAMS_STR = {
# 执行
# ════════════════════════════════════════════════════════
async def run_one(entry, symbol, interval, period_label, start, end):
async def run_one(entry, symbol, interval, pair_type, period_label, start, end):
make_config = entry["make_config"]
strategy_cls = entry["strategy_cls"]
sc = make_config(symbol)
bt = BacktestConfig(
symbol=symbol, interval=interval,
type=pair_type,
start_time=start, end_time=end,
initial_capital=INITIAL, warmup_bars=WARMUP,
)
@@ -852,14 +854,15 @@ async def main():
date_ranges: dict[tuple[str, str], tuple] = {}
for symbol in SYMBOLS:
for tf in TIMEFRAMES:
try:
s, e = await ds.fetch_symbol_date_range(symbol, tf)
bar_ms = {"2h": 7_200_000, "6h": 21_600_000}
estimated_bars = int((e - s).total_seconds() * 1000 / bar_ms[tf])
date_ranges[(symbol, tf)] = (s, e, estimated_bars)
print(f" {symbol} {tf:<4}: {s.date()} ~ {e.date()} (约{estimated_bars:,}根)")
except Exception as ex:
print(f" {symbol} {tf:<4}: 获取失败 — {ex}")
for pair_type in PAIR_TYPES:
try:
s, e = await ds.fetch_symbol_date_range(symbol, tf, type=pair_type)
bar_ms = {"1h": 3_600_000, "2h": 7_200_000, "4h": 14_400_000, "6h": 21_600_000, "8h": 28_800_000, "1d": 86_400_000}
estimated_bars = int((e - s).total_seconds() * 1000 / bar_ms[tf])
date_ranges[(symbol, tf, pair_type)] = (s, e, estimated_bars)
print(f" {symbol} {tf:<4} {pair_type:<5}: {s.date()} ~ {e.date()} (约{estimated_bars:,}根)")
except Exception as ex:
print(f" {symbol} {tf:<4} {pair_type:<5}: 获取失败 — {ex}")
await ds.close()
@@ -869,39 +872,41 @@ async def main():
for strat_name, entry in STRATEGY_REGISTRY.items():
for symbol in SYMBOLS:
for tf in TIMEFRAMES:
key = (symbol, tf)
if key not in date_ranges:
continue
fs, fe, est_bars = date_ranges[key]
for period_label, (period_start, period_end) in PERIODS.items():
actual_start = period_start or fs
actual_end = period_end or fe
if actual_start >= actual_end:
for pair_type in PAIR_TYPES:
key = (symbol, tf, pair_type)
if key not in date_ranges:
continue
fs, fe, est_bars = date_ranges[key]
min_bars = MIN_BARS_FOR_PERIOD.get(period_label, 50)
actual_bars = est_bars
if period_label != "全量":
actual_bars = int((actual_end - actual_start).total_seconds() * 1000 / {
"2h": 7_200_000, "6h": 21_600_000
}[tf])
for period_label, (period_start, period_end) in PERIODS.items():
actual_start = period_start or fs
actual_end = period_end or fe
if actual_start >= actual_end:
continue
if actual_bars < min_bars:
continue
min_bars = MIN_BARS_FOR_PERIOD.get(period_label, 50)
actual_bars = est_bars
if period_label != "全量":
actual_bars = int((actual_end - actual_start).total_seconds() * 1000 / {
"1h": 3_600_000, "2h": 7_200_000, "4h": 14_400_000, "6h": 21_600_000, "8h": 28_800_000, "1d": 86_400_000
}[tf])
tasks_info.append({
"strat_name": strat_name,
"entry": entry,
"symbol": symbol,
"tf": tf,
"period_label": period_label,
"start": actual_start,
"end": actual_end,
})
if actual_bars < min_bars:
continue
tasks_info.append({
"strat_name": strat_name,
"entry": entry,
"symbol": symbol,
"tf": tf,
"pair_type": pair_type,
"period_label": period_label,
"start": actual_start,
"end": actual_end,
})
total = len(tasks_info)
print(f"\n{total} 组回测任务 (9策略×4币种×2时间×4数据量 - 跳过数据不足)")
print(f"\n{total} 组回测任务 (9策略×4币种×6时间×2类型×4数据量 - 跳过数据不足)")
results: list[dict] = []
completed = 0
@@ -911,7 +916,7 @@ async def main():
nonlocal completed, errors
async with sem:
r, elapsed, err = await run_one(
info["entry"], info["symbol"], info["tf"],
info["entry"], info["symbol"], info["tf"], info["pair_type"],
info["period_label"], info["start"], info["end"],
)
completed += 1
@@ -924,12 +929,13 @@ async def main():
else:
m = r.metrics
status = f"{m.annual_return_pct:+.1f}%/yr"
print(f" [{completed}/{total}] {info['strat_name']} {info['symbol']} {info['tf']} {info['period_label']} ({elapsed:.1f}s) {status}", flush=True)
print(f" [{completed}/{total}] {info['strat_name']} {info['symbol']} {info['tf']} {info['pair_type']} {info['period_label']} ({elapsed:.1f}s) {status}", flush=True)
row = {
"策略名": info["strat_name"],
"币种": info["symbol"],
"时间级别": info["tf"],
"类型": info["pair_type"],
"数据量": info["period_label"],
"策略类型": info["entry"]["strategy_cls"].strategy_type if r else "",
"策略参数": STRATEGY_PARAMS_STR.get(info["strat_name"], ""),
@@ -986,7 +992,7 @@ async def main():
# ── 打印完整表格 ──
print()
print("" * 195)
print(" 2h / 6h 全维度策略对比回测结果(9策略 × 4币种 × 2时间 × 4数据量)")
print(" 1h-1d 全维度策略对比回测结果(9策略 × 4币种 × 6时间 × 2类型 × 4数据量)")
print("" * 195)
print()
@@ -997,35 +1003,36 @@ async def main():
first = strat_results[0]
print(f"{strat_name} | 类型: {first['策略类型']} | {first['策略描述']}")
print(f" 参数: {first['策略参数']}")
print(f" {'币种':<10} {'时间':<5} {'数据量':<6} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>7} {'胜率%':>7} {'盈亏比':>7} {'交易':>6} {'日期范围':<24}")
print(" " + "" * 185)
print(f" {'币种':<10} {'时间':<5} {'类型':<5} {'数据量':<6} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>7} {'胜率%':>7} {'盈亏比':>7} {'交易':>6} {'日期范围':<24}")
print(" " + "" * 195)
strat_results.sort(key=lambda x: (SYMBOLS.index(x["币种"]), TIMEFRAMES.index(x["时间级别"]), list(PERIODS.keys()).index(x["数据量"])))
strat_results.sort(key=lambda x: (SYMBOLS.index(x["币种"]), TIMEFRAMES.index(x["时间级别"]), PAIR_TYPES.index(x["类型"]), list(PERIODS.keys()).index(x["数据量"])))
for r in strat_results:
print(f" {r['币种']:<10} {r['时间级别']:<5} {r['数据量']:<6} {r['总收益%']:>7.1f}% {r['年化收益%']:>7.1f}% {r['夏普比率']:>7.2f} {r['最大回撤%']:>7.1f}% {r['胜率%']:>6.1f}% {r['盈亏比']:>7.2f} {r['交易次数']:>6} {r['日期范围']:<24}")
print(f" {r['币种']:<10} {r['时间级别']:<5} {r['类型']:<5} {r['数据量']:<6} {r['总收益%']:>7.1f}% {r['年化收益%']:>7.1f}% {r['夏普比率']:>7.2f} {r['最大回撤%']:>7.1f}% {r['胜率%']:>6.1f}% {r['盈亏比']:>7.2f} {r['交易次数']:>6} {r['日期范围']:<24}")
print()
# ── 终极汇总 ──
print("" * 195)
print(" ■ 终极汇总:每组(时间级别+数据量)下各币种最佳策略(按年化收益)")
print(" ■ 终极汇总:每组(时间级别+类型+数据量)下各币种最佳策略(按年化收益)\n(1h结果可能因数据量过大导致内存/耗时问题,已设MAX_CONCURRENCY=6")
print("" * 195)
print()
for tf in TIMEFRAMES:
for period_label in PERIODS:
subset = [r for r in results if r["时间级别"] == tf and r["数据量"] == period_label and r.get("总收益%", 0) != 0]
if not subset:
continue
subset.sort(key=lambda x: x.get("年化收益%", -9999), reverse=True)
for pair_type in PAIR_TYPES:
for period_label in PERIODS:
subset = [r for r in results if r["时间级别"] == tf and r["类型"] == pair_type and r["数据量"] == period_label and r.get("总收益%", 0) != 0]
if not subset:
continue
subset.sort(key=lambda x: x.get("年化收益%", -9999), reverse=True)
print(f"{tf} | {period_label}")
print(f" {'排名':<5} {'策略名':<22} {'币种':<10} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>7} {'胜率%':>7} {'盈亏比':>7} {'交易':>6}")
print(" " + "" * 130)
for i, r in enumerate(subset[:5]):
marker = ["🥇", "🥈", "🥉", " 4", " 5"][i]
print(f" {marker:<5} {r['策略名']:<22} {r['币种']:<10} {r['总收益%']:>7.1f}% {r['年化收益%']:>7.1f}% {r['夏普比率']:>7.2f} {r['最大回撤%']:>7.1f}% {r['胜率%']:>6.1f}% {r['盈亏比']:>7.2f} {r['交易次数']:>6}")
print()
print(f"{tf} | {pair_type} | {period_label}")
print(f" {'排名':<5} {'策略名':<22} {'币种':<10} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>7} {'胜率%':>7} {'盈亏比':>7} {'交易':>6}")
print(" " + "" * 135)
for i, r in enumerate(subset[:5]):
marker = ["🥇", "🥈", "🥉", " 4", " 5"][i]
print(f" {marker:<5} {r['策略名']:<22} {r['币种']:<10} {r['总收益%']:>7.1f}% {r['年化收益%']:>7.1f}% {r['夏普比率']:>7.2f} {r['最大回撤%']:>7.1f}% {r['胜率%']:>6.1f}% {r['盈亏比']:>7.2f} {r['交易次数']:>6}")
print()
# ── 保存 JSON ──
output_file = _project_root / "engine" / "example" / "comparison_2h_6h_result.json"
@@ -1034,6 +1041,7 @@ async def main():
"config": {
"symbols": SYMBOLS,
"timeframes": TIMEFRAMES,
"pair_types": PAIR_TYPES,
"periods": list(PERIODS.keys()),
"initial_capital": INITIAL,
"warmup_bars": WARMUP,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+285
View File
@@ -0,0 +1,285 @@
"""
从 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()
+3
View File
@@ -59,6 +59,7 @@ class LongShortEngine:
from engine.common.config import config as app_config
strategy_config.symbol = self.config.symbol
strategy_config.exchange = self.config.exchange
strategy_config.type = self.config.type
db_cfg = self._db_config or app_config.db
ds = DataService(db_cfg)
await ds.connect()
@@ -67,6 +68,8 @@ class LongShortEngine:
symbol=self.config.symbol, interval=self.config.interval,
start_time=self.config.start_time, end_time=self.config.end_time,
limit=1_000_000,
type=self.config.type,
exchange=self.config.exchange,
)
if len(klines) < self.config.warmup_bars + 2:
raise ValueError(f"数据不足:需 {self.config.warmup_bars+2},实际 {len(klines)}")
+2
View File
@@ -201,6 +201,8 @@ async def run_one(
warmup_bars=100,
)
strategy_config.symbol = symbol
strategy_config.exchange = bt.exchange
strategy_config.type = bt.type
strategy_config.name = f"{strategy_name}_{symbol}"
engine = BacktestEngine(bt, db_config=config.db)