feat: 新增2h/6h时间框架支持,策略重构为增量指标计算

- 数据层: build_aggregates_sql 新增 2h/6h 聚合视图,默认起始时间调整为 2017-05
- 模型层: KlineInterval 类型扩展 2h/6h,DataService 新增对应表名和毫秒映射
- 指标层: 新增 incremental.py 增量指标模块 (EmaInc/AtrInc/RsiInc/BbInc),O(1) per bar
- 策略重构: long_short.py 和 regime_all.py 从批量 ema/atr 迁移至增量指标,避免每 bar 重复全量计算
- regime 探测器: RegimeDetector3 改为增量 EMA200,detect() 接口简化
- 回测扩展: regime_timeframe_comparison 从 4h/1d 扩展至 2h/4h/6h/1d
- 新增示例: multi_strategy_report, vol_break_compare/periods, intraday_explore, top3_trades 等分析脚本
This commit is contained in:
Rekey
2026-06-13 19:30:25 +08:00
parent b5cdb41993
commit edc50e8809
20 changed files with 484544 additions and 34 deletions
@@ -0,0 +1,136 @@
# 牛熊自适应策略 — 多时间级别回测对比
> 生成时间:2026-06-12 12:09
## 一、全量数据(所有可用历史)
### 2h 周期
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L | 数据范围 |
|------|------|------|--------|------|------|------|------|------|------|---------|---------|---------|
| BTCUSDT | 2h | 全量 | +166.4% | +11.8% | 0.50 | -66.9% | 374 | 34.0% | 1.21 | +22365 | +5117 | 2017-08-17~2026-06-11 |
| ETHUSDT | 2h | 全量 | -2.9% | -0.3% | 0.16 | -71.6% | 329 | 32.2% | 1.06 | +19399 | -14483 | 2017-08-17~2026-06-11 |
| BNBUSDT | 2h | 全量 | +343.0% | +19.0% | 0.61 | -65.3% | 267 | 32.6% | 1.27 | +39949 | +5623 | 2017-11-06~2026-06-11 |
| SOLUSDT | 2h | 全量 | -68.1% | -17.9% | -0.40 | -74.0% | 163 | 31.3% | 0.79 | -246 | -5504 | 2020-08-11~2026-06-11 |
### 4h 周期
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L | 数据范围 |
|------|------|------|--------|------|------|------|------|------|------|---------|---------|---------|
| BTCUSDT | 4h | 全量 | +493.5% | +22.7% | 0.80 | -34.1% | 208 | 36.5% | 1.53 | +41513 | +14936 | 2017-08-17~2026-06-11 |
| ETHUSDT | 4h | 全量 | +831.8% | +29.2% | 0.88 | -48.9% | 162 | 38.9% | 1.57 | +60985 | +31285 | 2017-08-17~2026-06-11 |
| BNBUSDT | 4h | 全量 | +3282.2% | +51.4% | 1.15 | -38.0% | 150 | 36.7% | 1.60 | +403316 | -40234 | 2017-11-06~2026-06-11 |
| SOLUSDT | 4h | 全量 | +34.9% | +5.4% | 0.32 | -59.6% | 78 | 33.3% | 1.13 | +7744 | -3164 | 2020-08-11~2026-06-11 |
### 6h 周期
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L | 数据范围 |
|------|------|------|--------|------|------|------|------|------|------|---------|---------|---------|
| BTCUSDT | 6h | 全量 | +317.6% | +18.0% | 0.67 | -43.3% | 122 | 35.2% | 1.37 | +50431 | -14466 | 2017-08-17~2026-06-11 |
| ETHUSDT | 6h | 全量 | +231.4% | +14.9% | 0.58 | -54.9% | 98 | 38.8% | 1.33 | +19350 | +7040 | 2017-08-17~2026-06-11 |
| BNBUSDT | 6h | 全量 | +495.5% | +23.6% | 0.78 | -41.8% | 96 | 34.4% | 1.60 | +53465 | +212 | 2017-11-06~2026-06-11 |
| SOLUSDT | 6h | 全量 | +107.0% | +13.7% | 0.51 | -47.6% | 55 | 36.4% | 1.35 | +8969 | +2611 | 2020-08-11~2026-06-11 |
### 1d 周期
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L | 数据范围 |
|------|------|------|--------|------|------|------|------|------|------|---------|---------|---------|
| BTCUSDT | 1d | 全量 | +689.4% | +28.9% | 1.07 | -30.5% | 28 | 46.4% | 2.88 | +40825 | +28909 | 2017-08-17~2026-06-11 |
| ETHUSDT | 1d | 全量 | +216.7% | +15.2% | 0.69 | -39.2% | 20 | 40.0% | 2.64 | +4360 | +17454 | 2017-08-17~2026-06-11 |
| BNBUSDT | 1d | 全量 | +63.5% | +6.4% | 0.36 | -40.7% | 18 | 38.9% | 1.53 | +3220 | +3247 | 2017-11-06~2026-06-11 |
| SOLUSDT | 1d | 全量 | +247.1% | +27.3% | 0.90 | -60.5% | 15 | 33.3% | 1.64 | +9206 | +15504 | 2020-08-11~2026-06-11 |
## 二、近两年(2024.06 — 2026.06
### 2h 周期
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L |
|------|------|------|--------|------|------|------|------|------|------|---------|---------|
| BTCUSDT | 2h | 近2年 | +34.5% | +16.2% | 0.81 | -20.3% | 80 | 36.2% | 1.50 | +4390 | +76 |
| ETHUSDT | 2h | 近2年 | -50.2% | -29.8% | -1.17 | -53.4% | 82 | 22.0% | 0.57 | +1094 | -5591 |
| BNBUSDT | 2h | 近2年 | +1.1% | +0.5% | 0.12 | -24.6% | 56 | 28.6% | 1.10 | -1680 | +2341 |
| SOLUSDT | 2h | 近2年 | -23.1% | -12.4% | -0.52 | -34.1% | 49 | 26.5% | 0.78 | +2474 | -4307 |
### 4h 周期
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L |
|------|------|------|--------|------|------|------|------|------|------|---------|---------|
| BTCUSDT | 4h | 近2年 | +28.5% | +14.0% | 0.70 | -17.6% | 49 | 40.8% | 1.50 | +3072 | +391 |
| ETHUSDT | 4h | 近2年 | +13.1% | +6.7% | 0.38 | -20.9% | 38 | 42.1% | 1.26 | +1860 | -119 |
| BNBUSDT | 4h | 近2年 | -4.2% | -2.2% | -0.00 | -25.6% | 35 | 31.4% | 0.98 | +675 | -760 |
| SOLUSDT | 4h | 近2年 | +0.8% | +0.4% | 0.20 | -39.7% | 21 | 23.8% | 1.04 | +4083 | -3750 |
### 6h 周期
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L |
|------|------|------|--------|------|------|------|------|------|------|---------|---------|
| BTCUSDT | 6h | 近2年 | +6.3% | +3.3% | 0.26 | -30.4% | 25 | 36.0% | 1.16 | +3304 | -2368 |
| ETHUSDT | 6h | 近2年 | -14.1% | -7.9% | -0.25 | -26.9% | 23 | 34.8% | 0.74 | -608 | -577 |
| BNBUSDT | 6h | 近2年 | +11.0% | +5.8% | 0.37 | -21.3% | 23 | 30.4% | 1.34 | +2925 | -1544 |
| SOLUSDT | 6h | 近2年 | +16.1% | +8.4% | 0.45 | -19.9% | 13 | 46.2% | 1.44 | +641 | +1134 |
### 1d 周期
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L |
|------|------|------|--------|------|------|------|------|------|------|---------|---------|
| BTCUSDT | 1d | 近2年 | +42.8% | +30.4% | 1.40 | -15.4% | 5 | 60.0% | 5.38 | +148 | +4327 |
| ETHUSDT | 1d | 近2年 | +57.4% | +40.2% | 1.76 | -7.6% | 2 | 100.0% | 5903.01 | +0 | +5903 |
| BNBUSDT | 1d | 近2年 | -8.0% | -6.0% | -1.04 | -9.6% | 3 | 33.3% | 0.27 | -928 | +251 |
| SOLUSDT | 1d | 近2年 | +22.0% | +16.0% | 1.08 | -12.0% | 2 | 50.0% | 9.85 | +0 | +2403 |
---
## 三、全维度汇总
| 币种 | 周期 | 范围 | 总收益% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 |
|------|------|------|--------|------|------|------|------|------|
| BNBUSDT | 1d | 全量 | +63.5% | 0.36 | -40.7% | 18 | 38.9% | 1.53 |
| BNBUSDT | 2h | 全量 | +343.0% | 0.61 | -65.3% | 267 | 32.6% | 1.27 |
| BNBUSDT | 4h | 全量 | +3282.2% | 1.15 | -38.0% | 150 | 36.7% | 1.60 |
| BNBUSDT | 6h | 全量 | +495.5% | 0.78 | -41.8% | 96 | 34.4% | 1.60 |
| BNBUSDT | 1d | 近2年 | -8.0% | -1.04 | -9.6% | 3 | 33.3% | 0.27 |
| BNBUSDT | 2h | 近2年 | +1.1% | 0.12 | -24.6% | 56 | 28.6% | 1.10 |
| BNBUSDT | 4h | 近2年 | -4.2% | -0.00 | -25.6% | 35 | 31.4% | 0.98 |
| BNBUSDT | 6h | 近2年 | +11.0% | 0.37 | -21.3% | 23 | 30.4% | 1.34 |
| BTCUSDT | 1d | 全量 | +689.4% | 1.07 | -30.5% | 28 | 46.4% | 2.88 |
| BTCUSDT | 2h | 全量 | +166.4% | 0.50 | -66.9% | 374 | 34.0% | 1.21 |
| BTCUSDT | 4h | 全量 | +493.5% | 0.80 | -34.1% | 208 | 36.5% | 1.53 |
| BTCUSDT | 6h | 全量 | +317.6% | 0.67 | -43.3% | 122 | 35.2% | 1.37 |
| BTCUSDT | 1d | 近2年 | +42.8% | 1.40 | -15.4% | 5 | 60.0% | 5.38 |
| BTCUSDT | 2h | 近2年 | +34.5% | 0.81 | -20.3% | 80 | 36.2% | 1.50 |
| BTCUSDT | 4h | 近2年 | +28.5% | 0.70 | -17.6% | 49 | 40.8% | 1.50 |
| BTCUSDT | 6h | 近2年 | +6.3% | 0.26 | -30.4% | 25 | 36.0% | 1.16 |
| ETHUSDT | 1d | 全量 | +216.7% | 0.69 | -39.2% | 20 | 40.0% | 2.64 |
| ETHUSDT | 2h | 全量 | -2.9% | 0.16 | -71.6% | 329 | 32.2% | 1.06 |
| ETHUSDT | 4h | 全量 | +831.8% | 0.88 | -48.9% | 162 | 38.9% | 1.57 |
| ETHUSDT | 6h | 全量 | +231.4% | 0.58 | -54.9% | 98 | 38.8% | 1.33 |
| ETHUSDT | 1d | 近2年 | +57.4% | 1.76 | -7.6% | 2 | 100.0% | 5903.01 |
| ETHUSDT | 2h | 近2年 | -50.2% | -1.17 | -53.4% | 82 | 22.0% | 0.57 |
| ETHUSDT | 4h | 近2年 | +13.1% | 0.38 | -20.9% | 38 | 42.1% | 1.26 |
| ETHUSDT | 6h | 近2年 | -14.1% | -0.25 | -26.9% | 23 | 34.8% | 0.74 |
| SOLUSDT | 1d | 全量 | +247.1% | 0.90 | -60.5% | 15 | 33.3% | 1.64 |
| SOLUSDT | 2h | 全量 | -68.1% | -0.40 | -74.0% | 163 | 31.3% | 0.79 |
| SOLUSDT | 4h | 全量 | +34.9% | 0.32 | -59.6% | 78 | 33.3% | 1.13 |
| SOLUSDT | 6h | 全量 | +107.0% | 0.51 | -47.6% | 55 | 36.4% | 1.35 |
| SOLUSDT | 1d | 近2年 | +22.0% | 1.08 | -12.0% | 2 | 50.0% | 9.85 |
| SOLUSDT | 2h | 近2年 | -23.1% | -0.52 | -34.1% | 49 | 26.5% | 0.78 |
| SOLUSDT | 4h | 近2年 | +0.8% | 0.20 | -39.7% | 21 | 23.8% | 1.04 |
| SOLUSDT | 6h | 近2年 | +16.1% | 0.45 | -19.9% | 13 | 46.2% | 1.44 |
## 四、各币种最佳组合(按夏普排序)
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% |
|------|------|------|--------|------|------|------|------|------|
| BTCUSDT | **1d** | 近2年 | +42.8% | +30.4% | 1.40 | -15.4% | 5 | 60.0% |
| ETHUSDT | **1d** | 近2年 | +57.4% | +40.2% | 1.76 | -7.6% | 2 | 100.0% |
| BNBUSDT | **4h** | 全量 | +3282.2% | +51.4% | 1.15 | -38.0% | 150 | 36.7% |
| SOLUSDT | **1d** | 近2年 | +22.0% | +16.0% | 1.08 | -12.0% | 2 | 50.0% |
---
## 五、结论
- **4h vs 1d**:低波动大市值币种(BTC)偏向日线(1d),高波动币种(ETH/BNB)偏向4h
- **全量 vs 近两年**:近两年市场环境与长周期统计可能有显著差异,需结合当前市场结构选择周期
- **交易频率**1d 交易数约为 4h 的 1/5 ~ 1/10,适合低频策略
+1 -1
View File
@@ -14,7 +14,7 @@ from pydantic import BaseModel, Field, field_validator
# K 线周期类型
# ============================================================
KlineInterval = Literal["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w"]
KlineInterval = Literal["1m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "1d", "1w"]
# ============================================================
+4
View File
@@ -36,7 +36,9 @@ INTERVAL_TO_TABLE: dict[KlineInterval, str] = {
"15m": "klines_15m",
"30m": "klines_30m",
"1h": "klines_1h",
"2h": "klines_2h",
"4h": "klines_4h",
"6h": "klines_6h",
"1d": "klines_1d",
"1w": "klines_1w",
}
@@ -48,7 +50,9 @@ INTERVAL_MS: dict[KlineInterval, int] = {
"15m": 900_000,
"30m": 1_800_000,
"1h": 3_600_000,
"2h": 7_200_000,
"4h": 14_400_000,
"6h": 21_600_000,
"1d": 86_400_000,
"1w": 604_800_000,
}
+226
View File
@@ -0,0 +1,226 @@
"""
读取 full_comparison_result.json 并生成多维度排序对比表
用法:
source .venv/bin/activate && python example/analyze_comparison.py
"""
import json
import sys
from pathlib import Path
_project_root = Path(__file__).resolve().parent.parent.parent
if str(_project_root) not in sys.path:
sys.path.insert(0, str(_project_root))
JSON_PATH = _project_root / "engine" / "example" / "full_comparison_result.json"
with open(JSON_PATH, "r", encoding="utf-8") as f:
data = json.load(f)
results = data["results"]
cfg = data["config"]
# ── 通用排序列 ──
METRICS = [
"总收益%", "年化收益%", "夏普比率", "最大回撤%", "胜率%",
"盈亏比", "交易次数", "平均盈亏", "最佳盈亏", "最差盈亏", "卡尔玛比率",
]
# 哪些指标越大越好(正序),哪些越小越好(倒序)
DESCENDING = {"总收益%", "年化收益%", "夏普比率", "胜率%", "盈亏比", "平均盈亏", "最佳盈亏", "卡尔玛比率"}
ASCENDING = {"最大回撤%", "交易次数", "最差盈亏", "耗时s"}
def sort_results(items, key, descending=True):
"""排序,返回 top N"""
return sorted(items, key=lambda x: x.get(key, -9999) if key in DESCENDING else -x.get(key, 9999), reverse=descending)
def print_table(title, rows, fields, col_widths):
"""打印格式化表格"""
print()
print("" * len(title))
print(title)
print("" * len(title))
print()
# header
header = ""
for i, f in enumerate(fields):
header += f" {f:<{col_widths[i]}}"
print(header)
# separator
sep = " " + "" * (sum(col_widths) + sum(c - len(str(fields[i])) for i, c in enumerate(col_widths)))
print(sep)
for r in rows:
line = ""
for i, f in enumerate(fields):
val = r.get(f, "")
if isinstance(val, float):
if abs(val) >= 1000:
val_str = f"{val:>{col_widths[i]}.0f}"
elif abs(val) >= 100:
val_str = f"{val:>{col_widths[i]}.1f}"
elif abs(val) >= 10:
val_str = f"{val:>{col_widths[i]}.2f}"
else:
val_str = f"{val:>{col_widths[i]}.3f}"
else:
val_str = f"{str(val):<{col_widths[i]}}"
line += f" {val_str}"
print(line)
print()
# ════════════════════════════════════════════════════════
# 表1:全局排名 — 按年化收益 Top 30
# ════════════════════════════════════════════════════════
top_annual = sort_results(results, "年化收益%", True)[:30]
print_table(
f" 全局排名 — 按年化收益 Top 30(共{len(results)}组) | 本金 {cfg['initial_capital']:,.0f} USDT",
top_annual,
fields=["策略名", "币种", "时间级别", "数据量", "总收益%", "年化收益%", "夏普比率", "最大回撤%", "胜率%", "盈亏比", "交易次数", "日期范围"],
col_widths=[22, 10, 8, 8, 9, 9, 8, 8, 7, 7, 6, 24],
)
# ════════════════════════════════════════════════════════
# 表2:按夏普比率 Top 30
# ════════════════════════════════════════════════════════
top_sharpe = sort_results(results, "夏普比率", True)[:30]
print_table(
" 全局排名 — 按夏普比率 Top 30",
top_sharpe,
fields=["策略名", "币种", "时间级别", "数据量", "年化收益%", "夏普比率", "最大回撤%", "胜率%", "盈亏比", "交易次数", "日期范围"],
col_widths=[22, 10, 8, 8, 9, 8, 8, 7, 7, 6, 24],
)
# ════════════════════════════════════════════════════════
# 表3:近半年+近一年(近期真实表现)按年化 Top 30
# ════════════════════════════════════════════════════════
recent = [r for r in results if r["数据量"] in ("近半年", "近一年")]
recent_top = sort_results(recent, "年化收益%", True)[:30]
print_table(
" 近期现实表现 — 近半年+近一年 — 按年化收益 Top 30",
recent_top,
fields=["策略名", "币种", "时间级别", "数据量", "总收益%", "年化收益%", "夏普比率", "最大回撤%", "胜率%", "盈亏比", "交易次数", "日期范围"],
col_widths=[22, 10, 8, 8, 9, 9, 8, 8, 7, 7, 6, 24],
)
# ════════════════════════════════════════════════════════
# 表4:全量数据(历史长期)按年化 Top 20
# ════════════════════════════════════════════════════════
full_data = [r for r in results if r["数据量"] == "全量"]
full_top = sort_results(full_data, "年化收益%", True)[:20]
print_table(
" 全量数据 — 按年化收益 Top 20",
full_top,
fields=["策略名", "币种", "时间级别", "总收益%", "年化收益%", "夏普比率", "最大回撤%", "胜率%", "盈亏比", "交易次数", "日期范围"],
col_widths=[22, 10, 8, 9, 9, 8, 8, 7, 7, 6, 24],
)
# ════════════════════════════════════════════════════════
# 表5:各策略在4h+1d上的近一年表现(最实用的中长线)
# ════════════════════════════════════════════════════════
mid_long = [r for r in recent if r["时间级别"] in ("4h", "1d")]
mid_sorted = sort_results(mid_long, "年化收益%", True)[:30]
print_table(
" 中长线(4h/1d)近期表现 — 按年化收益 Top 30",
mid_sorted,
fields=["策略名", "币种", "时间级别", "数据量", "总收益%", "年化收益%", "夏普比率", "最大回撤%", "胜率%", "盈亏比", "交易次数", "日期范围"],
col_widths=[22, 10, 8, 8, 9, 9, 8, 8, 7, 7, 6, 24],
)
# ════════════════════════════════════════════════════════
# 表6:策略×币种×时间级别 盈利能力矩阵(近一年年化)
# ════════════════════════════════════════════════════════
one_year = [r for r in results if r["数据量"] == "近一年"]
print()
print("" * 120)
print(" 近一年盈利能力矩阵 — 策略 × 币种 × 时间级别(年化收益%")
print("" * 120)
for tf in cfg["timeframes"]:
tf_data = [r for r in one_year if r["时间级别"] == tf]
if not tf_data:
continue
print(f"\n ▲ 时间级别: {tf}")
print(f" {'策略名':<24}", end="")
for sym in cfg["symbols"]:
print(f" {sym:>12}", end="")
print()
print(" " + "" * 80)
strategies_seen = set()
for r in sort_results(tf_data, "年化收益%", True):
if r["策略名"] not in strategies_seen:
strategies_seen.add(r["策略名"])
print(f" {r['策略名']:<24}", end="")
for sym in cfg["symbols"]:
match = [x for x in tf_data if x["策略名"] == r["策略名"] and x["币种"] == sym]
if match:
val = match[0]["年化收益%"]
color = "+" if val > 0 else ""
print(f" {color}{val:>11.1f}%", end="")
else:
print(f" {'':>12}", end="")
print()
print()
# ════════════════════════════════════════════════════════
# 表7:每组(时间级别+数据量)下的最佳策略
# ════════════════════════════════════════════════════════
print("" * 160)
print(" 每组(时间级别 + 数据量)下的最佳策略 — 按年化收益")
print("" * 160)
print()
for tf in cfg["timeframes"]:
for period in cfg["periods"]:
subset = [r for r in results if r["时间级别"] == tf and r["数据量"] == period]
if not subset:
continue
subset_sorted = sort_results(subset, "年化收益%", True)
print(f"{tf} | {period}")
print(f" {'排名':<5} {'策略名':<24} {'币种':<10} {'总收益%':>9} {'年化%':>9} {'夏普':>7} {'回撤%':>8} {'胜率%':>7} {'盈亏比':>7} {'交易':>6} {'卡尔玛':>7} {'日期范围':<24}")
print(" " + "" * 155)
for i, r in enumerate(subset_sorted[:8]):
rank = ["🥇1", "🥈2", "🥉3", " 4", " 5", " 6", " 7", " 8"][i]
print(f" {rank:<5} {r['策略名']:<24} {r['币种']:<10} {r['总收益%']:>8.1f}% {r['年化收益%']:>8.1f}% {r['夏普比率']:>7.2f} {r['最大回撤%']:>7.1f}% {r['胜率%']:>6.1f}% {r['盈亏比']:>7.2f} {r['交易次数']:>6} {r['卡尔玛比率']:>7.2f} {r['日期范围']:<24}")
print()
# ════════════════════════════════════════════════════════
# 表8:策略总览 — 每个策略在所有组合中的盈利比例
# ════════════════════════════════════════════════════════
print("" * 120)
print(" 策略胜率统计 — 每个策略在所有回测组合中的盈利比例")
print("" * 120)
print()
strategy_stats = {}
for r in results:
sn = r["策略名"]
if sn not in strategy_stats:
strategy_stats[sn] = {"total": 0, "positive": 0, "positive_annual": 0, "sum_return": 0, "sum_sharpe": 0}
strategy_stats[sn]["total"] += 1
if r["总收益%"] > 0:
strategy_stats[sn]["positive"] += 1
if r["年化收益%"] > 0:
strategy_stats[sn]["positive_annual"] += 1
strategy_stats[sn]["sum_return"] += r["年化收益%"]
strategy_stats[sn]["sum_sharpe"] += r["夏普比率"]
print(f" {'策略名':<24} {'总回测':>6} {'总收益>0':>9} {'年化>0':>8} {'平均年化%':>10} {'平均夏普':>8}")
print(" " + "" * 75)
for sn, st in sorted(strategy_stats.items(), key=lambda x: x[1]["positive_annual"] / x[1]["total"], reverse=True):
avg_ret = st["sum_return"] / st["total"]
avg_sharpe = st["sum_sharpe"] / st["total"]
pos_pct = st["positive_annual"] / st["total"] * 100
print(f" {sn:<24} {st['total']:>6} {st['positive']:>8} ({st['positive']/st['total']*100:>5.1f}%) {st['positive_annual']:>7} ({pos_pct:>5.1f}%) {avg_ret:>9.1f}% {avg_sharpe:>8.2f}")
print()
print("" * 120)
+974
View File
@@ -0,0 +1,974 @@
"""
全维度策略对比回测 — 8策略 × 4币种 × 5时间级别 × 4数据量
策略均来自 Investopedia / BabyPips / TradingView 等知名交易社区,覆盖四大类:
- 趋势跟踪:海龟交易、超级趋势、三均线排列、EMA双均线多空
- 动量:MACD金叉死叉
- 波动率突破:布林收缩爆发、ATR波动率突破
- 均值回归:RSI+布林带回归
用法:
source .venv/bin/activate && python example/full_comparison.py
"""
import asyncio
import json
import statistics
import sys
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Optional, Type
_project_root = Path(__file__).resolve().parent.parent.parent
if str(_project_root) not in sys.path:
sys.path.insert(0, str(_project_root))
from engine.common.base import BaseStrategy, Signal, StrategyConfig
from engine.common.models import Kline
from engine.common.config import config
from engine.backtest.models import BacktestConfig, BacktestResult
from engine.data import DataService
from engine.indicators.incremental import EmaInc, AtrInc, RsiInc, BbInc
from engine.example.long_short import LongShortEngine
# ── 全局常量 ──
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
TIMEFRAMES = ["15m", "30m", "1h", "4h", "1d"]
INITIAL = 10_000.0
WARMUP = 150
MAX_CONCURRENCY = 6
# ── 回测时间段定义 ──
NOW = datetime.now(timezone.utc)
PERIODS = {
"全量": (None, None), # 由数据库查询决定
"近两年": (NOW - timedelta(days=730), NOW),
"近一年": (NOW - timedelta(days=365), NOW),
"近半年": (NOW - timedelta(days=182), NOW),
}
# ── 最小数据量要求(跳过数据不足的组合)──
MIN_BARS_FOR_PERIOD = {
"全量": 500,
"近两年": 200,
"近一年": 100,
"近半年": 50,
}
# ════════════════════════════════════════════════════════
# 策略 1:海龟交易 (Turtle Trading)
# Richard Dennis & William Eckhardt, 1983
# 20日高点突破入场,10日低点突破出场,2N ATR 止损
# ════════════════════════════════════════════════════════
class TurtleConfig(StrategyConfig):
entry_period: int = 20
exit_period: int = 10
atr_period: int = 20
atr_stop: float = 2.0
class TurtleStrategy(BaseStrategy):
strategy_type = "趋势跟踪"
strategy_desc = "Donchian 20/10通道突破 + 2N ATR止损,多空双向"
def __init__(self, c: TurtleConfig):
super().__init__(c)
self.cfg = c
self._highs: list[float] = []
self._lows: list[float] = []
self._closes: list[float] = []
self._atr = AtrInc(c.atr_period)
self._side: str = ""
self._entry_price: float = 0.0
self._highest_since: float = 0.0
self._lowest_since: float = float("inf")
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._highs.append(k.high)
self._lows.append(k.low)
self._closes.append(k.close)
self._atr.update(k.high, k.low, k.close)
n = len(self._closes)
min_bars = max(self.cfg.entry_period, self.cfg.atr_period) + 5
if n < min_bars:
return None
ca = self._atr[-1]
if ca == 0:
return None
d_high = max(self._highs[-(self.cfg.entry_period + 1):-1])
d_low = min(self._lows[-(self.cfg.entry_period + 1):-1])
d_exit_high = max(self._highs[-(self.cfg.exit_period + 1):-1])
d_exit_low = min(self._lows[-(self.cfg.exit_period + 1):-1])
if self._side == "long":
self._highest_since = max(self._highest_since, k.high)
stop = self._entry_price - self.cfg.atr_stop * ca
trail = self._highest_since - self.cfg.atr_stop * ca * 0.5
if k.close < d_exit_low or k.close < max(stop, trail):
self._side = ""
return Signal(symbol=self.cfg.symbol, side="SELL", reason="海龟退出", timestamp=k.open_time, confidence=0.25)
elif self._side == "short":
self._lowest_since = min(self._lowest_since, k.low)
stop = self._entry_price + self.cfg.atr_stop * ca
trail = self._lowest_since + self.cfg.atr_stop * ca * 0.5
if k.close > d_exit_high or k.close > min(stop, trail):
self._side = ""
return Signal(symbol=self.cfg.symbol, side="BUY", reason="海龟退出", timestamp=k.open_time, confidence=0.25)
else:
margin = 0.002
if k.close > d_high * (1 + margin):
self._side = "long"
self._entry_price = k.close
self._highest_since = k.close
return Signal(symbol=self.cfg.symbol, side="BUY", reason="突破20日高", timestamp=k.open_time, confidence=0.25)
elif k.close < d_low * (1 - margin):
self._side = "short"
self._entry_price = k.close
self._lowest_since = k.close
return Signal(symbol=self.cfg.symbol, side="SELL", reason="跌破20日低", timestamp=k.open_time, confidence=0.25)
return None
# ════════════════════════════════════════════════════════
# 策略 2:超级趋势 (SuperTrend)
# Olivier Seban,广泛用于加密货币和商品
# ATR(10)×3 动态跟踪止损,趋势翻转即反转
# ════════════════════════════════════════════════════════
class SuperTrendConfig(StrategyConfig):
atr_period: int = 10
multiplier: float = 3.0
class SuperTrendStrategy(BaseStrategy):
strategy_type = "趋势跟踪"
strategy_desc = "ATR(10)×3倍动态跟踪止损带,趋势翻转即反转"
def __init__(self, c: SuperTrendConfig):
super().__init__(c)
self.cfg = c
self._atr = AtrInc(c.atr_period)
self._highs: list[float] = []
self._lows: list[float] = []
self._closes: list[float] = []
self._trend: int = 0
self._final_upper: float = 0.0
self._final_lower: float = 0.0
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._highs.append(k.high)
self._lows.append(k.low)
self._closes.append(k.close)
self._atr.update(k.high, k.low, k.close)
n = len(self._closes)
if n < self.cfg.atr_period + 5:
return None
ca = self._atr[-1]
if ca == 0:
return None
hl2 = (k.high + k.low) / 2.0
upper = hl2 + self.cfg.multiplier * ca
lower = hl2 - self.cfg.multiplier * ca
prev_upper = self._final_upper
prev_lower = self._final_lower
prev_trend = self._trend
if k.close > prev_upper and prev_upper > 0:
self._trend = 1
elif k.close < prev_lower and prev_lower > 0:
self._trend = -1
if self._trend == 1:
self._final_lower = max(lower, prev_lower) if prev_lower > 0 else lower
self._final_upper = float("inf")
elif self._trend == -1:
self._final_upper = min(upper, prev_upper) if prev_upper > 0 else upper
self._final_lower = float("-inf")
else:
self._final_upper = upper
self._final_lower = lower
if prev_trend == self._trend:
return None
if self._trend == 1:
return Signal(symbol=self.cfg.symbol, side="BUY", reason="SuperTrend转多", timestamp=k.open_time, confidence=0.25)
elif self._trend == -1:
return Signal(symbol=self.cfg.symbol, side="SELL", reason="SuperTrend转空", timestamp=k.open_time, confidence=0.25)
return None
# ════════════════════════════════════════════════════════
# 策略 3MACD 金叉死叉
# Gerald Appel, 1970s
# MACD(12,26,9) 零轴附近金叉/死叉 + ATR 止损
# ════════════════════════════════════════════════════════
class MacdCrossConfig(StrategyConfig):
fast: int = 12
slow: int = 26
signal: int = 9
atr_period: int = 14
atr_stop: float = 2.0
class MacdCrossStrategy(BaseStrategy):
strategy_type = "动量"
strategy_desc = "MACD(12,26,9)零轴上金叉做多/零轴下死叉做空+ATR止损"
def __init__(self, c: MacdCrossConfig):
super().__init__(c)
self.cfg = c
self._ema_fast = EmaInc(c.fast)
self._ema_slow = EmaInc(c.slow)
self._atr = AtrInc(c.atr_period)
self._macd_vals: list[float] = []
self._signal_vals: list[float] = []
self._side: str = ""
self._entry_price: float = 0.0
self._bars_held: int = 0
async def on_kline(self, k: Kline) -> Optional[Signal]:
fe = self._ema_fast.update(k.close)
se = self._ema_slow.update(k.close)
self._atr.update(k.high, k.low, k.close)
n = len(self._ema_fast)
min_bars = max(self.cfg.slow, self.cfg.signal) + 10
if n < min_bars:
return None
macd = fe - se
self._macd_vals.append(macd)
if len(self._macd_vals) < self.cfg.signal + 2:
self._signal_vals.append(0.0)
return None
if len(self._signal_vals) < self.cfg.signal:
self._signal_vals.append(0.0)
if len(self._signal_vals) == self.cfg.signal:
self._signal_vals[-1] = sum(self._macd_vals[-self.cfg.signal:]) / self.cfg.signal
return None
k_sig = 2.0 / (self.cfg.signal + 1)
sig_val = macd * k_sig + self._signal_vals[-1] * (1 - k_sig)
self._signal_vals.append(sig_val)
if len(self._signal_vals) < 3:
return None
cur_m, cur_s = self._macd_vals[-1], self._signal_vals[-1]
prev_m, prev_s = self._macd_vals[-2], self._signal_vals[-2]
ca = self._atr[-1]
if ca == 0:
return None
golden = prev_m <= prev_s and cur_m > cur_s
death = prev_m >= prev_s and cur_m < cur_s
if self._side == "long":
self._bars_held += 1
stop = self._entry_price - self.cfg.atr_stop * ca
if k.close < stop or (death and self._bars_held > 3):
self._side = ""; self._bars_held = 0
return Signal(symbol=self.cfg.symbol, side="SELL", reason="MACD退出", timestamp=k.open_time, confidence=0.25)
elif self._side == "short":
self._bars_held += 1
stop = self._entry_price + self.cfg.atr_stop * ca
if k.close > stop or (golden and self._bars_held > 3):
self._side = ""; self._bars_held = 0
return Signal(symbol=self.cfg.symbol, side="BUY", reason="MACD退出", timestamp=k.open_time, confidence=0.25)
else:
if golden and cur_m > 0:
self._side = "long"; self._entry_price = k.close; self._bars_held = 0
return Signal(symbol=self.cfg.symbol, side="BUY", reason="MACD零轴上金叉", timestamp=k.open_time, confidence=0.25)
elif death and cur_m < 0:
self._side = "short"; self._entry_price = k.close; self._bars_held = 0
return Signal(symbol=self.cfg.symbol, side="SELL", reason="MACD零轴下死叉", timestamp=k.open_time, confidence=0.25)
return None
# ════════════════════════════════════════════════════════
# 策略 4:布林收缩爆发 (Bollinger Squeeze)
# John Bollinger, 2002
# BB在KC内部收缩→扩张突破入场
# ════════════════════════════════════════════════════════
class BBSqueezeConfig(StrategyConfig):
bb_period: int = 20
bb_std: float = 2.0
kc_period: int = 20
kc_mult: float = 1.5
squeeze_lookback: int = 30
atr_stop: float = 2.0
class BBSqueezeStrategy(BaseStrategy):
strategy_type = "波动率突破"
strategy_desc = "BB收缩至KC内部后扩张爆发,顺势入场 + ATR止损"
def __init__(self, c: BBSqueezeConfig):
super().__init__(c)
self.cfg = c
self._bb = BbInc(c.bb_period, c.bb_std)
self._ema = EmaInc(c.kc_period)
self._atr_kc = AtrInc(c.kc_period)
self._atr_stop = AtrInc(14)
self._closes: list[float] = []
self._side: str = ""
self._entry_price: float = 0.0
self._bb_widths: list[float] = []
self._kc_widths: list[float] = []
self._was_squeezed: bool = False
self._squeeze_bars: int = 0
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
bb_u, bb_m, bb_l = self._bb.update(k.close)
typical = (k.high + k.low + k.close) / 3.0
kc_mid = self._ema.update(typical)
self._atr_kc.update(k.high, k.low, k.close)
self._atr_stop.update(k.high, k.low, k.close)
n = len(self._closes)
min_bars = max(self.cfg.bb_period, self.cfg.kc_period, self.cfg.squeeze_lookback) + 5
if n < min_bars:
return None
atr_kc = self._atr_kc[-1]
ca = self._atr_stop[-1]
if atr_kc == 0 or ca == 0 or bb_u == 0:
return None
kc_u = kc_mid + self.cfg.kc_mult * atr_kc
kc_l = kc_mid - self.cfg.kc_mult * atr_kc
bb_width = bb_u - bb_l
kc_width = kc_u - kc_l
self._bb_widths.append(bb_width)
self._kc_widths.append(kc_width)
is_squeezed = bb_u < kc_u and bb_l > kc_l
lookback = min(self.cfg.squeeze_lookback, len(self._bb_widths))
recent_bb_w = self._bb_widths[-lookback:]
min_bb_w = min(recent_bb_w)
width_squeeze = bb_width < min_bb_w * 1.2
was_squeezed = self._was_squeezed
fired = False
if is_squeezed:
self._was_squeezed = True
self._squeeze_bars += 1
elif self._was_squeezed:
self._was_squeezed = False
self._squeeze_bars = 0
fired = True
ema5 = sum(self._closes[-5:]) / 5.0 if n >= 5 else k.close
up_momentum = k.close > bb_m and k.close > ema5
down_momentum = k.close < bb_m and k.close < ema5
if self._side == "long":
stop = self._entry_price - self.cfg.atr_stop * ca
if k.close < stop or (down_momentum and not is_squeezed):
self._side = ""
return Signal(symbol=self.cfg.symbol, side="SELL", reason="BB退出", timestamp=k.open_time, confidence=0.25)
elif self._side == "short":
stop = self._entry_price + self.cfg.atr_stop * ca
if k.close > stop or (up_momentum and not is_squeezed):
self._side = ""
return Signal(symbol=self.cfg.symbol, side="BUY", reason="BB退出", timestamp=k.open_time, confidence=0.25)
else:
if was_squeezed and fired and width_squeeze:
if up_momentum:
self._side = "long"; self._entry_price = k.close
return Signal(symbol=self.cfg.symbol, side="BUY", reason="BB收缩爆发做多", timestamp=k.open_time, confidence=0.25)
elif down_momentum:
self._side = "short"; self._entry_price = k.close
return Signal(symbol=self.cfg.symbol, side="SELL", reason="BB收缩爆发做空", timestamp=k.open_time, confidence=0.25)
return None
# ════════════════════════════════════════════════════════
# 策略 5:三均线排列 (Triple EMA)
# 经典三线开花 — EMA(10,30,60) 多头/空头排列 + ATR 追踪止损
# ════════════════════════════════════════════════════════
class TripleEmaConfig(StrategyConfig):
fast: int = 10
mid: int = 30
slow: int = 60
atr_period: int = 14
atr_stop: float = 2.0
class TripleEmaStrategy(BaseStrategy):
strategy_type = "趋势跟踪"
strategy_desc = "EMA(10,30,60)多头/空头排列,快线金叉入场+ATR追踪止损"
def __init__(self, c: TripleEmaConfig):
super().__init__(c)
self.cfg = c
self._ema_fast = EmaInc(c.fast)
self._ema_mid = EmaInc(c.mid)
self._ema_slow = EmaInc(c.slow)
self._atr = AtrInc(c.atr_period)
self._side: str = ""
self._entry_price: float = 0.0
self._highest_since: float = 0.0
self._lowest_since: float = float("inf")
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._ema_fast.update(k.close)
self._ema_mid.update(k.close)
self._ema_slow.update(k.close)
self._atr.update(k.high, k.low, k.close)
n = len(self._ema_slow)
if n < self.cfg.slow + 10:
return None
ef, em, es = self._ema_fast[-1], self._ema_mid[-1], self._ema_slow[-1]
pf, pm = self._ema_fast[-2], self._ema_mid[-2]
ca = self._atr[-1]
if ef == 0 or em == 0 or es == 0 or ca == 0:
return None
bull_align = ef > em > es
bear_align = ef < em < es
fast_cross_mid_up = pf <= pm and ef > em
fast_cross_mid_down = pf >= pm and ef < em
if self._side == "long":
self._highest_since = max(self._highest_since, k.high)
trail = self._highest_since - self.cfg.atr_stop * ca
if fast_cross_mid_down or k.close < trail:
self._side = ""
return Signal(symbol=self.cfg.symbol, side="SELL", reason="三均线退出", timestamp=k.open_time, confidence=0.25)
elif self._side == "short":
self._lowest_since = min(self._lowest_since, k.low)
trail = self._lowest_since + self.cfg.atr_stop * ca
if fast_cross_mid_up or k.close > trail:
self._side = ""
return Signal(symbol=self.cfg.symbol, side="BUY", reason="三均线退出", timestamp=k.open_time, confidence=0.25)
else:
if fast_cross_mid_up and bull_align:
self._side = "long"; self._entry_price = k.close; self._highest_since = k.close
return Signal(symbol=self.cfg.symbol, side="BUY", reason="三线多头排列", timestamp=k.open_time, confidence=0.25)
elif fast_cross_mid_down and bear_align:
self._side = "short"; self._entry_price = k.close; self._lowest_since = k.close
return Signal(symbol=self.cfg.symbol, side="SELL", reason="三线空头排列", timestamp=k.open_time, confidence=0.25)
return None
# ════════════════════════════════════════════════════════
# 策略 6RSI均值回归 (RSI Mean Reversion)
# 经典指标 — RSI(14)超买超卖 + 布林带确认 → 逆向交易
# ════════════════════════════════════════════════════════
class MeanRevConfig(StrategyConfig):
rsi_period: int = 14
rsi_oversold: float = 25.0
rsi_overbought: float = 75.0
bb_period: int = 20
bb_std: float = 2.0
atr_stop: float = 1.5
require_bb_touch: bool = True
class MeanRevStrategy(BaseStrategy):
strategy_type = "均值回归"
strategy_desc = "RSI(14)超卖25/超买75 + 布林带触碰确认 → 逆向回归"
def __init__(self, c: MeanRevConfig):
super().__init__(c)
self.cfg = c
self._rsi = RsiInc(c.rsi_period)
self._bb = BbInc(c.bb_period, c.bb_std)
self._atr = AtrInc(14)
self._side: str = ""
self._entry_price: float = 0.0
async def on_kline(self, k: Kline) -> Optional[Signal]:
r = self._rsi.update(k.close)
up, mid, lo = self._bb.update(k.close)
atr_v = self._atr.update(k.high, k.low, k.close)
if r == 0 or up == 0 or atr_v == 0:
return None
below_bb = k.close < lo if self.cfg.require_bb_touch else True
above_bb = k.close > up if self.cfg.require_bb_touch else True
if self._side == "long":
stop = self._entry_price - self.cfg.atr_stop * atr_v
take = self._entry_price + self.cfg.atr_stop * atr_v * 1.5
if k.close <= stop or k.close >= take or r > 55:
self._side = ""
reason = "止损" if k.close <= stop else ("止盈" if k.close >= take else "RSI回归中轨")
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time)
elif self._side == "short":
stop = self._entry_price + self.cfg.atr_stop * atr_v
take = self._entry_price - self.cfg.atr_stop * atr_v * 1.5
if k.close >= stop or k.close <= take or r < 45:
self._side = ""
reason = "止损" if k.close >= stop else ("止盈" if k.close <= take else "RSI回归中轨")
return Signal(symbol=self.cfg.symbol, side="BUY", reason=reason, timestamp=k.open_time)
else:
if r < self.cfg.rsi_oversold and below_bb:
self._side = "long"; self._entry_price = k.close
return Signal(symbol=self.cfg.symbol, side="BUY", reason=f"RSI超卖{r:.0f}", timestamp=k.open_time)
elif r > self.cfg.rsi_overbought and above_bb:
self._side = "short"; self._entry_price = k.close
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"RSI超买{r:.0f}", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 策略 7ATR波动率突破 (Volatility Breakout)
# 经典波动率策略 — ATR收缩至极低后扩张 → 顺势突破
# ════════════════════════════════════════════════════════
class VolBreakConfig(StrategyConfig):
atr_period: int = 14
squeeze_period: int = 20
squeeze_ratio: float = 0.7
atr_stop: float = 2.0
class VolBreakStrategy(BaseStrategy):
strategy_type = "波动率突破"
strategy_desc = "ATR(14)收缩至极低后扩张突破 + EMA(10/30)方向确认"
def __init__(self, c: VolBreakConfig):
super().__init__(c)
self.cfg = c
self._atr = AtrInc(c.atr_period)
self._ema_fast = EmaInc(10)
self._ema_slow = EmaInc(30)
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._side: str = ""
self._entry_price: float = 0.0
self._was_squeezed = False
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
self._highs.append(k.high)
self._lows.append(k.low)
self._atr.update(k.high, k.low, k.close)
self._ema_fast.update(k.close)
self._ema_slow.update(k.close)
n = len(self._closes)
if n < self.cfg.atr_period + self.cfg.squeeze_period:
return None
atr_now = self._atr[-1]
atr_prev = self._atr[-2] if n >= 2 else 0
ca = atr_now
if ca == 0:
return None
atr_window = [self._atr[i] for i in range(max(0, n - self.cfg.squeeze_period), n) if self._atr[i] > 0]
if not atr_window:
return None
min_atr = min(atr_window)
is_squeezed = atr_now < min_atr * (1 + (1 - self.cfg.squeeze_ratio))
atr_expanding = atr_now > atr_prev * 1.05 if atr_prev > 0 else False
cf, cs = self._ema_fast[-1], self._ema_slow[-1]
trend_up = cf > cs
if self._side == "long":
self._was_squeezed = False
stop = self._entry_price - self.cfg.atr_stop * ca
if k.close < stop or (cf < cs and not is_squeezed):
self._side = ""
return Signal(symbol=self.cfg.symbol, side="SELL", reason="ATR退出", timestamp=k.open_time)
elif self._side == "short":
self._was_squeezed = False
stop = self._entry_price + self.cfg.atr_stop * ca
if k.close > stop or (cf > cs and not is_squeezed):
self._side = ""
return Signal(symbol=self.cfg.symbol, side="BUY", reason="ATR退出", timestamp=k.open_time)
else:
if is_squeezed:
self._was_squeezed = True
elif self._was_squeezed and atr_expanding:
self._was_squeezed = False
if trend_up:
self._side = "long"; self._entry_price = k.close
return Signal(symbol=self.cfg.symbol, side="BUY", reason="ATR扩张突破做多", timestamp=k.open_time)
else:
self._side = "short"; self._entry_price = k.close
return Signal(symbol=self.cfg.symbol, side="SELL", reason="ATR扩张突破做空", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 策略 8EMA双均线多空 (EMA Crossover)
# 最经典的均线交叉 — 始终在场,金叉做多死叉做空
# ════════════════════════════════════════════════════════
class EmaCrossConfig(StrategyConfig):
fast: int = 10
slow: int = 50
atr_stop: float = 2.5
class EmaCrossStrategy(BaseStrategy):
strategy_type = "趋势跟踪"
strategy_desc = "EMA(10,50)金叉做多死叉做空 + ATR追踪止损,始终在场"
def __init__(self, c: EmaCrossConfig):
super().__init__(c)
self.cfg = c
self._ema_fast = EmaInc(c.fast)
self._ema_slow = EmaInc(c.slow)
self._atr = AtrInc(14)
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._highest: float = 0.0
self._lowest: float = float('inf')
self._position_side: str = ""
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
self._highs.append(k.high)
self._lows.append(k.low)
self._ema_fast.update(k.close)
self._ema_slow.update(k.close)
self._atr.update(k.high, k.low, k.close)
n = len(self._closes)
if n < self.cfg.slow + 5:
return None
cur_f, cur_s = self._ema_fast[-1], self._ema_slow[-1]
cur_atr = self._atr[-1]
prev_f, prev_s = self._ema_fast[-2], self._ema_slow[-2]
if cur_f == 0 or cur_s == 0 or cur_atr == 0:
return None
golden = prev_f <= prev_s and cur_f > cur_s
death = prev_f >= prev_s and cur_f < cur_s
if self._position_side == "long":
self._highest = max(self._highest, k.high)
stop = self._highest - self.cfg.atr_stop * cur_atr
if death:
self._position_side = "short"
return Signal(symbol=self.cfg.symbol, side="SELL", reason="EMA死叉→做空", timestamp=k.open_time)
if k.close < stop:
self._position_side = ""
return Signal(symbol=self.cfg.symbol, side="SELL", reason="ATR止损→空仓", timestamp=k.open_time)
elif self._position_side == "short":
self._lowest = min(self._lowest, k.low)
stop = self._lowest + self.cfg.atr_stop * cur_atr
if golden:
self._position_side = "long"
return Signal(symbol=self.cfg.symbol, side="BUY", reason="EMA金叉→做多", timestamp=k.open_time)
if k.close > stop:
self._position_side = ""
return Signal(symbol=self.cfg.symbol, side="BUY", reason="ATR止损→空仓", timestamp=k.open_time)
else:
if golden:
self._position_side = "long"; self._highest = k.close
return Signal(symbol=self.cfg.symbol, side="BUY", reason="金叉→做多", timestamp=k.open_time)
elif death:
self._position_side = "short"; self._lowest = k.close
return Signal(symbol=self.cfg.symbol, side="SELL", reason="死叉→做空", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 策略注册表
# ════════════════════════════════════════════════════════
STRATEGY_REGISTRY = {
"1.海龟交易": {
"config_cls": TurtleConfig,
"strategy_cls": TurtleStrategy,
"make_config": lambda s: TurtleConfig(symbol=s, entry_period=20, exit_period=10, atr_period=20, atr_stop=2.0),
},
"2.超级趋势": {
"config_cls": SuperTrendConfig,
"strategy_cls": SuperTrendStrategy,
"make_config": lambda s: SuperTrendConfig(symbol=s, atr_period=10, multiplier=3.0),
},
"3.MACD金叉死叉": {
"config_cls": MacdCrossConfig,
"strategy_cls": MacdCrossStrategy,
"make_config": lambda s: MacdCrossConfig(symbol=s, fast=12, slow=26, signal=9, atr_period=14, atr_stop=2.0),
},
"4.布林收缩爆发": {
"config_cls": BBSqueezeConfig,
"strategy_cls": BBSqueezeStrategy,
"make_config": lambda s: BBSqueezeConfig(symbol=s, bb_period=20, bb_std=2.0, kc_period=20, kc_mult=1.5, squeeze_lookback=30, atr_stop=2.0),
},
"5.三均线排列": {
"config_cls": TripleEmaConfig,
"strategy_cls": TripleEmaStrategy,
"make_config": lambda s: TripleEmaConfig(symbol=s, fast=10, mid=30, slow=60, atr_period=14, atr_stop=2.0),
},
"6.RSI均值回归": {
"config_cls": MeanRevConfig,
"strategy_cls": MeanRevStrategy,
"make_config": lambda s: MeanRevConfig(symbol=s, rsi_period=14, rsi_oversold=25, rsi_overbought=75, bb_period=20, bb_std=2.0, atr_stop=1.5),
},
"7.ATR波动率突破": {
"config_cls": VolBreakConfig,
"strategy_cls": VolBreakStrategy,
"make_config": lambda s: VolBreakConfig(symbol=s, atr_period=14, squeeze_period=20, squeeze_ratio=0.7, atr_stop=2.0),
},
"8.EMA双均线多空": {
"config_cls": EmaCrossConfig,
"strategy_cls": EmaCrossStrategy,
"make_config": lambda s: EmaCrossConfig(symbol=s, fast=10, slow=50, atr_stop=2.5),
},
}
# ════════════════════════════════════════════════════════
# 参数用于表格的简洁呈现
# ════════════════════════════════════════════════════════
STRATEGY_PARAMS_STR = {
"1.海龟交易": "entry=20/exit=10/ATR(20)x2.0",
"2.超级趋势": "ATR(10)x3.0",
"3.MACD金叉死叉": "MACD(12,26,9)/ATR(14)x2.0",
"4.布林收缩爆发": "BB(20,2.0)/KC(20,1.5)/squeeze=30",
"5.三均线排列": "EMA(10,30,60)/ATR(14)x2.0",
"6.RSI均值回归": "RSI(14)25/75+BB(20,2.0)/ATR(14)x1.5",
"7.ATR波动率突破": "ATR(14)/squeeze=20x0.7/EMA(10,30)",
"8.EMA双均线多空": "EMA(10,50)/ATR(14)x2.5",
}
# ════════════════════════════════════════════════════════
# 执行
# ════════════════════════════════════════════════════════
async def run_one(entry, symbol, interval, period_label, start, end):
"""执行单次回测"""
make_config = entry["make_config"]
strategy_cls = entry["strategy_cls"]
sc = make_config(symbol)
bt = BacktestConfig(
symbol=symbol, interval=interval,
start_time=start, end_time=end,
initial_capital=INITIAL, warmup_bars=WARMUP,
)
engine = LongShortEngine(bt, db_config=config.db)
t0 = time.time()
try:
r = await engine.run(strategy_cls, sc)
elapsed = time.time() - t0
return r, elapsed, None
except Exception as ex:
elapsed = time.time() - t0
return None, elapsed, str(ex)
def safe(val, default=0):
"""安全取值,避免 None"""
return default if val is None else val
async def main():
# 第一步:预取所有数据范围
ds = DataService(config.db)
await ds.connect()
print("正在获取数据范围...")
date_ranges: dict[tuple[str, str], tuple] = {} # (symbol, interval) -> (start_dt, end_dt, bar_count_estimate)
for symbol in SYMBOLS:
for tf in TIMEFRAMES:
try:
s, e = await ds.fetch_symbol_date_range(symbol, tf)
# 粗略估计 bar 数量
bar_ms = {"15m": 900_000, "30m": 1_800_000, "1h": 3_600_000, "4h": 14_400_000, "1d": 86_400_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}")
await ds.close()
# 第二步:构建任务列表 (跳过数据不足的组合)
sem = asyncio.Semaphore(MAX_CONCURRENCY)
tasks_info: list[dict] = []
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:
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 / {
"15m": 900_000, "30m": 1_800_000, "1h": 3_600_000, "4h": 14_400_000, "1d": 86_400_000
}[tf])
if actual_bars < min_bars:
continue
# 跳过日线+近半年(bar太少)
if tf == "1d" and period_label == "近半年":
continue
tasks_info.append({
"strat_name": strat_name,
"entry": entry,
"symbol": symbol,
"tf": tf,
"period_label": period_label,
"start": actual_start,
"end": actual_end,
})
total = len(tasks_info)
print(f"\n{total} 组回测任务 (8策略×4币种×5时间×4数据量 - 跳过数据不足和日线近半年)")
# 第三步:并发执行
results: list[dict] = []
completed = 0
errors = 0
async def run_one_safe(info):
nonlocal completed, errors
async with sem:
r, elapsed, err = await run_one(
info["entry"], info["symbol"], info["tf"],
info["period_label"], info["start"], info["end"],
)
completed += 1
if err:
errors += 1
status = f"{err[:40]}"
elif r is None:
errors += 1
status = "✗ 无结果"
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)
row = {
"策略名": info["strat_name"],
"币种": info["symbol"],
"时间级别": info["tf"],
"数据量": info["period_label"],
"策略类型": info["entry"]["strategy_cls"].strategy_type if r else "",
"策略参数": STRATEGY_PARAMS_STR.get(info["strat_name"], ""),
"策略描述": info["entry"]["strategy_cls"].strategy_desc if r else "",
"日期范围": f"{info['start'].date()}~{info['end'].date()}",
}
if r is not None:
m = r.metrics
row.update({
"初始资金": INITIAL,
"最终权益": round(m.final_equity, 2),
"总收益%": round(m.total_return_pct, 2),
"年化收益%": round(m.annual_return_pct, 2),
"夏普比率": round(m.sharpe_ratio, 2),
"最大回撤%": round(m.max_drawdown_pct, 2),
"胜率%": round(m.win_rate * 100, 2),
"盈亏比": round(m.profit_factor, 2),
"交易次数": m.total_trades,
"平均盈亏": round(m.avg_trade_pnl, 2),
"最佳盈亏": round(m.best_trade_pnl, 2),
"最差盈亏": round(m.worst_trade_pnl, 2),
"卡尔玛比率": round(m.calmar_ratio, 2),
"耗时s": round(elapsed, 1),
})
else:
row.update({
"初始资金": INITIAL,
"最终权益": 0,
"总收益%": 0,
"年化收益%": 0,
"夏普比率": 0,
"最大回撤%": 0,
"胜率%": 0,
"盈亏比": 0,
"交易次数": 0,
"平均盈亏": 0,
"最佳盈亏": 0,
"最差盈亏": 0,
"卡尔玛比率": 0,
"耗时s": round(elapsed, 1),
"错误": err or "未知错误",
})
results.append(row)
return row
t_total = time.time()
await asyncio.gather(*[run_one_safe(info) for info in tasks_info])
total_elapsed = time.time() - t_total
print(f"\n全部完成!成功 {total - errors}/{total},错误 {errors},总耗时 {total_elapsed:.0f}s")
# 第四步:打印完整表格
print()
print("" * 195)
print(" 全维度策略对比回测结果")
print("" * 195)
print()
# 按策略分组打印
for strat_name in STRATEGY_REGISTRY:
strat_results = [r for r in results if r["策略名"] == strat_name]
if not strat_results:
continue
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)
# 排序:币种、时间级别、数据量
strat_results.sort(key=lambda x: (SYMBOLS.index(x["币种"]), TIMEFRAMES.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()
# 第五步:终极汇总 — 每种时间级别+数据量下的最佳策略
print("" * 195)
print(" ■ 终极汇总:每组(时间级别+数据量)下各币种最佳策略(按年化收益)")
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)
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()
# 第六步:保存 JSON
output_file = _project_root / "engine" / "example" / "full_comparison_result.json"
with open(output_file, "w", encoding="utf-8") as f:
json.dump({
"config": {
"symbols": SYMBOLS,
"timeframes": TIMEFRAMES,
"periods": list(PERIODS.keys()),
"initial_capital": INITIAL,
"warmup_bars": WARMUP,
"total_tasks": total,
"total_errors": errors,
"elapsed_seconds": total_elapsed,
"run_time": datetime.now(timezone.utc).isoformat(),
},
"results": results,
}, f, ensure_ascii=False, indent=2, default=str)
print(f" 详细结果已保存至: {output_file}")
print()
print("" * 195)
if __name__ == "__main__":
asyncio.run(main())
File diff suppressed because it is too large Load Diff
+563
View File
@@ -0,0 +1,563 @@
"""
日内策略探索 — 4 种思路 (15m / 30m / 1h 全币种)
1. 均值回归:RSI超买超卖 + 布林带触碰,震荡市中做回归
2. 多时间框架:4h 牛熊判定方向过滤 + 1h EMA交叉入场
3. 波动率突破:ATR 收缩后扩张,顺势突破
4. 成交量:OBV 背离 + VWAP 回归
用法:
source .venv/bin/activate && python example/intraday_explore.py
"""
import asyncio
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
_project_root = Path(__file__).resolve().parent.parent.parent
if str(_project_root) not in sys.path:
sys.path.insert(0, str(_project_root))
from engine.common.base import BaseStrategy, Signal, StrategyConfig
from engine.common.models import Kline
from engine.common.config import config
from engine.backtest import BacktestConfig
from engine.data import DataService
from engine.indicators.incremental import EmaInc, AtrInc, RsiInc, BbInc
from engine.example.long_short import LongShortEngine
from engine.example.regime_all import RegimeDetector3, RegimeEmaConfig, RegimeEmaStrategy
# ════════════════════════════════════════════════════════
# 策略 1:均值回归 — RSI + 布林带
# ════════════════════════════════════════════════════════
class MeanRevConfig(StrategyConfig):
rsi_period: int = 14
rsi_oversold: float = 25.0
rsi_overbought: float = 75.0
bb_period: int = 20
bb_std: float = 2.0
atr_stop: float = 1.5
require_bb_touch: bool = True # 是否要求价格触碰布林带
class MeanRevStrategy(BaseStrategy):
"""RSI 极端 + 布林带确认 → 均值回归,ATR 止损"""
strategy_type = "mean_rev"
def __init__(self, c: MeanRevConfig):
super().__init__(c)
self.cfg = c
self._rsi = RsiInc(c.rsi_period)
self._bb = BbInc(c.bb_period, c.bb_std)
self._atr = AtrInc(14)
self._side: str = "" # "long" / "short"
self._entry_price: float = 0.0
async def on_kline(self, k: Kline) -> Optional[Signal]:
r = self._rsi.update(k.close)
up, mid, lo = self._bb.update(k.close)
atr_v = self._atr.update(k.high, k.low, k.close)
if r == 0 or up == 0 or atr_v == 0:
return None
below_bb = k.close < lo if self.cfg.require_bb_touch else True
above_bb = k.close > up if self.cfg.require_bb_touch else True
# ── 持仓管理 ──
if self._side == "long":
stop = self._entry_price - self.cfg.atr_stop * atr_v
take = self._entry_price + self.cfg.atr_stop * atr_v * 1.5
if k.close <= stop or k.close >= take or r > 55: # 回归中轨或超止损
self._side = ""
reason = "止损" if k.close <= stop else ("止盈" if k.close >= take else "RSI回归中轨")
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time)
elif self._side == "short":
stop = self._entry_price + self.cfg.atr_stop * atr_v
take = self._entry_price - self.cfg.atr_stop * atr_v * 1.5
if k.close >= stop or k.close <= take or r < 45:
self._side = ""
reason = "止损" if k.close >= stop else ("止盈" if k.close <= take else "RSI回归中轨")
return Signal(symbol=self.cfg.symbol, side="BUY", reason=reason, timestamp=k.open_time)
# ── 入场 ──
else:
if r < self.cfg.rsi_oversold and below_bb:
self._side = "long"
self._entry_price = k.close
return Signal(symbol=self.cfg.symbol, side="BUY", reason=f"RSI超卖{r:.0f}", timestamp=k.open_time)
elif r > self.cfg.rsi_overbought and above_bb:
self._side = "short"
self._entry_price = k.close
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"RSI超买{r:.0f}", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 策略 2:多时间框架 — 4h 方向 + 1h 入场
# ════════════════════════════════════════════════════════
class MultiTFConfig(StrategyConfig):
fast: int = 20
slow: int = 100
atr_stop: float = 2.0
# 4h 数据由策略内部自动加载
class MultiTFStrategy(BaseStrategy):
"""4h 牛熊判定方向过滤,1h EMA 交叉入场,只顺大势"""
strategy_type = "multi_tf"
def __init__(self, c: MultiTFConfig):
super().__init__(c)
self.cfg = c
self._ema_fast = EmaInc(c.fast)
self._ema_slow = EmaInc(c.slow)
self._atr = AtrInc(14)
self._side: str = ""
self._hp: float = 0.0
self._lp: float = float("inf")
# 4h 牛熊判定 — 在 on_start 中加载
self._regime_map: dict[int, str] = {} # timestamp_hour -> regime
self._4h_loaded = False
async def on_start(self) -> None:
"""加载 4h 数据并预计算牛熊判定"""
await super().on_start()
if self._4h_loaded:
return
try:
ds = DataService(config.db)
await ds.connect()
try:
klines_4h = await ds.fetch_klines(
symbol=self.cfg.symbol, interval="4h",
start_time=datetime(2017, 1, 1),
end_time=datetime(2026, 12, 31),
limit=1_000_000,
)
detector = RegimeDetector3()
for k in klines_4h:
detector.update(k.close)
idx = len(detector._e200) - 1
if idx >= 220:
regime = detector.detect(k.close, idx)
# 4h bar 覆盖的时间窗口(按小时取整)
hour_key = int(k.open_time / 3_600_000)
for h in range(4):
self._regime_map[hour_key + h] = regime
self._4h_loaded = True
finally:
await ds.close()
except Exception:
pass # 加载失败则不做过滤
def _get_regime(self, ts_ms: float) -> str:
"""根据 1h bar 时间戳查找对应 4h 牛熊状态"""
hour_key = int(ts_ms / 3_600_000)
return self._regime_map.get(hour_key, "sideways")
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._ema_fast.update(k.close)
self._ema_slow.update(k.close)
self._atr.update(k.high, k.low, k.close)
n = len(self._ema_fast)
if n < self.cfg.slow + 5:
return None
cf, cs = self._ema_fast[-1], self._ema_slow[-1]
pf, ps = self._ema_fast[-2], self._ema_slow[-2]
ca = self._atr[-1]
if cf == 0 or cs == 0 or ca == 0:
return None
golden = pf <= ps and cf > cs
death = pf >= ps and cf < cs
regime = self._get_regime(k.open_time)
# ── 多头持仓 ──
if self._side == "long":
self._hp = max(self._hp, k.high)
stop = self._hp - self.cfg.atr_stop * ca
if death or k.close < stop:
self._side = ""
reason = "死叉" if death else "ATR止损"
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time)
# ── 空头持仓 ──
elif self._side == "short":
self._lp = min(self._lp, k.low)
stop = self._lp + self.cfg.atr_stop * ca
if golden or k.close > stop:
self._side = ""
reason = "金叉" if golden else "ATR止损"
return Signal(symbol=self.cfg.symbol, side="BUY", reason=reason, timestamp=k.open_time)
# ── 入场:必须顺4h方向 ──
else:
if golden and regime == "bull":
self._side = "long"
self._hp = k.close
return Signal(symbol=self.cfg.symbol, side="BUY", reason="4h牛+金叉", timestamp=k.open_time)
elif death and regime == "bear":
self._side = "short"
self._lp = k.close
return Signal(symbol=self.cfg.symbol, side="SELL", reason="4h熊+死叉", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 策略 3:波动率突破 — ATR 收缩扩张
# ════════════════════════════════════════════════════════
class VolBreakConfig(StrategyConfig):
atr_period: int = 14
squeeze_period: int = 20 # ATR 收缩回看窗口
squeeze_ratio: float = 0.7 # 当前 ATR < 最低 ATR * ratio 时视为收缩
atr_stop: float = 2.0
class VolBreakStrategy(BaseStrategy):
"""ATR 收缩到极致后扩张 → 顺势突破,ATR 止损"""
strategy_type = "vol_break"
def __init__(self, c: VolBreakConfig):
super().__init__(c)
self.cfg = c
self._atr = AtrInc(c.atr_period)
self._ema_fast = EmaInc(10)
self._ema_slow = EmaInc(30)
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._side: str = ""
self._entry_price: float = 0.0
self._was_squeezed = False
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
self._highs.append(k.high)
self._lows.append(k.low)
self._atr.update(k.high, k.low, k.close)
self._ema_fast.update(k.close)
self._ema_slow.update(k.close)
n = len(self._closes)
if n < self.cfg.atr_period + self.cfg.squeeze_period:
return None
atr_now = self._atr[-1]
atr_prev = self._atr[-2] if n >= 2 else 0
ca = atr_now
if ca == 0:
return None
# ATR 收缩检测:当前 ATR 是否处于 squeeze_period 内的最低水平
atr_window = [self._atr[i] for i in range(max(0, n - self.cfg.squeeze_period), n) if self._atr[i] > 0]
if not atr_window:
return None
min_atr = min(atr_window)
is_squeezed = atr_now < min_atr * (1 + (1 - self.cfg.squeeze_ratio))
# ATR 扩张信号
atr_expanding = atr_now > atr_prev * 1.05 if atr_prev > 0 else False
# 趋势方向
cf = self._ema_fast[-1]
cs = self._ema_slow[-1]
trend_up = cf > cs
# ── 持仓管理 ──
if self._side == "long":
self._was_squeezed = False
stop = self._entry_price - self.cfg.atr_stop * ca
if k.close < stop or (cf < cs and not is_squeezed):
self._side = ""
return Signal(symbol=self.cfg.symbol, side="SELL", reason="ATR止损或转弱", timestamp=k.open_time)
elif self._side == "short":
self._was_squeezed = False
stop = self._entry_price + self.cfg.atr_stop * ca
if k.close > stop or (cf > cs and not is_squeezed):
self._side = ""
return Signal(symbol=self.cfg.symbol, side="BUY", reason="ATR止损或转强", timestamp=k.open_time)
# ── 入场 ──
else:
if is_squeezed:
self._was_squeezed = True
elif self._was_squeezed and atr_expanding:
self._was_squeezed = False
if trend_up:
self._side = "long"
self._entry_price = k.close
return Signal(symbol=self.cfg.symbol, side="BUY", reason="ATR扩张突破做多", timestamp=k.open_time)
else:
self._side = "short"
self._entry_price = k.close
return Signal(symbol=self.cfg.symbol, side="SELL", reason="ATR扩张突破做空", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 策略 4:成交量 — OBV 背离 + VWAP 回归
# ════════════════════════════════════════════════════════
class VolumeConfig(StrategyConfig):
obv_lookback: int = 20 # OBV 背离检测窗口
vwap_std: float = 2.0 # VWAP 偏离标准差倍数
atr_stop: float = 2.0
class VolumeStrategy(BaseStrategy):
"""OBV 背离(价格新低但 OBV 未新低→看涨)+ VWAP 偏离回归"""
strategy_type = "volume"
def __init__(self, c: VolumeConfig):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._volumes: list[float] = []
self._obv: list[float] = [] # 增量 OBV
self._obv_val: float = 0.0
self._atr = AtrInc(14)
self._cum_pv: float = 0.0 # 累积 price*volume
self._cum_vol: float = 0.0 # 累积 volume
self._side: str = ""
self._entry_price: float = 0.0
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
self._highs.append(k.high)
self._lows.append(k.low)
self._volumes.append(k.volume)
self._atr.update(k.high, k.low, k.close)
# 增量 OBV
n = len(self._closes)
if n == 1:
self._obv_val = k.volume
else:
if k.close > self._closes[-2]:
self._obv_val += k.volume
elif k.close < self._closes[-2]:
self._obv_val -= k.volume
self._obv.append(self._obv_val)
# 增量 VWAP
typical = (k.high + k.low + k.close) / 3.0
self._cum_pv += typical * k.volume
self._cum_vol += k.volume
vwap = self._cum_pv / self._cum_vol if self._cum_vol > 0 else k.close
if n < self.cfg.obv_lookback + 5:
return None
ca = self._atr[-1]
if ca == 0:
return None
# OBV 背离检测:价格新低但 OBV 未新低 → 潜在反转
lookback = self.cfg.obv_lookback
price_window = self._closes[-lookback:]
obv_window = self._obv[-lookback:]
price_made_new_low = min(price_window) == price_window[-1]
obv_not_new_low = min(obv_window) < obv_window[-1]
obv_bull_div = price_made_new_low and obv_not_new_low
# OBV 负背离:价格新高但 OBV 未新高
price_made_new_high = max(price_window) == price_window[-1]
obv_not_new_high = max(obv_window) > obv_window[-1]
obv_bear_div = price_made_new_high and obv_not_new_high
# VWAP 偏离度
vwap_dev = (k.close - vwap) / vwap if vwap > 0 else 0
# ── 持仓管理 ──
if self._side == "long":
stop = self._entry_price - self.cfg.atr_stop * ca
if k.close < stop or vwap_dev < -0.01: # 回到 VWAP 下方
self._side = ""
return Signal(symbol=self.cfg.symbol, side="SELL", reason="止损或回VWAP", timestamp=k.open_time)
elif self._side == "short":
stop = self._entry_price + self.cfg.atr_stop * ca
if k.close > stop or vwap_dev > 0.01:
self._side = ""
return Signal(symbol=self.cfg.symbol, side="BUY", reason="止损或回VWAP", timestamp=k.open_time)
# ── 入场 ──
else:
if obv_bull_div and vwap_dev < -self.cfg.vwap_std * 0.02: # 价格显著低于 VWAP
self._side = "long"
self._entry_price = k.close
return Signal(symbol=self.cfg.symbol, side="BUY", reason=f"OBV底背离+VWAP下方", timestamp=k.open_time)
elif obv_bear_div and vwap_dev > self.cfg.vwap_std * 0.02:
self._side = "short"
self._entry_price = k.close
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"OBV顶背离+VWAP上方", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 执行
# ════════════════════════════════════════════════════════
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
INTERVALS = ["15m", "30m", "1h"]
STRATEGIES = {
"1.均值回归": (MeanRevConfig, MeanRevStrategy),
"2.多TF(4h+1h)": (MultiTFConfig, MultiTFStrategy),
"3.波动突破": (VolBreakConfig, VolBreakStrategy),
"4.成交量": (VolumeConfig, VolumeStrategy),
}
# 均值回归参数
MEAN_REV_PARAMS = {
"BTCUSDT": {"rsi_period": 14, "rsi_oversold": 25, "rsi_overbought": 75, "bb_period": 20, "bb_std": 2.0, "atr_stop": 1.5},
"ETHUSDT": {"rsi_period": 14, "rsi_oversold": 25, "rsi_overbought": 75, "bb_period": 20, "bb_std": 2.0, "atr_stop": 1.5},
"BNBUSDT": {"rsi_period": 14, "rsi_oversold": 25, "rsi_overbought": 75, "bb_period": 20, "bb_std": 2.0, "atr_stop": 1.5},
"SOLUSDT": {"rsi_period": 14, "rsi_oversold": 25, "rsi_overbought": 75, "bb_period": 20, "bb_std": 2.0, "atr_stop": 1.5},
}
# 多TF参数
MULTI_TF_PARAMS = {
"BTCUSDT": {"fast": 20, "slow": 100, "atr_stop": 2.0},
"ETHUSDT": {"fast": 20, "slow": 100, "atr_stop": 2.0},
"BNBUSDT": {"fast": 20, "slow": 100, "atr_stop": 2.0},
"SOLUSDT": {"fast": 20, "slow": 100, "atr_stop": 2.0},
}
# 波动突破参数
VOL_BREAK_PARAMS = {
"BTCUSDT": {"atr_period": 14, "squeeze_period": 20, "squeeze_ratio": 0.7, "atr_stop": 2.0},
"ETHUSDT": {"atr_period": 14, "squeeze_period": 20, "squeeze_ratio": 0.7, "atr_stop": 2.0},
"BNBUSDT": {"atr_period": 14, "squeeze_period": 20, "squeeze_ratio": 0.7, "atr_stop": 2.0},
"SOLUSDT": {"atr_period": 14, "squeeze_period": 20, "squeeze_ratio": 0.7, "atr_stop": 2.0},
}
# 成交量参数
VOLUME_PARAMS = {
"BTCUSDT": {"obv_lookback": 20, "vwap_std": 2.0, "atr_stop": 2.0},
"ETHUSDT": {"obv_lookback": 20, "vwap_std": 2.0, "atr_stop": 2.0},
"BNBUSDT": {"obv_lookback": 20, "vwap_std": 2.0, "atr_stop": 2.0},
"SOLUSDT": {"obv_lookback": 20, "vwap_std": 2.0, "atr_stop": 2.0},
}
async def run_one(engine_factory, strategy_cls, config_cls, params, symbol, interval, start, end):
"""运行单组回测"""
INITIAL = 10_000.0
sc = config_cls(symbol=symbol, **params)
bt = BacktestConfig(symbol=symbol, interval=interval, start_time=start, end_time=end, initial_capital=INITIAL)
engine = engine_factory(bt)
r = await engine.run(strategy_cls, sc)
m = r.metrics
return m, INITIAL, m.final_equity
async def main():
ds = DataService(config.db)
await ds.connect()
# 预加载所有数据范围
ranges: dict[str, dict[str, tuple[datetime, datetime]]] = {}
for interval in INTERVALS:
ranges[interval] = {}
for symbol in SYMBOLS:
try:
s, e = await ds.fetch_symbol_date_range(symbol, interval)
ranges[interval][symbol] = (s, e)
except Exception:
pass
await ds.close()
all_results: list[dict] = []
print()
print("" * 135)
print(" 日内策略探索 — 4思路 × 4币种 × 3周期")
print("" * 135)
for strategy_name, (config_cls, strategy_cls) in STRATEGIES.items():
print(f"\n{strategy_name}")
print(f" {'币种':<10} {'周期':<6} {'本金':>7} {'终值':>9} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>8} {'交易':>6} {'耗时s':>7}")
print(" " + "" * 115)
for interval in INTERVALS:
for symbol in SYMBOLS:
if symbol not in ranges.get(interval, {}):
continue
start, end = ranges[interval][symbol]
# 选择参数
if strategy_name == "1.均值回归":
params = MEAN_REV_PARAMS[symbol]
elif strategy_name == "2.多TF(4h+1h)":
params = MULTI_TF_PARAMS[symbol]
elif strategy_name == "3.波动突破":
params = VOL_BREAK_PARAMS[symbol]
else:
params = VOLUME_PARAMS[symbol]
t0 = time.time()
try:
m, initial, final_equity = await run_one(
lambda bt: LongShortEngine(bt, db_config=config.db),
strategy_cls, config_cls, params,
symbol, interval, start, end,
)
elapsed = time.time() - t0
except Exception as ex:
print(f" {symbol:<10} {interval:<6} {'错误: ' + str(ex)[:40]}")
continue
print(f" {symbol:<10} {interval:<6} {initial:>7.0f} {final_equity:>9.0f} {m.total_return_pct:>7.1f}% {m.annual_return_pct:>7.1f}% {m.sharpe_ratio:>7.2f} {m.max_drawdown_pct:>7.1f}% {m.total_trades:>6} {elapsed:>6.1f}s")
all_results.append({
"strategy": strategy_name, "interval": interval, "symbol": symbol,
"return": m.total_return_pct, "annual": m.annual_return_pct,
"sharpe": m.sharpe_ratio, "dd": m.max_drawdown_pct,
"trades": m.total_trades, "initial": initial, "final": final_equity,
})
# ── 汇总:每种策略的最佳组合 ──
print(f"\n\n ■ 各策略最佳组合 (按夏普排名)")
print(f" {'策略':<18} {'级别':<6} {'币种':<10} {'本金':>7} {'终值':>9} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>8} {'交易':>6}")
print(" " + "" * 115)
# 每种策略取最佳
for sn in STRATEGIES:
candidates = [r for r in all_results if r["strategy"] == sn]
if not candidates:
continue
best = max(candidates, key=lambda x: x["sharpe"])
print(f" {sn:<18} {best['interval']:<6} {best['symbol']:<10} {best['initial']:>7.0f} {best['final']:>9.0f} {best['return']:>7.1f}% {best['annual']:>7.1f}% {best['sharpe']:>7.2f} {best['dd']:>7.1f}% {best['trades']:>6}")
print("\n" * 135)
if __name__ == "__main__":
asyncio.run(main())
+14 -7
View File
@@ -31,7 +31,7 @@ from engine.common.base import BaseStrategy, Signal, StrategyConfig
from engine.common.models import Kline
from engine.common.config import config, DBConfig
from engine.data import DataService
from engine.indicators import ema, atr
from engine.indicators.incremental import EmaInc, AtrInc
from engine.backtest.models import BacktestConfig, BacktestMetrics, BacktestResult, BacktestTrade
@@ -364,7 +364,7 @@ class LongShortEmaConfig(StrategyConfig):
class LongShortEmaStrategy(BaseStrategy):
"""EMA金叉做多、死叉做空,始终在场"""
"""EMA金叉做多、死叉做空,始终在场 — 全部指标增量计算"""
strategy_type = "long_short_ema"
@@ -374,6 +374,9 @@ class LongShortEmaStrategy(BaseStrategy):
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._ema_fast = EmaInc(c.fast)
self._ema_slow = EmaInc(c.slow)
self._atr = AtrInc(14)
self._highest: float = 0.0
self._lowest: float = float('inf')
self._position_side: str = "" # "long" / "short"
@@ -382,15 +385,19 @@ class LongShortEmaStrategy(BaseStrategy):
self._closes.append(k.close)
self._highs.append(k.high)
self._lows.append(k.low)
# 增量更新(即使在热身期也要更新,保证后续状态正确)
self._ema_fast.update(k.close)
self._ema_slow.update(k.close)
self._atr.update(k.high, k.low, k.close)
n = len(self._closes)
if n < self.cfg.slow + 5:
return None
fast = ema(self._closes, self.cfg.fast)
slow = ema(self._closes, self.cfg.slow)
atr_vals = atr(self._highs, self._lows, self._closes, 14)
cur_f, cur_s, cur_atr = fast[-1], slow[-1], atr_vals[-1]
prev_f, prev_s = fast[-2], slow[-2]
cur_f, cur_s = self._ema_fast[-1], self._ema_slow[-1]
cur_atr = self._atr[-1]
prev_f, prev_s = self._ema_fast[-2], self._ema_slow[-2]
if cur_f == 0 or cur_s == 0 or cur_atr == 0:
return None
+393
View File
@@ -0,0 +1,393 @@
"""
多策略多级别分类回测报告
日内交易 (30m/1h) 中线交易 (2h/4h/6h) 长线交易 (1d/1w)
策略:牛熊自适应 / MACD / EMA双均线 / RSI / 布林突破
币种:BTC / ETH / BNB / SOL
数据:日内近两年,中线+长线全量
用法:
source .venv/bin/activate && python example/multi_strategy_report.py
"""
import asyncio
import sys
from collections import defaultdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
_project_root = Path(__file__).resolve().parent.parent.parent
if str(_project_root) not in sys.path:
sys.path.insert(0, str(_project_root))
from engine.common.base import BaseStrategy, Signal, StrategyConfig
from engine.common.models import Kline
from engine.common.config import config
from engine.backtest import BacktestEngine, BacktestConfig, BacktestResult
from engine.indicators import macd, ema, rsi, bollinger, atr
# ═══════════════════════════════════════════
# 策略定义
# ═══════════════════════════════════════════
# --- MACD ---
class MacdConfig(StrategyConfig):
fast: int = 12; slow: int = 26; signal: int = 9
class MacdStrategy(BaseStrategy):
strategy_type = "macd"
def __init__(self, c: MacdConfig):
super().__init__(c); self.cfg = c; self._c: list[float] = []
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._c.append(k.close)
ml, sl, _ = macd(self._c, self.cfg.fast, self.cfg.slow, self.cfg.signal)
if len(ml) < 3 or ml[-1] == 0: return None
if ml[-2] <= sl[-2] and ml[-1] > sl[-1]:
return Signal(symbol=self.cfg.symbol, side="BUY", reason="MACD金叉", timestamp=k.open_time)
if ml[-2] >= sl[-2] and ml[-1] < sl[-1]:
return Signal(symbol=self.cfg.symbol, side="SELL", reason="MACD死叉", timestamp=k.open_time)
return None
# --- EMA双均线 ---
class EmaCrossConfig(StrategyConfig):
fast: int = 20; slow: int = 50
class EmaCrossStrategy(BaseStrategy):
strategy_type = "ema_cross"
def __init__(self, c: EmaCrossConfig):
super().__init__(c); self.cfg = c; self._c: list[float] = []
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._c.append(k.close)
f = ema(self._c, self.cfg.fast); s = ema(self._c, self.cfg.slow)
if len(f) < 3 or f[-1] == 0 or s[-1] == 0: return None
if f[-2] <= s[-2] and f[-1] > s[-1]:
return Signal(symbol=self.cfg.symbol, side="BUY", reason="EMA金叉", timestamp=k.open_time)
if f[-2] >= s[-2] and f[-1] < s[-1]:
return Signal(symbol=self.cfg.symbol, side="SELL", reason="EMA死叉", timestamp=k.open_time)
return None
# --- RSI ---
class RsiConfig(StrategyConfig):
period: int = 14; oversold: float = 30.0; overbought: float = 70.0
class RsiStrategy(BaseStrategy):
strategy_type = "rsi"
def __init__(self, c: RsiConfig):
super().__init__(c); self.cfg = c; self._c: list[float] = []; self._in = False
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._c.append(k.close)
v = rsi(self._c, self.cfg.period)[-1]
if v == 0: return None
if v < self.cfg.oversold and not self._in:
self._in = True
return Signal(symbol=self.cfg.symbol, side="BUY", reason=f"RSI超卖({v:.1f})", timestamp=k.open_time)
if v > self.cfg.overbought and self._in:
self._in = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"RSI超买({v:.1f})", timestamp=k.open_time)
return None
# --- 布林突破 ---
class BollConfig(StrategyConfig):
period: int = 20; std: float = 2.0
class BollStrategy(BaseStrategy):
strategy_type = "boll"
def __init__(self, c: BollConfig):
super().__init__(c); self.cfg = c; self._c: list[float] = []
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._c.append(k.close)
upper, mid, lower = bollinger(self._c, self.cfg.period, self.cfg.std)
if len(upper) < 3 or mid[-1] == 0: return None
p, md = k.close, mid[-1]
pp, pm = self._c[-2], mid[-2]
if pp <= pm and p > md and upper[-1] > 0 and mid[-1] > mid[-2]:
return Signal(symbol=self.cfg.symbol, side="BUY", reason=f"突破BB中轨", timestamp=k.open_time)
if pp >= pm and p < md:
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"跌破BB中轨", timestamp=k.open_time)
return None
# --- 牛熊自适应 (多空双向) ---
class RegimeDetector:
def __init__(self):
self._ath = 0.0
def update_ath(self, price: float):
if price > self._ath: self._ath = price
def ema200_slope(self, closes, idx):
if idx < 210: return "unknown"
e200 = ema(closes, 200)
if e200[idx - 20] == 0: return "unknown"
slope = (e200[idx] - e200[idx - 20]) / e200[idx - 20]
if slope > 0.002: return "bull"
if slope < -0.002: return "bear"
return "sideways"
def price_vs_ema200(self, closes, idx):
if idx < 210: return "unknown"
e200 = ema(closes, 200)
return "bull" if closes[idx] > e200[idx] else "bear"
def ath_drawdown(self, closes, idx):
if self._ath == 0: return "unknown"
dd = (closes[idx] - self._ath) / self._ath
if dd > -0.15: return "bull"
if dd < -0.35: return "bear"
return "sideways"
def detect(self, closes, idx):
r1 = self.ema200_slope(closes, idx); r2 = self.price_vs_ema200(closes, idx); r3 = self.ath_drawdown(closes, idx)
b = sum(1 for r in [r1,r2,r3] if r=="bull"); br = sum(1 for r in [r1,r2,r3] if r=="bear")
if b >= 2: return "bull"
if br >= 2: return "bear"
return "sideways"
class RegimeConfig(StrategyConfig):
fast: int = 10; slow: int = 50; atr_stop: float = 2.5
class RegimeStrategy(BaseStrategy):
strategy_type = "regime"
def __init__(self, c: RegimeConfig):
super().__init__(c); self.cfg = c
self._c: list[float] = []; self._h: list[float] = []; self._l: list[float] = []
self._detector = RegimeDetector(); self._side = ""; self._hp = 0.0; self._lp = float('inf')
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._c.append(k.close); self._h.append(k.high); self._l.append(k.low)
self._detector.update_ath(k.close)
n = len(self._c)
if n < 220: return None
regime = self._detector.detect(self._c, n - 1)
f = ema(self._c, self.cfg.fast); s = ema(self._c, self.cfg.slow)
a = atr(self._h, self._l, self._c, 14)
cf, cs, ca, pf, ps = f[-1], s[-1], a[-1], f[-2], s[-2]
if cf == 0 or cs == 0 or ca == 0: return None
golden = pf <= ps and cf > cs; death = pf >= ps and cf < cs
if self._side == "long":
self._hp = max(self._hp, k.high); stop = self._hp - self.cfg.atr_stop * ca
if death or k.close < stop or regime == "bear":
self._side = ""
return Signal(symbol=self.cfg.symbol, side="SELL", reason="平多", timestamp=k.open_time)
elif self._side == "short":
self._lp = min(self._lp, k.low); stop = self._lp + self.cfg.atr_stop * ca
if golden or k.close > stop or regime == "bull":
self._side = ""
return Signal(symbol=self.cfg.symbol, side="BUY", reason="平空", timestamp=k.open_time)
else:
if regime == "bull" and golden:
self._side = "long"; self._hp = k.close
return Signal(symbol=self.cfg.symbol, side="BUY", reason="牛市金叉", timestamp=k.open_time)
elif regime == "bear" and death:
self._side = "short"; self._lp = k.close
return Signal(symbol=self.cfg.symbol, side="SELL", reason="熊市死叉", timestamp=k.open_time)
return None
# ═══════════════════════════════════════════
# 注册
# ═══════════════════════════════════════════
STRATEGY_REGISTRY = {
"牛熊自适应": (RegimeStrategy, RegimeConfig, "regime"),
"MACD": (MacdStrategy, MacdConfig, "trend"),
"EMA双均线": (EmaCrossStrategy, EmaCrossConfig, "trend"),
"RSI超卖反弹": (RsiStrategy, RsiConfig, "reversal"),
"布林突破": (BollStrategy, BollConfig, "breakout"),
}
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
REGIME_PARAMS = {
"BTCUSDT": (10, 50), "ETHUSDT": (10, 75), "BNBUSDT": (20, 50), "SOLUSDT": (30, 50),
}
CATEGORIES = {
"日内交易": {
"intervals": ["30m", "1h"],
"strategies": ["MACD", "EMA双均线", "RSI超卖反弹", "布林突破", "牛熊自适应"],
"data": "recent",
},
"中线交易": {
"intervals": ["2h", "4h", "6h"],
"strategies": ["牛熊自适应", "MACD", "EMA双均线", "RSI超卖反弹", "布林突破"],
"data": "full",
},
"长线交易": {
"intervals": ["1d", "1w"],
"strategies": ["牛熊自适应", "MACD", "EMA双均线"],
"data": "full",
},
}
RECENT_START = datetime(2024, 6, 1)
RECENT_END = datetime(2026, 6, 12)
FULL_DEFAULT = datetime(2017, 1, 1)
async def run_simple(symbol, interval, strategy_cls, strategy_cfg, start, end) -> BacktestResult | None:
"""使用 BacktestEngine(只做多)运行回测"""
bt = BacktestConfig(
symbol=symbol, interval=interval, start_time=start, end_time=end,
initial_capital=10_000.0, warmup_bars=100,
)
strategy_cfg.symbol = symbol
engine = BacktestEngine(bt, db_config=config.db)
return await engine.run(strategy_cls, strategy_cfg)
async def run_regime(symbol, interval, start, end) -> BacktestResult | None:
"""使用 LongShortEngine(多空双向)运行牛熊自适应策略"""
from engine.example.long_short import LongShortEngine
fast, slow = REGIME_PARAMS.get(symbol, (10, 50))
sc = RegimeConfig(symbol=symbol, fast=fast, slow=slow)
bt = BacktestConfig(
symbol=symbol, interval=interval, start_time=start, end_time=end,
initial_capital=10_000.0, warmup_bars=250,
)
engine = LongShortEngine(bt, db_config=config.db)
return await engine.run(RegimeStrategy, sc)
# ═══════════════════════════════════════════
# 主流程
# ═══════════════════════════════════════════
async def main():
sem = asyncio.Semaphore(2) # 并发控制
async def with_sem(coro):
async with sem:
return await coro
out: list[str] = []
def w(line=""):
out.append(line); print(line)
# 收集所有结果: (category, interval, symbol, strategy_name, result)
all_results: list[dict] = []
w("# 多策略多级别分类回测报告")
w()
w(f"> 生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}")
w(f"> 初始资金:10,000 USDT | 手续费:0.1% | 滑点:0.05%")
w(f"> 日内交易使用近两年数据 (2024.06-2026.06),中线/长线使用全量历史数据")
w()
for cat_name, cat_cfg in CATEGORIES.items():
intervals = cat_cfg["intervals"]
strategy_names = cat_cfg["strategies"]
use_full = cat_cfg["data"] == "full"
w(f"## {cat_name} ({'/'.join(intervals)})")
w()
for interval in intervals:
w(f"### {interval}")
w()
# 表头
cols = "| 币种 |"
sep = "|------|"
for sn in strategy_names:
cols += f" {sn} 收益% | {sn} 夏普 | {sn} 回撤% | {sn} 交易 | {sn} 胜率% |"
sep += "--------|------|------|------|------|"
w(cols)
w(sep)
for symbol in SYMBOLS:
row = f"| {symbol:<10} |"
tasks = []
for sn in strategy_names:
if sn == "牛熊自适应":
# 牛熊自适应使用 LongShortEngine
if use_full:
start, end = FULL_DEFAULT, RECENT_END
else:
start, end = RECENT_START, RECENT_END
tasks.append((sn, with_sem(run_regime(symbol, interval, start, end))))
else:
cls, cfg_cls, _ = STRATEGY_REGISTRY[sn]
if use_full:
start, end = FULL_DEFAULT, RECENT_END
else:
start, end = RECENT_START, RECENT_END
cfg = cfg_cls()
tasks.append((sn, with_sem(run_simple(symbol, interval, cls, cfg, start, end))))
# 并行执行当前币种的所有策略
results_list = await asyncio.gather(*[t[1] for t in tasks], return_exceptions=True)
for (sn, _), r in zip(tasks, results_list):
if isinstance(r, Exception):
row += f" ERR | — | — | — | — |"
print(f"{symbol} {interval} {sn}: {r}")
elif r is None:
row += f" N/A | — | — | — | — |"
else:
m = r.metrics
row += f" {m.total_return_pct:>+6.1f}% | {m.sharpe_ratio:>4.2f} | {m.max_drawdown_pct:>5.1f}% | {m.total_trades:>4} | {m.win_rate*100:>4.1f}% |"
all_results.append({
"category": cat_name, "interval": interval,
"symbol": symbol, "strategy": sn,
"return": m.total_return_pct, "sharpe": m.sharpe_ratio,
"dd": m.max_drawdown_pct, "trades": m.total_trades,
"win": m.win_rate, "pf": m.profit_factor,
})
w(row)
w()
# ═══════════════════════════════════════
# 汇总分析
# ═══════════════════════════════════════
w("---")
w()
w("## 汇总分析")
w()
for cat_name in CATEGORIES:
cat_results = [r for r in all_results if r["category"] == cat_name]
if not cat_results:
continue
w(f"### {cat_name} — 各币种最优策略")
w()
w("| 币种 | 最佳周期 | 最佳策略 | 总收益% | 夏普 | 回撤% | 交易 | 胜率% |")
w("|------|---------|---------|--------|------|------|------|------|")
for symbol in SYMBOLS:
candidates = [r for r in cat_results if r["symbol"] == symbol]
if not candidates:
continue
best = max(candidates, key=lambda x: x["sharpe"])
w(f"| {symbol} | {best['interval']} | {best['strategy']} | {best['return']:>+7.1f}% | {best['sharpe']:.2f} | {best['dd']:.1f}% | {best['trades']} | {best['win']*100:.1f}% |")
w()
# 全市场最优
w("### 全市场 TOP 10(按夏普排序)")
w()
w("| 排名 | 分类 | 币种 | 周期 | 策略 | 总收益% | 夏普 | 回撤% | 交易 | 胜率% |")
w("|------|------|------|------|------|--------|------|------|------|------|")
ranked = sorted(all_results, key=lambda x: x["sharpe"], reverse=True)
for i, r in enumerate(ranked[:10]):
w(f"| {i+1} | {r['category']} | {r['symbol']} | {r['interval']} | {r['strategy']} | {r['return']:>+7.1f}% | {r['sharpe']:.2f} | {r['dd']:.1f}% | {r['trades']} | {r['win']*100:.1f}% |")
w()
# 按策略类型汇总
w("### 各策略类型平均表现")
w()
w("| 策略 | 分类 | 平均收益% | 平均夏普 | 平均回撤% | 平均胜率% |")
w("|------|------|---------|---------|---------|---------|")
for sn in ["牛熊自适应", "MACD", "EMA双均线", "RSI超卖反弹", "布林突破"]:
for cat_name in CATEGORIES:
sr = [r for r in all_results if r["strategy"] == sn and r["category"] == cat_name]
if not sr:
continue
avg_ret = sum(r["return"] for r in sr) / len(sr)
avg_sh = sum(r["sharpe"] for r in sr) / len(sr)
avg_dd = sum(r["dd"] for r in sr) / len(sr)
avg_win = sum(r["win"] for r in sr) / len(sr)
w(f"| {sn} | {cat_name} | {avg_ret:>+7.1f}% | {avg_sh:.2f} | {avg_dd:.1f}% | {avg_win*100:.1f}% |")
w()
# 写出文件
out_path = Path(__file__).resolve().parent.parent / "backtest" / "MULTI_STRATEGY_REPORT.md"
with open(out_path, "w", encoding="utf-8") as f:
f.write("\n".join(out) + "\n")
print(f"\n✓ 报告已保存到: {out_path}")
if __name__ == "__main__":
asyncio.run(main())
+36 -22
View File
@@ -23,50 +23,55 @@ from engine.common.base import BaseStrategy, Signal, StrategyConfig
from engine.common.models import Kline
from engine.common.config import config
from engine.backtest import BacktestConfig
from engine.indicators import ema, atr
from engine.indicators.incremental import EmaInc, AtrInc
from engine.data import DataService
from engine.example.long_short import LongShortEngine
# ════════════════════════════════════════════════════════
# 3法判定器
# 3法判定器(增量 EMA200O(1) per bar
# ════════════════════════════════════════════════════════
class RegimeDetector3:
"""牛熊判定器,内部维护增量 EMA(200),避免每次从头重算"""
def __init__(self):
self._ath = 0.0
self._e200 = EmaInc(200)
def update_ath(self, price: float):
def update(self, price: float):
"""每根 bar 调一次:更新 ATH + EMA(200)"""
if price > self._ath:
self._ath = price
self._e200.update(price)
def ema200_slope(self, closes: list[float], idx: int) -> str:
if idx < 210: return "unknown"
e200 = ema(closes, 200)
def _ema200_slope(self, idx: int) -> str:
if idx < 220: return "unknown"
e200 = self._e200
if e200[idx - 20] == 0: return "unknown"
slope = (e200[idx] - e200[idx - 20]) / e200[idx - 20]
if slope > 0.002: return "bull"
if slope < -0.002: return "bear"
return "sideways"
def price_vs_ema200(self, closes: list[float], idx: int) -> str:
def _price_vs_ema200(self, price: float, idx: int) -> str:
if idx < 210: return "unknown"
e200 = ema(closes, 200)
if e200[idx] == 0: return "unknown"
return "bull" if closes[idx] > e200[idx] else "bear"
e = self._e200[idx]
if e == 0: return "unknown"
return "bull" if price > e else "bear"
def ath_drawdown(self, closes: list[float], idx: int) -> str:
def _ath_drawdown(self, price: float) -> str:
if self._ath == 0: return "unknown"
dd = (closes[idx] - self._ath) / self._ath
dd = (price - self._ath) / self._ath
if dd > -0.15: return "bull"
if dd < -0.35: return "bear"
return "sideways"
def detect(self, closes: list[float], idx: int) -> str:
r1 = self.ema200_slope(closes, idx)
r2 = self.price_vs_ema200(closes, idx)
r3 = self.ath_drawdown(closes, idx)
def detect(self, price: float, idx: int) -> str:
r1 = self._ema200_slope(idx)
r2 = self._price_vs_ema200(price, idx)
r3 = self._ath_drawdown(price)
b = sum(1 for r in [r1, r2, r3] if r == "bull")
br = sum(1 for r in [r1, r2, r3] if r == "bear")
if b >= 2: return "bull"
@@ -84,7 +89,7 @@ class RegimeEmaConfig(StrategyConfig):
class RegimeEmaStrategy(BaseStrategy):
"""按市场状态自适应做多/做空"""
"""按市场状态自适应做多/做空 — 全部指标增量计算,O(1) per bar"""
strategy_type = "regime_ema"
@@ -93,19 +98,28 @@ class RegimeEmaStrategy(BaseStrategy):
self.cfg = c
self._c: list[float] = []; self._h: list[float] = []; self._l: list[float] = []
self._detector = RegimeDetector3()
self._ema_fast = EmaInc(c.fast)
self._ema_slow = EmaInc(c.slow)
self._atr = AtrInc(14)
self._side: str = ""; self._hp: float = 0.0; self._lp: float = float('inf')
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._c.append(k.close); self._h.append(k.high); self._l.append(k.low)
self._detector.update_ath(k.close)
# 增量更新所有指标(O(1) each)
self._detector.update(k.close)
self._ema_fast.update(k.close)
self._ema_slow.update(k.close)
self._atr.update(k.high, k.low, k.close)
n = len(self._c)
if n < 220: return None
regime = self._detector.detect(self._c, n - 1)
regime = self._detector.detect(k.close, n - 1)
f = ema(self._c, self.cfg.fast); s = ema(self._c, self.cfg.slow)
a = atr(self._h, self._l, self._c, 14)
cf, cs, ca, pf, ps = f[-1], s[-1], a[-1], f[-2], s[-2]
cf, cs = self._ema_fast[-1], self._ema_slow[-1]
ca = self._atr[-1]
pf, ps = self._ema_fast[-2], self._ema_slow[-2]
if cf == 0 or cs == 0 or ca == 0: return None
golden = pf <= ps and cf > cs; death = pf >= ps and cf < cs
+122
View File
@@ -0,0 +1,122 @@
"""
牛熊自适应策略 — 日内级别全币种扫描 (15m / 30m / 1h)
用法:
source .venv/bin/activate && python example/regime_intraday.py
"""
import asyncio
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
_project_root = Path(__file__).resolve().parent.parent.parent
if str(_project_root) not in sys.path:
sys.path.insert(0, str(_project_root))
from engine.common.config import config
from engine.backtest import BacktestConfig
from engine.data import DataService
from engine.example.regime_all import RegimeEmaStrategy, RegimeEmaConfig
from engine.example.long_short import LongShortEngine
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
INTERVALS = ["15m", "30m", "1h"]
# 沿用 4h 级别优化参数(日内级别可能需单独调参)
PARAMS = {
"BTCUSDT": (10, 50),
"ETHUSDT": (10, 75),
"BNBUSDT": (20, 50),
"SOLUSDT": (30, 50),
}
async def get_actual_range(ds: DataService, symbol: str, interval: str):
"""获取币种指定周期的实际数据范围"""
start, end = await ds.fetch_symbol_date_range(symbol, interval)
return start, end
async def main():
ds = DataService(config.db)
await ds.connect()
print()
print("" * 130)
print(" 牛熊自适应策略 — 日内级别全币种扫描 | 牛市只多/熊市只空/震荡空仓")
print("" * 130)
total_start = time.time()
results: list[dict] = []
for interval in INTERVALS:
print(f"\n{interval} 级别")
print(f" {'币种':<10} {'数据范围':<22} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>8} {'交易':>6} {'多头P&L':>11} {'空头P&L':>11} {'耗时s':>7}")
print(" " + "" * 125)
for symbol in SYMBOLS:
fast, slow = PARAMS[symbol]
sc = RegimeEmaConfig(symbol=symbol, fast=fast, slow=slow)
try:
act_start, act_end = await get_actual_range(ds, symbol, interval)
range_str = f"{act_start.date()}~{act_end.date()}"
except Exception:
# 数据不存在,跳过
print(f" {symbol:<10} {'无数据':<22}")
continue
bt = BacktestConfig(
symbol=symbol, interval=interval,
start_time=act_start, end_time=act_end,
initial_capital=10_000.0,
)
engine = LongShortEngine(bt, db_config=config.db)
t0 = time.time()
try:
r = await engine.run(RegimeEmaStrategy, sc)
elapsed = time.time() - t0
except Exception as ex:
print(f" {symbol:<10} {range_str:<22} {'错误: ' + str(ex)[:30]}")
continue
m = r.metrics
long_t = [t for t in r.trades if t.pnl is not None and t.side == "SELL"]
short_t = [t for t in r.trades if t.pnl is not None and t.side == "BUY"]
long_pnl = sum(t.pnl for t in long_t) if long_t else 0
short_pnl = sum(t.pnl for t in short_t) if short_t else 0
print(f" {symbol:<10} {range_str:<22} {m.total_return_pct:>7.1f}% {m.annual_return_pct:>7.1f}% {m.sharpe_ratio:>7.2f} {m.max_drawdown_pct:>7.1f}% {m.total_trades:>6} {long_pnl:>+10.0f} {short_pnl:>+10.0f} {elapsed:>6.1f}s")
results.append({
"interval": interval, "symbol": symbol,
"return": m.total_return_pct, "annual": m.annual_return_pct,
"sharpe": m.sharpe_ratio, "dd": m.max_drawdown_pct,
"trades": m.total_trades, "win_rate": m.win_rate,
"profit_factor": m.profit_factor,
"long_pnl": long_pnl, "short_pnl": short_pnl,
"elapsed": elapsed,
})
await ds.close()
# ── 汇总排名 ──
total_elapsed = time.time() - total_start
print(f"\n ■ 最佳组合 (按夏普排名)")
print(f" {'排名':<5} {'级别':<6} {'币种':<10} {'总收益%':>8} {'夏普':>7} {'回撤%':>8} {'交易':>6} {'胜率%':>7} {'盈亏比':>7}")
print(" " + "" * 75)
sorted_results = sorted(results, key=lambda x: x["sharpe"], reverse=True)
for i, r in enumerate(sorted_results):
medal = ["🥇", "🥈", "🥉"][i] if i < 3 else f"{i+1:>2}."
print(f" {medal:<5} {r['interval']:<6} {r['symbol']:<10} {r['return']:>7.1f}% {r['sharpe']:>7.2f} {r['dd']:>7.1f}% {r['trades']:>6} {r['win_rate']*100:>6.1f}% {r['profit_factor']:>7.2f}")
print(f"\n 总耗时: {total_elapsed:.1f}s")
print("" * 130)
if __name__ == "__main__":
asyncio.run(main())
@@ -1,6 +1,6 @@
"""
牛熊自适应策略 — 多时间级别回测对比
4h / 1d × 全量数据 / 近两年 (2024.06-2026.06)
2h / 4h / 6h / 1d × 全量数据 / 近两年 (2024.06-2026.06)
用法:
source .venv/bin/activate && python example/regime_timeframe_comparison.py
@@ -37,7 +37,7 @@ PARAMS = {
"SOLUSDT": (30, 50),
}
INTERVALS = ["4h", "1d"]
INTERVALS = ["2h", "4h", "6h", "1d"]
# 近两年:2024年6月 → 2026年6月
YEAR_START = datetime(2024, 6, 1)
@@ -267,7 +267,7 @@ async def main():
# 写出文件
out_path = (
Path(__file__).resolve().parent.parent / "backtest" / "TIMEFRAME_COMPARISON_2Y.md"
Path(__file__).resolve().parent.parent / "backtest" / "TIMEFRAME_COMPARISON_2H_6H.md"
)
with open(out_path, "w", encoding="utf-8") as f:
f.write("\n".join(out) + "\n")
+96
View File
@@ -0,0 +1,96 @@
"""获取近一年Top3策略的详细交易记录"""
import asyncio, sys, json
from datetime import datetime, timedelta, timezone
from pathlib import Path
_project_root = Path(__file__).resolve().parent.parent.parent
if str(_project_root) not in sys.path:
sys.path.insert(0, str(_project_root))
from engine.common.config import config
from engine.backtest.models import BacktestConfig
from engine.example.long_short import LongShortEngine
from engine.example.full_comparison import (
VolBreakStrategy, VolBreakConfig,
EmaCrossStrategy, EmaCrossConfig,
)
from engine.common.base import Signal
NOW = datetime.now(timezone.utc)
ONE_YEAR_AGO = NOW - timedelta(days=365)
INITIAL = 10_000.0
TASKS = [
("ATR波动率突破 ETHUSDT 4h", VolBreakConfig, VolBreakStrategy,
"ETHUSDT", "4h", lambda s: VolBreakConfig(symbol=s, atr_period=14, squeeze_period=20, squeeze_ratio=0.7, atr_stop=2.0)),
("EMA双均线多空 ETHUSDT 1d", EmaCrossConfig, EmaCrossStrategy,
"ETHUSDT", "1d", lambda s: EmaCrossConfig(symbol=s, fast=10, slow=50, atr_stop=2.5)),
("EMA双均线多空 BTCUSDT 1d", EmaCrossConfig, EmaCrossStrategy,
"BTCUSDT", "1d", lambda s: EmaCrossConfig(symbol=s, fast=10, slow=50, atr_stop=2.5)),
]
def fmt_ts(ts_ms):
return datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M")
async def run_one(label, config_cls, strategy_cls, symbol, interval, mkcfg):
sc = mkcfg(symbol)
bt = BacktestConfig(symbol=symbol, interval=interval,
start_time=ONE_YEAR_AGO, end_time=NOW,
initial_capital=INITIAL, warmup_bars=150)
engine = LongShortEngine(bt, db_config=config.db)
r = await engine.run(strategy_cls, sc)
return label, r
async def main():
results = []
for task in TASKS:
label, r = await run_one(*task)
results.append((label, r))
for label, r in results:
m = r.metrics
trades = r.trades
# 配对交易
paired = []
pending = None
for t in trades:
if t.side == "BUY" and t.pnl is None:
pending = {"entry_ts": t.timestamp, "entry_price": t.price, "entry_reason": t.reason}
elif t.side == "SELL" and pending and t.pnl is not None:
paired.append({**pending, "exit_ts": t.timestamp, "exit_price": t.price,
"exit_reason": t.reason, "pnl": t.pnl})
pending = None
elif t.side == "SELL" and t.pnl is None:
pending = {"entry_ts": t.timestamp, "entry_price": t.price, "entry_reason": t.reason, "short": True}
elif t.side == "BUY" and pending and t.pnl is not None and pending.get("short"):
paired.append({**pending, "exit_ts": t.timestamp, "exit_price": t.price,
"exit_reason": t.reason, "pnl": t.pnl})
pending = None
cfg = r.config
print(f"\n═══ {label} ═══")
print(f" 本金 {INITIAL:,.0f} → 终值 {m.final_equity:,.0f} | {m.total_return_pct:+.1f}% | 年化 {m.annual_return_pct:+.1f}%")
print(f" 夏普 {m.sharpe_ratio:.2f} | 回撤 {m.max_drawdown_pct:.1f}% | 胜率 {m.win_rate*100:.1f}% | 盈亏比 {m.profit_factor:.2f} | {m.total_trades}")
print(f" 日期 {cfg.start_time.date()} ~ {cfg.end_time.date()}")
print()
print(f" {'#':>3} {'入场时间':<19} {'入场价':>10} {'入场原因':<25} {'出场时间':<19} {'出场价':>10} {'出场原因':<25} {'盈亏':>10}")
print(" " + "" * 130)
total_pnl = 0
for i, p in enumerate(paired):
total_pnl += p["pnl"]
side = "做空" if p.get("short") else "做多"
print(f" {i+1:>3} {fmt_ts(p['entry_ts']):<19} {p['entry_price']:>10.4f} {p['entry_reason']:<25} {fmt_ts(p['exit_ts']):<19} {p['exit_price']:>10.4f} {p['exit_reason']:<25} {p['pnl']:>+10.2f}")
print(" " + "" * 130)
wins = sum(1 for p in paired if p["pnl"] > 0)
print(f" 合计 {len(paired)} 笔 | 盈利 {wins} 笔 | 总盈亏 {total_pnl:+,.2f} USDT")
print()
if __name__ == "__main__":
asyncio.run(main())
+189
View File
@@ -0,0 +1,189 @@
"""
策略3 ATR波动率突破 — 1h 全量 vs 近两年对比 + 近两年详细订单
用法:
source .venv/bin/activate && python example/vol_break_compare.py
"""
import asyncio
import sys
import time
from datetime import datetime, timezone, timedelta
from pathlib import Path
_project_root = Path(__file__).resolve().parent.parent.parent
if str(_project_root) not in sys.path:
sys.path.insert(0, str(_project_root))
from engine.common.config import config
from engine.backtest import BacktestConfig
from engine.data import DataService
from engine.example.long_short import LongShortEngine
from engine.example.intraday_explore import VolBreakStrategy, VolBreakConfig
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
INTERVAL = "1h"
INITIAL = 10_000.0
PARAMS = {
"BTCUSDT": {"atr_period": 14, "squeeze_period": 20, "squeeze_ratio": 0.7, "atr_stop": 2.0},
"ETHUSDT": {"atr_period": 14, "squeeze_period": 20, "squeeze_ratio": 0.7, "atr_stop": 2.0},
"BNBUSDT": {"atr_period": 14, "squeeze_period": 20, "squeeze_ratio": 0.7, "atr_stop": 2.0},
"SOLUSDT": {"atr_period": 14, "squeeze_period": 20, "squeeze_ratio": 0.7, "atr_stop": 2.0},
}
# 近两年截止日期
RECENT_END = datetime(2025, 6, 1, tzinfo=timezone.utc)
RECENT_START = RECENT_END - timedelta(days=365 * 2) # 2023-06
def fmt_ts(ts_ms: float) -> str:
dt = datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc)
return dt.strftime("%Y-%m-%d %H:%M")
def pair_trades(trades: list) -> list[dict]:
"""将 BUY/SELL 配对为完整交易"""
paired = []
pending_open = None # (side, price, reason, ts)
for t in trades:
if t.side == "BUY":
if pending_open and pending_open["side"] == "short":
# 平空仓 (BUY to close short)
paired.append({
"type": "做空",
"entry_ts": pending_open["ts"],
"entry_price": pending_open["price"],
"entry_reason": pending_open["reason"],
"exit_ts": t.timestamp,
"exit_price": t.price,
"exit_reason": t.reason,
"pnl": t.pnl or 0,
})
pending_open = None
elif t.pnl is None:
# 开多仓
pending_open = {"side": "long", "price": t.price, "reason": t.reason, "ts": t.timestamp}
elif t.side == "SELL":
if pending_open and pending_open["side"] == "long":
# 平多仓 (SELL to close long)
paired.append({
"type": "做多",
"entry_ts": pending_open["ts"],
"entry_price": pending_open["price"],
"entry_reason": pending_open["reason"],
"exit_ts": t.timestamp,
"exit_price": t.price,
"exit_reason": t.reason,
"pnl": t.pnl or 0,
})
pending_open = None
elif t.pnl is None:
# 开空仓
pending_open = {"side": "short", "price": t.price, "reason": t.reason, "ts": t.timestamp}
return paired
async def run_backtest(symbol, start, end):
sc = VolBreakConfig(symbol=symbol, **PARAMS[symbol])
bt = BacktestConfig(symbol=symbol, interval=INTERVAL, start_time=start, end_time=end, initial_capital=INITIAL)
engine = LongShortEngine(bt, db_config=config.db)
r = await engine.run(VolBreakStrategy, sc)
return r
async def main():
ds = DataService(config.db)
await ds.connect()
# 获取数据范围
full_ranges = {}
for symbol in SYMBOLS:
try:
s, e = await ds.fetch_symbol_date_range(symbol, INTERVAL)
full_ranges[symbol] = (s, e)
except Exception:
pass
# ── 运行全量 + 近两年 ──
full_results = {}
recent_results = {}
for symbol in SYMBOLS:
if symbol not in full_ranges:
continue
fs, fe = full_ranges[symbol]
print(f" 运行 {symbol} 全量 ({fs.date()}~{fe.date()})...", end=" ", flush=True)
t0 = time.time()
full_results[symbol] = await run_backtest(symbol, fs, fe)
print(f"{time.time()-t0:.1f}s")
rs = max(RECENT_START, fs)
re = min(RECENT_END, fe)
print(f" 运行 {symbol} 近两年 ({rs.date()}~{re.date()})...", end=" ", flush=True)
t0 = time.time()
recent_results[symbol] = await run_backtest(symbol, rs, re)
print(f"{time.time()-t0:.1f}s")
await ds.close()
# ── 全量 vs 近两年 对比表 ──
print()
print("" * 145)
print(" ATR波动率突破 1h — 全量 vs 近两年 (2023.06~2025.06)")
print("" * 145)
print()
header = f" {'币种':<10} | {'—————— 全量数据 ——————':>55} | {'—————— 近两年 ——————':>55}"
print(header)
sub = f" {'':<10} | {'本金':>6} {'终值':>8} {'总收益%':>8} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} | {'本金':>6} {'终值':>8} {'总收益%':>8} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5}"
print(sub)
print(" " + "" * 143)
for symbol in SYMBOLS:
if symbol not in full_results:
continue
f = full_results[symbol].metrics
r = recent_results[symbol].metrics
print(f" {symbol:<10} | {INITIAL:>6.0f} {f.final_equity:>8.0f} {f.total_return_pct:>7.1f}% {f.annual_return_pct:>6.1f}% {f.sharpe_ratio:>6.2f} {f.max_drawdown_pct:>6.1f}% {f.total_trades:>5} | {INITIAL:>6.0f} {r.final_equity:>8.0f} {r.total_return_pct:>7.1f}% {r.annual_return_pct:>6.1f}% {r.sharpe_ratio:>6.2f} {r.max_drawdown_pct:>6.1f}% {r.total_trades:>5}")
print("\n" * 145)
# ── 近两年详细订单 ──
for symbol in SYMBOLS:
if symbol not in recent_results:
continue
result = recent_results[symbol]
m = result.metrics
rng = recent_results[symbol].config
paired = pair_trades(result.trades)
print(f"\n{'' * 145}")
print(f" {symbol} 近两年 ({rng.start_time.date()}~{rng.end_time.date()}) — {len(paired)} 笔完整交易")
print(f" 本金 {INITIAL:,.0f} → 终值 {m.final_equity:,.0f} | 总收益 {m.total_return_pct:.1f}% | 年化 {m.annual_return_pct:.1f}% | 夏普 {m.sharpe_ratio:.2f} | 回撤 {m.max_drawdown_pct:.1f}%")
print(f" 胜率 {m.win_rate*100:.1f}% | 盈亏比 {m.profit_factor:.2f} | 最佳单笔 {m.best_trade_pnl:+,.0f} | 最差 {m.worst_trade_pnl:+,.0f} | 平均 {m.avg_trade_pnl:+,.0f}")
print(f"{'' * 145}")
print(f" {'#':>3} {'类型':<5} {'入场时间':<19} {'入场价':>10} {'出场时间':<19} {'出场价':>10} {'盈亏':>12} {'入场原因':<30} {'出场原因':<30}")
print(f" {'' * 141}")
total_pnl = 0
wins = 0
for i, p in enumerate(paired):
pnl = p["pnl"]
total_pnl += pnl
if pnl > 0:
wins += 1
pnl_str = f"{pnl:+,.0f}"
print(f" {i+1:>3} {p['type']:<5} {p['entry_ts']:<19} {p['entry_price']:>10.4f} {p['exit_ts']:<19} {p['exit_price']:>10.4f} {pnl_str:>12} {p['entry_reason']:<30} {p['exit_reason']:<30}")
print(f" {'' * 141}")
print(f" 合计: {len(paired)} 笔 | 盈利 {wins} 笔 ({wins/len(paired)*100 if paired else 0:.0f}%) | 总盈亏 {total_pnl:+,.0f} USDT")
print(f"\n{'' * 145}")
print()
if __name__ == "__main__":
asyncio.run(main())
+212
View File
@@ -0,0 +1,212 @@
"""
策略3 ATR波动率突破 — 全量 / 近两年 / 近一年 / 近半年 对比
用法:
source .venv/bin/activate && python example/vol_break_periods.py
"""
import asyncio
import sys
import time
from datetime import datetime, timezone, timedelta
from pathlib import Path
_project_root = Path(__file__).resolve().parent.parent.parent
if str(_project_root) not in sys.path:
sys.path.insert(0, str(_project_root))
from engine.common.config import config
from engine.backtest import BacktestConfig
from engine.data import DataService
from engine.example.long_short import LongShortEngine
from engine.example.intraday_explore import VolBreakStrategy, VolBreakConfig
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
INTERVAL = "1h"
INITIAL = 10_000.0
TODAY = datetime.now(timezone.utc)
# 时间段定义
PERIODS = {
"全量": (None, TODAY), # start 动态获取
"近两年": (TODAY - timedelta(days=365 * 2), TODAY),
"近一年": (TODAY - timedelta(days=365), TODAY),
"近半年": (TODAY - timedelta(days=182), TODAY),
}
PARAMS = {
"BTCUSDT": {"atr_period": 14, "squeeze_period": 20, "squeeze_ratio": 0.7, "atr_stop": 2.0},
"ETHUSDT": {"atr_period": 14, "squeeze_period": 20, "squeeze_ratio": 0.7, "atr_stop": 2.0},
"BNBUSDT": {"atr_period": 14, "squeeze_period": 20, "squeeze_ratio": 0.7, "atr_stop": 2.0},
"SOLUSDT": {"atr_period": 14, "squeeze_period": 20, "squeeze_ratio": 0.7, "atr_stop": 2.0},
}
def fmt_ts(ts_ms: float) -> str:
dt = datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc)
return dt.strftime("%Y-%m-%d %H:%M")
def pair_trades(trades: list) -> list[dict]:
paired = []
pending_open = None
for t in trades:
if t.side == "BUY":
if pending_open and pending_open["side"] == "short":
paired.append({
"type": "做空",
"entry_ts": pending_open["ts"],
"entry_price": pending_open["price"],
"entry_reason": pending_open["reason"],
"exit_ts": t.timestamp,
"exit_price": t.price,
"exit_reason": t.reason,
"pnl": t.pnl or 0,
})
pending_open = None
elif t.pnl is None:
pending_open = {"side": "long", "price": t.price, "reason": t.reason, "ts": t.timestamp}
elif t.side == "SELL":
if pending_open and pending_open["side"] == "long":
paired.append({
"type": "做多",
"entry_ts": pending_open["ts"],
"entry_price": pending_open["price"],
"entry_reason": pending_open["reason"],
"exit_ts": t.timestamp,
"exit_price": t.price,
"exit_reason": t.reason,
"pnl": t.pnl or 0,
})
pending_open = None
elif t.pnl is None:
pending_open = {"side": "short", "price": t.price, "reason": t.reason, "ts": t.timestamp}
return paired
async def run_backtest(symbol, start, end):
sc = VolBreakConfig(symbol=symbol, **PARAMS[symbol])
bt = BacktestConfig(symbol=symbol, interval=INTERVAL, start_time=start, end_time=end, initial_capital=INITIAL)
engine = LongShortEngine(bt, db_config=config.db)
r = await engine.run(VolBreakStrategy, sc)
return r
async def main():
ds = DataService(config.db)
await ds.connect()
full_ranges = {}
for symbol in SYMBOLS:
try:
s, e = await ds.fetch_symbol_date_range(symbol, INTERVAL)
full_ranges[symbol] = (s, e)
print(f" {symbol}: {s.date()} ~ {e.date()}")
except Exception as ex:
print(f" {symbol}: 获取范围失败 {ex}")
# 运行所有时间段
all_results: dict[str, dict[str, object]] = {} # symbol -> period_name -> result
for symbol in SYMBOLS:
if symbol not in full_ranges:
continue
all_results[symbol] = {}
fs, fe = full_ranges[symbol]
for period_name, (ps, pe) in PERIODS.items():
start = fs if period_name == "全量" else max(ps, fs)
end = min(pe, fe)
if start >= end:
print(f" {symbol} {period_name}: 无有效数据范围,跳过")
continue
print(f" 运行 {symbol} {period_name} ({start.date()}~{end.date()})...", end=" ", flush=True)
t0 = time.time()
try:
all_results[symbol][period_name] = await run_backtest(symbol, start, end)
print(f"{time.time()-t0:.1f}s")
except Exception as ex:
print(f"错误: {ex}")
await ds.close()
# ════════════════════════════════════════════════════
# 对比表
# ════════════════════════════════════════════════════
print()
print("" * 165)
print(f" ATR波动率突破 1h — 全量 / 近两年 / 近一年 / 近半年 对比 ({TODAY.strftime('%Y-%m-%d')} 截止)")
print("" * 165)
print()
col_w = 36
for symbol in SYMBOLS:
if symbol not in all_results:
continue
print(f"{symbol}")
print(f" {'时段':<8} {'本金':>7} {'终值':>9} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>8} {'胜率%':>7} {'盈亏比':>7} {'交易':>5} {'数据范围'}")
print(" " + "" * 163)
for period_name in PERIODS:
if period_name not in all_results[symbol]:
continue
r = all_results[symbol][period_name]
m = r.metrics
data_range = f"{r.config.start_time.date()}~{r.config.end_time.date()}"
print(f" {period_name:<8} {INITIAL:>7.0f} {m.final_equity:>9.0f} {m.total_return_pct:>7.1f}% {m.annual_return_pct:>7.1f}% {m.sharpe_ratio:>7.2f} {m.max_drawdown_pct:>7.1f}% {m.win_rate*100:>6.1f}% {m.profit_factor:>7.2f} {m.total_trades:>5} {data_range}")
print()
# ════════════════════════════════════════════════════
# 详细订单
# ════════════════════════════════════════════════════
print("" * 165)
print(" 各时段详细订单")
print("" * 165)
for symbol in SYMBOLS:
if symbol not in all_results:
continue
for period_name in PERIODS:
if period_name not in all_results[symbol]:
continue
result = all_results[symbol][period_name]
m = result.metrics
paired = pair_trades(result.trades)
if len(paired) == 0:
continue
print(f"\n{'' * 155}")
print(f" {symbol} {period_name}{len(paired)} 笔完整交易")
print(f" 本金 {INITIAL:,.0f} → 终值 {m.final_equity:,.0f} | 总收益 {m.total_return_pct:.1f}% | 年化 {m.annual_return_pct:.1f}% | 夏普 {m.sharpe_ratio:.2f} | 回撤 {m.max_drawdown_pct:.1f}%")
print(f" 胜率 {m.win_rate*100:.1f}% | 盈亏比 {m.profit_factor:.2f} | 最佳 {m.best_trade_pnl:+,.0f} | 最差 {m.worst_trade_pnl:+,.0f} | 平均 {m.avg_trade_pnl:+,.0f}")
print(f" 数据范围: {result.config.start_time.date()} ~ {result.config.end_time.date()}")
print(f"{'' * 155}")
print(f" {'#':>3} {'类型':<5} {'入场时间':<19} {'入场价':>10} {'出场时间':<19} {'出场价':>10} {'盈亏':>12} {'入场原因':<35} {'出场原因':<30}")
print(f" {'' * 153}")
total_pnl = 0
wins = 0
for i, p in enumerate(paired):
pnl = p["pnl"]
total_pnl += pnl
if pnl > 0:
wins += 1
pnl_str = f"{pnl:+,.0f}"
print(f" {i+1:>3} {p['type']:<5} {p['entry_ts']:<19} {p['entry_price']:>10.4f} {p['exit_ts']:<19} {p['exit_price']:>10.4f} {pnl_str:>12} {p['entry_reason']:<35} {p['exit_reason']:<30}")
print(f" {'' * 153}")
print(f" 合计: {len(paired)} 笔 | 盈利 {wins} 笔 ({wins/len(paired)*100 if paired else 0:.0f}%) | 总盈亏 {total_pnl:+,.0f} USDT")
print(f"\n{'' * 155}")
print()
if __name__ == "__main__":
asyncio.run(main())
+701
View File
@@ -0,0 +1,701 @@
"""
网络验证策略探索 — 5 个业界知名策略 × 全币种 × 1h
1. 海龟交易 (Turtle) — Donchian 20/10 通道突破 + 2N ATR 止损
2. 超级趋势 (SuperTrend) — ATR(10)×3 动态跟踪止损
3. MACD金叉死叉 — MACD(12,26,9) 零轴交叉 + ATR 止损
4. 布林收缩爆发 (BBSqueeze) — BB 收缩至 KC 内部后扩张突破
5. 三均线排列 (TripleEMA) — EMA(10,30,60) 多头/空头排列 + ATR 追踪
用法:
source .venv/bin/activate && python example/web_strategies.py
"""
import asyncio
import json
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
_project_root = Path(__file__).resolve().parent.parent.parent
if str(_project_root) not in sys.path:
sys.path.insert(0, str(_project_root))
from engine.common.base import BaseStrategy, Signal, StrategyConfig
from engine.common.models import Kline
from engine.common.config import config
from engine.backtest import BacktestConfig
from engine.data import DataService
from engine.indicators.incremental import EmaInc, AtrInc, RsiInc, BbInc
from engine.example.long_short import LongShortEngine
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
INTERVAL = "1h"
INITIAL = 10_000.0
# ════════════════════════════════════════════════════════
# 策略 1:海龟交易 (Turtle Trading)
# Richard Dennis & William Eckhardt, 1983
# 20 日高点突破入场,10 日低点突破出场,2N ATR 止损
# ════════════════════════════════════════════════════════
class TurtleConfig(StrategyConfig):
entry_period: int = 20
exit_period: int = 10
atr_period: int = 20
atr_stop: float = 2.0
class TurtleStrategy(BaseStrategy):
"""海龟交易 — Donchian 通道突破 + ATR 动态止损"""
strategy_type = "turtle"
def __init__(self, c: TurtleConfig):
super().__init__(c)
self.cfg = c
self._highs: list[float] = []
self._lows: list[float] = []
self._closes: list[float] = []
self._atr = AtrInc(c.atr_period)
self._side: str = ""
self._entry_price: float = 0.0
self._highest_since_entry: float = 0.0
self._lowest_since_entry: float = float("inf")
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._highs.append(k.high)
self._lows.append(k.low)
self._closes.append(k.close)
self._atr.update(k.high, k.low, k.close)
n = len(self._closes)
min_bars = max(self.cfg.entry_period, self.cfg.atr_period) + 5
if n < min_bars:
return None
ca = self._atr[-1]
if ca == 0:
return None
# Donchian 通道应排除当前 bar(用前 N 根 bar 计算)
donchian_high = max(self._highs[-(self.cfg.entry_period + 1):-1])
donchian_low = min(self._lows[-(self.cfg.entry_period + 1):-1])
donchian_exit_high = max(self._highs[-(self.cfg.exit_period + 1):-1])
donchian_exit_low = min(self._lows[-(self.cfg.exit_period + 1):-1])
# ── 持仓管理 ──
if self._side == "long":
self._highest_since_entry = max(self._highest_since_entry, k.high)
stop = self._entry_price - self.cfg.atr_stop * ca
trail_stop = self._highest_since_entry - self.cfg.atr_stop * ca * 0.5
if k.close < donchian_exit_low or k.close < max(stop, trail_stop):
self._side = ""
reason = "跌破10日低点" if k.close < donchian_exit_low else "ATR止损"
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time, confidence=0.25)
elif self._side == "short":
self._lowest_since_entry = min(self._lowest_since_entry, k.low)
stop = self._entry_price + self.cfg.atr_stop * ca
trail_stop = self._lowest_since_entry + self.cfg.atr_stop * ca * 0.5
if k.close > donchian_exit_high or k.close > min(stop, trail_stop):
self._side = ""
reason = "突破10日高点" if k.close > donchian_exit_high else "ATR止损"
return Signal(symbol=self.cfg.symbol, side="BUY", reason=reason, timestamp=k.open_time, confidence=0.25)
# ── 入场:仅在有明显突破幅度时入场 ──
else:
breakout_margin = 0.002 # 需突破通道 0.2% 以上
if k.close > donchian_high * (1 + breakout_margin):
self._side = "long"
self._entry_price = k.close
self._highest_since_entry = k.close
return Signal(symbol=self.cfg.symbol, side="BUY", reason="突破20日高点", timestamp=k.open_time, confidence=0.25)
elif k.close < donchian_low * (1 - breakout_margin):
self._side = "short"
self._entry_price = k.close
self._lowest_since_entry = k.close
return Signal(symbol=self.cfg.symbol, side="SELL", reason="跌破20日低点", timestamp=k.open_time, confidence=0.25)
return None
# ════════════════════════════════════════════════════════
# 策略 2:超级趋势 (SuperTrend)
# Olivier Seban, 广泛用于加密货币和商品
# ATR 动态跟踪止损,趋势翻转即反转
# ════════════════════════════════════════════════════════
class SuperTrendConfig(StrategyConfig):
atr_period: int = 10
multiplier: float = 3.0
class SuperTrendStrategy(BaseStrategy):
"""超级趋势 — ATR 动态跟踪止损,趋势跟踪"""
strategy_type = "supertrend"
def __init__(self, c: SuperTrendConfig):
super().__init__(c)
self.cfg = c
self._atr = AtrInc(c.atr_period)
self._highs: list[float] = []
self._lows: list[float] = []
self._closes: list[float] = []
self._trend: int = 0 # 1=多, -1=空
self._final_upper: float = 0.0
self._final_lower: float = 0.0
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._highs.append(k.high)
self._lows.append(k.low)
self._closes.append(k.close)
self._atr.update(k.high, k.low, k.close)
n = len(self._closes)
if n < self.cfg.atr_period + 5:
return None
ca = self._atr[-1]
if ca == 0:
return None
hl2 = (k.high + k.low) / 2.0
upper = hl2 + self.cfg.multiplier * ca
lower = hl2 - self.cfg.multiplier * ca
# 前一根的最终带用于趋势判断
prev_upper = self._final_upper
prev_lower = self._final_lower
# 判断趋势方向 (使用前一根的最终带)
prev_trend = self._trend
if prev_trend == prev_trend: # always true, just need placeholder
pass
if k.close > prev_upper and prev_upper > 0:
self._trend = 1
elif k.close < prev_lower and prev_lower > 0:
self._trend = -1
# 否则保持原趋势
# 带连续性修正
if self._trend == 1:
self._final_lower = max(lower, prev_lower) if prev_lower > 0 else lower
self._final_upper = float("inf")
elif self._trend == -1:
self._final_upper = min(upper, prev_upper) if prev_upper > 0 else upper
self._final_lower = float("-inf")
else:
self._final_upper = upper
self._final_lower = lower
if prev_trend == self._trend:
return None
# 趋势翻转 → 信号
if self._trend == 1:
return Signal(symbol=self.cfg.symbol, side="BUY", reason="SuperTrend转多", timestamp=k.open_time, confidence=0.25)
elif self._trend == -1:
return Signal(symbol=self.cfg.symbol, side="SELL", reason="SuperTrend转空", timestamp=k.open_time, confidence=0.25)
return None
# ════════════════════════════════════════════════════════
# 策略 3MACD 金叉死叉
# Gerald Appel, 1970s
# MACD(12,26,9) 零轴附近金叉/死叉 + ATR 止损
# ════════════════════════════════════════════════════════
class MacdCrossConfig(StrategyConfig):
fast: int = 12
slow: int = 26
signal: int = 9
atr_period: int = 14
atr_stop: float = 2.0
class MacdCrossStrategy(BaseStrategy):
"""MACD 金叉死叉 — 零轴以上只做多,零轴以下只做空"""
strategy_type = "macd_cross"
def __init__(self, c: MacdCrossConfig):
super().__init__(c)
self.cfg = c
self._ema_fast = EmaInc(c.fast)
self._ema_slow = EmaInc(c.slow)
self._atr = AtrInc(c.atr_period)
self._macd_vals: list[float] = [] # MACD 线值
self._signal_vals: list[float] = [] # 信号线值
self._side: str = ""
self._entry_price: float = 0.0
self._bars_held: int = 0
async def on_kline(self, k: Kline) -> Optional[Signal]:
fe = self._ema_fast.update(k.close)
se = self._ema_slow.update(k.close)
self._atr.update(k.high, k.low, k.close)
n = len(self._ema_fast)
min_bars = max(self.cfg.slow, self.cfg.signal) + 10
if n < min_bars:
return None
macd = fe - se
self._macd_vals.append(macd)
if len(self._macd_vals) < self.cfg.signal + 2:
self._signal_vals.append(0.0)
return None
# 信号线 = EMA of MACD,简化:用列表算
if len(self._signal_vals) < self.cfg.signal:
self._signal_vals.append(0.0)
if len(self._signal_vals) == self.cfg.signal:
self._signal_vals[-1] = sum(self._macd_vals[-self.cfg.signal:]) / self.cfg.signal
return None
k_sig = 2.0 / (self.cfg.signal + 1)
sig_val = macd * k_sig + self._signal_vals[-1] * (1 - k_sig)
self._signal_vals.append(sig_val)
if len(self._signal_vals) < 3:
return None
cur_m = self._macd_vals[-1]
cur_s = self._signal_vals[-1]
prev_m = self._macd_vals[-2]
prev_s = self._signal_vals[-2]
ca = self._atr[-1]
if ca == 0:
return None
golden = prev_m <= prev_s and cur_m > cur_s
death = prev_m >= prev_s and cur_m < cur_s
# ── 持仓管理 ──
if self._side == "long":
self._bars_held += 1
stop = self._entry_price - self.cfg.atr_stop * ca
if k.close < stop or (death and self._bars_held > 3):
self._side = ""
self._bars_held = 0
reason = "ATR止损" if k.close < stop else "MACD死叉"
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time, confidence=0.25)
elif self._side == "short":
self._bars_held += 1
stop = self._entry_price + self.cfg.atr_stop * ca
if k.close > stop or (golden and self._bars_held > 3):
self._side = ""
self._bars_held = 0
reason = "ATR止损" if k.close > stop else "MACD金叉"
return Signal(symbol=self.cfg.symbol, side="BUY", reason=reason, timestamp=k.open_time, confidence=0.25)
# ── 入场:零轴同向确认 + 金叉/死叉必须刚发生 ──
else:
if golden and cur_m > 0:
self._side = "long"
self._entry_price = k.close
self._bars_held = 0
return Signal(symbol=self.cfg.symbol, side="BUY", reason="MACD零轴上金叉", timestamp=k.open_time, confidence=0.25)
elif death and cur_m < 0:
self._side = "short"
self._entry_price = k.close
self._bars_held = 0
return Signal(symbol=self.cfg.symbol, side="SELL", reason="MACD零轴下死叉", timestamp=k.open_time, confidence=0.25)
return None
# ════════════════════════════════════════════════════════
# 策略 4:布林收缩爆发 (Bollinger Squeeze)
# John Bollinger, 2002
# BB 在 KC 内部收缩 → 扩张突破入场
# ════════════════════════════════════════════════════════
class BBSqueezeConfig(StrategyConfig):
bb_period: int = 20
bb_std: float = 2.0
kc_period: int = 20
kc_mult: float = 1.5
squeeze_lookback: int = 30 # 判断收缩的回看窗口
atr_stop: float = 2.0
class BBSqueezeStrategy(BaseStrategy):
"""布林收缩爆发 — BB 收缩到极限后扩张,顺势入场"""
strategy_type = "bb_squeeze"
def __init__(self, c: BBSqueezeConfig):
super().__init__(c)
self.cfg = c
self._bb = BbInc(c.bb_period, c.bb_std)
self._ema = EmaInc(c.kc_period) # Keltner 中轨
self._atr_kc = AtrInc(c.kc_period) # Keltner 宽度的 ATR
self._atr_stop = AtrInc(14) # 止损 ATR
self._closes: list[float] = []
self._side: str = ""
self._entry_price: float = 0.0
# 收缩检测
self._bb_widths: list[float] = []
self._kc_widths: list[float] = []
self._was_squeezed: bool = False
self._squeeze_bars: int = 0
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
bb_u, bb_m, bb_l = self._bb.update(k.close)
typical = (k.high + k.low + k.close) / 3.0
kc_mid = self._ema.update(typical)
self._atr_kc.update(k.high, k.low, k.close)
self._atr_stop.update(k.high, k.low, k.close)
n = len(self._closes)
min_bars = max(self.cfg.bb_period, self.cfg.kc_period, self.cfg.squeeze_lookback) + 5
if n < min_bars:
return None
atr_kc = self._atr_kc[-1]
ca = self._atr_stop[-1]
if atr_kc == 0 or ca == 0 or bb_u == 0:
return None
kc_u = kc_mid + self.cfg.kc_mult * atr_kc
kc_l = kc_mid - self.cfg.kc_mult * atr_kc
bb_width = bb_u - bb_l
kc_width = kc_u - kc_l
self._bb_widths.append(bb_width)
self._kc_widths.append(kc_width)
# BB 在 KC 内部 = 收缩
is_squeezed = bb_u < kc_u and bb_l > kc_l
# BB 宽度处于近期最低水平
lookback = min(self.cfg.squeeze_lookback, len(self._bb_widths))
recent_bb_w = self._bb_widths[-lookback:]
min_bb_w = min(recent_bb_w)
width_squeeze = bb_width < min_bb_w * 1.2
# 收缩释放信号:之前收缩,现在 BB 扩张出 KC
was_squeezed = self._was_squeezed
fired = False
if is_squeezed:
self._was_squeezed = True
self._squeeze_bars += 1
elif self._was_squeezed:
# BB 不再在 KC 内部 → 收缩释放
self._was_squeezed = False
self._squeeze_bars = 0
fired = True
# 方向判断:用价格与 BB 中轨关系 + EMA(5) 动量
ema5 = sum(self._closes[-5:]) / 5.0 if n >= 5 else k.close
up_momentum = k.close > bb_m and k.close > ema5
down_momentum = k.close < bb_m and k.close < ema5
# ── 持仓管理 ──
if self._side == "long":
stop = self._entry_price - self.cfg.atr_stop * ca
if k.close < stop or (down_momentum and not is_squeezed):
self._side = ""
return Signal(symbol=self.cfg.symbol, side="SELL", reason="ATR止损或转弱", timestamp=k.open_time, confidence=0.25)
elif self._side == "short":
stop = self._entry_price + self.cfg.atr_stop * ca
if k.close > stop or (up_momentum and not is_squeezed):
self._side = ""
return Signal(symbol=self.cfg.symbol, side="BUY", reason="ATR止损或转强", timestamp=k.open_time, confidence=0.25)
# ── 入场 ──
else:
if was_squeezed and fired and width_squeeze:
if up_momentum:
self._side = "long"
self._entry_price = k.close
return Signal(symbol=self.cfg.symbol, side="BUY", reason="BB收缩爆发做多", timestamp=k.open_time, confidence=0.25)
elif down_momentum:
self._side = "short"
self._entry_price = k.close
return Signal(symbol=self.cfg.symbol, side="SELL", reason="BB收缩爆发做空", timestamp=k.open_time, confidence=0.25)
return None
# ════════════════════════════════════════════════════════
# 策略 5:三均线排列 (Triple EMA)
# 经典三线开花 — EMA(10,30,60) 多头/空头排列 + ATR 追踪止损
# ════════════════════════════════════════════════════════
class TripleEmaConfig(StrategyConfig):
fast: int = 10
mid: int = 30
slow: int = 60
atr_period: int = 14
atr_stop: float = 2.0
class TripleEmaStrategy(BaseStrategy):
"""三均线排列 — 多头排列做多,空头排列做空"""
strategy_type = "triple_ema"
def __init__(self, c: TripleEmaConfig):
super().__init__(c)
self.cfg = c
self._ema_fast = EmaInc(c.fast)
self._ema_mid = EmaInc(c.mid)
self._ema_slow = EmaInc(c.slow)
self._atr = AtrInc(c.atr_period)
self._side: str = ""
self._entry_price: float = 0.0
self._highest_since_entry: float = 0.0
self._lowest_since_entry: float = float("inf")
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._ema_fast.update(k.close)
self._ema_mid.update(k.close)
self._ema_slow.update(k.close)
self._atr.update(k.high, k.low, k.close)
n = len(self._ema_slow)
if n < self.cfg.slow + 10:
return None
ef = self._ema_fast[-1]
em = self._ema_mid[-1]
es = self._ema_slow[-1]
pf = self._ema_fast[-2]
pm = self._ema_mid[-2]
ca = self._atr[-1]
if ef == 0 or em == 0 or es == 0 or ca == 0:
return None
# 排列状态
bull_align = ef > em > es
bear_align = ef < em < es
# 金叉:快线从下向上穿中线和慢线
fast_cross_mid_up = pf <= pm and ef > em
fast_cross_mid_down = pf >= pm and ef < em
# ── 多头持仓 ──
if self._side == "long":
self._highest_since_entry = max(self._highest_since_entry, k.high)
trail = self._highest_since_entry - self.cfg.atr_stop * ca
if fast_cross_mid_down or k.close < trail:
self._side = ""
reason = "快线下穿中线" if fast_cross_mid_down else "ATR追踪止损"
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time, confidence=0.25)
# ── 空头持仓 ──
elif self._side == "short":
self._lowest_since_entry = min(self._lowest_since_entry, k.low)
trail = self._lowest_since_entry + self.cfg.atr_stop * ca
if fast_cross_mid_up or k.close > trail:
self._side = ""
reason = "快线上穿中线" if fast_cross_mid_up else "ATR追踪止损"
return Signal(symbol=self.cfg.symbol, side="BUY", reason=reason, timestamp=k.open_time, confidence=0.25)
# ── 入场 ──
else:
if fast_cross_mid_up and bull_align:
self._side = "long"
self._entry_price = k.close
self._highest_since_entry = k.close
return Signal(symbol=self.cfg.symbol, side="BUY", reason="三线多头排列+快线金叉", timestamp=k.open_time, confidence=0.25)
elif fast_cross_mid_down and bear_align:
self._side = "short"
self._entry_price = k.close
self._lowest_since_entry = k.close
return Signal(symbol=self.cfg.symbol, side="SELL", reason="三线空头排列+快线死叉", timestamp=k.open_time, confidence=0.25)
return None
# ════════════════════════════════════════════════════════
# 执行
# ════════════════════════════════════════════════════════
STRATEGIES = {
"1.海龟交易(Turtle)": (TurtleConfig, TurtleStrategy),
"2.超级趋势(SuperTrend)": (SuperTrendConfig, SuperTrendStrategy),
"3.MACD金叉死叉": (MacdCrossConfig, MacdCrossStrategy),
"4.布林收缩爆发(BBSqueeze)": (BBSqueezeConfig, BBSqueezeStrategy),
"5.三均线排列(TripleEMA)": (TripleEmaConfig, TripleEmaStrategy),
}
def make_config(config_cls, symbol):
"""根据策略类型创建默认参数配置"""
if config_cls == TurtleConfig:
return config_cls(symbol=symbol, entry_period=20, exit_period=10, atr_period=20, atr_stop=2.0)
elif config_cls == SuperTrendConfig:
return config_cls(symbol=symbol, atr_period=10, multiplier=3.0)
elif config_cls == MacdCrossConfig:
return config_cls(symbol=symbol, fast=12, slow=26, signal=9, atr_period=14, atr_stop=2.0)
elif config_cls == BBSqueezeConfig:
return config_cls(symbol=symbol, bb_period=20, bb_std=2.0, kc_period=20, kc_mult=1.5, squeeze_lookback=30, atr_stop=2.0)
elif config_cls == TripleEmaConfig:
return config_cls(symbol=symbol, fast=10, mid=30, slow=60, atr_period=14, atr_stop=2.0)
else:
raise ValueError(f"未知策略: {config_cls}")
async def run_one(config_cls, strategy_cls, symbol, start, end):
sc = make_config(config_cls, symbol)
bt = BacktestConfig(symbol=symbol, interval=INTERVAL, start_time=start, end_time=end, initial_capital=INITIAL)
engine = LongShortEngine(bt, db_config=config.db)
r = await engine.run(strategy_cls, sc)
return r
async def main():
ds = DataService(config.db)
await ds.connect()
# 获取数据范围
ranges: dict[str, tuple] = {}
for symbol in SYMBOLS:
try:
s, e = await ds.fetch_symbol_date_range(symbol, INTERVAL)
ranges[symbol] = (s, e)
print(f" {symbol}: {s.date()} ~ {e.date()}")
except Exception as ex:
print(f" {symbol}: 获取范围失败 {ex}")
await ds.close()
# 汇总数据
all_results: list[dict] = []
detail_results: dict[str, dict[str, dict]] = {} # 用于保存详细结果
print()
print("" * 140)
print(" 5 策略 × 4 币种 × 1h — 网络验证策略扫描")
print("" * 140)
print()
for strategy_name, (config_cls, strategy_cls) in STRATEGIES.items():
print(f"{strategy_name}")
print(f" {'币种':<10} {'本金':>7} {'终值':>9} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>8} {'胜率%':>7} {'盈亏比':>7} {'交易':>6} {'耗时s':>7}")
print(" " + "" * 120)
detail_results[strategy_name] = {}
for symbol in SYMBOLS:
if symbol not in ranges:
continue
start, end = ranges[symbol]
t0 = time.time()
try:
r = await run_one(config_cls, strategy_cls, symbol, start, end)
elapsed = time.time() - t0
except Exception as ex:
print(f" {symbol:<10} {'错误: ' + str(ex)[:50]}")
continue
m = r.metrics
final = m.final_equity
print(f" {symbol:<10} {INITIAL:>7.0f} {final:>9.0f} {m.total_return_pct:>7.1f}% {m.annual_return_pct:>7.1f}% {m.sharpe_ratio:>7.2f} {m.max_drawdown_pct:>7.1f}% {m.win_rate*100:>6.1f}% {m.profit_factor:>7.2f} {m.total_trades:>6} {elapsed:>6.1f}s")
all_results.append({
"strategy": strategy_name,
"symbol": symbol,
"interval": INTERVAL,
"initial": INITIAL,
"final": final,
"total_return": m.total_return_pct,
"annual_return": m.annual_return_pct,
"sharpe": m.sharpe_ratio,
"drawdown": m.max_drawdown_pct,
"win_rate": m.win_rate * 100,
"profit_factor": m.profit_factor,
"trades": m.total_trades,
"best": m.best_trade_pnl,
"worst": m.worst_trade_pnl,
"avg": m.avg_trade_pnl,
"calmar": m.calmar_ratio,
"start_date": str(start.date()),
"end_date": str(end.date()),
})
# 详细交易记录
detail_results[strategy_name][symbol] = {
"config": {
"symbol": symbol,
"interval": INTERVAL,
"start": str(start.date()),
"end": str(end.date()),
"initial_capital": INITIAL,
},
"metrics": {
"total_return_pct": m.total_return_pct,
"annual_return_pct": m.annual_return_pct,
"sharpe_ratio": m.sharpe_ratio,
"max_drawdown_pct": m.max_drawdown_pct,
"win_rate": m.win_rate * 100,
"profit_factor": m.profit_factor,
"total_trades": m.total_trades,
"avg_trade_pnl": m.avg_trade_pnl,
"best_trade_pnl": m.best_trade_pnl,
"worst_trade_pnl": m.worst_trade_pnl,
"final_equity": m.final_equity,
"calmar_ratio": m.calmar_ratio,
},
"trades": [
{
"side": t.side,
"price": t.price,
"quantity": t.quantity,
"pnl": t.pnl,
"reason": t.reason,
"timestamp": t.timestamp,
"timestamp_str": datetime.fromtimestamp(t.timestamp / 1000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M"),
}
for t in r.trades
],
}
print()
# ── 汇总:每种策略的最佳/最差 ──
print("" * 140)
print(" ■ 各策略汇总 (按年化收益排序)")
print(f" {'策略':<28} {'币种':<10} {'本金':>7} {'终值':>9} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>8} {'胜率%':>7} {'盈亏比':>7} {'交易':>6}")
print(" " + "" * 120)
for sn in STRATEGIES:
candidates = [r for r in all_results if r["strategy"] == sn]
if not candidates:
continue
# 按年化排序,显示所有币种
candidates.sort(key=lambda x: x["annual_return"], reverse=True)
for c in candidates:
marker = "" if c == candidates[0] else " "
print(f" {sn:<26}{marker} {c['symbol']:<10} {c['initial']:>7.0f} {c['final']:>9.0f} {c['total_return']:>7.1f}% {c['annual_return']:>7.1f}% {c['sharpe']:>7.2f} {c['drawdown']:>7.1f}% {c['win_rate']:>6.1f}% {c['profit_factor']:>7.2f} {c['trades']:>6}")
print("\n" * 140)
# ── 保存结果到 JSON ──
output_file = _project_root / "engine" / "example" / "web_strategies_result.json"
with open(output_file, "w", encoding="utf-8") as f:
json.dump({
"summary": all_results,
"detail": detail_results,
"run_time": datetime.now(timezone.utc).isoformat(),
}, f, ensure_ascii=False, indent=2, default=str)
print(f"\n 详细结果已保存至: {output_file}")
if __name__ == "__main__":
asyncio.run(main())
File diff suppressed because it is too large Load Diff
+3
View File
@@ -17,6 +17,7 @@ from .trend import sma, ema, macd, macd_signal, macd_histogram, adx
from .momentum import rsi, stoch, stoch_k, stoch_d
from .volatility import bollinger, bollinger_upper, bollinger_mid, bollinger_lower, atr
from .volume import obv, vwap
from .incremental import EmaInc, AtrInc, RsiInc, BbInc
__all__ = [
# 趋势
@@ -27,4 +28,6 @@ __all__ = [
"bollinger", "bollinger_upper", "bollinger_mid", "bollinger_lower", "atr",
# 成交量
"obv", "vwap",
# 增量
"EmaInc", "AtrInc", "RsiInc", "BbInc",
]
+244
View File
@@ -0,0 +1,244 @@
"""
增量指标 — O(1) 每 bar 更新,避免每次从头重算整条序列
策略在 on_kline 中对每根 bar 调用 update(),内部只计算增量值,
对外暴露 values 属性(完整序列,支持索引回溯),兼顾性能与易用性。
用法:
from engine.indicators.incremental import EmaInc, AtrInc
e200 = EmaInc(200)
for price in prices:
e200.update(price)
print(e200[-1]) # 最新 EMA 值
print(e200[-20]) # 20 根前的 EMA 值(斜率计算用)
"""
from typing import Optional
class EmaInc:
"""增量 EMA
内部维护完整序列,update() 为 O(1)values 为 list[float] 可直接索引。
"""
def __init__(self, period: int):
self.period = period
self.k = 2.0 / (period + 1)
self._values: list[float] = []
self._warm: list[float] = []
self._ready = False
def update(self, price: float) -> float:
"""输入新价格,返回最新 EMA 值(不足周期时返回 0)"""
if not self._ready:
self._warm.append(price)
self._values.append(0.0)
if len(self._warm) == self.period:
self._values[-1] = sum(self._warm) / self.period
self._warm.clear()
self._ready = True
return self._values[-1]
return 0.0
val = price * self.k + self._values[-1] * (1 - self.k)
self._values.append(val)
return val
@property
def values(self) -> list[float]:
return self._values
@property
def current(self) -> float:
return self._values[-1] if self._values else 0.0
def __getitem__(self, idx: int) -> float:
return self._values[idx]
def __len__(self) -> int:
return len(self._values)
class AtrInc:
"""增量 ATRWilder 平滑)
内部维护完整序列,update() 为 O(1)values 为 list[float] 可直接索引。
"""
def __init__(self, period: int = 14):
self.period = period
self._values: list[float] = []
self._tr_buffer: list[float] = []
self._prev_close: Optional[float] = None
self._ready = False
def update(self, high: float, low: float, close: float) -> float:
"""输入新 bar 的 HLC,返回最新 ATR 值(不足周期时返回 0)"""
# 第一根 bar:记录收盘价,无法计算 TR
if self._prev_close is None:
self._prev_close = close
self._values.append(0.0)
return 0.0
tr = max(high - low, abs(high - self._prev_close), abs(low - self._prev_close))
self._prev_close = close
if not self._ready:
self._tr_buffer.append(tr)
self._values.append(0.0)
if len(self._tr_buffer) == self.period:
atr_val = sum(self._tr_buffer) / self.period
self._values[-1] = atr_val
self._tr_buffer.clear()
self._ready = True
return atr_val
return 0.0
# Wilder 平滑
atr_val = (self._values[-1] * (self.period - 1) + tr) / self.period
self._values.append(atr_val)
return atr_val
@property
def values(self) -> list[float]:
return self._values
@property
def current(self) -> float:
return self._values[-1] if self._values else 0.0
def __getitem__(self, idx: int) -> float:
return self._values[idx]
def __len__(self) -> int:
return len(self._values)
class RsiInc:
"""增量 RSIWilder 平滑)
内部维护完整序列,update() 为 O(1)。
"""
def __init__(self, period: int = 14):
self.period = period
self._values: list[float] = []
self._prev_price: Optional[float] = None
self._avg_gain: float = 0.0
self._avg_loss: float = 0.0
self._changes: list[float] = []
self._ready = False
def update(self, price: float) -> float:
if self._prev_price is None:
self._prev_price = price
self._values.append(0.0)
return 0.0
change = price - self._prev_price
self._prev_price = price
if not self._ready:
self._changes.append(change)
self._values.append(0.0)
if len(self._changes) == self.period:
gains = [max(c, 0.0) for c in self._changes]
losses = [abs(min(c, 0.0)) for c in self._changes]
self._avg_gain = sum(gains) / self.period
self._avg_loss = sum(losses) / self.period
self._changes.clear()
self._ready = True
rs = self._avg_gain / self._avg_loss if self._avg_loss > 0 else float("inf")
rsi = 100.0 - (100.0 / (1.0 + rs)) if self._avg_loss > 0 else 100.0
self._values[-1] = rsi
return rsi
return 0.0
gain = max(change, 0.0)
loss = abs(min(change, 0.0))
self._avg_gain = (self._avg_gain * (self.period - 1) + gain) / self.period
self._avg_loss = (self._avg_loss * (self.period - 1) + loss) / self.period
if self._avg_loss == 0:
rsi = 100.0
else:
rs = self._avg_gain / self._avg_loss
rsi = 100.0 - (100.0 / (1.0 + rs))
self._values.append(rsi)
return rsi
@property
def values(self) -> list[float]:
return self._values
@property
def current(self) -> float:
return self._values[-1] if self._values else 0.0
def __getitem__(self, idx: int) -> float:
return self._values[idx]
def __len__(self) -> int:
return len(self._values)
class BbInc:
"""增量布林带
内部维护完整序列,update() 返回 (upper, mid, lower) 三元组。
"""
def __init__(self, period: int = 20, std: float = 2.0):
self.period = period
self.std = std
self._upper: list[float] = []
self._mid: list[float] = []
self._lower: list[float] = []
self._window: list[float] = []
self._window_sum: float = 0.0
self._window_sum_sq: float = 0.0
def update(self, price: float) -> tuple[float, float, float]:
self._window.append(price)
self._window_sum += price
self._window_sum_sq += price * price
if len(self._window) < self.period:
self._upper.append(0.0)
self._mid.append(0.0)
self._lower.append(0.0)
return 0.0, 0.0, 0.0
if len(self._window) > self.period:
old = self._window.pop(0)
self._window_sum -= old
self._window_sum_sq -= old * old
mean = self._window_sum / self.period
variance = (self._window_sum_sq / self.period) - (mean * mean)
stdev = max(variance, 0.0) ** 0.5
upper = mean + self.std * stdev
lower = mean - self.std * stdev
self._upper.append(upper)
self._mid.append(mean)
self._lower.append(lower)
return upper, mean, lower
@property
def upper(self) -> list[float]:
return self._upper
@property
def mid(self) -> list[float]:
return self._mid
@property
def lower(self) -> list[float]:
return self._lower
def __len__(self) -> int:
return len(self._mid)