Compare commits

...

10 Commits

Author SHA1 Message Date
Rekey 6708abaf56 feat(data/service): add bnkline.ts — Binance REST client K-line wrapper
封装 Client(多交易所 REST 客户端)的 binance 实现,
提供 fetchKlines 服务层函数,复用限流、数据转换、
连续性过滤等既有逻辑,参数顺序更自然(endTime 在 limit 前)。
2026-06-14 18:50:42 +08:00
Rekey 9351dec226 refactor: migrate API keys to config, extend Kline intervals, add DB extensions
Security:
- Move hardcoded Binance API key/secret from rest.ts to env.yaml (exchange config segment)
- Add ExchangeConfig validation in config/validators.ts
- Export typed exchange config from config/index.ts
- Update AGENTS/07-caveats.md to reflect the new policy

Kline intervals (add 3m / 2h / 6h / 8h / 1mon):
- TypeScript: update KlineInterval type, KLINE_INTERVAL_MS mapping, build_aggregates_sql refresh chain
- Python: sync KlineInterval Literal type, INTERVAL_TO_TABLE and INTERVAL_MS mappings, db_test table list
- SQL: add 5 continuous aggregate materialized views (klines_3m/2h/6h/8h/1mon) with indexes
- SQL: extend default kline_intervals in trading_pairs table
- SQL: add cross-sectional query indexes for klines_1d and klines_1w

DB:
- Enable pg_prewarm extension (backtest warmup)
- Enable pg_stat_statements extension (slow query monitoring)

Other:
- data/run/exchange.ts: graceful pgsql shutdown after backfill completes
- Config path: load from data/env.yaml (symlink) instead of project root
2026-06-14 18:45:01 +08:00
Rekey a9c45cce39 chore: add Reasonix config files and env.yaml symlink
- .reasonix.toml / reasonix.toml — Reasonix 工具配置
- data/env.yaml — 软链接指向项目根目录 env.yaml(统一配置加载路径)
2026-06-14 18:44:43 +08:00
Rekey d5ec69217e 将 AGENTS.md 拆分为 AGENTS/ 目录,按主题分文件并新增中文优先规则 2026-06-14 07:29:00 +08:00
Rekey 0cd2cbbb79 feat(engine): 新增 2h/6h 与 1h 策略对比回测
- comparison_2h_6h: 9 策略 × 4 币种 × 2 周期 × 4 数据量 = 288 次回测
  - 包含海龟、超级趋势、MACD、布林收缩、三均线、RSI 回归、
    ATR 波动率突破、EMA 多空、牛熊自适应
  - 结论:6h 夏普显著优于 2h(69% 组合),ATR 策略霸榜
  - 自动生成 Markdown 回测报告

- vol_break_1h_6h: ATR 波动率突破 × 1h/2h/4h/6h 近半年对比
2026-06-14 00:15:16 +08:00
Rekey edc50e8809 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 等分析脚本
2026-06-13 19:30:25 +08:00
Rekey b5cdb41993 chore: 更新 README 架构文档与数据库测试脚本
- README.md: 更新数据层 Node.js→Bun,common→engine/common,同步目录树结构
- db_test.py: TimescaleDB 数据库连接与基础查询测试脚本
2026-06-12 10:27:11 +08:00
Rekey 515e61c517 feat(engine): 添加策略示例集(18 个 Demo)
- backtest_demo.py: 回测基础演示
- strategy_simple.py / three_ema.py / long_short.py: 基础策略(双均线/三均线/多空)
- strategy_optimize*.py (3 版本): 参数优化示例(网格搜索/贝叶斯/遗传算法)
- multi_tf_*.py (4 版本): 多时间框架策略(EMA200/多周期共振/混合信号)
- regime_*.py (4 版本): 市场状态检测(趋势/震荡/波动率区间/全状态)
- cross_section.py: 截面多品种策略
- factor_demo.py: 多因子模型演示
- strategy_battle.py / strategy_more.py: 策略对比与组合
- full_cycle.py: 全流程演示(数据→回测→分析)
- data.py: 数据读取示例
2026-06-12 10:27:04 +08:00
Rekey 4da520c14b feat(engine): 添加事件驱动回测引擎
- backtest/engine.py: 事件驱动回测引擎核心,支持 K 线推进/订单撮合/权益曲线
- backtest/models.py: 回测数据模型(订单/成交/持仓/账户快照)
- backtest/README.md: 回测模块使用说明
- backtest/STRATEGY.md: 策略开发指南与最佳实践
- backtest/TIMEFRAME_COMPARISON*.md: 多周期回测对比分析报告
2026-06-12 10:26:53 +08:00
Rekey 212f6fedad feat(engine): 添加数据服务层与技术指标库
- data/service.py: 数据拉取服务,从 TimescaleDB 读取 K 线/Ticker 等行情数据
- indicators/momentum.py: 动量类指标(RSI/MACD/Stochastic 等)
- indicators/trend.py: 趋势类指标(EMA/SMA/ADX/SuperTrend 等)
- indicators/volatility.py: 波动率指标(Bollinger/ATR/Keltner 等)
- indicators/volume.py: 成交量指标(OBV/VWAP/MFI 等)
2026-06-12 10:26:45 +08:00
79 changed files with 501836 additions and 126 deletions
+2
View File
@@ -7,3 +7,5 @@ db/pgsql/
# Python # Python
__pycache__/ __pycache__/
.venv/ .venv/
.codegraph
+4
View File
@@ -0,0 +1,4 @@
# 项目级 Reasonix 配置 — 优先级高于全局 config.toml
# 参考: ~/Library/Application Support/reasonix/config.toml
[agent]
reasoning_language = "zh" # 强制推理过程使用中文
-66
View File
@@ -1,66 +0,0 @@
# AGENTS.md
## 最关键
- **运行时是 Bun,不是 Node.js**。执行 TS 文件用 `bun run <file>`,不能用 `node``npm``npx`
- **双语言项目**`data/` 是 TypeScript (Bun)`engine/` 是 Python 3.10+。两个模块通过 Redis Pub/Sub 通信。
- **data/ 必须在 data/ 目录下运行**:`package.json``data/` 中,依赖安装到 `data/node_modules`。命令如 `bun install``bun run lint` 需要 `workdir=data`
- **engine/ 必须在 engine/ 目录下运行**Python 虚拟环境在 `engine/.venv/`,导入包使用相对路径(`from common import Kline`)。命令如 `python -c "from common import Kline"` 需要 `workdir=engine`
## 常用命令
```bash
# 依赖安装(在 data/ 目录下)
bun install
# 运行数据补全(拉取历史 K 线)
bun run data/run/exchange.ts --concurrency 2
# 运行连续聚合刷新(dry-run 看 SQL,加 --execute 实际执行)
bun run data/run/build_aggregates_sql.ts # 纯输出 SQL
bun run data/run/build_aggregates_sql.ts --execute # 实际刷新
bun run data/run/build_aggregates_sql.ts --start 2025-01 --end 2025-06 --execute
# 代码检查与格式化
bun run lint # eslint
bun run format # prettier
# 构建检查
bun run build # tsc 类型检查
```
## 基础设施
- **数据库**`docker compose up -d` 启动 TimescaleDB(端口 5432+ Adminer(端口 8080)。
- **配置源**:项目根目录 `env.yaml` 是 TS 和 Python 共享的唯一配置。`data/config/index.ts` 读取并校验它。
- **数据库连接**host 在 `env.yaml` 中配,当前指向 `10.0.0.7`(远程)。本地开发需改为 `localhost`
## 架构约定
- **`synchronize: false`**TypeORM 不会自动同步 schema。修改实体后需要手动迁移或手动改表。
- **`@timescaledb/typeorm` 是 v0.0.1 实验版**。K 线实体的 `@Hypertable` 装饰器可能不稳定。标准 SQL 集成用 TimescaleDB 连续聚合视图(`klines_5m``klines_15m` 等)。
- **数据模型对齐**TS 侧 `data/types/base.ts` 定义的类型与 Python 侧 `engine/common/models.py` 的 Pydantic 模型必须保持字段一致。TS 侧 K 线价格为 `string` 类型(精度),写库时 `Number()` 转换。
- **K 线 4 列复合主键**`[exchange, symbol, interval, time]`。K 线分区列是 `time`TimescaleDB 要求分区列必须在主键中)。
## Python 引擎
- **workdir 必须是 engine/**:导入包使用 `from common import ...`(如 `from common import Kline, BaseStrategy`)。
- **未完成模块**:策略管理器、信号总线、回测引擎、参数优化器仅存在于 `ENGINE.md` 设计文档中,尚未实现。当前仅 `common/base.py`(策略基类)和 `common/models.py`(数据模型)、`common/logger.py`(日志)可用。
- 引擎入口 `engine/__init__.py` 导出 `Kline, KlineInterval, OrderBook, Ticker, Trade`
- 引擎配置在 `engine/env.yaml`(与根 `env.yaml` 不同,是引擎专属配置)。
- Pydantic v2 的 `field_validator` 处理 TS 侧字符串 → Python float/int 转换。
## 项目现状
- **已实现**:TS 数据模块的配置加载、TypeORM 实体、Binance REST K 线拉取与批量 UPSERT、连续聚合刷新脚本。
- **未实现**WebSocket 行情采集、K 线合成管道、Redis 发布、策略管理器、信号总线、回测、风控、交易执行、API 网关。这些在 `README.md``engine/ENGINE.md` 中有详细设计文档。
- **`data/run/main.ts` 不存在**`dev` 脚本指向的文件尚未创建。当前实际可运行的入口是 `run/exchange.ts`(数据补全)和 `run/build_aggregates_sql.ts`(聚合刷新)。
- **无测试、无 CI**`package.json` 定义了 `vitest` 脚本但测试尚未编写。无 CI 配置文件。
## 注意事项
- **`data/exchanges/rest.ts` 包含硬编码的 Binance API Key**(第 105-106 行),不要提交到公开仓库。
- `env.yaml` 包含明文数据库密码且被 git 追踪,注意安全。
- 未安装 Python 依赖(如 pydantic),`engine/` 目录有独立的 `.venv/`
- `db/pgsql/``.gitignore` 中,这是 TimescaleDB 数据目录(Docker volume 映射)。
- `KLINE_INTERVAL_MS` 常量定义了两处:`data/exchanges/rest.ts``data/types/kline.ts` 的类型定义。新增周期需同步。
+7
View File
@@ -0,0 +1,7 @@
# 最关键
- **中文优先**:所有回答和推理过程必须使用中文,代码、标识符、文件名、技术术语保持原文不翻译。
- **运行时是 Bun,不是 Node.js**。执行 TS 文件用 `bun run <file>`,不能用 `node``npm``npx`
- **双语言项目**`data/` 是 TypeScript (Bun)`engine/` 是 Python 3.10+。两个模块通过 Redis Pub/Sub 通信。
- **data/ 必须在 data/ 目录下运行**:`package.json``data/` 中,依赖安装到 `data/node_modules`。命令如 `bun install``bun run lint` 需要 `workdir=data`
- **engine/ 必须在 engine/ 目录下运行**Python 虚拟环境在 `engine/.venv/`,导入包使用相对路径(`from common import Kline`)。命令如 `python -c "from common import Kline"` 需要 `workdir=engine`
+21
View File
@@ -0,0 +1,21 @@
# 常用命令
```bash
# 依赖安装(在 data/ 目录下)
bun install
# 运行数据补全(拉取历史 K 线)
bun run data/run/exchange.ts --concurrency 2
# 运行连续聚合刷新(dry-run 看 SQL,加 --execute 实际执行)
bun run data/run/build_aggregates_sql.ts # 纯输出 SQL
bun run data/run/build_aggregates_sql.ts --execute # 实际刷新
bun run data/run/build_aggregates_sql.ts --start 2025-01 --end 2025-06 --execute
# 代码检查与格式化
bun run lint # eslint
bun run format # prettier
# 构建检查
bun run build # tsc 类型检查
```
+5
View File
@@ -0,0 +1,5 @@
# 基础设施
- **数据库**`docker compose up -d` 启动 TimescaleDB(端口 5432+ Adminer(端口 8080)。
- **配置源**:项目根目录 `env.yaml` 是 TS 和 Python 共享的唯一配置。`data/config/index.ts` 读取并校验它。
- **数据库连接**host 在 `env.yaml` 中配,当前指向 `10.0.0.7`(远程)。本地开发需改为 `localhost`
+6
View File
@@ -0,0 +1,6 @@
# 架构约定
- **`synchronize: false`**TypeORM 不会自动同步 schema。修改实体后需要手动迁移或手动改表。
- **`@timescaledb/typeorm` 是 v0.0.1 实验版**。K 线实体的 `@Hypertable` 装饰器可能不稳定。标准 SQL 集成用 TimescaleDB 连续聚合视图(`klines_5m``klines_15m` 等)。
- **数据模型对齐**TS 侧 `data/types/base.ts` 定义的类型与 Python 侧 `engine/common/models.py` 的 Pydantic 模型必须保持字段一致。TS 侧 K 线价格为 `string` 类型(精度),写库时 `Number()` 转换。
- **K 线 4 列复合主键**`[exchange, symbol, interval, time]`。K 线分区列是 `time`TimescaleDB 要求分区列必须在主键中)。
+7
View File
@@ -0,0 +1,7 @@
# Python 引擎
- **workdir 必须是 engine/**:导入包使用 `from common import ...`(如 `from common import Kline, BaseStrategy`)。
- **未完成模块**:策略管理器、信号总线、回测引擎、参数优化器仅存在于 `ENGINE.md` 设计文档中,尚未实现。当前仅 `common/base.py`(策略基类)和 `common/models.py`(数据模型)、`common/logger.py`(日志)可用。
- 引擎入口 `engine/__init__.py` 导出 `Kline, KlineInterval, OrderBook, Ticker, Trade`
- 引擎配置在 `engine/env.yaml`(与根 `env.yaml` 不同,是引擎专属配置)。
- Pydantic v2 的 `field_validator` 处理 TS 侧字符串 → Python float/int 转换。
+6
View File
@@ -0,0 +1,6 @@
# 项目现状
- **已实现**:TS 数据模块的配置加载、TypeORM 实体、Binance REST K 线拉取与批量 UPSERT、连续聚合刷新脚本。
- **未实现**WebSocket 行情采集、K 线合成管道、Redis 发布、策略管理器、信号总线、回测、风控、交易执行、API 网关。这些在 `README.md``engine/ENGINE.md` 中有详细设计文档。
- **`data/run/main.ts` 不存在**`dev` 脚本指向的文件尚未创建。当前实际可运行的入口是 `run/exchange.ts`(数据补全)和 `run/build_aggregates_sql.ts`(聚合刷新)。
- **无测试、无 CI**`package.json` 定义了 `vitest` 脚本但测试尚未编写。无 CI 配置文件。
+7
View File
@@ -0,0 +1,7 @@
# 注意事项
- **所有 API Key 统一在 `env.yaml` 中管理**,禁止在代码中硬编码。新增交易所时在 `exchange` 段添加对应子配置即可。
- `env.yaml` 包含明文密钥和数据库密码且被 git 追踪,注意安全,不要提交到公开仓库。
- 未安装 Python 依赖(如 pydantic),`engine/` 目录有独立的 `.venv/`
- `db/pgsql/``.gitignore` 中,这是 TimescaleDB 数据目录(Docker volume 映射)。
- `KLINE_INTERVAL_MS` 常量定义了两处:`data/exchanges/rest.ts``data/types/kline.ts` 的类型定义。新增周期需同步。
+21 -28
View File
@@ -35,7 +35,7 @@
| 层 | 语言 | 职责 | | 层 | 语言 | 职责 |
|---|------|------| |---|------|------|
| **数据层** | TypeScript (Node.js) | 行情采集、WebSocket 连接管理、K 线合成、数据写入 | | **数据层** | TypeScript (Bun) | 行情采集、WebSocket 连接管理、K 线合成、数据写入 |
| **业务层** | Python 3.10+ | 策略引擎、回测、风控、交易执行 | | **业务层** | Python 3.10+ | 策略引擎、回测、风控、交易执行 |
| **接口层** | TypeScript / Python | FastAPI (Python) 或 NestJS (TS) 提供 REST/WS API | | **接口层** | TypeScript / Python | FastAPI (Python) 或 NestJS (TS) 提供 REST/WS API |
@@ -45,7 +45,7 @@
| 分类 | 技术 / 库 | 说明 | | 分类 | 技术 / 库 | 说明 |
| ------------ | ---------------------------------- | -------------------------------------- | | ------------ | ---------------------------------- | -------------------------------------- |
| **数据层语言** | **TypeScript 5.x (Node.js 20+)** | 行情采集、WebSocket 管理、数据管道 | | **数据层语言** | **TypeScript 5.x (Bun)** | 行情采集、WebSocket 管理、数据管道 |
| **业务层语言** | **Python 3.10+** | 策略引擎、回测、风控逻辑 | | **业务层语言** | **Python 3.10+** | 策略引擎、回测、风控逻辑 |
| **时序数据库** | **TimescaleDB (推荐)** | K 线数据存储,基于 PostgreSQL 扩展 | | **时序数据库** | **TimescaleDB (推荐)** | K 线数据存储,基于 PostgreSQL 扩展 |
| 关系型数据库 | PostgreSQL 16+ | 订单、策略配置、用户数据等(TimescaleDB 基于 PG,可共用)| | 关系型数据库 | PostgreSQL 16+ | 订单、策略配置、用户数据等(TimescaleDB 基于 PG,可共用)|
@@ -87,7 +87,7 @@
|------|------| |------|------|
| Node.js WebSocket 性能优异,天然适合高并发连接 | 需要维护两套技术栈 | | Node.js WebSocket 性能优异,天然适合高并发连接 | 需要维护两套技术栈 |
| TypeScript 类型系统在数据管道中减少运行时错误 | 跨语言调试稍复杂 | | TypeScript 类型系统在数据管道中减少运行时错误 | 跨语言调试稍复杂 |
| **共享类型**:前后端可用同一套 TypeScript 类型定义 | 部署需要 Node.js + Python 双运行时 | | **共享类型**:前后端可用同一套 TypeScript 类型定义 | 部署需要 Bun + Python 双运行时 |
| 事件驱动模型与行情数据流天然匹配 | 团队需要双语言能力 | | 事件驱动模型与行情数据流天然匹配 | 团队需要双语言能力 |
| npm 生态有成熟的交易 SDKccxt 等) | | | npm 生态有成熟的交易 SDKccxt 等) | |
@@ -940,7 +940,7 @@ export class TimescaleDBStorage {
##### 6. Python 读取实现(策略引擎侧) ##### 6. Python 读取实现(策略引擎侧)
```python ```python
# common/storage.py # engine/common/storage.py
import asyncpg import asyncpg
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
@@ -1160,6 +1160,7 @@ volumes:
| 子模块 | 职责 | 关键技术点 | | 子模块 | 职责 | 关键技术点 |
| -------------- | ---------------------------------------------- | -------------------------------------- | | -------------- | ---------------------------------------------- | -------------------------------------- |
| **通用模块** | 策略基类、数据模型、日志、配置 | `engine/common/` 目录,基础类型定义 |
| **策略管理器** | 策略注册、启动、停止、热加载 | 插件化架构、动态导入、`importlib` | | **策略管理器** | 策略注册、启动、停止、热加载 | 插件化架构、动态导入、`importlib` |
| **信号分发器** | 将策略产生的交易信号分发到交易执行模块 | 事件总线、消息队列 | | **信号分发器** | 将策略产生的交易信号分发到交易执行模块 | 事件总线、消息队列 |
| **回测引擎** | 使用历史数据模拟策略执行,评估收益、回撤等指标 | `vectorbt` / `backtrader`、事件驱动 | | **回测引擎** | 使用历史数据模拟策略执行,评估收益、回撤等指标 | `vectorbt` / `backtrader`、事件驱动 |
@@ -1168,27 +1169,15 @@ volumes:
#### 策略基类设计 #### 策略基类设计
```python ```python
class BaseStrategy(ABC): from common import BaseStrategy, StrategyConfig, Signal
"""所有策略的基类""" from common.models import Kline, Ticker, OrderBook
def __init__(self, config: StrategyConfig): class MyStrategy(BaseStrategy):
self.config = config """策略示例 — 所有策略继承 BaseStrategy"""
self.position = 0.0 strategy_type = "my_strategy"
self.pnl = 0.0
@abstractmethod
async def on_ticker(self, ticker: Ticker) -> Signal | None:
"""处理 ticker 数据,返回交易信号"""
...
@abstractmethod
async def on_kline(self, kline: Kline) -> Signal | None: async def on_kline(self, kline: Kline) -> Signal | None:
"""处理 K 线数据,返回交易信号"""
... ...
async def on_orderbook(self, orderbook: OrderBook) -> Signal | None:
"""处理深度数据(可选实现)"""
return None
``` ```
--- ---
@@ -1466,7 +1455,7 @@ async with redis.pubsub() as pubsub:
| ---- | ------------------------------------------------------------ | ----------------------------------- | | ---- | ------------------------------------------------------------ | ----------------------------------- |
| 1.1 | 初始化 TypeScript 数据项目(`data/`),配置 `package.json` | 项目结构,ESLint + Prettier 配置 | | 1.1 | 初始化 TypeScript 数据项目(`data/`),配置 `package.json` | 项目结构,ESLint + Prettier 配置 |
| 1.2 | 初始化 Python 项目,配置 `poetry` / `uv` 依赖管理 | `pyproject.toml`,项目目录结构 | | 1.2 | 初始化 Python 项目,配置 `poetry` / `uv` 依赖管理 | `pyproject.toml`,项目目录结构 |
| 1.3 | 定义共享类型:TypeScript `types/` + Python `common/models.py` | 双端对齐的数据模型 | | 1.3 | 定义共享类型:TypeScript `types/` + Python `engine/common/models.py` | 双端对齐的数据模型 |
| 1.4 | 实现 TS 交易所适配器基类 + Binance 适配器(`data/src/exchanges/` | 统一接口 + WebSocket 连接 | | 1.4 | 实现 TS 交易所适配器基类 + Binance 适配器(`data/src/exchanges/` | 统一接口 + WebSocket 连接 |
| 1.5 | 实现 TS 行情采集器,WebSocket 订阅实时 ticker 和 K 线 | 实时行情流入 | | 1.5 | 实现 TS 行情采集器,WebSocket 订阅实时 ticker 和 K 线 | 实时行情流入 |
| 1.6 | 实现 TS K 线合成器(`data/src/pipeline/kline-synthesizer.ts`) | 多周期 K 线实时合成 | | 1.6 | 实现 TS K 线合成器(`data/src/pipeline/kline-synthesizer.ts`) | 多周期 K 线实时合成 |
@@ -1481,7 +1470,7 @@ async with redis.pubsub() as pubsub:
| 步骤 | 任务 | 产出物 | | 步骤 | 任务 | 产出物 |
| ---- | ------------------------------------------------------------ | -------------------------------- | | ---- | ------------------------------------------------------------ | -------------------------------- |
| 2.1 | 实现策略基类(`engine/base.py` | `BaseStrategy` 抽象基类 | | 2.1 | 实现策略基类(`engine/common/base.py`) | `BaseStrategy` 抽象基类 |
| 2.2 | 实现策略管理器(`engine/manager.py`),支持策略注册和生命周期 | 策略热加载、启动/停止控制 | | 2.2 | 实现策略管理器(`engine/manager.py`),支持策略注册和生命周期 | 策略热加载、启动/停止控制 |
| 2.3 | 实现信号分发器(`engine/signals.py`) | 事件总线,策略到执行器的信号传递 | | 2.3 | 实现信号分发器(`engine/signals.py`) | 事件总线,策略到执行器的信号传递 |
| 2.4 | 实现回测引擎(`engine/backtest.py`) | 历史数据回测,收益曲线、回撤等指标 | | 2.4 | 实现回测引擎(`engine/backtest.py`) | 历史数据回测,收益曲线、回撤等指标 |
@@ -1590,7 +1579,12 @@ trade/
├── engine/ # 🐍 Python 策略引擎 ├── engine/ # 🐍 Python 策略引擎
│ ├── __init__.py │ ├── __init__.py
│ ├── base.py # BaseStrategy 基类 │ ├── env.yaml # 引擎环境配置
│ ├── common/ # 引擎通用模块
│ │ ├── __init__.py
│ │ ├── base.py # BaseStrategy 基类
│ │ ├── models.py # 数据模型(Kline/Ticker/Trade/OrderBook
│ │ └── logger.py # 结构化日志
│ ├── manager.py # 策略管理器 │ ├── manager.py # 策略管理器
│ ├── signals.py # 信号分发器 │ ├── signals.py # 信号分发器
│ ├── backtest.py # 回测引擎 │ ├── backtest.py # 回测引擎
@@ -1632,10 +1626,9 @@ trade/
│ ├── grid_trading.py # 网格交易策略 │ ├── grid_trading.py # 网格交易策略
│ └── arbitrage.py # 套利策略(可选) │ └── arbitrage.py # 套利策略(可选)
├── common/ # 🐍 Python 公共工具模块 ├── common/ # 🐍 Python 公共基础设施(跨模块共享配置、工具等)
│ ├── __init__.py │ ├── __init__.py
│ ├── logger.py # 日志配置 │ ├── config.py # 全局配置
│ ├── models.py # 数据模型(Pydantic,与 TS types 对应)
│ ├── constants.py # 常量定义 │ ├── constants.py # 常量定义
│ └── utils.py # 工具函数 │ └── utils.py # 工具函数
@@ -1662,7 +1655,7 @@ trade/
``` ```
┌──────────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────────┐ ┌──────────────┐ ┌─────────────┐
│ TS 数据模块 │ │ PostgreSQL │ │ Grafana │ │ TS 数据模块 │ │ PostgreSQL │ │ Grafana │
│ (Node.js 进程) │ │ (业务数据) │ │ (可视化) │ │ (Bun 进程) │ │ (业务数据) │ │ (可视化) │
└──────┬───────────┘ └──────────────┘ └──────┬──────┘ └──────┬───────────┘ └──────────────┘ └──────┬──────┘
│ │ │ │
│ ┌──────────────┐ │ │ ┌──────────────┐ │
+26 -5
View File
@@ -11,7 +11,7 @@
// const ds = new DataSource({ ...pgsql }); // const ds = new DataSource({ ...pgsql });
// const redisClient = new Redis(redis.url); // const redisClient = new Redis(redis.url);
// //
// 配置文件位置:<project_root>/env.yaml // 配置文件位置:data/env.yaml(软链接 → 项目根目录 env.yaml)
// TypeScript / Python 模块共享同一份配置。 // TypeScript / Python 模块共享同一份配置。
// ============================================================ // ============================================================
@@ -39,12 +39,14 @@ function getProjectRoot(): string {
} }
/** /**
* 项目根目录读取 env.yaml 并解析为原始对象。 * 读取 data/env.yaml(软链接指向项目根目录 env.yaml并解析为原始对象。
* 文件不存在时抛出明确错误,不做静默降级。 * 文件不存在时抛出明确错误,不做静默降级。
*/ */
function loadYamlConfig(): unknown { function loadYamlConfig(): unknown {
const root = getProjectRoot(); const __filename = fileURLToPath(import.meta.url);
const yamlPath = resolve(root, "env.yaml"); const __dirname = dirname(__filename);
// config/index.ts → config/ → data/env.yaml
const yamlPath = resolve(__dirname, "../env.yaml");
let content: string; let content: string;
try { try {
@@ -52,7 +54,7 @@ function loadYamlConfig(): unknown {
} catch { } catch {
throw new Error( throw new Error(
`[config] 无法读取配置文件: ${yamlPath}\n` + `[config] 无法读取配置文件: ${yamlPath}\n` +
`请确保项目根目录存在 env.yaml(可参考 data/.env.example 的结构)`, `请确保 data/env.yaml 软链接指向项目根目录的 env.yaml`,
); );
} }
@@ -131,6 +133,19 @@ export const logging = {
pretty: rawConfig.logging.node_env === "development", pretty: rawConfig.logging.node_env === "development",
} as const; } as const;
/**
* 交易所 API 密钥配置
*
* 按交易所 ID 索引,目前仅 binance。
* 新增交易所时在 env.yaml 的 exchange 段添加对应子配置即可。
*/
export const exchange = {
binance: {
apiKey: rawConfig.exchange.binance.api_key,
apiSecret: rawConfig.exchange.binance.api_secret,
},
} as const;
// ============================================================ // ============================================================
// 4. 工具:运行时打印配置概要(不含敏感信息) // 4. 工具:运行时打印配置概要(不含敏感信息)
// ============================================================ // ============================================================
@@ -150,6 +165,12 @@ export function printConfigSummary(): void {
url: redis.url.replace(/\/\/.*@/, "//***@"), // 隐藏密码 url: redis.url.replace(/\/\/.*@/, "//***@"), // 隐藏密码
publishEnabled: redis.publishEnabled, publishEnabled: redis.publishEnabled,
}, },
exchange: {
binance: {
apiKey: exchange.binance.apiKey.slice(0, 6) + "***",
apiSecret: "***",
},
},
logging: { logging: {
level: logging.level, level: logging.level,
nodeEnv: logging.nodeEnv, nodeEnv: logging.nodeEnv,
+34
View File
@@ -15,6 +15,7 @@
export interface EnvConfig { export interface EnvConfig {
db: DbConfig; db: DbConfig;
redis: RedisConfig; redis: RedisConfig;
exchange: ExchangeConfig;
logging: LoggingConfig; logging: LoggingConfig;
} }
@@ -31,6 +32,18 @@ export interface RedisConfig {
publish_enabled: boolean; publish_enabled: boolean;
} }
/** 交易所 API 密钥配置(按交易所 ID 索引) */
export interface ExchangeConfig {
binance: ExchangeApiKeys;
// 未来扩展:okx、bybit 等
[exchangeId: string]: ExchangeApiKeys | undefined;
}
export interface ExchangeApiKeys {
api_key: string;
api_secret: string;
}
export interface LoggingConfig { export interface LoggingConfig {
level: "trace" | "debug" | "info" | "warn" | "error" | "fatal"; level: "trace" | "debug" | "info" | "warn" | "error" | "fatal";
node_env: "development" | "production" | "test"; node_env: "development" | "production" | "test";
@@ -77,6 +90,21 @@ export function validateConfig(raw: unknown): EnvConfig {
const redisUrl = assertString(redisObj["url"], "redis.url"); const redisUrl = assertString(redisObj["url"], "redis.url");
const redisPublishEnabled = assertBoolean(redisObj["publish_enabled"], "redis.publish_enabled"); const redisPublishEnabled = assertBoolean(redisObj["publish_enabled"], "redis.publish_enabled");
// --- exchange ---
const exchange = obj["exchange"];
if (typeof exchange !== "object" || exchange === null) {
throw new Error("[config] env.yaml 缺少 exchange 配置段");
}
const exObj = exchange as Record<string, unknown>;
const binance = exObj["binance"];
if (typeof binance !== "object" || binance === null) {
throw new Error("[config] env.yaml exchange 缺少 binance 配置");
}
const binanceObj = binance as Record<string, unknown>;
const binanceApiKey = assertString(binanceObj["api_key"], "exchange.binance.api_key");
const binanceApiSecret = assertString(binanceObj["api_secret"], "exchange.binance.api_secret");
// --- logging --- // --- logging ---
const logging = obj["logging"]; const logging = obj["logging"];
if (typeof logging !== "object" || logging === null) { if (typeof logging !== "object" || logging === null) {
@@ -99,6 +127,12 @@ export function validateConfig(raw: unknown): EnvConfig {
url: redisUrl, url: redisUrl,
publish_enabled: redisPublishEnabled, publish_enabled: redisPublishEnabled,
}, },
exchange: {
binance: {
api_key: binanceApiKey,
api_secret: binanceApiSecret,
},
},
logging: { logging: {
level: logLevel, level: logLevel,
node_env: nodeEnv, node_env: nodeEnv,
+7 -1
View File
@@ -13,12 +13,18 @@
-- - klines 基表由 02-init-tables.sql 创建为 TimescaleDB hypertable -- - klines 基表由 02-init-tables.sql 创建为 TimescaleDB hypertable
-- - 连续聚合视图由 03-continuous-aggregates.sql 创建 -- - 连续聚合视图由 03-continuous-aggregates.sql 创建
-- - TypeORM 的 synchronize:true 与 SQL 脚本互为 fallback(开发/生产双路径) -- - TypeORM 的 synchronize:true 与 SQL 脚本互为 fallback(开发/生产双路径)
-- - 本脚本为 init-db 链的第一环,负责扩展启用 -- - 本脚本为 init-db 链的第一环,负责扩展启用TimescaleDB / pg_prewarm / pg_stat_statements
-- ============================================================ -- ============================================================
-- 启用 TimescaleDB 扩展(必须最先执行) -- 启用 TimescaleDB 扩展(必须最先执行)
CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE; CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;
-- 启用 pg_prewarm 扩展(回测预热,减少首轮查询延迟)
CREATE EXTENSION IF NOT EXISTS pg_prewarm;
-- 启用 pg_stat_statements 扩展(慢查询监控,零成本)
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- 验证扩展已启用 -- 验证扩展已启用
DO $$ DO $$
BEGIN BEGIN
+1 -1
View File
@@ -100,7 +100,7 @@ CREATE TABLE IF NOT EXISTS trading_pairs (
kline_interval VARCHAR(100) NOT NULL DEFAULT '1m', kline_interval VARCHAR(100) NOT NULL DEFAULT '1m',
-- K 线合成周期列表(逗号分隔,如 "1m,5m,15m,1h,4h,1d" -- K 线合成周期列表(逗号分隔,如 "1m,5m,15m,1h,4h,1d"
kline_intervals VARCHAR(100) NOT NULL DEFAULT '1m,5m,15m,1h,4h,1d', kline_intervals VARCHAR(100) NOT NULL DEFAULT '1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,1d,1w,1mon',
-- 历史 K 线最后补全时间(UTC)。默认 Unix epoch 起始, -- 历史 K 线最后补全时间(UTC)。默认 Unix epoch 起始,
-- 新交易对从 epoch 起始时间开始全量补拉。 -- 新交易对从 epoch 起始时间开始全量补拉。
+175 -1
View File
@@ -2,7 +2,7 @@
-- 03-continuous-aggregates.sql — K 线分层连续聚合视图 -- 03-continuous-aggregates.sql — K 线分层连续聚合视图
-- ============================================================ -- ============================================================
-- 从 klines(1m)基表创建分层连续聚合物化视图链: -- 从 klines(1m)基表创建分层连续聚合物化视图链:
-- 1m → 5m → 15m → 30m → 1h → 4h → 1d → 1w -- 1m → 3m → 5m → 15m → 30m → 1h → 2h → 4h → 6h → 8h → 1d → 1w → 1mon
-- --
-- 执行前提: -- 执行前提:
-- 1. klines hypertable 已创建(由 02-init-tables.sql 创建) -- 1. klines hypertable 已创建(由 02-init-tables.sql 创建)
@@ -36,6 +36,37 @@
-- 3. 接入实时数据(模式 A 启用 policy / 模式 B 应用层触发) -- 3. 接入实时数据(模式 A 启用 policy / 模式 B 应用层触发)
-- ============================================================ -- ============================================================
-- ============================================================
-- 3m K 线(从 1m 基表聚合)
-- ============================================================
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_3m
WITH (timescaledb.continuous) AS
SELECT
time_bucket('3 minutes', time) AS time,
exchange,
symbol,
'3m'::text AS interval,
FIRST(open, time) AS open,
MAX(high) AS high,
MIN(low) AS low,
LAST(close, time) AS close,
SUM(volume) AS volume,
SUM(quote_volume) AS quote_volume,
SUM(taker_buy_base_vol) AS taker_buy_base_vol,
SUM(taker_buy_quote_vol) AS taker_buy_quote_vol,
SUM(trade_count)::integer AS trade_count
FROM klines
GROUP BY time_bucket('3 minutes', klines.time), exchange, symbol
WITH NO DATA;
-- 【模式 A 用户】取消下面注释以启用定时调度刷新
-- SELECT add_continuous_aggregate_policy('klines_3m',
-- start_offset => INTERVAL '1 day',
-- end_offset => INTERVAL '3 minutes',
-- schedule_interval => INTERVAL '3 minutes',
-- if_not_exists => TRUE
-- );
-- ============================================================ -- ============================================================
-- 5m K 线(从 1m 基表聚合) -- 5m K 线(从 1m 基表聚合)
-- ============================================================ -- ============================================================
@@ -160,6 +191,37 @@ WITH NO DATA;
-- if_not_exists => TRUE -- if_not_exists => TRUE
-- ); -- );
-- ============================================================
-- 2h K 线(从 1h 聚合,分层链)
-- ============================================================
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_2h
WITH (timescaledb.continuous) AS
SELECT
time_bucket('2 hours', time) AS time,
exchange,
symbol,
'2h'::text AS interval,
FIRST(open, time) AS open,
MAX(high) AS high,
MIN(low) AS low,
LAST(close, time) AS close,
SUM(volume) AS volume,
SUM(quote_volume) AS quote_volume,
SUM(taker_buy_base_vol) AS taker_buy_base_vol,
SUM(taker_buy_quote_vol) AS taker_buy_quote_vol,
SUM(trade_count)::integer AS trade_count
FROM klines_1h
GROUP BY time_bucket('2 hours', klines_1h.time), exchange, symbol
WITH NO DATA;
-- 【模式 A 用户】取消下面注释以启用定时调度刷新
-- SELECT add_continuous_aggregate_policy('klines_2h',
-- start_offset => INTERVAL '10 days',
-- end_offset => INTERVAL '2 hours',
-- schedule_interval => INTERVAL '2 hours',
-- if_not_exists => TRUE
-- );
-- ============================================================ -- ============================================================
-- 4h K 线(从 1h 聚合,分层链) -- 4h K 线(从 1h 聚合,分层链)
-- ============================================================ -- ============================================================
@@ -191,6 +253,68 @@ WITH NO DATA;
-- if_not_exists => TRUE -- if_not_exists => TRUE
-- ); -- );
-- ============================================================
-- 6h K 线(从 1h 聚合,分层链)
-- ============================================================
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_6h
WITH (timescaledb.continuous) AS
SELECT
time_bucket('6 hours', time) AS time,
exchange,
symbol,
'6h'::text AS interval,
FIRST(open, time) AS open,
MAX(high) AS high,
MIN(low) AS low,
LAST(close, time) AS close,
SUM(volume) AS volume,
SUM(quote_volume) AS quote_volume,
SUM(taker_buy_base_vol) AS taker_buy_base_vol,
SUM(taker_buy_quote_vol) AS taker_buy_quote_vol,
SUM(trade_count)::integer AS trade_count
FROM klines_1h
GROUP BY time_bucket('6 hours', klines_1h.time), exchange, symbol
WITH NO DATA;
-- 【模式 A 用户】取消下面注释以启用定时调度刷新
-- SELECT add_continuous_aggregate_policy('klines_6h',
-- start_offset => INTERVAL '20 days',
-- end_offset => INTERVAL '6 hours',
-- schedule_interval => INTERVAL '6 hours',
-- if_not_exists => TRUE
-- );
-- ============================================================
-- 8h K 线(从 4h 聚合,分层链)
-- ============================================================
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_8h
WITH (timescaledb.continuous) AS
SELECT
time_bucket('8 hours', time) AS time,
exchange,
symbol,
'8h'::text AS interval,
FIRST(open, time) AS open,
MAX(high) AS high,
MIN(low) AS low,
LAST(close, time) AS close,
SUM(volume) AS volume,
SUM(quote_volume) AS quote_volume,
SUM(taker_buy_base_vol) AS taker_buy_base_vol,
SUM(taker_buy_quote_vol) AS taker_buy_quote_vol,
SUM(trade_count)::integer AS trade_count
FROM klines_4h
GROUP BY time_bucket('8 hours', klines_4h.time), exchange, symbol
WITH NO DATA;
-- 【模式 A 用户】取消下面注释以启用定时调度刷新
-- SELECT add_continuous_aggregate_policy('klines_8h',
-- start_offset => INTERVAL '30 days',
-- end_offset => INTERVAL '8 hours',
-- schedule_interval => INTERVAL '8 hours',
-- if_not_exists => TRUE
-- );
-- ============================================================ -- ============================================================
-- 1d K 线(从 4h 聚合,分层链) -- 1d K 线(从 4h 聚合,分层链)
-- ============================================================ -- ============================================================
@@ -253,25 +377,75 @@ WITH NO DATA;
-- if_not_exists => TRUE -- if_not_exists => TRUE
-- ); -- );
-- ============================================================
-- 1mon K 线(从 1d 聚合,分层链)
-- ============================================================
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_1mon
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 month', time) AS time,
exchange,
symbol,
'1mon'::text AS interval,
FIRST(open, time) AS open,
MAX(high) AS high,
MIN(low) AS low,
LAST(close, time) AS close,
SUM(volume) AS volume,
SUM(quote_volume) AS quote_volume,
SUM(taker_buy_base_vol) AS taker_buy_base_vol,
SUM(taker_buy_quote_vol) AS taker_buy_quote_vol,
SUM(trade_count)::integer AS trade_count
FROM klines_1d
GROUP BY time_bucket('1 month', klines_1d.time), exchange, symbol
WITH NO DATA;
-- 【模式 A 用户】取消下面注释以启用定时调度刷新
-- SELECT add_continuous_aggregate_policy('klines_1mon',
-- start_offset => INTERVAL '365 days',
-- end_offset => INTERVAL '1 day',
-- schedule_interval => INTERVAL '1 day',
-- if_not_exists => TRUE
-- );
-- ============================================================ -- ============================================================
-- 推荐索引:加速按 symbol + time 的查询 -- 推荐索引:加速按 symbol + time 的查询
-- ============================================================ -- ============================================================
CREATE INDEX IF NOT EXISTS idx_klines_3m_symbol_time ON klines_3m (exchange, symbol, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_5m_symbol_time ON klines_5m (exchange, symbol, time DESC); CREATE INDEX IF NOT EXISTS idx_klines_5m_symbol_time ON klines_5m (exchange, symbol, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_15m_symbol_time ON klines_15m (exchange, symbol, time DESC); CREATE INDEX IF NOT EXISTS idx_klines_15m_symbol_time ON klines_15m (exchange, symbol, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_30m_symbol_time ON klines_30m (exchange, symbol, time DESC); CREATE INDEX IF NOT EXISTS idx_klines_30m_symbol_time ON klines_30m (exchange, symbol, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_1h_symbol_time ON klines_1h (exchange, symbol, time DESC); CREATE INDEX IF NOT EXISTS idx_klines_1h_symbol_time ON klines_1h (exchange, symbol, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_2h_symbol_time ON klines_2h (exchange, symbol, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_4h_symbol_time ON klines_4h (exchange, symbol, time DESC); CREATE INDEX IF NOT EXISTS idx_klines_4h_symbol_time ON klines_4h (exchange, symbol, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_6h_symbol_time ON klines_6h (exchange, symbol, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_8h_symbol_time ON klines_8h (exchange, symbol, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_1d_symbol_time ON klines_1d (exchange, symbol, time DESC); CREATE INDEX IF NOT EXISTS idx_klines_1d_symbol_time ON klines_1d (exchange, symbol, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_1w_symbol_time ON klines_1w (exchange, symbol, time DESC); CREATE INDEX IF NOT EXISTS idx_klines_1w_symbol_time ON klines_1w (exchange, symbol, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_1mon_symbol_time ON klines_1mon (exchange, symbol, time DESC);
-- ============================================================
-- 截面查询索引:加速同一时间点多品种回测查询
-- 查询模式:WHERE exchange='binance' AND time='2024-01-01' AND symbol IN (…)
-- 回测中跨品种截面查询最常见于日线和周线,因此只在这两层建额外索引。
-- 如需其他周期(如 1h、4h)的截面查询,按同样模式扩展。
-- ============================================================
CREATE INDEX IF NOT EXISTS idx_klines_1d_exchange_time_symbol ON klines_1d (exchange, time DESC, symbol);
CREATE INDEX IF NOT EXISTS idx_klines_1w_exchange_time_symbol ON klines_1w (exchange, time DESC, symbol);
-- ============================================================ -- ============================================================
-- 首次创建后手动刷新所有视图(填充历史数据) -- 首次创建后手动刷新所有视图(填充历史数据)
-- 取消注释以下行执行: -- 取消注释以下行执行:
-- ============================================================ -- ============================================================
-- CALL refresh_continuous_aggregate('klines_3m', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_5m', NULL, NULL); -- CALL refresh_continuous_aggregate('klines_5m', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_15m', NULL, NULL); -- CALL refresh_continuous_aggregate('klines_15m', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_30m', NULL, NULL); -- CALL refresh_continuous_aggregate('klines_30m', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_1h', NULL, NULL); -- CALL refresh_continuous_aggregate('klines_1h', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_2h', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_4h', NULL, NULL); -- CALL refresh_continuous_aggregate('klines_4h', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_6h', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_8h', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_1d', NULL, NULL); -- CALL refresh_continuous_aggregate('klines_1d', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_1w', NULL, NULL); -- CALL refresh_continuous_aggregate('klines_1w', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_1mon', NULL, NULL);
+1
View File
@@ -0,0 +1 @@
../env.yaml
+8 -2
View File
@@ -1,19 +1,25 @@
import { MainClient, type Kline as BinanceRestKline } from "binance"; import { MainClient, type Kline as BinanceRestKline } from "binance";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { exchange } from "../config";
import { BaseRestClient } from './base_rest'; import { BaseRestClient } from './base_rest';
import type { KlineInterval, Kline, MarketInfo } from '../types'; import type { KlineInterval, Kline, MarketInfo } from '../types';
/** K 线周期 → 毫秒数映射(用于时间桶计算) */ /** K 线周期 → 毫秒数映射(用于时间桶计算) */
export const KLINE_INTERVAL_MS: Record<KlineInterval, number> = { export const KLINE_INTERVAL_MS: Record<KlineInterval, number> = {
"1m": 60_000, "1m": 60_000,
"3m": 180_000,
"5m": 300_000, "5m": 300_000,
"15m": 900_000, "15m": 900_000,
"30m": 1_800_000, "30m": 1_800_000,
"1h": 3_600_000, "1h": 3_600_000,
"2h": 7_200_000,
"4h": 14_400_000, "4h": 14_400_000,
"6h": 21_600_000,
"8h": 28_800_000,
"1d": 86_400_000, "1d": 86_400_000,
"1w": 604_800_000, "1w": 604_800_000,
"1mon": 2_592_000_000,
}; };
// ============================================================ // ============================================================
@@ -102,8 +108,8 @@ async function fetchBinanceKlines(
limit = 500, limit = 500,
): Promise<Kline[]> { ): Promise<Kline[]> {
const client = new MainClient({ const client = new MainClient({
api_key: 'ONSJKIGRpDYLn6FdV17aAKfjclZ4I2LzamflhuMpsoRQA427lLKeyJlGtg2RZ7DH', api_key: exchange.binance.apiKey,
api_secret: '5Mfv4TgvDlRzCHbtl2nJL4mVHUvMm8pyjKiRjMoosBMxrhlqMw6CuQbg2qbS2Npd', api_secret: exchange.binance.apiSecret,
}, { }, {
timeout: 3000, timeout: 3000,
}); });
+24 -12
View File
@@ -3,8 +3,8 @@
// ============================================================ // ============================================================
// 用途: // 用途:
// 按月份粒度逐月刷新 klines 分层连续聚合物化视图链: // 按月份粒度逐月刷新 klines 分层连续聚合物化视图链:
// 5m → 15m → 30m → 1h → 4h → 1d → 1w // 基表 1m → 3m / 5m → 15m → 30m → 1h → 2h / 4h / 6h → 8h / 1d → 1w / 1mon
// 每层依赖下一层的数据,因此严格按从低到高顺序刷新。 // 严格按依赖顺序从低到高刷新。
// //
// 使用方式: // 使用方式:
// # 仅生成 SQL 不执行(dry-run,默认) // # 仅生成 SQL 不执行(dry-run,默认)
@@ -36,29 +36,41 @@ import { logger } from "../utils/logger";
/** /**
* 分层聚合视图链(按依赖顺序:低层级 → 高层级) * 分层聚合视图链(按依赖顺序:低层级 → 高层级)
* *
* 刷新顺序至关重要 * 实际依赖关系(来自于 03-continuous-aggregates.sql
* klines_5m 源数据来自 klines1m 基表) * 基表 1m
* klines_15m 源数据来自 klines_5m * ├── 3m (直接聚合 1m
* klines_30m 源数据来自 klines_15m * └── 5m (直接聚合 1m)→ 15m → 30m → 1h ──→ 2h
* klines_1h 源数据来自 klines_30m * ├── 4h ──→ 8h
* klines_4h 源数据来自 klines_1h * │ └── 1d ──→ 1w
* klines_1d 源数据来自 klines_4h * │ └── 1mon
* klines_1w 源数据来自 klines_1d * └── 6h
* *
* 必须严格按此顺序刷新,否则高层级聚合会缺少数据。 * 刷新时必须严格按此顺序,因为:
* - klines_3m / klines_5m 无聚合依赖,可最先刷新
* - klines_15m 依赖 klines_5m
* - klines_30m 依赖 klines_15m
* - klines_1h 依赖 klines_30m
* - klines_2h / klines_4h / klines_6h 依赖 klines_1h
* - klines_8h / klines_1d 依赖 klines_4h
* - klines_1w / klines_1mon 依赖 klines_1d
*/ */
const AGGREGATE_VIEWS = [ const AGGREGATE_VIEWS = [
"klines_3m",
"klines_5m", "klines_5m",
"klines_15m", "klines_15m",
"klines_30m", "klines_30m",
"klines_1h", "klines_1h",
"klines_2h",
"klines_4h", "klines_4h",
"klines_6h",
"klines_8h",
"klines_1d", "klines_1d",
"klines_1w", "klines_1w",
"klines_1mon",
] as const; ] as const;
/** 默认起始年月 */ /** 默认起始年月 */
const DEFAULT_START = { year: 2017, month: 8 }; // 2018-09 const DEFAULT_START = { year: 2017, month: 5 }; // 2018-09
/** 默认结束年月 */ /** 默认结束年月 */
const DEFAULT_END = { year: 2026, month: 6 }; // 2019-01 const DEFAULT_END = { year: 2026, month: 6 }; // 2019-01
+9
View File
@@ -42,3 +42,12 @@ for (const pair of allPairs) {
console.error("拉取失败:", err); console.error("拉取失败:", err);
} }
} }
// 所有币种回补完成以后等待1秒关闭pgsql连接退出此进程
await new Promise((resolve) => setTimeout(resolve, 1000));
const { AppDataSource } = await import("../db/data-source");
if (AppDataSource.isInitialized) {
await AppDataSource.destroy();
console.log("pgsql 连接已关闭");
}
process.exit(0);
+28
View File
@@ -0,0 +1,28 @@
import { Client } from "../exchanges/rest";
import type { Kline, KlineInterval } from "../types";
const client = new Client("binance");
/**
* 获取 Binance K 线数据(基于 MainClient REST API)。
*
* 内部复用 Client(多交易所 REST 客户端)的 binance 实现,
* 包含限流、Binance SDK 原生转换、连续性过滤等逻辑。
* 返回本系统标准化 {@link Kline} 数组。
*
* @param symbol - 交易对符号(如 "BTCUSDT"
* @param interval - K 线周期(如 "1h"、"4h"、"1d"
* @param startTime - 起始时间(Unix ms
* @param endTime - 结束时间(Unix ms),可选;不传则拉取到最新
* @param limit - 单次拉取条数,默认 500(最大 1000)
*/
export async function fetchKlines(
symbol: string,
interval: KlineInterval,
startTime: number,
endTime?: number,
limit = 500,
): Promise<Kline[]> {
// Client.fetchKlines 参数顺序:symbol, interval, startTime, limit, endTime
return client.fetchKlines(symbol, interval, startTime, limit, endTime);
}
+6 -1
View File
@@ -1,10 +1,15 @@
/** K 线周期枚举 */ /** K 线周期枚举 */
export type KlineInterval = export type KlineInterval =
| "1m" | "1m"
| "3m"
| "5m" | "5m"
| "15m" | "15m"
| "30m" | "30m"
| "1h" | "1h"
| "2h"
| "4h" | "4h"
| "6h"
| "8h"
| "1d" | "1d"
| "1w"; | "1w"
| "1mon";
+184
View File
@@ -0,0 +1,184 @@
# engine/backtest — 回测引擎
事件驱动的历史回测框架,基于 `DataService` 从 TimescaleDB 读取历史 K 线,
按时间顺序逐根推送给策略,模拟订单成交、跟踪资金曲线、计算绩效指标。
## 快速开始
```python
import asyncio
from datetime import datetime
from engine.backtest import BacktestEngine, BacktestConfig
from engine.common.config import config
async def main():
bt_config = BacktestConfig(
symbol="BTCUSDT",
interval="1h",
start_time=datetime(2025, 1, 1),
end_time=datetime(2025, 6, 1),
initial_capital=10_000.0,
)
engine = BacktestEngine(bt_config, db_config=config.db)
result = await engine.run(MyStrategy, my_strategy_config)
print(result.summary())
asyncio.run(main())
```
## 核心概念
### 回测流程
```
加载历史 K 线 → 预热阶段 → 主循环 → 计算指标 → 输出结果
逐根 K 线推送:
1. 执行上根 Bar 产生的买单(在开盘价执行)
2. 推送 K 线给策略 → 产生信号
3. 卖出信号立即执行,买入信号延迟到下一根 Bar
4. 记录资金曲线
```
### 避免未来函数
- **买入信号**:在当前 K 线收盘生成 → **下一根 K 线开盘价**执行
- **卖出信号**:在当前 K 线收盘生成 → **当前 K 线收盘价**执行
这样可以避免使用已知收盘价来获利的偏差。
### 交易成本
引擎模拟以下交易成本:
| 成本项 | 默认值 | 说明 |
|--------|--------|------|
| 手续费 | 0.1% | 按成交额收取 |
| 滑点 | 0.05% | 买卖双向滑点 |
### 绩效指标
回测完成后自动计算以下指标:
| 指标 | 说明 |
|------|------|
| 总收益率 | (最终权益 - 初始资金) / 初始资金 × 100% |
| 年化收益率 | 以复利方式年化 |
| 夏普比率 | (日均收益 / 日收益标准差) × √365 |
| 最大回撤 | 权益从峰值下跌的最大百分比 |
| 回撤持续天数 | 从峰值到恢复(或结束)的最长天数 |
| 胜率 | 盈利交易 / 总交易 |
| 盈亏比 | 总盈利 / 总亏损绝对值 |
| 卡尔玛比率 | 年化收益 / 最大回撤绝对值 |
## API 参考
### BacktestConfig
```python
@dataclass
class BacktestConfig:
symbol: str # 交易对
exchange: str = "binance" # 交易所
interval: str = "1h" # K 线周期
start_time: datetime | None = None # 起始时间
end_time: datetime | None = None # 结束时间
commission_pct: float = 0.001 # 手续费率
slippage_pct: float = 0.0005 # 滑点率
min_order_qty: float = 0.001 # 最小下单量
initial_capital: float = 10_000.0 # 初始资金
warmup_bars: int = 100 # 预热条数
```
### BacktestEngine
```python
class BacktestEngine:
def __init__(self, config: BacktestConfig, db_config=None)
async def run(
self,
strategy_cls: Type[BaseStrategy],
strategy_config: StrategyConfig,
) -> BacktestResult
```
### BacktestResult
```python
@dataclass
class BacktestResult:
config: BacktestConfig # 回测配置
strategy_config: dict # 策略配置
metrics: BacktestMetrics # 绩效指标
trades: list[BacktestTrade] # 交易记录
equity_curve: list[dict] # 资金曲线
def summary(self) -> str # 人类可读摘要
```
### 编写策略
策略必须继承 `BaseStrategy`,实现 `on_kline()` 方法:
```python
from engine.common.base import BaseStrategy, Signal, StrategyConfig
from engine.common.models import Kline
class MyConfig(StrategyConfig):
param1: int = 10
class MyStrategy(BaseStrategy):
strategy_type = "my_strategy"
def __init__(self, config: MyConfig):
super().__init__(config)
self._closes = []
async def on_kline(self, kline: Kline) -> Signal | None:
self._closes.append(kline.close)
# 策略逻辑 ...
if 买入条件:
return Signal(
symbol=self.config.symbol,
side="BUY",
signal_type="MARKET",
reason="...",
timestamp=kline.open_time,
)
return None
```
### 技术指标库
`engine/indicators/` 提供常用的技术指标计算函数,纯 Python 实现,无外部依赖:
```python
from engine.indicators import sma, ema, macd, rsi, bollinger, atr, obv, vwap
closes = [100.0, 101.0, 102.0, ...]
ma = sma(closes, period=20) # 简单移动平均
ema_vals = ema(closes, period=12) # 指数移动平均
rsi_vals = rsi(closes, period=14) # RSI [0, 100]
upper, mid, lower = bollinger(closes, period=20, std=2) # 布林带
macd_line, signal, hist = macd(closes, fast=12, slow=26, signal=9) # MACD
atr_vals = atr(highs, lows, closes, period=14) # ATR
```
| 模块 | 指标 | 函数 |
|------|------|------|
| `trend` | 趋势 | `sma`, `ema`, `macd`, `macd_signal`, `macd_histogram` |
| `momentum` | 动量 | `rsi`, `stoch`, `stoch_k`, `stoch_d` |
| `volatility` | 波动率 | `bollinger`, `bollinger_upper`, `bollinger_mid`, `bollinger_lower`, `atr` |
| `volume` | 成交量 | `obv`, `vwap` |
所有函数返回与输入等长的 `list[float]`,不足周期的位置填充为 `0.0`
## 运行示例
```bash
cd engine
source .venv/bin/activate
python example/backtest_demo.py
```
+178
View File
@@ -0,0 +1,178 @@
# 牛熊自适应趋势跟踪策略
## 概述
通过识别市场所处的牛熊状态,自适应地选择做多或做空方向,在震荡市中空仓等待。
核心思想:**牛市不逆势做空,熊市不逆势做多。**
---
## 市场状态判定(3 法投票)
每根 4h K 线收盘后,用以下三种方法独立判定当前市场状态:
### 方法 1EMA200 斜率
```
计算:EMA200 近 20 根 K 线的变化率
判定:斜率 > +0.2% → 牛
斜率 < -0.2% → 熊
其他 → 震荡
```
EMA200 向上倾斜说明长期趋势向上,向下倾斜说明长期趋势向下。
### 方法 2:价格 vs EMA200
```
判定:当前收盘价 > EMA200 → 牛
当前收盘价 < EMA200 → 熊
```
最直接的趋势判定——价格在年线上方就是多头市场。
### 方法 3ATH 回撤
```
追踪历史最高价 (ATH)
计算:(当前价 - ATH) / ATH
判定:回撤 > -15%(距高点不到15%)→ 牛
回撤 < -35%(距高点超过35%)→ 熊
回撤在 15%-35% 之间 → 震荡
```
加密市场经典规律:从高点回撤超过 35% 通常意味着熊市确认。
### 综合投票
```
三种方法独立投票,2/3 多数决:
牛票 >= 2 → 牛市 → 只做多
熊票 >= 2 → 熊市 → 只做空
其他 → 震荡 → 空仓等待
```
---
## 交易信号
使用 **EMA(10, 50) 双均线交叉** 作为入场信号:
| 方向 | 入场条件 | 出场条件 |
|------|---------|---------|
| 做多 | 牛市 + EMA10 金叉 EMA50 | EMA10 死叉 EMA50,或 ATR 止损,或状态转熊 |
| 做空 | 熊市 + EMA10 死叉 EMA50 | EMA10 金叉 EMA50,或 ATR 止损,或状态转牛 |
### ATR 动态止损
```
做多止损:入场后最高价 - 2.5 × ATR(14)
做空止损:入场后最低价 + 2.5 × ATR(14)
```
止损触发后平仓但不反手,空仓等待下一个交叉信号 + 状态确认。
---
## 参数配置
| 参数 | 值 | 说明 |
|------|----|------|
| 周期 | 4h | 交易时间级别 |
| EMA 快线 | 10 | 短期趋势 |
| EMA 慢线 | 50 | 中期趋势 |
| EMA 趋势 | 200 | 长期趋势基准 |
| ATR 周期 | 14 | 波动率计算 |
| ATR 止损倍率 | 2.5 | 止损宽度 |
| 手续费 | 0.1% | 单边 |
| 滑点 | 0.05% | 单边 |
不同币种的 EMA 快慢线参数已做优化:
| 币种 | 快线 | 慢线 |
|------|------|------|
| BTC | 10 | 50 |
| ETH | 10 | 75 |
| BNB | 20 | 50 |
| SOL | 30 | 50 |
---
## 回测结果(2017-2026 全周期,4h
| 币种 | 数据范围 | 总收益 | 年化 | 夏普 | 最大回撤 | 交易数 | 多头P&L | 空头P&L |
|------|---------|--------|------|------|---------|--------|---------|---------|
| BTC | 2017.08-2026.06 | +494% | 22.5% | 0.80 | -34.1% | 208 | +41,513 | +14,936 |
| ETH | 2017.08-2026.06 | +4,240% | 53.7% | 1.24 | -37.3% | 205 | +262,427 | +194,635 |
| BNB | 2017.11-2026.06 | +1,375% | 37.0% | 0.92 | -44.6% | 190 | +88,684 | +63,905 |
| SOL | 2020.08-2026.06 | +65% | 9.1% | 0.41 | -56.6% | 134 | +13,743 | -4,385 |
---
## 多时间级别对比(1h / 4h / 1d)
同一策略在不同 K 线周期上的表现:
| 币种 | 周期 | 总收益 | 年化 | 夏普 | 最大回撤 | 交易数 | 胜率 |
|------|------|--------|------|------|---------|--------|------|
| BTC | 1h | -78% | -15.7% | -0.35 | -94.8% | 744 | 24.2% |
| BTC | 4h | **+494%** | **22.5%** | **0.80** | -34.1% | 208 | 36.5% |
| BTC | 1d | **+660%** | **26.8%** | **0.99** | **-30.5%** | **28** | 42.9% |
| ETH | 1h | -65% | -10.9% | -0.07 | -88.8% | 755 | 28.4% |
| ETH | 4h | **+4,240%** | **53.7%** | **1.24** | -37.3% | 205 | 40.5% |
| ETH | 1d | +692% | 27.4% | 0.87 | -59.5% | 25 | 44.0% |
| BNB | 4h | **+1,375%** | **37.0%** | **0.92** | -44.6% | 190 | 37.4% |
| BNB | 1d | +914% | 32.1% | 0.80 | -51.5% | 26 | 38.5% |
| SOL | 4h | +65% | 9.1% | 0.41 | -56.6% | 134 | 35.1% |
| SOL | 1d | **+454%** | **36.0%** | **0.92** | -43.2% | **20** | 35.0% |
### 各币种最佳周期
| 币种 | 最佳周期 | 收益 | 夏普 | 原因 |
|------|---------|------|------|------|
| BTC | **1d** | +660% | 0.99 | 大盘稳定,日线信号最干净 |
| ETH | **4h** | +4,240% | 1.24 | 趋势转换快,4h 反应速度最优 |
| BNB | **4h** | +1,375% | 0.92 | 弹性大,需要 4h 捕捉波动 |
| SOL | **1d** | +454% | 0.92 | 波动剧烈,日线过滤噪音最有效 |
### 周期选择规律
```
高波动币种 ──→ 短周期(4h)──→ 捕捉快节奏趋势(ETH、BNB)
低波动币种 ──→ 长周期(1d)──→ 过滤噪音假信号(BTC、SOL)
1h 对所有币种均不可用 ──→ 744-755 笔交易,摩擦成本吃掉一切
```
### BTC 逐年表现
| 年份 | 市场性质 | 收益率 | 夏普比率 |
|------|---------|--------|---------|
| 2017 | 牛市 | +38.2% | 2.59 |
| 2018 | 熊市 | -13.8% | -0.56 |
| 2019 | 反弹 | +72.6% | 1.93 |
| 2020 | 牛初 | +72.4% | 1.43 |
| 2021 | 牛市 | +10.7% | 0.48 |
| 2022 | 熊市 | +2.0% | 0.23 |
| 2023 | 震荡 | +2.3% | 0.22 |
| 2024-25 | 牛市 | +75.4% | 1.28 |
8 年中 7 年盈利,熊市不亏钱,牛市吃足利润。
---
## 设计要点
1. **跨周期一致性**:所有信号在同一 4h 周期上,不跨级,避免多时间框架的延迟叠加
2. **状态优先于信号**:先判断能做哪个方向,再在该方向上找入场点
3. **止损不反手**:止损后空仓等下一个信号,避免震荡市中反复被止损
4. **少即是多**:3 种判定方法刚好覆盖趋势方向、当前价格位置、极端状态三个不冗余的维度
---
## 代码位置
- 策略实现:`engine/example/regime_all.py`
- 多空引擎:`engine/example/long_short.py`LongShortEngine
+84
View File
@@ -0,0 +1,84 @@
# 牛熊自适应策略 — 多时间级别回测对比
> 生成时间:2026-06-12 09:37
## 一、全量数据(所有可用历史)
### 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 |
### 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 |
## 二、近一年(2025.06 — 2026.06
### 4h 周期
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L |
|------|------|------|--------|------|------|------|------|------|------|---------|---------|
| BTCUSDT | 4h | 近1年 | -5.2% | -5.7% | -0.30 | -17.6% | 21 | 28.6% | 0.88 | -326 | +15 |
| ETHUSDT | 4h | 近1年 | +6.8% | +7.5% | 0.40 | -20.9% | 19 | 42.1% | 1.26 | +2704 | -1800 |
| BNBUSDT | 4h | 近1年 | +15.0% | +16.5% | 0.72 | -25.6% | 20 | 35.0% | 1.48 | +2375 | -640 |
| SOLUSDT | 4h | 近1年 | -21.6% | -23.3% | -0.94 | -32.8% | 15 | 26.7% | 0.47 | -205 | -1815 |
### 1d 周期
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L |
|------|------|------|--------|------|------|------|------|------|------|---------|---------|
| BTCUSDT | 1d | 近1年 | +6.4% | +19.9% | 0.73 | -11.8% | 1 | 100.0% | 650.80 | +651 | +0 |
| ETHUSDT | 1d | 近1年 | -0.1% | -0.4% | 0.18 | -15.7% | 1 | 0.0% | 0.00 | -3 | +0 |
| BNBUSDT | 1d | 近1年 | +1.6% | +4.8% | 1.28 | -1.2% | 1 | 100.0% | 277.22 | +0 | +277 |
| SOLUSDT | 1d | 近1年 | +25.5% | +94.3% | 3.41 | -4.1% | 1 | 100.0% | 2752.24 | +0 | +2752 |
---
## 三、全维度汇总
| 币种 | 周期 | 范围 | 总收益% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 |
|------|------|------|--------|------|------|------|------|------|
| BNBUSDT | 1d | 全量 | +63.5% | 0.36 | -40.7% | 18 | 38.9% | 1.53 |
| BNBUSDT | 4h | 全量 | +3282.2% | 1.15 | -38.0% | 150 | 36.7% | 1.60 |
| BNBUSDT | 1d | 近1年 | +1.6% | 1.28 | -1.2% | 1 | 100.0% | 277.22 |
| BNBUSDT | 4h | 近1年 | +15.0% | 0.72 | -25.6% | 20 | 35.0% | 1.48 |
| BTCUSDT | 1d | 全量 | +689.4% | 1.07 | -30.5% | 28 | 46.4% | 2.88 |
| BTCUSDT | 4h | 全量 | +493.5% | 0.80 | -34.1% | 208 | 36.5% | 1.53 |
| BTCUSDT | 1d | 近1年 | +6.4% | 0.73 | -11.8% | 1 | 100.0% | 650.80 |
| BTCUSDT | 4h | 近1年 | -5.2% | -0.30 | -17.6% | 21 | 28.6% | 0.88 |
| ETHUSDT | 1d | 全量 | +216.7% | 0.69 | -39.2% | 20 | 40.0% | 2.64 |
| ETHUSDT | 4h | 全量 | +831.8% | 0.88 | -48.9% | 162 | 38.9% | 1.57 |
| ETHUSDT | 1d | 近1年 | -0.1% | 0.18 | -15.7% | 1 | 0.0% | 0.00 |
| ETHUSDT | 4h | 近1年 | +6.8% | 0.40 | -20.9% | 19 | 42.1% | 1.26 |
| SOLUSDT | 1d | 全量 | +247.1% | 0.90 | -60.5% | 15 | 33.3% | 1.64 |
| SOLUSDT | 4h | 全量 | +34.9% | 0.32 | -59.6% | 78 | 33.3% | 1.13 |
| SOLUSDT | 1d | 近1年 | +25.5% | 3.41 | -4.1% | 1 | 100.0% | 2752.24 |
| SOLUSDT | 4h | 近1年 | -21.6% | -0.94 | -32.8% | 15 | 26.7% | 0.47 |
## 四、各币种最佳组合(按夏普排序)
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% |
|------|------|------|--------|------|------|------|------|------|
| BTCUSDT | **1d** | 全量 | +689.4% | +28.9% | 1.07 | -30.5% | 28 | 46.4% |
| ETHUSDT | **4h** | 全量 | +831.8% | +29.2% | 0.88 | -48.9% | 162 | 38.9% |
| BNBUSDT | **1d** | 近1年 | +1.6% | +4.8% | 1.28 | -1.2% | 1 | 100.0% |
| SOLUSDT | **1d** | 近1年 | +25.5% | +94.3% | 3.41 | -4.1% | 1 | 100.0% |
---
## 五、结论
- **4h vs 1d**:低波动大市值币种(BTC)偏向日线(1d),高波动币种(ETH/BNB)偏向4h
- **全量 vs 近一年**:近一年市场环境与长周期统计可能有显著差异,需结合当前市场结构选择周期
- **交易频率**1d 交易数约为 4h 的 1/5 ~ 1/10,适合低频策略
@@ -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,适合低频策略
@@ -0,0 +1,84 @@
# 牛熊自适应策略 — 多时间级别回测对比
> 生成时间:2026-06-12 09:42
## 一、全量数据(所有可用历史)
### 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 |
### 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
### 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 |
### 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 | 4h | 全量 | +3282.2% | 1.15 | -38.0% | 150 | 36.7% | 1.60 |
| BNBUSDT | 1d | 近2年 | -8.0% | -1.04 | -9.6% | 3 | 33.3% | 0.27 |
| BNBUSDT | 4h | 近2年 | -4.2% | -0.00 | -25.6% | 35 | 31.4% | 0.98 |
| BTCUSDT | 1d | 全量 | +689.4% | 1.07 | -30.5% | 28 | 46.4% | 2.88 |
| BTCUSDT | 4h | 全量 | +493.5% | 0.80 | -34.1% | 208 | 36.5% | 1.53 |
| BTCUSDT | 1d | 近2年 | +42.8% | 1.40 | -15.4% | 5 | 60.0% | 5.38 |
| BTCUSDT | 4h | 近2年 | +28.5% | 0.70 | -17.6% | 49 | 40.8% | 1.50 |
| ETHUSDT | 1d | 全量 | +216.7% | 0.69 | -39.2% | 20 | 40.0% | 2.64 |
| ETHUSDT | 4h | 全量 | +831.8% | 0.88 | -48.9% | 162 | 38.9% | 1.57 |
| ETHUSDT | 1d | 近2年 | +57.4% | 1.76 | -7.6% | 2 | 100.0% | 5903.01 |
| ETHUSDT | 4h | 近2年 | +13.1% | 0.38 | -20.9% | 38 | 42.1% | 1.26 |
| SOLUSDT | 1d | 全量 | +247.1% | 0.90 | -60.5% | 15 | 33.3% | 1.64 |
| SOLUSDT | 4h | 全量 | +34.9% | 0.32 | -59.6% | 78 | 33.3% | 1.13 |
| SOLUSDT | 1d | 近2年 | +22.0% | 1.08 | -12.0% | 2 | 50.0% | 9.85 |
| SOLUSDT | 4h | 近2年 | +0.8% | 0.20 | -39.7% | 21 | 23.8% | 1.04 |
## 四、各币种最佳组合(按夏普排序)
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% |
|------|------|------|--------|------|------|------|------|------|
| 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,适合低频策略
+21
View File
@@ -0,0 +1,21 @@
"""
回测引擎模块
提供事件驱动的历史回测能力:
- BacktestEngine — 核心回测引擎
- BacktestConfig — 回测配置
- BacktestTrade — 交易记录
- BacktestMetrics — 绩效指标
- BacktestResult — 完整回测结果
"""
from .engine import BacktestEngine
from .models import BacktestConfig, BacktestMetrics, BacktestResult, BacktestTrade
__all__ = [
"BacktestEngine",
"BacktestConfig",
"BacktestTrade",
"BacktestMetrics",
"BacktestResult",
]
+478
View File
@@ -0,0 +1,478 @@
"""
回测引擎核心 — 事件驱动的历史回测
逐根 K 线推送给策略,模拟订单成交,跟踪资金曲线,计算绩效指标。
用法:
from engine.backtest import BacktestEngine, BacktestConfig
from engine.common.config import config
bt_config = BacktestConfig(
symbol="BTCUSDT",
interval="1h",
start_time=datetime(2025, 1, 1),
end_time=datetime(2025, 6, 1),
initial_capital=10000.0,
)
engine = BacktestEngine(bt_config, db_config=config.db)
result = await engine.run(MyStrategy, my_strategy_config)
print(result.summary())
"""
import asyncio
from datetime import datetime, timezone
from typing import Optional, Type
from ..common.base import BaseStrategy, Signal, StrategyConfig
from ..common.models import Kline
from ..data.service import DataService
from .models import BacktestConfig, BacktestMetrics, BacktestResult, BacktestTrade
# ── 资金曲线采样间隔(用于减少内存,每隔 N 根 Bar 记录一次)──
EQUITY_SAMPLE_INTERVAL = 1 # 每根都记录
class BacktestEngine:
"""事件驱动回测引擎
按时间顺序逐根 K 线推送给策略,模拟:
- 订单成交(含手续费、滑点)
- 持仓管理与盈亏计算
- 资金曲线追踪
- 绩效指标统计
信号在 K 线收盘生成,在下一根 K 线开盘时以「开盘价」执行,
避免使用已知收盘价的未来函数偏差。
"""
def __init__(self, config: BacktestConfig, db_config=None):
"""
Args:
config: 回测配置(交易对、周期、时间范围、资金等)
db_config: 数据库连接配置(DBConfig 实例)。
如果不传,引擎将在 run() 内从 engine.common.config 自动加载。
"""
self.config = config
self._db_config = db_config
# ── 投资组合状态 ──
self._cash: float = config.initial_capital
self._position: float = 0.0
self._avg_entry_price: float = 0.0
# ── 记录 ──
self._trades: list[BacktestTrade] = []
self._equity: list[dict] = []
# ── 待执行信号(BUY 信号在下一根 Bar 开盘时执行)──
self._pending_buy: Optional[Signal] = None
# ================================================================
# 主入口
# ================================================================
async def run(
self,
strategy_cls: Type[BaseStrategy],
strategy_config: StrategyConfig,
) -> BacktestResult:
"""执行回测。
流程:
1. 连接数据库并加载历史 K 线
2. 创建策略实例并调用 on_start()
3. 预热阶段:喂 warmup_bars 根 K 线
4. 主循环:逐根 K 线推给策略 → 模拟成交 → 更新资金曲线
5. 对剩余持仓按最后一根 K 线收盘价强制平仓
6. 调用策略 on_stop()
7. 计算绩效指标
Args:
strategy_cls: 策略类(继承 BaseStrategy
strategy_config: 策略配置实例
Returns:
BacktestResult: 包含交易记录、资金曲线和绩效指标
"""
# 确保 strategy_config 与回测配置对齐
strategy_config.symbol = self.config.symbol
strategy_config.exchange = self.config.exchange
# 1. 连接数据库并加载数据
from ..common.config import config as app_config
db_cfg = self._db_config or app_config.db
ds = DataService(db_cfg)
await ds.connect()
try:
klines = await ds.fetch_klines(
symbol=self.config.symbol,
interval=self.config.interval,
start_time=self.config.start_time,
end_time=self.config.end_time,
limit=1_000_000, # 足够大的 limit,实际由 start/end 约束
)
if len(klines) < self.config.warmup_bars + 2:
raise ValueError(
f"数据不足:需要至少 {self.config.warmup_bars + 2} 根 K 线,"
f"实际只有 {len(klines)}"
)
# 2. 创建策略实例
strategy = strategy_cls(strategy_config)
await strategy.on_start()
# 重置状态
self._cash = self.config.initial_capital
self._position = 0.0
self._avg_entry_price = 0.0
self._trades = []
self._equity = []
self._pending_buy = None
# 3. 预热阶段
warmup_end = self.config.warmup_bars
for i in range(warmup_end):
await strategy.on_kline(klines[i])
# 4. 主循环
for i in range(warmup_end, len(klines)):
kline = klines[i]
# 4a. 先执行上一根 bar 产生的待执行买单
if self._pending_buy is not None:
self._execute_buy(self._pending_buy, kline)
self._pending_buy = None
# 4b. 推送 K 线给策略
signal = await strategy.on_kline(kline)
# 4c. 处理信号
if signal is not None and signal.side == "SELL":
self._execute_sell(signal, kline)
elif signal is not None and signal.side == "BUY":
# BUY 信号延迟到下一根 bar 执行,避免未来函数
self._pending_buy = signal
# LIMIT / CANCEL 信号暂不支持
# 4d. 记录资金曲线
if i % EQUITY_SAMPLE_INTERVAL == 0:
self._record_equity(kline)
# 5. 对剩余持仓按最后一根 K 线收盘价强平
if self._position > 0 and len(klines) > 0:
last_kline = klines[-1]
self._execute_sell(
Signal(
symbol=self.config.symbol,
side="SELL",
signal_type="MARKET",
quantity=self._position,
confidence=1.0,
reason="回测结束 — 强制平仓",
timestamp=last_kline.open_time,
),
last_kline,
)
# 6. 停止策略
await strategy.on_stop()
# 7. 计算指标
metrics = self._compute_metrics()
return BacktestResult(
config=self.config,
strategy_config=strategy_config.model_dump(),
metrics=metrics,
trades=self._trades,
equity_curve=self._equity,
)
finally:
await ds.close()
async def run_batch(
self,
strategy_cls: Type[BaseStrategy],
configs: list[StrategyConfig],
) -> list[BacktestResult]:
"""批量回测(并行执行多个策略配置)。
适用于参数扫描场景。
"""
tasks = [
self.run(strategy_cls, cfg)
for cfg in configs
]
return await asyncio.gather(*tasks)
# ================================================================
# 交易模拟
# ================================================================
def _execute_buy(self, signal: Signal, kline: Kline) -> None:
"""执行买入(在下一根 K 线的开盘价执行)"""
# 执行价格 = 开盘价 + 滑点
exec_price = kline.open * (1 + self.config.slippage_pct)
# 确定数量
qty = signal.quantity
if qty is None:
# 按最大仓位比例计算
max_notional = self._cash * signal.confidence
qty = max_notional / exec_price
# 取整到最小下单量
qty = self._round_qty(qty)
# 检查最小下单量
if qty < self.config.min_order_qty:
return
notional = exec_price * qty
commission = notional * self.config.commission_pct
total_cost = notional + commission
# 检查余额
if total_cost > self._cash:
# 按可用资金重新计算可买数量
max_qty = (self._cash / (exec_price * (1 + self.config.commission_pct)))
qty = self._round_qty(max_qty)
if qty < self.config.min_order_qty:
return
notional = exec_price * qty
commission = notional * self.config.commission_pct
total_cost = notional + commission
# 更新持仓
if self._position > 0:
total_value = self._avg_entry_price * self._position + notional
self._position += qty
self._avg_entry_price = total_value / self._position if self._position > 0 else 0
else:
self._position = qty
self._avg_entry_price = exec_price
self._cash -= total_cost
# 记录交易
self._trades.append(BacktestTrade(
timestamp=kline.open_time,
symbol=self.config.symbol,
side="BUY",
price=exec_price,
quantity=qty,
notional=notional,
commission=commission,
slippage=exec_price - kline.open,
reason=signal.reason,
))
def _execute_sell(self, signal: Signal, kline: Kline) -> None:
"""执行卖出(在当前 K 线的收盘价执行)"""
exec_price = kline.close * (1 - self.config.slippage_pct)
# 确定数量
qty = signal.quantity
if qty is None:
qty = self._position # 全部卖出
qty = min(qty, self._position) # 不能超卖
qty = self._round_qty(qty)
if qty < self.config.min_order_qty or self._position < self.config.min_order_qty:
return
notional = exec_price * qty
commission = notional * self.config.commission_pct
net_proceeds = notional - commission
# 计算盈亏
pnl = (exec_price - self._avg_entry_price) * qty - commission
# 更新持仓
self._position -= qty
if self._position < self.config.min_order_qty:
self._position = 0.0
self._avg_entry_price = 0.0
self._cash += net_proceeds
# 记录交易
self._trades.append(BacktestTrade(
timestamp=kline.open_time,
symbol=self.config.symbol,
side="SELL",
price=exec_price,
quantity=qty,
notional=notional,
commission=commission,
slippage=kline.close - exec_price,
pnl=pnl,
reason=signal.reason,
))
# ================================================================
# 资金曲线
# ================================================================
def _record_equity(self, kline: Kline) -> None:
"""记录当前时间点的权益和回撤"""
equity = self._cash + self._position * kline.close
# 计算回撤
if not self._equity:
self._peak_equity = equity
elif equity > self._peak_equity:
self._peak_equity = equity
drawdown = (equity - self._peak_equity) / self._peak_equity * 100 if self._peak_equity > 0 else 0.0
self._equity.append({
"timestamp": kline.open_time,
"equity": equity,
"drawdown": drawdown,
"position": self._position,
})
# ================================================================
# 绩效指标计算
# ================================================================
def _compute_metrics(self) -> BacktestMetrics:
"""从交易记录和资金曲线计算全部绩效指标"""
if not self._equity:
return BacktestMetrics()
initial_capital = self.config.initial_capital
final_equity = self._equity[-1]["equity"]
# ── 总收益率 ──
total_return_pct = (final_equity - initial_capital) / initial_capital * 100
# ── 年化收益率 ──
first_ts = self._equity[0]["timestamp"]
last_ts = self._equity[-1]["timestamp"]
days = (last_ts - first_ts) / (1000 * 86400)
if days > 0 and final_equity > 0 and initial_capital > 0:
annual_return_pct = ((final_equity / initial_capital) ** (365 / days) - 1) * 100
else:
annual_return_pct = 0.0
# ── 日收益率 → 夏普比率 ──
daily_returns = self._compute_daily_returns()
if len(daily_returns) > 1:
import statistics
mean_ret = statistics.mean(daily_returns)
std_ret = statistics.stdev(daily_returns) if len(daily_returns) > 1 else 0.0
sharpe_ratio = (mean_ret / std_ret * (365 ** 0.5)) if std_ret > 0 else 0.0
else:
sharpe_ratio = 0.0
# ── 最大回撤 & 回撤持续天数 ──
max_drawdown_pct, max_dd_days = self._compute_max_drawdown()
# ── 交易统计 ──
sells = [t for t in self._trades if t.side == "SELL" and t.pnl is not None]
total_trades = len(sells)
if total_trades > 0:
winners = [t for t in sells if t.pnl > 0]
losers = [t for t in sells if t.pnl <= 0]
win_rate = len(winners) / total_trades
gross_profit = sum(t.pnl for t in winners)
gross_loss = abs(sum(t.pnl for t in losers))
profit_factor = gross_profit / gross_loss if gross_loss > 0 else (gross_profit if gross_profit > 0 else 0.0)
avg_trade_pnl = sum(t.pnl for t in sells) / total_trades
best_trade_pnl = max(t.pnl for t in sells)
worst_trade_pnl = min(t.pnl for t in sells)
else:
win_rate = 0.0
profit_factor = 0.0
avg_trade_pnl = 0.0
best_trade_pnl = 0.0
worst_trade_pnl = 0.0
# ── 卡尔玛比率 ──
if max_drawdown_pct < 0:
calmar_ratio = annual_return_pct / abs(max_drawdown_pct)
else:
calmar_ratio = 0.0
return BacktestMetrics(
total_return_pct=total_return_pct,
annual_return_pct=annual_return_pct,
sharpe_ratio=sharpe_ratio,
max_drawdown_pct=max_drawdown_pct,
max_drawdown_duration_days=max_dd_days,
win_rate=win_rate,
profit_factor=profit_factor,
total_trades=total_trades,
avg_trade_pnl=avg_trade_pnl,
best_trade_pnl=best_trade_pnl,
worst_trade_pnl=worst_trade_pnl,
calmar_ratio=calmar_ratio,
final_equity=final_equity,
)
def _compute_daily_returns(self) -> list[float]:
"""从资金曲线提取每日收益率序列"""
if not self._equity:
return []
# 按日期分组,取每日最后一根 bar 的权益
from collections import defaultdict
daily: dict[str, float] = {}
for point in self._equity:
dt = datetime.fromtimestamp(point["timestamp"] / 1000, tz=timezone.utc)
date_key = dt.strftime("%Y-%m-%d")
daily[date_key] = point["equity"]
sorted_dates = sorted(daily.keys())
returns = []
for i in range(1, len(sorted_dates)):
prev = daily[sorted_dates[i - 1]]
curr = daily[sorted_dates[i]]
if prev > 0:
returns.append((curr - prev) / prev)
return returns
def _compute_max_drawdown(self) -> tuple[float, int]:
"""计算最大回撤百分比和最大回撤持续天数"""
if not self._equity:
return 0.0, 0
peak = self._equity[0]["equity"]
max_dd = 0.0
dd_start_idx = 0
max_dd_days = 0
for i, point in enumerate(self._equity):
equity = point["equity"]
if equity > peak:
peak = equity
dd_start_idx = i
dd = (equity - peak) / peak * 100
if dd < max_dd:
max_dd = dd
# 计算从 peak 日期到当前的持续时间
peak_ts = self._equity[dd_start_idx]["timestamp"]
curr_ts = point["timestamp"]
dd_days = int((curr_ts - peak_ts) / (1000 * 86400))
if dd_days > max_dd_days:
max_dd_days = dd_days
return max_dd, max_dd_days
# ================================================================
# 工具方法
# ================================================================
def _round_qty(self, qty: float, decimals: int = 8) -> float:
"""将数量向下取整到指定位数"""
factor = 10 ** decimals
return int(qty * factor) / factor
+146
View File
@@ -0,0 +1,146 @@
"""
回测引擎数据模型 — 配置、交易记录、绩效指标和结果
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
@dataclass
class BacktestConfig:
"""回测配置
Attributes:
symbol: 交易对(如 BTCUSDT
exchange: 交易所标识
interval: K 线周期
start_time: 回测起始时间(None 表示从最早可用数据开始)
end_time: 回测结束时间(None 表示到最新可用数据结束)
commission_pct: 手续费率(0.001 = 0.1%
slippage_pct: 滑点率(0.0005 = 0.05%
min_order_qty: 最小下单量
initial_capital: 初始资金(Quote 币种,如 USDT)
warmup_bars: 预热 K 线条数(策略初始化指标所需最少数据量)
"""
symbol: str
exchange: str = "binance"
interval: str = "1h"
start_time: Optional[datetime] = None
end_time: Optional[datetime] = None
# 交易成本
commission_pct: float = 0.001
slippage_pct: float = 0.0005
min_order_qty: float = 0.001
# 资金
initial_capital: float = 10_000.0
# 数据
warmup_bars: int = 100
@dataclass
class BacktestTrade:
"""单笔回测交易记录"""
timestamp: float
"""成交时间(Unix 毫秒)"""
symbol: str
"""交易对"""
side: str
"""方向:BUY / SELL"""
price: float
"""成交价格(含滑点)"""
quantity: float
"""成交数量(Base 币种)"""
notional: float
"""成交额(Quote 币种 = price × quantity"""
commission: float
"""手续费"""
slippage: float
"""滑点成本"""
pnl: Optional[float] = None
"""平仓盈亏(BUY 时为 None,SELL 时有效)"""
reason: str = ""
"""交易原因(来自 Signal.reason"""
@dataclass
class BacktestMetrics:
"""回测绩效指标"""
total_return_pct: float = 0.0
"""总收益率(%"""
annual_return_pct: float = 0.0
"""年化收益率(%"""
sharpe_ratio: float = 0.0
"""夏普比率(无风险利率假定为 0"""
max_drawdown_pct: float = 0.0
"""最大回撤(%),以负值表示"""
max_drawdown_duration_days: int = 0
"""最大回撤持续天数"""
win_rate: float = 0.0
"""胜率(0-1"""
profit_factor: float = 0.0
"""盈亏比(总盈利 / 总亏损绝对值)"""
total_trades: int = 0
"""总交易次数"""
avg_trade_pnl: float = 0.0
"""平均每笔盈亏"""
best_trade_pnl: float = 0.0
"""最佳单笔盈亏"""
worst_trade_pnl: float = 0.0
"""最差单笔盈亏"""
calmar_ratio: float = 0.0
"""卡尔玛比率(年化收益 / 最大回撤绝对值)"""
final_equity: float = 0.0
"""最终权益"""
@dataclass
class BacktestResult:
"""完整回测结果"""
config: BacktestConfig
"""回测配置"""
strategy_config: dict
"""策略配置(转为 dict 便于序列化)"""
metrics: BacktestMetrics
"""绩效指标"""
trades: list[BacktestTrade] = field(default_factory=list)
"""交易记录"""
equity_curve: list[dict] = field(default_factory=list)
"""资金曲线 [{"timestamp": float, "equity": float, "drawdown": float}, ...]"""
@property
def total_bars(self) -> int:
"""回测 K 线总数"""
return len(self.equity_curve)
def summary(self) -> str:
"""生成人类可读的摘要"""
m = self.metrics
lines = [
"=" * 60,
f" 回测结果摘要 — {self.config.symbol} {self.config.interval}",
"=" * 60,
f" 初始资金: {self.config.initial_capital:>12.2f} USDT",
f" 最终权益: {m.final_equity:>12.2f} USDT",
f" 总收益率: {m.total_return_pct:>11.2f}%",
f" 年化收益率: {m.annual_return_pct:>11.2f}%",
f" 夏普比率: {m.sharpe_ratio:>12.2f}",
f" 卡尔玛比率: {m.calmar_ratio:>12.2f}",
f" 最大回撤: {m.max_drawdown_pct:>11.2f}%",
f" 回撤持续: {m.max_drawdown_duration_days:>9}",
f" 总交易次数: {m.total_trades:>11}",
f" 胜率: {m.win_rate:>11.1%}",
f" 盈亏比: {m.profit_factor:>12.2f}",
f" 平均盈亏: {m.avg_trade_pnl:>12.4f} USDT",
f" 最佳盈亏: {m.best_trade_pnl:>12.4f} USDT",
f" 最差盈亏: {m.worst_trade_pnl:>12.4f} USDT",
"=" * 60,
]
return "\n".join(lines)
+1 -1
View File
@@ -14,7 +14,7 @@ from pydantic import BaseModel, Field, field_validator
# K 线周期类型 # K 线周期类型
# ============================================================ # ============================================================
KlineInterval = Literal["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w"] KlineInterval = Literal["1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "1d", "1w", "1mon"]
# ============================================================ # ============================================================
+5
View File
@@ -0,0 +1,5 @@
# engine.data — K 线数据服务
from .service import DataService
__all__ = ["DataService"]
+350
View File
@@ -0,0 +1,350 @@
"""
K 线数据服务 — 从 TimescaleDB 读取历史 K 线数据,为回测引擎提供数据源
用法:
from engine.common.config import config
from engine.data import DataService
ds = DataService(config.db)
await ds.connect()
# 获取 BTCUSDT 1h K 线,按时间范围过滤
klines = await ds.fetch_klines(
symbol="BTCUSDT",
interval="1h",
start_time=datetime(2025, 1, 1),
end_time=datetime(2026, 1, 1),
)
await ds.close()
"""
import asyncio
from datetime import datetime, timezone
from decimal import Decimal
from typing import AsyncGenerator
import asyncpg
from ..common.config import DBConfig
from ..common.models import Kline, KlineInterval
# ── 周期 → 表名映射 ──
INTERVAL_TO_TABLE: dict[KlineInterval, str] = {
"1m": "klines",
"3m": "klines_3m",
"5m": "klines_5m",
"15m": "klines_15m",
"30m": "klines_30m",
"1h": "klines_1h",
"2h": "klines_2h",
"4h": "klines_4h",
"6h": "klines_6h",
"8h": "klines_8h",
"1d": "klines_1d",
"1w": "klines_1w",
"1mon": "klines_1mon",
}
# ── 周期毫秒数 ──
INTERVAL_MS: dict[KlineInterval, int] = {
"1m": 60_000,
"3m": 180_000,
"5m": 300_000,
"15m": 900_000,
"30m": 1_800_000,
"1h": 3_600_000,
"2h": 7_200_000,
"4h": 14_400_000,
"6h": 21_600_000,
"8h": 28_800_000,
"1d": 86_400_000,
"1w": 604_800_000,
"1mon": 2_592_000_000,
}
DEFAULT_BATCH_SIZE = 5000
def dt_to_unix_ms(dt: datetime) -> float:
"""datetime → Unix 毫秒时间戳"""
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.timestamp() * 1000
def _to_float(val) -> float:
"""将 Decimal / int 等数值转为 float"""
if val is None:
return 0.0
if isinstance(val, Decimal):
return float(val)
return float(val)
class DataService:
"""K 线数据服务
封装 TimescaleDB 中 K 线数据的查询逻辑,
将数据库行转换为 engine.common.models.Kline 模型,
供回测引擎消费。
"""
def __init__(self, db_config: DBConfig, pool_size: int = 4):
self._db_config = db_config
self._pool_size = pool_size
self._pool: asyncpg.Pool | None = None
self._col_cache: dict[str, set[str]] = {}
# ── 生命周期 ──
@property
def dsn(self) -> str:
db = self._db_config
return f"postgresql://{db.user}:{db.password}@{db.host}:{db.port}/{db.name}"
async def connect(self) -> None:
"""建立数据库连接池"""
self._pool = await asyncpg.create_pool(
dsn=self.dsn,
min_size=1,
max_size=self._pool_size,
)
async def close(self) -> None:
"""关闭连接池"""
if self._pool:
await self._pool.close()
self._pool = None
@property
def is_connected(self) -> bool:
return self._pool is not None
# ── 元数据查询 ──
async def fetch_available_symbols(
self, interval: KlineInterval = "1m"
) -> list[str]:
"""获取指定周期下所有有数据的交易对"""
table = INTERVAL_TO_TABLE[interval]
async with self._pool.acquire() as conn:
rows = await conn.fetch(
f"SELECT DISTINCT symbol FROM {table} ORDER BY symbol"
)
return [r["symbol"] for r in rows]
async def fetch_symbol_date_range(
self, symbol: str, interval: KlineInterval
) -> tuple[datetime, datetime]:
"""获取指定交易对 + 周期的数据起止时间"""
table = INTERVAL_TO_TABLE[interval]
async with self._pool.acquire() as conn:
row = await conn.fetchrow(
f"""
SELECT MIN(time) AS min_time, MAX(time) AS max_time
FROM {table}
WHERE symbol = $1
""",
symbol,
)
if row is None or row["min_time"] is None:
raise ValueError(f"无数据: {symbol} {interval}")
return row["min_time"], row["max_time"]
async def fetch_klines_count(
self,
symbol: str,
interval: KlineInterval,
start_time: datetime | None = None,
end_time: datetime | None = None,
) -> int:
"""获取指定条件的 K 线条数(用于预判数据量)"""
table = INTERVAL_TO_TABLE[interval]
conditions = ["symbol = $1", "interval = $2"]
params: list = [symbol, interval]
idx = 3
if start_time is not None:
conditions.append(f"time >= ${idx}")
params.append(start_time)
idx += 1
if end_time is not None:
conditions.append(f"time < ${idx}")
params.append(end_time)
idx += 1
where = " AND ".join(conditions)
async with self._pool.acquire() as conn:
count = await conn.fetchval(
f"SELECT COUNT(*) FROM {table} WHERE {where}", *params
)
return count
# ── 核心查询 ──
async def fetch_klines(
self,
symbol: str,
interval: KlineInterval,
start_time: datetime | None = None,
end_time: datetime | None = None,
limit: int = 1000,
offset: int = 0,
) -> list[Kline]:
"""获取 K 线数据,返回 Pydantic 模型列表
Args:
symbol: 交易对(如 BTCUSDT
interval: K 线周期
start_time: 起始时间(包含)
end_time: 结束时间(不包含)
limit: 最大返回条数
offset: 分页偏移
Returns:
按时间升序排列的 Kline 列表
"""
table = INTERVAL_TO_TABLE[interval]
interval_ms = INTERVAL_MS[interval]
conditions = ["symbol = $1", "interval = $2"]
params: list = [symbol, interval]
idx = 3
if start_time is not None:
conditions.append(f"time >= ${idx}")
params.append(start_time)
idx += 1
if end_time is not None:
conditions.append(f"time < ${idx}")
params.append(end_time)
idx += 1
where = " AND ".join(conditions)
cols = await self._get_columns(table)
select_cols = [
"time", "exchange", "symbol", "interval",
"open", "high", "low", "close", "volume",
]
for extra in (
"trade_count", "quote_volume", "taker_buy_base_vol",
"taker_buy_quote_vol", "is_closed",
):
if extra in cols:
select_cols.append(extra)
async with self._pool.acquire() as conn:
rows = await conn.fetch(
f"""
SELECT {', '.join(select_cols)}
FROM {table}
WHERE {where}
ORDER BY time ASC
LIMIT ${idx} OFFSET ${idx + 1}
""",
*params,
limit,
offset,
)
return [self._row_to_kline(r, interval, interval_ms) for r in rows]
async def stream_klines(
self,
symbol: str,
interval: KlineInterval,
start_time: datetime | None = None,
end_time: datetime | None = None,
batch_size: int = DEFAULT_BATCH_SIZE,
) -> AsyncGenerator[list[Kline], None]:
"""流式获取 K 线数据,适合大数据集
每次返回一批 K 线列表,避免一次性加载过多数据到内存。
Yields:
每批 Kline 列表(按时间升序)
"""
offset = 0
while True:
batch = await self.fetch_klines(
symbol=symbol,
interval=interval,
start_time=start_time,
end_time=end_time,
limit=batch_size,
offset=offset,
)
if not batch:
break
yield batch
offset += len(batch)
async def fetch_multi_klines(
self,
symbols: list[str],
interval: KlineInterval,
start_time: datetime | None = None,
end_time: datetime | None = None,
limit: int = 1000,
) -> dict[str, list[Kline]]:
"""批量获取多个交易对的 K 线
Returns:
{symbol: [Kline, ...]} 字典
"""
tasks = [
self.fetch_klines(
symbol=sym,
interval=interval,
start_time=start_time,
end_time=end_time,
limit=limit,
)
for sym in symbols
]
results = await asyncio.gather(*tasks)
return dict(zip(symbols, results))
# ── 内部方法 ──
async def _get_columns(self, table: str) -> set[str]:
"""获取表的列名集合(带缓存)"""
if table not in self._col_cache:
async with self._pool.acquire() as conn:
rows = await conn.fetch(
"""
SELECT column_name
FROM information_schema.columns
WHERE table_name = $1
""",
table,
)
self._col_cache[table] = {r["column_name"] for r in rows}
return self._col_cache[table]
@staticmethod
def _row_to_kline(
row: asyncpg.Record, interval: KlineInterval, interval_ms: int
) -> Kline:
"""将数据库行转换为 Kline 模型"""
open_time = dt_to_unix_ms(row["time"])
return Kline(
exchange=row["exchange"],
symbol=row["symbol"],
interval=interval,
openTime=open_time,
closeTime=open_time + interval_ms,
open=_to_float(row["open"]),
high=_to_float(row["high"]),
low=_to_float(row["low"]),
close=_to_float(row["close"]),
volume=_to_float(row["volume"]),
quoteVolume=_to_float(row.get("quote_volume", 0)),
takerBuyBaseVol=_to_float(row.get("taker_buy_base_vol", 0)),
takerBuyQuoteVol=_to_float(row.get("taker_buy_quote_vol", 0)),
tradeCount=int(row.get("trade_count") or 0),
isClosed=bool(row.get("is_closed", True)),
)
+152
View File
@@ -0,0 +1,152 @@
"""
数据库 K 线读取测试 — 只读,从 TimescaleDB 读取各周期 K 线并打印
用法:
python db_test.py # 使用 env.yaml 中的 host
python db_test.py --host localhost # 覆盖 host(如 SSH 隧道后)
"""
import asyncio
import sys
import asyncpg
from common.config import config as app_config
# ── 各周期对应的表/视图 ──
INTERVAL_TABLES: dict[str, str] = {
"1m": "klines",
"3m": "klines_3m",
"5m": "klines_5m",
"15m": "klines_15m",
"30m": "klines_30m",
"1h": "klines_1h",
"2h": "klines_2h",
"4h": "klines_4h",
"6h": "klines_6h",
"8h": "klines_8h",
"1d": "klines_1d",
"1w": "klines_1w",
"1mon": "klines_1mon",
}
LIMIT = 5
def parse_args() -> str | None:
"""解析命令行参数,返回 host 覆盖值"""
args = sys.argv[1:]
host_override = None
i = 0
while i < len(args):
if args[i] == "--host" and i + 1 < len(args):
host_override = args[i + 1]
i += 2
elif args[i].startswith("--host="):
host_override = args[i].split("=", 1)[1]
i += 1
else:
i += 1
return host_override
async def main():
host_override = parse_args()
db = app_config.db
if host_override:
db = db.model_copy(update={"host": host_override})
dsn = f"postgresql://{db.user}:{db.password}@{db.host}:{db.port}/{db.name}"
print(f"连接 {db.host}:{db.port}/{db.name} ...")
conn = await asyncpg.connect(dsn)
try:
print()
print("=" * 85)
print(" TimescaleDB K 线数据读取测试(只读)")
print("=" * 85)
for interval, table in INTERVAL_TABLES.items():
# 检查表/视图是否存在(含 TimescaleDB 连续聚合视图)
exists = await conn.fetchval(
"""
SELECT EXISTS (
SELECT 1 FROM pg_matviews WHERE matviewname = $1
UNION
SELECT 1 FROM pg_tables WHERE tablename = $1
UNION
SELECT 1 FROM pg_views WHERE viewname = $1
)
""",
table,
)
if not exists:
print(f"\n [{interval}] {table} — 不存在,跳过")
continue
# 获取该表/视图的实际列名,避免查询不存在的列报错
columns = await conn.fetch(
"""
SELECT column_name
FROM information_schema.columns
WHERE table_name = $1
ORDER BY ordinal_position
""",
table,
)
col_names = {r["column_name"] for r in columns}
# 选择存在的列进行查询
select_cols = ["time", "exchange", "symbol", "interval", "open", "high", "low", "close", "volume"]
if "trade_count" in col_names:
select_cols.append("trade_count")
if "is_closed" in col_names:
select_cols.append("is_closed")
rows = await conn.fetch(
f"""
SELECT {', '.join(select_cols)}
FROM {table}
WHERE interval = $1
ORDER BY time DESC
LIMIT $2
""",
interval,
LIMIT,
)
print(f"\n{'' * 85}")
print(f" [{interval}] {table}{len(rows)}")
print(f"{'' * 85}")
if not rows:
print(" (无数据)")
continue
for k in rows:
t = k["time"].strftime("%Y-%m-%d %H:%M:%S")
is_closed = k.get("is_closed")
if is_closed is not None:
mark = "" if is_closed else ""
else:
mark = " " # 聚合视图无此字段
trade_count = k.get("trade_count", "-")
print(
f" {t} [{mark}] {k['symbol']:10s} {k['interval']:4s}"
f" O={k['open']:>12.4f} H={k['high']:>12.4f}"
f" L={k['low']:>12.4f} C={k['close']:>12.4f}"
f" V={k['volume']:>10.4f} trades={trade_count}"
)
print(f"\n{'=' * 85}")
print(" 读取完成")
print("=" * 85)
finally:
await conn.close()
if __name__ == "__main__":
asyncio.run(main())
+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)
+218
View File
@@ -0,0 +1,218 @@
"""
回测引擎使用示例 — 双策略演示
用法:
source .venv/bin/activate && python example/backtest_demo.py
"""
import asyncio
import sys
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
from engine.indicators import sma, rsi
# ============================================================
# 策略 1:双均线交叉
# ============================================================
class MACrossConfig(StrategyConfig):
fast_period: int = 7
slow_period: int = 25
class MACrossStrategy(BaseStrategy):
"""双均线交叉策略 — 使用 engine.indicators.sma 计算均线"""
strategy_type = "ma_cross"
def __init__(self, config: MACrossConfig):
super().__init__(config)
self.config: MACrossConfig = config
self._closes: list[float] = []
self._last_signal: Optional[str] = None
async def on_kline(self, kline: Kline) -> Optional[Signal]:
self._closes.append(kline.close)
# 使用指标库计算 SMA
fast_ma = sma(self._closes, self.config.fast_period)
slow_ma = sma(self._closes, self.config.slow_period)
fast = fast_ma[-1]
slow = slow_ma[-1]
if fast == 0.0 or slow == 0.0:
return None
if fast > slow and self._last_signal != "BUY":
self._last_signal = "BUY"
return Signal(
symbol=self.config.symbol,
side="BUY",
signal_type="MARKET",
confidence=0.8,
reason=f"金叉 MA{self.config.fast_period}>{self.config.slow_period}",
timestamp=kline.open_time,
)
if fast < slow and self._last_signal != "SELL":
self._last_signal = "SELL"
return Signal(
symbol=self.config.symbol,
side="SELL",
signal_type="MARKET",
confidence=0.8,
reason=f"死叉 MA{self.config.fast_period}<{self.config.slow_period}",
timestamp=kline.open_time,
)
return None
# ============================================================
# 策略 2RSI 超买超卖
# ============================================================
class RSIStrategyConfig(StrategyConfig):
period: int = 14
oversold: float = 30.0 # 超卖阈值
overbought: float = 70.0 # 超买阈值
class RSIStrategy(BaseStrategy):
"""RSI 超买超卖策略 — 使用 engine.indicators.rsi 计算 RSI
RSI 低于超卖线 → 买入;RSI 高于超买线 → 卖出。
"""
strategy_type = "rsi"
def __init__(self, config: RSIStrategyConfig):
super().__init__(config)
self.config: RSIStrategyConfig = config
self._closes: list[float] = []
self._has_position = False
async def on_kline(self, kline: Kline) -> Optional[Signal]:
self._closes.append(kline.close)
# 使用指标库计算 RSI
rsi_vals = rsi(self._closes, self.config.period)
current_rsi = rsi_vals[-1]
if current_rsi == 0.0:
return None
# 超卖 → 买入
if current_rsi < self.config.oversold and not self._has_position:
self._has_position = True
return Signal(
symbol=self.config.symbol,
side="BUY",
signal_type="MARKET",
confidence=0.7,
reason=f"RSI超卖 ({current_rsi:.1f} < {self.config.oversold})",
timestamp=kline.open_time,
)
# 超买 → 卖出
if current_rsi > self.config.overbought and self._has_position:
self._has_position = False
return Signal(
symbol=self.config.symbol,
side="SELL",
signal_type="MARKET",
confidence=0.7,
reason=f"RSI超买 ({current_rsi:.1f} > {self.config.overbought})",
timestamp=kline.open_time,
)
return None
# ============================================================
# 主函数
# ============================================================
async def run_backtest(
engine: BacktestEngine,
strategy_cls,
strategy_config: StrategyConfig,
label: str,
):
"""运行一次回测并打印结果"""
print(f"\n{'' * 60}")
print(f" {label}")
print(f"{'' * 60}")
result = await engine.run(strategy_cls, strategy_config)
print(result.summary())
# 最近 5 笔交易
if result.trades:
print(f"\n 最近 5 笔交易:")
print(f" {'时间':<22} {'方向':<6} {'价格':>10} {'数量':>10} {'盈亏':>10} 原因")
for t in result.trades[-5:]:
dt = datetime.fromtimestamp(t.timestamp / 1000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M")
pnl_str = f"{t.pnl:+.4f}" if t.pnl is not None else ""
print(f" {dt:<22} {t.side:<6} {t.price:>10.4f} {t.quantity:>10.6f} {pnl_str:>10} {t.reason}")
return result
async def main():
# ── 回测配置 ──
bt_config = BacktestConfig(
symbol="ETHUSDT",
interval="4h",
start_time=datetime(2024, 1, 1),
end_time=datetime(2026, 1, 1),
initial_capital=10_000.0,
commission_pct=0.001,
slippage_pct=0.0005,
warmup_bars=100,
)
print(f"\n回测: {bt_config.symbol} {bt_config.interval}")
print(f"时间: {bt_config.start_time.date()} ~ {bt_config.end_time.date()}")
print(f"初始资金: {bt_config.initial_capital:.2f} USDT")
engine = BacktestEngine(bt_config, db_config=config.db)
# ── 策略 1:双均线交叉 ──
ma_config = MACrossConfig(
name="ma_cross_eth",
symbol="ETHUSDT",
fast_period=7,
slow_period=25,
)
await run_backtest(engine, MACrossStrategy, ma_config, "策略 1:双均线交叉 (MA7/MA25)")
# ── 策略 2:RSI 超买超卖 ──
rsi_config = RSIStrategyConfig(
name="rsi_eth",
symbol="ETHUSDT",
period=14,
oversold=30.0,
overbought=70.0,
)
await run_backtest(engine, RSIStrategy, rsi_config, "策略 2RSI 超买超卖 (30/70)")
print("\n全部回测完成。")
if __name__ == "__main__":
asyncio.run(main())
File diff suppressed because it is too large Load Diff
+804
View File
@@ -0,0 +1,804 @@
# 2h / 6h 策略全维度对比回测报告
> **回测日期**2026-06-13 11:36:37 UTC
> **总耗时**28.7 秒
> **测试维度**:9 策略 × 4 币种 × 2 时间级别 × 4 数据量 = 288 次回测
> **初始资金**$10,000 | **预热 Bar**150
> **错误数**0 | **手续费**0.1% | **滑点**0.05% | **引擎**LongShortEngine(多空双向)
---
## 一、策略概览
| # | 策略名称 | 类型 | 参数 | 描述 |
|---|----------|------|------|------|
| 1 | **海龟交易** | 趋势跟踪 | `entry=20/exit=10/ATR(20)x2.0` | Donchian 20/10通道突破 + 2N ATR止损,多空双向 |
| 2 | **超级趋势** | 趋势跟踪 | `ATR(10)x3.0` | ATR(10)×3倍动态跟踪止损带,趋势翻转即反转 |
| 3 | **MACD金叉死叉** | 动量 | `MACD(12,26,9)/ATR(14)x2.0` | MACD(12,26,9)零轴上金叉做多/零轴下死叉做空+ATR止损 |
| 4 | **布林收缩爆发** | 波动率突破 | `BB(20,2.0)/KC(20,1.5)/squeeze=30` | BB收缩至KC内部后扩张爆发,顺势入场 + ATR止损 |
| 5 | **三均线排列** | 趋势跟踪 | `EMA(10,30,60)/ATR(14)x2.0` | EMA(10,30,60)多头/空头排列,快线金叉入场+ATR追踪止损 |
| 6 | **RSI均值回归** | 均值回归 | `RSI(14)25/75+BB(20,2.0)/ATR(14)x1.5` | RSI(14)超卖25/超买75 + 布林带触碰确认 → 逆向回归 |
| 7 | **ATR波动率突破** | 波动率突破 | `ATR(14)/squeeze=20x0.7/EMA(10,30)` | ATR(14)收缩至极低后扩张突破 + EMA(10/30)方向确认 |
| 8 | **EMA双均线多空** | 趋势跟踪 | `EMA(10,50)/ATR(14)x2.5` | EMA(10,50)金叉做多死叉做空 + ATR追踪止损,始终在场 |
| 9 | **牛熊自适应** | 牛熊自适应 | `EMA200投票(斜率+价格+ATH)牛多熊空` | EMA200斜率+价格vsEMA200+ATH回撤3选2投票,牛市只多/熊市只空 |
---
## 二、全量数据 TOP 20(按夏普比率排名)
| 排名 | 策略 | 币种 | TF | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 交易数 | 卡尔玛 |
|------|------|------|----|---------|-------|------|-------|-------|--------|--------|--------|
| 🥇 | 超级趋势 | SOLUSDT | 6h | +620.1% | +41.1% | **1.40** | -37.0% | 39.5% | 2.21 | 86 | 1.11 |
| 🥈 | ATR波动率突破 | SOLUSDT | 2h | +11666.5% | +127.5% | **1.39** | -59.8% | 37.6% | 1.37 | 202 | 2.13 |
| 🥉 | ATR波动率突破 | SOLUSDT | 6h | +4450.9% | +94.7% | **1.22** | -84.4% | 23.7% | 1.25 | 59 | 1.12 |
| 4 | 牛熊自适应 | BNBUSDT | 6h | +3592.9% | +52.9% | **1.22** | -44.5% | 37.5% | 1.99 | 128 | 1.19 |
| 5 | ATR波动率突破 | BTCUSDT | 2h | +4550.8% | +54.8% | **1.13** | -52.6% | 31.4% | 1.43 | 334 | 1.04 |
| 6 | ATR波动率突破 | ETHUSDT | 6h | +5102.3% | +57.3% | **1.04** | -47.6% | 36.2% | 2.17 | 94 | 1.21 |
| 7 | 超级趋势 | BNBUSDT | 2h | +512.5% | +23.6% | **0.94** | -37.0% | 40.3% | 1.55 | 392 | 0.64 |
| 8 | 三均线排列 | BTCUSDT | 6h | +86.1% | +7.4% | **0.93** | -13.3% | 37.8% | 1.82 | 148 | 0.56 |
| 9 | ATR波动率突破 | BTCUSDT | 6h | +1509.3% | +37.5% | **0.92** | -55.8% | 31.9% | 1.42 | 94 | 0.67 |
| 10 | ATR波动率突破 | ETHUSDT | 2h | +1771.2% | +39.6% | **0.85** | -59.9% | 31.7% | 1.30 | 338 | 0.66 |
| 11 | 超级趋势 | BTCUSDT | 6h | +113.3% | +9.1% | **0.83** | -16.2% | 36.6% | 1.65 | 134 | 0.56 |
| 12 | ATR波动率突破 | BNBUSDT | 2h | +2258.5% | +44.6% | **0.83** | -72.5% | 29.5% | 1.22 | 302 | 0.62 |
| 13 | EMA双均线多空 | BTCUSDT | 6h | +1148.0% | +33.6% | **0.82** | -57.2% | 41.2% | 1.54 | 240 | 0.59 |
| 14 | MACD金叉死叉 | ETHUSDT | 6h | +108.3% | +8.8% | **0.81** | -15.3% | 38.0% | 1.43 | 305 | 0.57 |
| 15 | 牛熊自适应 | ETHUSDT | 6h | +748.9% | +27.8% | **0.80** | -73.0% | 34.1% | 1.27 | 132 | 0.38 |
| 16 | 三均线排列 | BNBUSDT | 6h | +93.9% | +8.1% | **0.77** | -21.1% | 40.8% | 1.83 | 147 | 0.38 |
| 17 | 牛熊自适应 | BNBUSDT | 2h | +845.9% | +30.0% | **0.77** | -68.2% | 35.1% | 1.25 | 382 | 0.44 |
| 18 | MACD金叉死叉 | SOLUSDT | 6h | +84.6% | +11.3% | **0.73** | -27.7% | 42.4% | 1.34 | 217 | 0.41 |
| 19 | 超级趋势 | BNBUSDT | 6h | +288.2% | +17.3% | **0.72** | -35.9% | 40.4% | 2.03 | 146 | 0.48 |
| 20 | 三均线排列 | ETHUSDT | 6h | +81.9% | +7.1% | **0.72** | -18.5% | 35.1% | 1.67 | 154 | 0.38 |
---
## 三、各策略全量数据详细表现(2h vs 6h × 4 币种)
### 1.海龟交易
> **类型**:趋势跟踪 | **参数**`entry=20/exit=10/ATR(20)x2.0`
> Donchian 20/10通道突破 + 2N ATR止损,多空双向
| 币种 | TF | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 交易数 | 卡尔玛 |
|------|----|---------|-------|------|-------|-------|--------|--------|--------|
| BNBUSDT | 2h | -64.4% | -11.4% | **-1.09** | -67.5% | 31.0% | 0.86 | 1644 | -0.17 |
| BNBUSDT | 6h | +96.1% | +8.2% | **0.64** | -34.1% | 35.0% | 1.30 | 605 | 0.24 |
| BTCUSDT | 2h | -71.9% | -13.5% | **-1.71** | -72.0% | 29.8% | 0.74 | 1622 | -0.19 |
| BTCUSDT | 6h | +9.6% | +1.1% | **0.17** | -19.1% | 35.1% | 1.13 | 579 | 0.06 |
| ETHUSDT | 2h | -64.2% | -11.0% | **-1.18** | -64.8% | 31.2% | 0.85 | 1684 | -0.17 |
| ETHUSDT | 6h | +16.4% | +1.8% | **0.22** | -28.6% | 37.2% | 1.13 | 596 | 0.06 |
| SOLUSDT | 2h | -60.5% | -14.8% | **-1.13** | -60.9% | 32.6% | 0.86 | 1241 | -0.24 |
| SOLUSDT | 6h | -23.3% | -4.5% | **-0.30** | -41.1% | 36.2% | 0.92 | 437 | -0.11 |
> 🏆 **海龟交易 最优**BNBUSDT 6h,夏普 **0.64**,总收益 **+96.1%**,年化 **+8.2%**,交易 605 次
### 2.超级趋势
> **类型**:趋势跟踪 | **参数**`ATR(10)x3.0`
> ATR(10)×3倍动态跟踪止损带,趋势翻转即反转
| 币种 | TF | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 交易数 | 卡尔玛 |
|------|----|---------|-------|------|-------|-------|--------|--------|--------|
| BNBUSDT | 2h | +512.5% | +23.6% | **0.94** | -37.0% | 40.3% | 1.55 | 392 | 0.64 |
| BNBUSDT | 6h | +288.2% | +17.3% | **0.72** | -35.9% | 40.4% | 2.03 | 146 | 0.48 |
| BTCUSDT | 2h | -36.4% | -5.0% | **-0.39** | -48.0% | 32.2% | 0.86 | 401 | -0.10 |
| BTCUSDT | 6h | +113.3% | +9.1% | **0.83** | -16.2% | 36.6% | 1.65 | 134 | 0.56 |
| ETHUSDT | 2h | -13.3% | -1.6% | **-0.04** | -48.1% | 34.1% | 0.99 | 407 | -0.03 |
| ETHUSDT | 6h | +5.9% | +0.7% | **0.12** | -42.7% | 44.2% | 1.06 | 129 | 0.02 |
| SOLUSDT | 2h | -47.1% | -10.4% | **-0.52** | -56.6% | 35.8% | 0.76 | 285 | -0.18 |
| SOLUSDT | 6h | +620.1% | +41.1% | **1.40** | -37.0% | 39.5% | 2.21 | 86 | 1.11 |
> 🏆 **超级趋势 最优**SOLUSDT 6h,夏普 **1.40**,总收益 **+620.1%**,年化 **+41.1%**,交易 86 次
### 3.MACD金叉死叉
> **类型**:动量 | **参数**`MACD(12,26,9)/ATR(14)x2.0`
> MACD(12,26,9)零轴上金叉做多/零轴下死叉做空+ATR止损
| 币种 | TF | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 交易数 | 卡尔玛 |
|------|----|---------|-------|------|-------|-------|--------|--------|--------|
| BNBUSDT | 2h | +97.6% | +8.3% | **0.55** | -37.8% | 37.7% | 1.22 | 917 | 0.22 |
| BNBUSDT | 6h | +43.0% | +4.3% | **0.37** | -18.3% | 38.5% | 1.26 | 288 | 0.23 |
| BTCUSDT | 2h | +7.9% | +0.9% | **0.14** | -19.2% | 36.5% | 1.12 | 873 | 0.04 |
| BTCUSDT | 6h | +6.0% | +0.7% | **0.12** | -28.8% | 37.0% | 1.08 | 316 | 0.02 |
| ETHUSDT | 2h | +103.5% | +8.4% | **0.67** | -26.8% | 37.2% | 1.26 | 867 | 0.31 |
| ETHUSDT | 6h | +108.3% | +8.8% | **0.81** | -15.3% | 38.0% | 1.43 | 305 | 0.57 |
| SOLUSDT | 2h | +49.3% | +7.2% | **0.51** | -25.0% | 38.2% | 1.19 | 617 | 0.29 |
| SOLUSDT | 6h | +84.6% | +11.3% | **0.73** | -27.7% | 42.4% | 1.34 | 217 | 0.41 |
> 🏆 **MACD金叉死叉 最优**ETHUSDT 6h,夏普 **0.81**,总收益 **+108.3%**,年化 **+8.8%**,交易 305 次
### 4.布林收缩爆发
> **类型**:波动率突破 | **参数**`BB(20,2.0)/KC(20,1.5)/squeeze=30`
> BB收缩至KC内部后扩张爆发,顺势入场 + ATR止损
| 币种 | TF | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 交易数 | 卡尔玛 |
|------|----|---------|-------|------|-------|-------|--------|--------|--------|
| BNBUSDT | 2h | +25.7% | +2.7% | **0.30** | -18.6% | 35.9% | 1.38 | 159 | 0.15 |
| BNBUSDT | 6h | +51.4% | +5.0% | **0.54** | -22.1% | 39.1% | 1.80 | 69 | 0.23 |
| BTCUSDT | 2h | -7.4% | -0.9% | **-0.19** | -19.0% | 30.8% | 0.90 | 117 | -0.05 |
| BTCUSDT | 6h | -6.8% | -0.8% | **-0.15** | -18.1% | 32.3% | 0.87 | 62 | -0.04 |
| ETHUSDT | 2h | +20.9% | +2.2% | **0.38** | -13.4% | 42.3% | 1.42 | 137 | 0.16 |
| ETHUSDT | 6h | +8.4% | +0.9% | **0.19** | -18.4% | 35.7% | 1.23 | 56 | 0.05 |
| SOLUSDT | 2h | -12.4% | -2.2% | **-0.23** | -27.2% | 29.1% | 0.87 | 134 | -0.08 |
| SOLUSDT | 6h | -5.5% | -1.0% | **-0.09** | -18.2% | 26.2% | 0.89 | 42 | -0.05 |
> 🏆 **布林收缩爆发 最优**BNBUSDT 6h,夏普 **0.54**,总收益 **+51.4%**,年化 **+5.0%**,交易 69 次
### 5.三均线排列
> **类型**:趋势跟踪 | **参数**`EMA(10,30,60)/ATR(14)x2.0`
> EMA(10,30,60)多头/空头排列,快线金叉入场+ATR追踪止损
| 币种 | TF | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 交易数 | 卡尔玛 |
|------|----|---------|-------|------|-------|-------|--------|--------|--------|
| BNBUSDT | 2h | +32.9% | +3.4% | **0.34** | -30.2% | 32.4% | 1.23 | 432 | 0.11 |
| BNBUSDT | 6h | +93.9% | +8.1% | **0.77** | -21.1% | 40.8% | 1.83 | 147 | 0.38 |
| BTCUSDT | 2h | +14.0% | +1.5% | **0.26** | -26.5% | 33.5% | 1.21 | 412 | 0.06 |
| BTCUSDT | 6h | +86.1% | +7.4% | **0.93** | -13.3% | 37.8% | 1.82 | 148 | 0.56 |
| ETHUSDT | 2h | -24.3% | -3.1% | **-0.40** | -37.5% | 28.5% | 0.90 | 418 | -0.08 |
| ETHUSDT | 6h | +81.9% | +7.1% | **0.72** | -18.5% | 35.1% | 1.67 | 154 | 0.38 |
| SOLUSDT | 2h | +34.4% | +5.2% | **0.54** | -15.0% | 38.9% | 1.29 | 270 | 0.35 |
| SOLUSDT | 6h | +19.2% | +3.1% | **0.34** | -11.8% | 38.4% | 1.32 | 86 | 0.26 |
> 🏆 **三均线排列 最优**BTCUSDT 6h,夏普 **0.93**,总收益 **+86.1%**,年化 **+7.4%**,交易 148 次
### 6.RSI均值回归
> **类型**:均值回归 | **参数**`RSI(14)25/75+BB(20,2.0)/ATR(14)x1.5`
> RSI(14)超卖25/超买75 + 布林带触碰确认 → 逆向回归
| 币种 | TF | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 交易数 | 卡尔玛 |
|------|----|---------|-------|------|-------|-------|--------|--------|--------|
| BNBUSDT | 2h | -96.5% | -32.5% | **-0.73** | -97.3% | 43.5% | 0.45 | 411 | -0.33 |
| BNBUSDT | 6h | -80.3% | -17.4% | **-0.08** | -96.2% | 42.3% | 0.95 | 168 | -0.18 |
| BTCUSDT | 2h | -98.5% | -38.2% | **-1.35** | -98.8% | 35.8% | 0.52 | 480 | -0.39 |
| BTCUSDT | 6h | -85.8% | -20.1% | **-0.52** | -89.8% | 38.1% | 0.77 | 194 | -0.22 |
| ETHUSDT | 2h | -99.7% | -47.9% | **-1.33** | -99.8% | 38.6% | 0.62 | 497 | -0.48 |
| ETHUSDT | 6h | -98.1% | -36.6% | **-0.87** | -99.0% | 32.7% | 0.65 | 199 | -0.37 |
| SOLUSDT | 2h | -89.9% | -32.6% | **-0.65** | -93.0% | 40.4% | 0.82 | 250 | -0.35 |
| SOLUSDT | 6h | -95.6% | -42.0% | **-0.61** | -97.0% | 38.4% | 0.60 | 99 | -0.43 |
> 🏆 **RSI均值回归 最优**BNBUSDT 6h,夏普 **-0.08**,总收益 **-80.3%**,年化 **-17.4%**,交易 168 次
### 7.ATR波动率突破
> **类型**:波动率突破 | **参数**`ATR(14)/squeeze=20x0.7/EMA(10,30)`
> ATR(14)收缩至极低后扩张突破 + EMA(10/30)方向确认
| 币种 | TF | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 交易数 | 卡尔玛 |
|------|----|---------|-------|------|-------|-------|--------|--------|--------|
| BNBUSDT | 2h | +2258.5% | +44.6% | **0.83** | -72.5% | 29.5% | 1.22 | 302 | 0.62 |
| BNBUSDT | 6h | +676.2% | +27.3% | **0.67** | -87.8% | 26.2% | 1.34 | 107 | 0.31 |
| BTCUSDT | 2h | +4550.8% | +54.8% | **1.13** | -52.6% | 31.4% | 1.43 | 334 | 1.04 |
| BTCUSDT | 6h | +1509.3% | +37.5% | **0.92** | -55.8% | 31.9% | 1.42 | 94 | 0.67 |
| ETHUSDT | 2h | +1771.2% | +39.6% | **0.85** | -59.9% | 31.7% | 1.30 | 338 | 0.66 |
| ETHUSDT | 6h | +5102.3% | +57.3% | **1.04** | -47.6% | 36.2% | 2.17 | 94 | 1.21 |
| SOLUSDT | 2h | +11666.5% | +127.5% | **1.39** | -59.8% | 37.6% | 1.37 | 202 | 2.13 |
| SOLUSDT | 6h | +4450.9% | +94.7% | **1.22** | -84.4% | 23.7% | 1.25 | 59 | 1.12 |
> 🏆 **ATR波动率突破 最优**SOLUSDT 2h,夏普 **1.39**,总收益 **+11666.5%**,年化 **+127.5%**,交易 202 次
### 8.EMA双均线多空
> **类型**:趋势跟踪 | **参数**`EMA(10,50)/ATR(14)x2.5`
> EMA(10,50)金叉做多死叉做空 + ATR追踪止损,始终在场
| 币种 | TF | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 交易数 | 卡尔玛 |
|------|----|---------|-------|------|-------|-------|--------|--------|--------|
| BNBUSDT | 2h | +35.9% | +3.6% | **0.39** | -94.0% | 37.2% | 1.07 | 753 | 0.04 |
| BNBUSDT | 6h | -278.8% | +0.0% | **-1.64** | -318.9% | 0.0% | 0.00 | 1 | 0.00 |
| BTCUSDT | 2h | -99.3% | -43.4% | **-0.48** | -99.4% | 32.8% | 0.67 | 711 | -0.44 |
| BTCUSDT | 6h | +1148.0% | +33.6% | **0.82** | -57.2% | 41.2% | 1.54 | 240 | 0.59 |
| ETHUSDT | 2h | -99.8% | -52.1% | **-0.60** | -99.9% | 33.0% | 0.57 | 794 | -0.52 |
| ETHUSDT | 6h | -114.3% | +0.0% | **-1.11** | -110.5% | 20.0% | 0.22 | 10 | 0.00 |
| SOLUSDT | 2h | -120.9% | +0.0% | **-1.06** | -113.0% | 32.1% | 0.74 | 84 | 0.00 |
| SOLUSDT | 6h | -141.6% | +0.0% | **-1.28** | -153.2% | 29.2% | 0.44 | 24 | 0.00 |
> 🏆 **EMA双均线多空 最优**BTCUSDT 6h,夏普 **0.82**,总收益 **+1148.0%**,年化 **+33.6%**,交易 240 次
### 9.牛熊自适应
> **类型**:牛熊自适应 | **参数**`EMA200投票(斜率+价格+ATH)牛多熊空`
> EMA200斜率+价格vsEMA200+ATH回撤3选2投票,牛市只多/熊市只空
| 币种 | TF | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 交易数 | 卡尔玛 |
|------|----|---------|-------|------|-------|-------|--------|--------|--------|
| BNBUSDT | 2h | +845.9% | +30.0% | **0.77** | -68.2% | 35.1% | 1.25 | 382 | 0.44 |
| BNBUSDT | 6h | +3592.9% | +52.9% | **1.22** | -44.5% | 37.5% | 1.99 | 128 | 1.19 |
| BTCUSDT | 2h | +89.9% | +7.6% | **0.40** | -66.9% | 33.6% | 1.18 | 375 | 0.11 |
| BTCUSDT | 6h | +317.6% | +17.8% | **0.67** | -43.3% | 35.2% | 1.37 | 122 | 0.41 |
| ETHUSDT | 2h | -20.9% | -2.6% | **0.10** | -80.0% | 30.0% | 1.04 | 400 | -0.03 |
| ETHUSDT | 6h | +748.9% | +27.8% | **0.80** | -73.0% | 34.1% | 1.27 | 132 | 0.38 |
| SOLUSDT | 2h | -62.5% | -15.5% | **-0.21** | -86.1% | 32.5% | 0.92 | 271 | -0.18 |
| SOLUSDT | 6h | +219.6% | +22.5% | **0.68** | -74.0% | 34.6% | 1.43 | 78 | 0.30 |
> 🏆 **牛熊自适应 最优**BNBUSDT 6h,夏普 **1.22**,总收益 **+3592.9%**,年化 **+52.9%**,交易 128 次
---
## 四、各币种全量数据 — 策略横向对比
### BNBUSDT
| 排名 | 策略 | TF | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 交易数 | 卡尔玛 |
|------|------|----|---------|-------|------|-------|-------|--------|--------|--------|
| 🥇 | 牛熊自适应 | 6h | +3592.9% | +52.9% | **1.22** | -44.5% | 37.5% | 1.99 | 128 | 1.19 |
| 🥈 | 超级趋势 | 2h | +512.5% | +23.6% | **0.94** | -37.0% | 40.3% | 1.55 | 392 | 0.64 |
| 🥉 | ATR波动率突破 | 2h | +2258.5% | +44.6% | **0.83** | -72.5% | 29.5% | 1.22 | 302 | 0.62 |
| 4 | 三均线排列 | 6h | +93.9% | +8.1% | **0.77** | -21.1% | 40.8% | 1.83 | 147 | 0.38 |
| 5 | 牛熊自适应 | 2h | +845.9% | +30.0% | **0.77** | -68.2% | 35.1% | 1.25 | 382 | 0.44 |
| 6 | 超级趋势 | 6h | +288.2% | +17.3% | **0.72** | -35.9% | 40.4% | 2.03 | 146 | 0.48 |
| 7 | ATR波动率突破 | 6h | +676.2% | +27.3% | **0.67** | -87.8% | 26.2% | 1.34 | 107 | 0.31 |
| 8 | 海龟交易 | 6h | +96.1% | +8.2% | **0.64** | -34.1% | 35.0% | 1.30 | 605 | 0.24 |
| 9 | MACD金叉死叉 | 2h | +97.6% | +8.3% | **0.55** | -37.8% | 37.7% | 1.22 | 917 | 0.22 |
| 10 | 布林收缩爆发 | 6h | +51.4% | +5.0% | **0.54** | -22.1% | 39.1% | 1.80 | 69 | 0.23 |
| 11 | EMA双均线多空 | 2h | +35.9% | +3.6% | **0.39** | -94.0% | 37.2% | 1.07 | 753 | 0.04 |
| 12 | MACD金叉死叉 | 6h | +43.0% | +4.3% | **0.37** | -18.3% | 38.5% | 1.26 | 288 | 0.23 |
| 13 | 三均线排列 | 2h | +32.9% | +3.4% | **0.34** | -30.2% | 32.4% | 1.23 | 432 | 0.11 |
| 14 | 布林收缩爆发 | 2h | +25.7% | +2.7% | **0.30** | -18.6% | 35.9% | 1.38 | 159 | 0.15 |
| 15 | RSI均值回归 | 6h | -80.3% | -17.4% | **-0.08** | -96.2% | 42.3% | 0.95 | 168 | -0.18 |
| 16 | RSI均值回归 | 2h | -96.5% | -32.5% | **-0.73** | -97.3% | 43.5% | 0.45 | 411 | -0.33 |
| 17 | 海龟交易 | 2h | -64.4% | -11.4% | **-1.09** | -67.5% | 31.0% | 0.86 | 1644 | -0.17 |
| 18 | EMA双均线多空 | 6h | -278.8% | +0.0% | **-1.64** | -318.9% | 0.0% | 0.00 | 1 | 0.00 |
> 🏆 **BNBUSDT 最优**:牛熊自适应 6h,夏普 **1.22**,总收益 **+3592.9%**
### BTCUSDT
| 排名 | 策略 | TF | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 交易数 | 卡尔玛 |
|------|------|----|---------|-------|------|-------|-------|--------|--------|--------|
| 🥇 | ATR波动率突破 | 2h | +4550.8% | +54.8% | **1.13** | -52.6% | 31.4% | 1.43 | 334 | 1.04 |
| 🥈 | 三均线排列 | 6h | +86.1% | +7.4% | **0.93** | -13.3% | 37.8% | 1.82 | 148 | 0.56 |
| 🥉 | ATR波动率突破 | 6h | +1509.3% | +37.5% | **0.92** | -55.8% | 31.9% | 1.42 | 94 | 0.67 |
| 4 | 超级趋势 | 6h | +113.3% | +9.1% | **0.83** | -16.2% | 36.6% | 1.65 | 134 | 0.56 |
| 5 | EMA双均线多空 | 6h | +1148.0% | +33.6% | **0.82** | -57.2% | 41.2% | 1.54 | 240 | 0.59 |
| 6 | 牛熊自适应 | 6h | +317.6% | +17.8% | **0.67** | -43.3% | 35.2% | 1.37 | 122 | 0.41 |
| 7 | 牛熊自适应 | 2h | +89.9% | +7.6% | **0.40** | -66.9% | 33.6% | 1.18 | 375 | 0.11 |
| 8 | 三均线排列 | 2h | +14.0% | +1.5% | **0.26** | -26.5% | 33.5% | 1.21 | 412 | 0.06 |
| 9 | 海龟交易 | 6h | +9.6% | +1.1% | **0.17** | -19.1% | 35.1% | 1.13 | 579 | 0.06 |
| 10 | MACD金叉死叉 | 2h | +7.9% | +0.9% | **0.14** | -19.2% | 36.5% | 1.12 | 873 | 0.04 |
| 11 | MACD金叉死叉 | 6h | +6.0% | +0.7% | **0.12** | -28.8% | 37.0% | 1.08 | 316 | 0.02 |
| 12 | 布林收缩爆发 | 6h | -6.8% | -0.8% | **-0.15** | -18.1% | 32.3% | 0.87 | 62 | -0.04 |
| 13 | 布林收缩爆发 | 2h | -7.4% | -0.9% | **-0.19** | -19.0% | 30.8% | 0.90 | 117 | -0.05 |
| 14 | 超级趋势 | 2h | -36.4% | -5.0% | **-0.39** | -48.0% | 32.2% | 0.86 | 401 | -0.10 |
| 15 | EMA双均线多空 | 2h | -99.3% | -43.4% | **-0.48** | -99.4% | 32.8% | 0.67 | 711 | -0.44 |
| 16 | RSI均值回归 | 6h | -85.8% | -20.1% | **-0.52** | -89.8% | 38.1% | 0.77 | 194 | -0.22 |
| 17 | RSI均值回归 | 2h | -98.5% | -38.2% | **-1.35** | -98.8% | 35.8% | 0.52 | 480 | -0.39 |
| 18 | 海龟交易 | 2h | -71.9% | -13.5% | **-1.71** | -72.0% | 29.8% | 0.74 | 1622 | -0.19 |
> 🏆 **BTCUSDT 最优**ATR波动率突破 2h,夏普 **1.13**,总收益 **+4550.8%**
### ETHUSDT
| 排名 | 策略 | TF | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 交易数 | 卡尔玛 |
|------|------|----|---------|-------|------|-------|-------|--------|--------|--------|
| 🥇 | ATR波动率突破 | 6h | +5102.3% | +57.3% | **1.04** | -47.6% | 36.2% | 2.17 | 94 | 1.21 |
| 🥈 | ATR波动率突破 | 2h | +1771.2% | +39.6% | **0.85** | -59.9% | 31.7% | 1.30 | 338 | 0.66 |
| 🥉 | MACD金叉死叉 | 6h | +108.3% | +8.8% | **0.81** | -15.3% | 38.0% | 1.43 | 305 | 0.57 |
| 4 | 牛熊自适应 | 6h | +748.9% | +27.8% | **0.80** | -73.0% | 34.1% | 1.27 | 132 | 0.38 |
| 5 | 三均线排列 | 6h | +81.9% | +7.1% | **0.72** | -18.5% | 35.1% | 1.67 | 154 | 0.38 |
| 6 | MACD金叉死叉 | 2h | +103.5% | +8.4% | **0.67** | -26.8% | 37.2% | 1.26 | 867 | 0.31 |
| 7 | 布林收缩爆发 | 2h | +20.9% | +2.2% | **0.38** | -13.4% | 42.3% | 1.42 | 137 | 0.16 |
| 8 | 海龟交易 | 6h | +16.4% | +1.8% | **0.22** | -28.6% | 37.2% | 1.13 | 596 | 0.06 |
| 9 | 布林收缩爆发 | 6h | +8.4% | +0.9% | **0.19** | -18.4% | 35.7% | 1.23 | 56 | 0.05 |
| 10 | 超级趋势 | 6h | +5.9% | +0.7% | **0.12** | -42.7% | 44.2% | 1.06 | 129 | 0.02 |
| 11 | 牛熊自适应 | 2h | -20.9% | -2.6% | **0.10** | -80.0% | 30.0% | 1.04 | 400 | -0.03 |
| 12 | 超级趋势 | 2h | -13.3% | -1.6% | **-0.04** | -48.1% | 34.1% | 0.99 | 407 | -0.03 |
| 13 | 三均线排列 | 2h | -24.3% | -3.1% | **-0.40** | -37.5% | 28.5% | 0.90 | 418 | -0.08 |
| 14 | EMA双均线多空 | 2h | -99.8% | -52.1% | **-0.60** | -99.9% | 33.0% | 0.57 | 794 | -0.52 |
| 15 | RSI均值回归 | 6h | -98.1% | -36.6% | **-0.87** | -99.0% | 32.7% | 0.65 | 199 | -0.37 |
| 16 | EMA双均线多空 | 6h | -114.3% | +0.0% | **-1.11** | -110.5% | 20.0% | 0.22 | 10 | 0.00 |
| 17 | 海龟交易 | 2h | -64.2% | -11.0% | **-1.18** | -64.8% | 31.2% | 0.85 | 1684 | -0.17 |
| 18 | RSI均值回归 | 2h | -99.7% | -47.9% | **-1.33** | -99.8% | 38.6% | 0.62 | 497 | -0.48 |
> 🏆 **ETHUSDT 最优**ATR波动率突破 6h,夏普 **1.04**,总收益 **+5102.3%**
### SOLUSDT
| 排名 | 策略 | TF | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 交易数 | 卡尔玛 |
|------|------|----|---------|-------|------|-------|-------|--------|--------|--------|
| 🥇 | 超级趋势 | 6h | +620.1% | +41.1% | **1.40** | -37.0% | 39.5% | 2.21 | 86 | 1.11 |
| 🥈 | ATR波动率突破 | 2h | +11666.5% | +127.5% | **1.39** | -59.8% | 37.6% | 1.37 | 202 | 2.13 |
| 🥉 | ATR波动率突破 | 6h | +4450.9% | +94.7% | **1.22** | -84.4% | 23.7% | 1.25 | 59 | 1.12 |
| 4 | MACD金叉死叉 | 6h | +84.6% | +11.3% | **0.73** | -27.7% | 42.4% | 1.34 | 217 | 0.41 |
| 5 | 牛熊自适应 | 6h | +219.6% | +22.5% | **0.68** | -74.0% | 34.6% | 1.43 | 78 | 0.30 |
| 6 | 三均线排列 | 2h | +34.4% | +5.2% | **0.54** | -15.0% | 38.9% | 1.29 | 270 | 0.35 |
| 7 | MACD金叉死叉 | 2h | +49.3% | +7.2% | **0.51** | -25.0% | 38.2% | 1.19 | 617 | 0.29 |
| 8 | 三均线排列 | 6h | +19.2% | +3.1% | **0.34** | -11.8% | 38.4% | 1.32 | 86 | 0.26 |
| 9 | 布林收缩爆发 | 6h | -5.5% | -1.0% | **-0.09** | -18.2% | 26.2% | 0.89 | 42 | -0.05 |
| 10 | 牛熊自适应 | 2h | -62.5% | -15.5% | **-0.21** | -86.1% | 32.5% | 0.92 | 271 | -0.18 |
| 11 | 布林收缩爆发 | 2h | -12.4% | -2.2% | **-0.23** | -27.2% | 29.1% | 0.87 | 134 | -0.08 |
| 12 | 海龟交易 | 6h | -23.3% | -4.5% | **-0.30** | -41.1% | 36.2% | 0.92 | 437 | -0.11 |
| 13 | 超级趋势 | 2h | -47.1% | -10.4% | **-0.52** | -56.6% | 35.8% | 0.76 | 285 | -0.18 |
| 14 | RSI均值回归 | 6h | -95.6% | -42.0% | **-0.61** | -97.0% | 38.4% | 0.60 | 99 | -0.43 |
| 15 | RSI均值回归 | 2h | -89.9% | -32.6% | **-0.65** | -93.0% | 40.4% | 0.82 | 250 | -0.35 |
| 16 | EMA双均线多空 | 2h | -120.9% | +0.0% | **-1.06** | -113.0% | 32.1% | 0.74 | 84 | 0.00 |
| 17 | 海龟交易 | 2h | -60.5% | -14.8% | **-1.13** | -60.9% | 32.6% | 0.86 | 1241 | -0.24 |
| 18 | EMA双均线多空 | 6h | -141.6% | +0.0% | **-1.28** | -153.2% | 29.2% | 0.44 | 24 | 0.00 |
> 🏆 **SOLUSDT 最优**:超级趋势 6h,夏普 **1.40**,总收益 **+620.1%**
---
## 五、2h vs 6h — 周期对比分析
### BNBUSDT
#### 📅 全量
| 策略 | 2h 夏普 | 2h 收益% | 6h 夏普 | 6h 收益% | 更优周期 |
|------|---------|----------|---------|----------|----------|
| 海龟交易 | -1.09 | -64.4% | +0.64 | +96.1% | 6h ✅ |
| 超级趋势 | +0.94 | +512.5% | +0.72 | +288.2% | 2h ✅ |
| MACD金叉死叉 | +0.55 | +97.6% | +0.37 | +43.0% | 2h ✅ |
| 布林收缩爆发 | +0.30 | +25.7% | +0.54 | +51.4% | 6h ✅ |
| 三均线排列 | +0.34 | +32.9% | +0.77 | +93.9% | 6h ✅ |
| RSI均值回归 | -0.73 | -96.5% | -0.08 | -80.3% | 6h ✅ |
| ATR波动率突破 | +0.83 | +2258.5% | +0.67 | +676.2% | 2h ✅ |
| EMA双均线多空 | +0.39 | +35.9% | -1.64 | -278.8% | 2h ✅ |
| 牛熊自适应 | +0.77 | +845.9% | +1.22 | +3592.9% | 6h ✅ |
> 全量:6h 胜 5/92h 胜 4/9
#### 📅 近两年
| 策略 | 2h 夏普 | 2h 收益% | 6h 夏普 | 6h 收益% | 更优周期 |
|------|---------|----------|---------|----------|----------|
| 海龟交易 | -1.61 | -19.0% | -1.04 | -11.8% | 6h ✅ |
| 超级趋势 | -0.26 | -5.5% | -0.15 | -3.3% | 6h ✅ |
| MACD金叉死叉 | -0.66 | -9.9% | -0.29 | -4.8% | 6h ✅ |
| 布林收缩爆发 | -0.21 | -2.1% | +0.54 | +5.2% | 6h ✅ |
| 三均线排列 | -1.54 | -12.6% | +0.30 | +2.7% | 6h ✅ |
| RSI均值回归 | -0.17 | -11.3% | -0.45 | -24.6% | 2h ✅ |
| ATR波动率突破 | -0.02 | -13.1% | +0.09 | -5.5% | 6h ✅ |
| EMA双均线多空 | -0.37 | -32.7% | +0.35 | +9.7% | 6h ✅ |
| 牛熊自适应 | +0.26 | +6.2% | +0.26 | +6.6% | 2h ✅ |
> 近两年:6h 胜 7/92h 胜 2/9
#### 📅 近一年
| 策略 | 2h 夏普 | 2h 收益% | 6h 夏普 | 6h 收益% | 更优周期 |
|------|---------|----------|---------|----------|----------|
| 海龟交易 | -0.67 | -4.7% | -0.88 | -5.4% | 2h ✅ |
| 超级趋势 | -0.37 | -3.4% | -0.91 | -6.9% | 2h ✅ |
| MACD金叉死叉 | +0.98 | +7.1% | -0.52 | -4.0% | 2h ✅ |
| 布林收缩爆发 | -1.04 | -6.0% | -0.77 | -1.7% | 6h ✅ |
| 三均线排列 | -1.78 | -7.1% | +1.13 | +6.3% | 6h ✅ |
| RSI均值回归 | +0.70 | +13.6% | +0.56 | +13.3% | 2h ✅ |
| ATR波动率突破 | +1.21 | +44.0% | +0.76 | +24.5% | 2h ✅ |
| EMA双均线多空 | -0.98 | -31.9% | +0.16 | -5.6% | 6h ✅ |
| 牛熊自适应 | -0.30 | -5.8% | +0.88 | +20.0% | 6h ✅ |
> 近一年:6h 胜 4/92h 胜 5/9
#### 📅 近半年
| 策略 | 2h 夏普 | 2h 收益% | 6h 夏普 | 6h 收益% | 更优周期 |
|------|---------|----------|---------|----------|----------|
| 海龟交易 | +0.13 | +0.1% | -1.75 | -4.5% | 2h ✅ |
| 超级趋势 | -0.41 | -1.4% | +1.84 | +6.6% | 6h ✅ |
| MACD金叉死叉 | +0.34 | +0.6% | -0.05 | -0.3% | 2h ✅ |
| 布林收缩爆发 | +0.46 | +1.2% | -2.50 | -8.9% | 2h ✅ |
| 三均线排列 | -1.09 | -1.5% | +0.61 | +1.1% | 6h ✅ |
| RSI均值回归 | +0.08 | -0.3% | -1.11 | -12.9% | 2h ✅ |
| ATR波动率突破 | +2.05 | +30.5% | +2.08 | +62.9% | 6h ✅ |
| EMA双均线多空 | +0.78 | +7.6% | -0.74 | -7.0% | 2h ✅ |
| 牛熊自适应 | -1.95 | -5.8% | -1.48 | -5.3% | 6h ✅ |
> 近半年:6h 胜 4/92h 胜 5/9
### BTCUSDT
#### 📅 全量
| 策略 | 2h 夏普 | 2h 收益% | 6h 夏普 | 6h 收益% | 更优周期 |
|------|---------|----------|---------|----------|----------|
| 海龟交易 | -1.71 | -71.9% | +0.17 | +9.6% | 6h ✅ |
| 超级趋势 | -0.39 | -36.4% | +0.83 | +113.3% | 6h ✅ |
| MACD金叉死叉 | +0.14 | +7.9% | +0.12 | +6.0% | 2h ✅ |
| 布林收缩爆发 | -0.19 | -7.4% | -0.15 | -6.8% | 6h ✅ |
| 三均线排列 | +0.26 | +14.0% | +0.93 | +86.1% | 6h ✅ |
| RSI均值回归 | -1.35 | -98.5% | -0.52 | -85.8% | 6h ✅ |
| ATR波动率突破 | +1.13 | +4550.8% | +0.92 | +1509.3% | 2h ✅ |
| EMA双均线多空 | -0.48 | -99.3% | +0.82 | +1148.0% | 6h ✅ |
| 牛熊自适应 | +0.40 | +89.9% | +0.67 | +317.6% | 6h ✅ |
> 全量:6h 胜 7/92h 胜 2/9
#### 📅 近两年
| 策略 | 2h 夏普 | 2h 收益% | 6h 夏普 | 6h 收益% | 更优周期 |
|------|---------|----------|---------|----------|----------|
| 海龟交易 | -3.39 | -30.2% | -0.58 | -7.1% | 6h ✅ |
| 超级趋势 | -0.89 | -13.3% | -0.60 | -9.1% | 6h ✅ |
| MACD金叉死叉 | -0.22 | -3.3% | -0.09 | -1.8% | 6h ✅ |
| 布林收缩爆发 | -0.80 | -4.0% | -0.01 | -0.1% | 6h ✅ |
| 三均线排列 | -0.93 | -6.8% | +0.35 | +3.1% | 6h ✅ |
| RSI均值回归 | -0.66 | -28.2% | -0.58 | -23.5% | 6h ✅ |
| ATR波动率突破 | +0.49 | +24.6% | +0.18 | +0.7% | 2h ✅ |
| EMA双均线多空 | -1.54 | -68.9% | +1.27 | +116.6% | 6h ✅ |
| 牛熊自适应 | +0.75 | +31.0% | +0.26 | +6.3% | 2h ✅ |
> 近两年:6h 胜 7/92h 胜 2/9
#### 📅 近一年
| 策略 | 2h 夏普 | 2h 收益% | 6h 夏普 | 6h 收益% | 更优周期 |
|------|---------|----------|---------|----------|----------|
| 海龟交易 | -3.58 | -14.3% | -0.28 | -1.5% | 6h ✅ |
| 超级趋势 | -1.89 | -11.6% | -0.96 | -5.4% | 6h ✅ |
| MACD金叉死叉 | -0.47 | -3.2% | +0.42 | +2.6% | 6h ✅ |
| 布林收缩爆发 | +0.12 | +0.2% | -0.19 | -0.6% | 2h ✅ |
| 三均线排列 | +0.07 | +0.2% | -0.68 | -2.1% | 2h ✅ |
| RSI均值回归 | -0.45 | -10.6% | -0.29 | -7.8% | 6h ✅ |
| ATR波动率突破 | +1.47 | +54.0% | +0.04 | -3.2% | 2h ✅ |
| EMA双均线多空 | -2.55 | -54.7% | -0.24 | -10.4% | 6h ✅ |
| 牛熊自适应 | +0.30 | +3.7% | -1.09 | -14.4% | 2h ✅ |
> 近一年:6h 胜 5/92h 胜 4/9
#### 📅 近半年
| 策略 | 2h 夏普 | 2h 收益% | 6h 夏普 | 6h 收益% | 更优周期 |
|------|---------|----------|---------|----------|----------|
| 海龟交易 | -4.83 | -10.7% | -1.66 | -5.4% | 6h ✅ |
| 超级趋势 | -0.25 | -1.3% | +1.07 | +3.8% | 6h ✅ |
| MACD金叉死叉 | +1.07 | +3.8% | +0.48 | +1.6% | 2h ✅ |
| 布林收缩爆发 | -1.02 | -0.7% | -0.60 | -0.8% | 6h ✅ |
| 三均线排列 | +1.07 | +1.9% | -2.56 | -2.7% | 2h ✅ |
| RSI均值回归 | -0.12 | -2.8% | -1.34 | -16.2% | 2h ✅ |
| ATR波动率突破 | +2.05 | +42.5% | +2.68 | +39.4% | 6h ✅ |
| EMA双均线多空 | -2.97 | -32.3% | -0.75 | -10.4% | 6h ✅ |
| 牛熊自适应 | +0.04 | -0.3% | -1.14 | -4.7% | 2h ✅ |
> 近半年:6h 胜 5/92h 胜 4/9
### ETHUSDT
#### 📅 全量
| 策略 | 2h 夏普 | 2h 收益% | 6h 夏普 | 6h 收益% | 更优周期 |
|------|---------|----------|---------|----------|----------|
| 海龟交易 | -1.18 | -64.2% | +0.22 | +16.4% | 6h ✅ |
| 超级趋势 | -0.04 | -13.3% | +0.12 | +5.9% | 6h ✅ |
| MACD金叉死叉 | +0.67 | +103.5% | +0.81 | +108.3% | 6h ✅ |
| 布林收缩爆发 | +0.38 | +20.9% | +0.19 | +8.4% | 2h ✅ |
| 三均线排列 | -0.40 | -24.3% | +0.72 | +81.9% | 6h ✅ |
| RSI均值回归 | -1.33 | -99.7% | -0.87 | -98.1% | 6h ✅ |
| ATR波动率突破 | +0.85 | +1771.2% | +1.04 | +5102.3% | 6h ✅ |
| EMA双均线多空 | -0.60 | -99.8% | -1.11 | -114.3% | 2h ✅ |
| 牛熊自适应 | +0.10 | -20.9% | +0.80 | +748.9% | 6h ✅ |
> 全量:6h 胜 7/92h 胜 2/9
#### 📅 近两年
| 策略 | 2h 夏普 | 2h 收益% | 6h 夏普 | 6h 收益% | 更优周期 |
|------|---------|----------|---------|----------|----------|
| 海龟交易 | -1.67 | -22.6% | +0.51 | +7.3% | 6h ✅ |
| 超级趋势 | +0.10 | +0.8% | +0.50 | +10.5% | 6h ✅ |
| MACD金叉死叉 | -0.11 | -2.7% | +0.40 | +6.6% | 6h ✅ |
| 布林收缩爆发 | +0.95 | +10.0% | -0.59 | -3.5% | 2h ✅ |
| 三均线排列 | -1.67 | -19.1% | +0.37 | +4.9% | 6h ✅ |
| RSI均值回归 | -1.18 | -55.3% | -0.09 | -15.0% | 6h ✅ |
| ATR波动率突破 | +0.40 | +16.6% | +0.96 | +97.6% | 6h ✅ |
| EMA双均线多空 | -1.24 | -80.9% | -0.26 | -42.3% | 6h ✅ |
| 牛熊自适应 | -1.54 | -58.9% | -0.67 | -32.8% | 6h ✅ |
> 近两年:6h 胜 8/92h 胜 1/9
#### 📅 近一年
| 策略 | 2h 夏普 | 2h 收益% | 6h 夏普 | 6h 收益% | 更优周期 |
|------|---------|----------|---------|----------|----------|
| 海龟交易 | -1.96 | -12.9% | -0.68 | -4.6% | 6h ✅ |
| 超级趋势 | +0.92 | +10.0% | -0.61 | -4.5% | 2h ✅ |
| MACD金叉死叉 | +1.08 | +9.6% | -0.44 | -5.0% | 2h ✅ |
| 布林收缩爆发 | +1.27 | +7.5% | -0.35 | -1.1% | 2h ✅ |
| 三均线排列 | -0.85 | -5.7% | -0.09 | -0.7% | 6h ✅ |
| RSI均值回归 | -0.21 | -10.1% | +0.16 | -0.1% | 6h ✅ |
| ATR波动率突破 | +1.65 | +87.6% | +0.60 | +16.2% | 2h ✅ |
| EMA双均线多空 | -2.07 | -72.4% | -0.40 | -33.3% | 6h ✅ |
| 牛熊自适应 | -1.65 | -32.1% | -1.45 | -26.0% | 6h ✅ |
> 近一年:6h 胜 5/92h 胜 4/9
#### 📅 近半年
| 策略 | 2h 夏普 | 2h 收益% | 6h 夏普 | 6h 收益% | 更优周期 |
|------|---------|----------|---------|----------|----------|
| 海龟交易 | -1.94 | -5.6% | -2.05 | -5.6% | 2h ✅ |
| 超级趋势 | -1.19 | -5.5% | -1.03 | -3.6% | 6h ✅ |
| MACD金叉死叉 | +0.90 | +3.8% | +0.06 | +0.0% | 2h ✅ |
| 布林收缩爆发 | +1.27 | +4.3% | -2.43 | -3.1% | 2h ✅ |
| 三均线排列 | +0.48 | +1.2% | -3.93 | -5.0% | 2h ✅ |
| RSI均值回归 | +1.10 | +14.3% | -0.51 | -12.3% | 2h ✅ |
| ATR波动率突破 | +1.35 | +26.6% | +1.53 | +26.7% | 6h ✅ |
| EMA双均线多空 | -1.10 | -26.5% | -0.77 | -16.6% | 6h ✅ |
| 牛熊自适应 | +0.35 | +2.7% | -4.13 | -23.3% | 2h ✅ |
> 近半年:6h 胜 3/92h 胜 6/9
### SOLUSDT
#### 📅 全量
| 策略 | 2h 夏普 | 2h 收益% | 6h 夏普 | 6h 收益% | 更优周期 |
|------|---------|----------|---------|----------|----------|
| 海龟交易 | -1.13 | -60.5% | -0.30 | -23.3% | 6h ✅ |
| 超级趋势 | -0.52 | -47.1% | +1.40 | +620.1% | 6h ✅ |
| MACD金叉死叉 | +0.51 | +49.3% | +0.73 | +84.6% | 6h ✅ |
| 布林收缩爆发 | -0.23 | -12.4% | -0.09 | -5.5% | 6h ✅ |
| 三均线排列 | +0.54 | +34.4% | +0.34 | +19.2% | 2h ✅ |
| RSI均值回归 | -0.65 | -89.9% | -0.61 | -95.6% | 6h ✅ |
| ATR波动率突破 | +1.39 | +11666.5% | +1.22 | +4450.9% | 2h ✅ |
| EMA双均线多空 | -1.06 | -120.9% | -1.28 | -141.6% | 2h ✅ |
| 牛熊自适应 | -0.21 | -62.5% | +0.68 | +219.6% | 6h ✅ |
> 全量:6h 胜 6/92h 胜 3/9
#### 📅 近两年
| 策略 | 2h 夏普 | 2h 收益% | 6h 夏普 | 6h 收益% | 更优周期 |
|------|---------|----------|---------|----------|----------|
| 海龟交易 | -2.40 | -34.5% | +0.41 | +7.2% | 6h ✅ |
| 超级趋势 | -0.11 | -4.4% | +0.10 | +0.8% | 6h ✅ |
| MACD金叉死叉 | +0.05 | -0.2% | -0.52 | -11.6% | 2h ✅ |
| 布林收缩爆发 | -0.11 | -1.0% | -1.01 | -7.2% | 2h ✅ |
| 三均线排列 | -0.14 | -2.5% | -0.38 | -5.0% | 2h ✅ |
| RSI均值回归 | -0.94 | -51.3% | -1.04 | -53.9% | 2h ✅ |
| ATR波动率突破 | -0.03 | -29.0% | -0.43 | -58.8% | 2h ✅ |
| EMA双均线多空 | -1.30 | -88.3% | +0.16 | -19.8% | 6h ✅ |
| 牛熊自适应 | -0.04 | -12.3% | +0.49 | +21.4% | 6h ✅ |
> 近两年:6h 胜 4/92h 胜 5/9
#### 📅 近一年
| 策略 | 2h 夏普 | 2h 收益% | 6h 夏普 | 6h 收益% | 更优周期 |
|------|---------|----------|---------|----------|----------|
| 海龟交易 | -2.78 | -19.3% | -0.77 | -4.9% | 6h ✅ |
| 超级趋势 | +0.43 | +4.7% | +1.03 | +9.9% | 6h ✅ |
| MACD金叉死叉 | -0.20 | -2.6% | -0.47 | -4.0% | 2h ✅ |
| 布林收缩爆发 | +0.32 | +1.1% | -1.37 | -5.4% | 2h ✅ |
| 三均线排列 | -1.57 | -9.0% | -0.35 | -2.3% | 6h ✅ |
| RSI均值回归 | -1.96 | -45.0% | -0.18 | -16.4% | 6h ✅ |
| ATR波动率突破 | +0.68 | +23.6% | -0.64 | -42.8% | 2h ✅ |
| EMA双均线多空 | -2.70 | -81.6% | -1.08 | -53.8% | 6h ✅ |
| 牛熊自适应 | +0.19 | +1.3% | -0.47 | -15.6% | 2h ✅ |
> 近一年:6h 胜 5/92h 胜 4/9
#### 📅 近半年
| 策略 | 2h 夏普 | 2h 收益% | 6h 夏普 | 6h 收益% | 更优周期 |
|------|---------|----------|---------|----------|----------|
| 海龟交易 | -2.14 | -7.3% | -0.80 | -2.5% | 6h ✅ |
| 超级趋势 | -1.33 | -6.4% | +1.03 | +4.5% | 6h ✅ |
| MACD金叉死叉 | -0.29 | -1.8% | +0.54 | +2.2% | 6h ✅ |
| 布林收缩爆发 | +1.35 | +2.4% | -2.14 | -1.1% | 2h ✅ |
| 三均线排列 | -2.30 | -5.5% | -3.51 | -6.2% | 2h ✅ |
| RSI均值回归 | -1.58 | -24.9% | -1.53 | -24.3% | 6h ✅ |
| ATR波动率突破 | +1.22 | +22.0% | +2.49 | +40.4% | 6h ✅ |
| EMA双均线多空 | -2.08 | -40.2% | -2.61 | -47.9% | 2h ✅ |
| 牛熊自适应 | +1.64 | +18.0% | -2.97 | -20.6% | 2h ✅ |
> 近半年:6h 胜 5/92h 胜 4/9
---
## 六、策略时效性分析 — 全量 vs 近期夏普衰减
> ⚠️ 衰减 = 近一年夏普比全量低 0.5 以上 | ✅ 稳定 = 差值在 ±0.5 | 📈 改善 = 近一年更高
### 海龟交易
| 币种 | TF | 全量夏普 | 近两年 | 近一年 | 近半年 | 趋势 |
|------|----|----------|--------|--------|--------|------|
| BNBUSDT | 2h | -1.09 | -1.61 | -0.67 | +0.13 | ✅ 稳定 |
| BNBUSDT | 6h | +0.64 | -1.04 | -0.88 | -1.75 | ⚠️ 衰减 |
| BTCUSDT | 2h | -1.71 | -3.39 | -3.58 | -4.83 | ⚠️ 衰减 |
| BTCUSDT | 6h | +0.17 | -0.58 | -0.28 | -1.66 | ✅ 稳定 |
| ETHUSDT | 2h | -1.18 | -1.67 | -1.96 | -1.94 | ⚠️ 衰减 |
| ETHUSDT | 6h | +0.22 | +0.51 | -0.68 | -2.05 | ⚠️ 衰减 |
| SOLUSDT | 2h | -1.13 | -2.40 | -2.78 | -2.14 | ⚠️ 衰减 |
| SOLUSDT | 6h | -0.30 | +0.41 | -0.77 | -0.80 | ✅ 稳定 |
### 超级趋势
| 币种 | TF | 全量夏普 | 近两年 | 近一年 | 近半年 | 趋势 |
|------|----|----------|--------|--------|--------|------|
| BNBUSDT | 2h | +0.94 | -0.26 | -0.37 | -0.41 | ⚠️ 衰减 |
| BNBUSDT | 6h | +0.72 | -0.15 | -0.91 | +1.84 | ⚠️ 衰减 |
| BTCUSDT | 2h | -0.39 | -0.89 | -1.89 | -0.25 | ⚠️ 衰减 |
| BTCUSDT | 6h | +0.83 | -0.60 | -0.96 | +1.07 | ⚠️ 衰减 |
| ETHUSDT | 2h | -0.04 | +0.10 | +0.92 | -1.19 | 📈 改善 |
| ETHUSDT | 6h | +0.12 | +0.50 | -0.61 | -1.03 | ⚠️ 衰减 |
| SOLUSDT | 2h | -0.52 | -0.11 | +0.43 | -1.33 | 📈 改善 |
| SOLUSDT | 6h | +1.40 | +0.10 | +1.03 | +1.03 | ✅ 稳定 |
### MACD金叉死叉
| 币种 | TF | 全量夏普 | 近两年 | 近一年 | 近半年 | 趋势 |
|------|----|----------|--------|--------|--------|------|
| BNBUSDT | 2h | +0.55 | -0.66 | +0.98 | +0.34 | ✅ 稳定 |
| BNBUSDT | 6h | +0.37 | -0.29 | -0.52 | -0.05 | ⚠️ 衰减 |
| BTCUSDT | 2h | +0.14 | -0.22 | -0.47 | +1.07 | ⚠️ 衰减 |
| BTCUSDT | 6h | +0.12 | -0.09 | +0.42 | +0.48 | ✅ 稳定 |
| ETHUSDT | 2h | +0.67 | -0.11 | +1.08 | +0.90 | ✅ 稳定 |
| ETHUSDT | 6h | +0.81 | +0.40 | -0.44 | +0.06 | ⚠️ 衰减 |
| SOLUSDT | 2h | +0.51 | +0.05 | -0.20 | -0.29 | ⚠️ 衰减 |
| SOLUSDT | 6h | +0.73 | -0.52 | -0.47 | +0.54 | ⚠️ 衰减 |
### 布林收缩爆发
| 币种 | TF | 全量夏普 | 近两年 | 近一年 | 近半年 | 趋势 |
|------|----|----------|--------|--------|--------|------|
| BNBUSDT | 2h | +0.30 | -0.21 | -1.04 | +0.46 | ⚠️ 衰减 |
| BNBUSDT | 6h | +0.54 | +0.54 | -0.77 | -2.50 | ⚠️ 衰减 |
| BTCUSDT | 2h | -0.19 | -0.80 | +0.12 | -1.02 | ✅ 稳定 |
| BTCUSDT | 6h | -0.15 | -0.01 | -0.19 | -0.60 | ✅ 稳定 |
| ETHUSDT | 2h | +0.38 | +0.95 | +1.27 | +1.27 | 📈 改善 |
| ETHUSDT | 6h | +0.19 | -0.59 | -0.35 | -2.43 | ⚠️ 衰减 |
| SOLUSDT | 2h | -0.23 | -0.11 | +0.32 | +1.35 | 📈 改善 |
| SOLUSDT | 6h | -0.09 | -1.01 | -1.37 | -2.14 | ⚠️ 衰减 |
### 三均线排列
| 币种 | TF | 全量夏普 | 近两年 | 近一年 | 近半年 | 趋势 |
|------|----|----------|--------|--------|--------|------|
| BNBUSDT | 2h | +0.34 | -1.54 | -1.78 | -1.09 | ⚠️ 衰减 |
| BNBUSDT | 6h | +0.77 | +0.30 | +1.13 | +0.61 | ✅ 稳定 |
| BTCUSDT | 2h | +0.26 | -0.93 | +0.07 | +1.07 | ✅ 稳定 |
| BTCUSDT | 6h | +0.93 | +0.35 | -0.68 | -2.56 | ⚠️ 衰减 |
| ETHUSDT | 2h | -0.40 | -1.67 | -0.85 | +0.48 | ✅ 稳定 |
| ETHUSDT | 6h | +0.72 | +0.37 | -0.09 | -3.93 | ⚠️ 衰减 |
| SOLUSDT | 2h | +0.54 | -0.14 | -1.57 | -2.30 | ⚠️ 衰减 |
| SOLUSDT | 6h | +0.34 | -0.38 | -0.35 | -3.51 | ⚠️ 衰减 |
### RSI均值回归
| 币种 | TF | 全量夏普 | 近两年 | 近一年 | 近半年 | 趋势 |
|------|----|----------|--------|--------|--------|------|
| BNBUSDT | 2h | -0.73 | -0.17 | +0.70 | +0.08 | 📈 改善 |
| BNBUSDT | 6h | -0.08 | -0.45 | +0.56 | -1.11 | 📈 改善 |
| BTCUSDT | 2h | -1.35 | -0.66 | -0.45 | -0.12 | 📈 改善 |
| BTCUSDT | 6h | -0.52 | -0.58 | -0.29 | -1.34 | ✅ 稳定 |
| ETHUSDT | 2h | -1.33 | -1.18 | -0.21 | +1.10 | 📈 改善 |
| ETHUSDT | 6h | -0.87 | -0.09 | +0.16 | -0.51 | 📈 改善 |
| SOLUSDT | 2h | -0.65 | -0.94 | -1.96 | -1.58 | ⚠️ 衰减 |
| SOLUSDT | 6h | -0.61 | -1.04 | -0.18 | -1.53 | ✅ 稳定 |
### ATR波动率突破
| 币种 | TF | 全量夏普 | 近两年 | 近一年 | 近半年 | 趋势 |
|------|----|----------|--------|--------|--------|------|
| BNBUSDT | 2h | +0.83 | -0.02 | +1.21 | +2.05 | ✅ 稳定 |
| BNBUSDT | 6h | +0.67 | +0.09 | +0.76 | +2.08 | ✅ 稳定 |
| BTCUSDT | 2h | +1.13 | +0.49 | +1.47 | +2.05 | ✅ 稳定 |
| BTCUSDT | 6h | +0.92 | +0.18 | +0.04 | +2.68 | ⚠️ 衰减 |
| ETHUSDT | 2h | +0.85 | +0.40 | +1.65 | +1.35 | 📈 改善 |
| ETHUSDT | 6h | +1.04 | +0.96 | +0.60 | +1.53 | ✅ 稳定 |
| SOLUSDT | 2h | +1.39 | -0.03 | +0.68 | +1.22 | ⚠️ 衰减 |
| SOLUSDT | 6h | +1.22 | -0.43 | -0.64 | +2.49 | ⚠️ 衰减 |
### EMA双均线多空
| 币种 | TF | 全量夏普 | 近两年 | 近一年 | 近半年 | 趋势 |
|------|----|----------|--------|--------|--------|------|
| BNBUSDT | 2h | +0.39 | -0.37 | -0.98 | +0.78 | ⚠️ 衰减 |
| BNBUSDT | 6h | -1.64 | +0.35 | +0.16 | -0.74 | 📈 改善 |
| BTCUSDT | 2h | -0.48 | -1.54 | -2.55 | -2.97 | ⚠️ 衰减 |
| BTCUSDT | 6h | +0.82 | +1.27 | -0.24 | -0.75 | ⚠️ 衰减 |
| ETHUSDT | 2h | -0.60 | -1.24 | -2.07 | -1.10 | ⚠️ 衰减 |
| ETHUSDT | 6h | -1.11 | -0.26 | -0.40 | -0.77 | 📈 改善 |
| SOLUSDT | 2h | -1.06 | -1.30 | -2.70 | -2.08 | ⚠️ 衰减 |
| SOLUSDT | 6h | -1.28 | +0.16 | -1.08 | -2.61 | ✅ 稳定 |
### 牛熊自适应
| 币种 | TF | 全量夏普 | 近两年 | 近一年 | 近半年 | 趋势 |
|------|----|----------|--------|--------|--------|------|
| BNBUSDT | 2h | +0.77 | +0.26 | -0.30 | -1.95 | ⚠️ 衰减 |
| BNBUSDT | 6h | +1.22 | +0.26 | +0.88 | -1.48 | ✅ 稳定 |
| BTCUSDT | 2h | +0.40 | +0.75 | +0.30 | +0.04 | ✅ 稳定 |
| BTCUSDT | 6h | +0.67 | +0.26 | -1.09 | -1.14 | ⚠️ 衰减 |
| ETHUSDT | 2h | +0.10 | -1.54 | -1.65 | +0.35 | ⚠️ 衰减 |
| ETHUSDT | 6h | +0.80 | -0.67 | -1.45 | -4.13 | ⚠️ 衰减 |
| SOLUSDT | 2h | -0.21 | -0.04 | +0.19 | +1.64 | ✅ 稳定 |
| SOLUSDT | 6h | +0.68 | +0.49 | -0.47 | -2.97 | ⚠️ 衰减 |
---
## 七、全量数据 综合评分 TOP 20
> 综合评分 = 夏普比率×0.4 + 年化收益归一化×0.3 + 卡尔玛归一化×0.2 - 回撤归一化×0.1
| 排名 | 策略 | 币种 | TF | 总收益% | 年化% | 夏普 | 回撤% | 胜率% | 盈亏比 | 综合评分 |
|------|------|------|----|---------|-------|------|-------|-------|--------|----------|
| 🥇 | ATR波动率突破 | SOLUSDT | 2h | +11666.5% | +127.5% | **1.39** | -59.8% | 37.6% | 1.37 | **1.037** |
| 🥈 | ATR波动率突破 | SOLUSDT | 6h | +4450.9% | +94.7% | **1.22** | -84.4% | 23.7% | 1.25 | **0.789** |
| 🥉 | 超级趋势 | SOLUSDT | 6h | +620.1% | +41.1% | **1.40** | -37.0% | 39.5% | 2.21 | **0.749** |
| 4 | 牛熊自适应 | BNBUSDT | 6h | +3592.9% | +52.9% | **1.22** | -44.5% | 37.5% | 1.99 | **0.710** |
| 5 | ATR波动率突破 | BTCUSDT | 2h | +4550.8% | +54.8% | **1.13** | -52.6% | 31.4% | 1.43 | **0.662** |
| 6 | ATR波动率突破 | ETHUSDT | 6h | +5102.3% | +57.3% | **1.04** | -47.6% | 36.2% | 2.17 | **0.650** |
| 7 | ATR波动率突破 | BTCUSDT | 6h | +1509.3% | +37.5% | **0.92** | -55.8% | 31.9% | 1.42 | **0.502** |
| 8 | 超级趋势 | BNBUSDT | 2h | +512.5% | +23.6% | **0.94** | -37.0% | 40.3% | 1.55 | **0.480** |
| 9 | ATR波动率突破 | ETHUSDT | 2h | +1771.2% | +39.6% | **0.85** | -59.9% | 31.7% | 1.30 | **0.476** |
| 10 | ATR波动率突破 | BNBUSDT | 2h | +2258.5% | +44.6% | **0.83** | -72.5% | 29.5% | 1.22 | **0.472** |
| 11 | EMA双均线多空 | BTCUSDT | 6h | +1148.0% | +33.6% | **0.82** | -57.2% | 41.2% | 1.54 | **0.444** |
| 12 | 三均线排列 | BTCUSDT | 6h | +86.1% | +7.4% | **0.93** | -13.3% | 37.8% | 1.82 | **0.438** |
| 13 | 超级趋势 | BTCUSDT | 6h | +113.3% | +9.1% | **0.83** | -16.2% | 36.6% | 1.65 | **0.401** |
| 14 | 牛熊自适应 | BNBUSDT | 2h | +845.9% | +30.0% | **0.77** | -68.2% | 35.1% | 1.25 | **0.399** |
| 15 | 牛熊自适应 | ETHUSDT | 6h | +748.9% | +27.8% | **0.80** | -73.0% | 34.1% | 1.27 | **0.398** |
| 16 | MACD金叉死叉 | ETHUSDT | 6h | +108.3% | +8.8% | **0.81** | -15.3% | 38.0% | 1.43 | **0.393** |
| 17 | 超级趋势 | BNBUSDT | 6h | +288.2% | +17.3% | **0.72** | -35.9% | 40.4% | 2.03 | **0.363** |
| 18 | 三均线排列 | BNBUSDT | 6h | +93.9% | +8.1% | **0.77** | -21.1% | 40.8% | 1.83 | **0.356** |
| 19 | MACD金叉死叉 | SOLUSDT | 6h | +84.6% | +11.3% | **0.73** | -27.7% | 42.4% | 1.34 | **0.348** |
| 20 | 牛熊自适应 | BTCUSDT | 6h | +317.6% | +17.8% | **0.67** | -43.3% | 35.2% | 1.37 | **0.335** |
---
## 八、关键发现
### 🔑 周期选择:6h 显著优于 2h
在全量数据中,**25/36(69%)** 的策略-币种组合在 6h 上夏普比率更高。
**原因分析**:2h 交易频率过高,手续费侵蚀严重。以海龟交易 BTC 为例:
- 2h 全量:1,622 笔交易 → 终值仅 $2,807(亏损 71.9%),夏普 -1.71
- 6h 全量:579 笔交易 → 终值 $10,963(盈利 9.6%),夏普 +0.17
- 差异:2h 比 6h 多产生 1,043 笔交易,多付 2 倍以上手续费
- 手续费成本 = 0.1% × 2(开平)= 0.2% / 笔 × 1,622 笔 ≈ 324% 本金摩擦成本
### 🏆 综合最优策略
**ATR波动率突破** — SOLUSDT 2h
- 总收益:**+11666.5%**
- 年化收益:**+127.5%**
- 夏普比率:**1.39**
- 最大回撤:-59.8%
- 盈亏比:1.37
- 交易次数:202
### 📊 各币种最优策略(全量,按夏普)
| 币种 | 最优策略 | TF | 总收益% | 年化% | 夏普 | 回撤% | 交易数 |
|------|----------|----|---------|-------|------|-------|--------|
| BNBUSDT | **牛熊自适应** | 6h | +3592.9% | +52.9% | **1.22** | -44.5% | 128 |
| BTCUSDT | **ATR波动率突破** | 2h | +4550.8% | +54.8% | **1.13** | -52.6% | 334 |
| ETHUSDT | **ATR波动率突破** | 6h | +5102.3% | +57.3% | **1.04** | -47.6% | 94 |
| SOLUSDT | **超级趋势** | 6h | +620.1% | +41.1% | **1.40** | -37.0% | 86 |
### 💡 ATR 策略对比
| 维度 | 超级趋势(纯ATR跟踪) | ATR波动率突破(squeeze-expand |
|------|----------------------|-------------------------------|
| 最优夏普 | 1.40 (SOLUSDT 6h) | 1.39 (SOLUSDT 2h) |
| 最优总收益 | +620.1% | +11666.5% |
| 参数 | ATR(10)×3.0 | ATR(14) squeeze=20×0.7 + EMA(10,30) |
| 特点 | 简单、始终在场 | 需要 squeeze 检测,非始终在场 |
| 适用场景 | 强趋势市场(BNB SOL 全量表现突出)| 波动率周期明显的市场(SOL ETH 极佳)|
**结论**:ATR波动率突破在高波动币种(SOL)上能产生天文数字级收益(+11,667%),但回撤也极大(-59.8%~-84.4%)。超级趋势更稳定,在 BNB 上夏普 0.94,回撤仅 -37%,适合稳健配置。
### ⚡ 高频策略亏损分析
在 2h 周期上,交易次数 > 500 的组合:
- 亏损组合:6 个(平均交易 1282 次)
- 盈利组合:5 个(平均交易 805 次)
高交易频率 ≠ 高收益。2h 上 MACD金叉死叉是全量 2h 中唯一普遍不亏的策略(BTC 2h 全量 +7.9%),因为它有 zero-line 方向过滤和 3 bar 最小持仓限制。
### 📈 策略类型平均表现(全量,4币种×2TF 平均)
| 类型 | 策略 | 平均夏普 | 平均收益% | 平均回撤% |
|------|------|----------|-----------|-----------|
| 趋势跟踪 | 海龟交易 | -0.55 | -20.3% | -48.5% |
| 趋势跟踪 | 超级趋势 | +0.38 | +180.4% | -40.2% |
| 动量 | MACD金叉死叉 | +0.49 | +62.5% | -24.9% |
| 波动率突破 | 布林收缩爆发 | +0.09 | +9.3% | -19.4% |
| 趋势跟踪 | 三均线排列 | +0.44 | +42.3% | -21.7% |
| 均值回归 | RSI均值回归 | -0.77 | -93.1% | -96.4% |
| 波动率突破 | ATR波动率突破 | +1.01 | +3998.2% | -65.0% |
| 趋势跟踪 | EMA双均线多空 | -0.62 | +41.1% | -130.8% |
| 牛熊自适应 | 牛熊自适应 | +0.55 | +716.4% | -67.0% |
---
*报告由 `comparison_2h_6h_result.json` 自动生成于 2026-06-13 11:36:37 UTC。回测引擎:LongShortEngine,初始本金 $10,000,单边手续费 0.1%,滑点 0.05%。*
*所有收益均为回测模拟结果,不构成投资建议。历史表现不代表未来收益。*
File diff suppressed because it is too large Load Diff
+234
View File
@@ -0,0 +1,234 @@
"""
横截面动量 — 选强弃弱 + 趋势/均值回归入场
策略:
1. 每根 4h K 线,计算 4 个币种过去 N 根 K 线的收益率
2. 按收益率排名,只有前 2 名允许做多
3. 趋势入场:EMA(10,50) 金叉 + 排名前2 → 买入
4. 回归入场:RSI < 35 + 排名前2 → 回调买入
5. 出场:排名跌出前2 或 EMA死叉 或 ATR止损
币种:BTC/ETH/BNB/SOL | 4h | 2024-2026
"""
import asyncio
import sys
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
from engine.data import DataService
from engine.indicators import ema, atr, rsi as calc_rsi
ALL_SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
class CrossSectionConfig(StrategyConfig):
lookback: int = 20 # 排名回溯周期
rank_threshold: int = 2 # 只做前N名
ema_fast: int = 10
ema_slow: int = 50
rsi_period: int = 14
rsi_entry: float = 35.0
atr_stop: float = 2.5
data_start: Optional[datetime] = None
data_end: Optional[datetime] = None
class CrossSectionStrategy(BaseStrategy):
"""横截面动量 — 只做强势币种"""
strategy_type = "cross_section"
def __init__(self, c: CrossSectionConfig):
super().__init__(c)
self.cfg = c
# 所有币种的数据 {symbol: [Kline]}
self._all_klines: dict[str, list[Kline]] = {}
self._all_closes: dict[str, list[float]] = {}
# 当前币种的数据
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._highest: float = 0.0
self._in_position = False
async def on_start(self):
from engine.common.config import config as app_config
ds = DataService(app_config.db)
await ds.connect()
try:
for sym in ALL_SYMBOLS:
klines = await ds.fetch_klines(
symbol=sym, interval="4h",
start_time=self.cfg.data_start, end_time=self.cfg.data_end,
limit=1_000_000,
)
self._all_klines[sym] = klines
self._all_closes[sym] = [k.close for k in klines]
finally:
await ds.close()
await super().on_start()
def _get_rank(self, ts: float) -> dict[str, float]:
"""计算所有币种在指定时间戳的排名收益率,返回 {symbol: return%}"""
scores = {}
for sym in ALL_SYMBOLS:
klines = self._all_klines.get(sym, [])
if not klines:
scores[sym] = -999
continue
# 找到时间戳 <= ts 的最新K线索引
idx = len(klines) - 1
for i in range(len(klines) - 1, -1, -1):
if klines[i].open_time <= ts:
idx = i
break
# 计算过去 lookback 根K线的收益率
start_idx = max(0, idx - self.cfg.lookback)
if start_idx >= idx:
scores[sym] = 0
else:
start_price = self._all_closes[sym][start_idx]
end_price = self._all_closes[sym][idx]
scores[sym] = (end_price / start_price - 1) * 100 if start_price > 0 else 0
return scores
def _my_rank(self, ts: float) -> int:
"""当前币种在全部币种中的排名(1=最强)"""
scores = self._get_rank(ts)
my_score = scores.get(self.cfg.symbol, -999)
# 高于我的分数有几个
better = sum(1 for s in scores.values() if s > my_score)
return better + 1
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
self._highs.append(k.high)
self._lows.append(k.low)
n = len(self._closes)
if n < self.cfg.ema_slow + 10:
return None
fast = ema(self._closes, self.cfg.ema_fast)
slow = ema(self._closes, self.cfg.ema_slow)
atr_vals = atr(self._highs, self._lows, self._closes, 14)
rsi_vals = calc_rsi(self._closes, self.cfg.rsi_period)
cur_f, cur_s = fast[-1], slow[-1]
prev_f, prev_s = fast[-2], slow[-2]
cur_atr = atr_vals[-1]
cur_rsi = rsi_vals[-1]
if cur_f == 0 or cur_s == 0 or cur_atr == 0 or cur_rsi == 0:
return None
rank = self._my_rank(k.open_time)
is_top = rank <= self.cfg.rank_threshold
golden = prev_f <= prev_s and cur_f > cur_s # 趋势入场
death = prev_f >= prev_s and cur_f < cur_s # 趋势出场
oversold = cur_rsi < self.cfg.rsi_entry # 均值回归入场
# ── 出场 ──
if self._in_position:
self._highest = max(self._highest, k.high)
stop = self._highest - self.cfg.atr_stop * cur_atr
if not is_top:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL",
reason=f"排名跌出前{self.cfg.rank_threshold}(#{rank})", timestamp=k.open_time)
if death:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason="EMA死叉", timestamp=k.open_time)
if k.close < stop:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason="ATR止损", timestamp=k.open_time)
# ── 入场 ──
if not self._in_position and is_top:
# 趋势信号:金叉
if golden:
self._in_position = True
self._highest = k.close
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"金叉+#{rank}横截面动量", timestamp=k.open_time)
# 回归信号:RSI超卖
if oversold:
self._in_position = True
self._highest = k.close
return Signal(symbol=self.cfg.symbol, side="BUY",
confidence=0.7, # 回归信号稍降仓位
reason=f"RSI超卖+#{rank}横截面动量 RSI={cur_rsi:.0f}",
timestamp=k.open_time)
return None
# ═══════════════════════════════════════════════
DATE_START = datetime(2024, 1, 1)
DATE_END = datetime(2026, 1, 1)
async def main():
print()
print("" * 110)
print(" 横截面动量 — 只做最强 + 趋势/回归双信号 | 4h | 2024-2026")
print("" * 110)
print(f" {'币种':<10} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} {'盈亏比':>6}")
print("" * 110)
results = {}
for symbol in ALL_SYMBOLS:
sc = CrossSectionConfig(symbol=symbol, data_start=DATE_START, data_end=DATE_END)
bt = BacktestConfig(symbol=symbol, interval="4h",
start_time=DATE_START, end_time=DATE_END, initial_capital=10_000.0)
engine = BacktestEngine(bt, db_config=config.db)
r = await engine.run(CrossSectionStrategy, sc)
m = r.metrics
results[symbol] = (m, r)
# 统计排名分布和信号类型
trend_signals = sum(1 for t in r.trades if t.side == "BUY" and "金叉" in t.reason)
meanrev_signals = sum(1 for t in r.trades if t.side == "BUY" and "RSI" in t.reason)
exits_rank = sum(1 for t in r.trades if t.side == "SELL" and "排名" in t.reason)
print(f" {symbol:<10} {m.total_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}% {m.profit_factor:>6.2f}")
print(f" {'':<10} └ 趋势入场:{trend_signals} 回归入场:{meanrev_signals} 排名出场:{exits_rank}")
sells = [t for t in r.trades if t.side == "SELL" and t.pnl is not None]
for t in sells[-2:]:
dt = datetime.fromtimestamp(t.timestamp / 1000, tz=timezone.utc).strftime("%m-%d %H:%M")
print(f" {'':<10}{dt} {t.pnl:>+8.2f} {t.reason}")
# ── 对比 ──
print("" * 110)
print("\n ■ 对比:纯趋势跟踪 vs 横截面动量")
TREND = {
"BTCUSDT": ("EMA v3(10,50)", 39.9, 1.03, 20),
"ETHUSDT": ("EMA v3(10,75)", 53.6, 1.04, 18),
"BNBUSDT": ("EMA v1(20,50)", 52.0, 0.71, 41),
"SOLUSDT": ("EMA v3(30,50)", 73.6, 1.18, 13),
}
print(f" {'币种':<10} {'纯趋势':>24}{'横截面动量':>24}")
print(f" {'':<10} {'收益% 夏普 交易':>24}{'收益% 夏普 交易':>24}")
for sym in ALL_SYMBOLS:
t_name, t_ret, t_sh, t_tr = TREND[sym]
m, r = results[sym]
print(f" {sym:<10} {t_ret:>5.1f}% {t_sh:>5.2f} {t_tr:>4}次 → {m.total_return_pct:>5.1f}% {m.sharpe_ratio:>5.2f} {m.total_trades:>4}")
print("\n" * 110)
if __name__ == "__main__":
asyncio.run(main())
+62
View File
@@ -0,0 +1,62 @@
"""
DataService 使用示例 — 读取各周期 K 线并打印
用法:
python example/data.py
"""
import asyncio
import sys
from pathlib import Path
# 确保项目根目录在 sys.path 中,以便使用 engine 包导入
_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.data import DataService
from engine.common.config import config
async def main():
print(config.db)
ds = DataService(config.db)
await ds.connect()
try:
# 1. 查看可用交易对
symbols = await ds.fetch_available_symbols("1m")
print(f"可用交易对: {symbols}")
# 2. 各周期读取最新 3 条 BTCUSDT K 线
target = "BTCUSDT"
for interval in ["1m", "5m", "15m", "30m", "1h", "4h", "1d"]:
klines = await ds.fetch_klines(
symbol=target,
interval=interval,
limit=3,
)
print(f"\n{'' * 70}")
print(f" [{interval}] {target}{len(klines)}")
print(f"{'' * 70}")
for k in klines:
print(
f" {k.open_time:.0f} O={k.open:>12.4f} H={k.high:>12.4f}"
f" L={k.low:>12.4f} C={k.close:>12.4f}"
f" V={k.volume:>10.4f} trades={k.trade_count}"
)
# 3. 日期范围
start, end = await ds.fetch_symbol_date_range(target, "1d")
print(f"\n{target} 1d 数据范围: {start} ~ {end}")
print("\n完成")
finally:
await ds.close()
if __name__ == "__main__":
asyncio.run(main())
+253
View File
@@ -0,0 +1,253 @@
"""
多因子组合回测 — 三重共振策略
随机挑选 3 个技术指标组合成一个策略:
- MACD (趋势因子) — 金叉/死叉判断方向
- RSI (动量因子) — 阈值过滤避免追高抄底
- Bollinger (波动率因子) — 中轨确认趋势强度
用法:
source .venv/bin/activate && python example/factor_demo.py
"""
import asyncio
import sys
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
from engine.indicators import macd, rsi, bollinger, atr
# ============================================================
# 三重共振策略
# ============================================================
class TripleFactorConfig(StrategyConfig):
"""三重因子组合策略配置"""
# MACD
macd_fast: int = 12
macd_slow: int = 26
macd_signal: int = 9
# RSI
rsi_period: int = 14
rsi_oversold: float = 30.0 # 超卖线(入场需在此之上)
rsi_overbought: float = 65.0 # 入场过热线(入场需在此之下)
rsi_exit: float = 75.0 # 卖出线
# Bollinger
bb_period: int = 20
bb_std: float = 2.0
# ATR 动态止损倍数(0 表示不启用)
atr_period: int = 14
atr_stop_mult: float = 2.0
class TripleFactorStrategy(BaseStrategy):
"""三重共振策略
┌─────────────┬──────────────────────────────────────┐
│ 因子 │ 作用 │
├─────────────┼──────────────────────────────────────┤
│ MACD (趋势) │ 金叉=看多入场信号,死叉=看空出场信号 │
│ RSI (动量) │ 30<RSI<65 区间入场,RSI>75 过热出场 │
│ BB (波动) │ 价格>中轨确认多头趋势,跌破下轨出场 │
└─────────────┴──────────────────────────────────────┘
入场(三重共振):
1. MACD 金叉(上穿信号线)
2. RSI 在 [30, 65] 区间(合理动量)
3. 价格 > 布林中轨(趋势向上)
出场(任一触发):
1. MACD 死叉(下穿信号线)
2. RSI > 75(过热)
3. 价格 < 布林下轨(趋势破位)
"""
strategy_type = "triple_factor"
def __init__(self, config: TripleFactorConfig):
super().__init__(config)
self.cfg: TripleFactorConfig = config
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
async def on_kline(self, kline: Kline) -> Optional[Signal]:
self._closes.append(kline.close)
self._highs.append(kline.high)
self._lows.append(kline.low)
n = len(self._closes)
max_period = max(
self.cfg.macd_slow + self.cfg.macd_signal,
self.cfg.rsi_period + 1,
self.cfg.bb_period,
)
if n < max_period:
return None
# ── 全量计算因子(每个 bar 一次)──
macd_line, signal_line, _hist = macd(
self._closes,
fast=self.cfg.macd_fast,
slow=self.cfg.macd_slow,
signal=self.cfg.macd_signal,
)
rsi_vals = rsi(self._closes, period=self.cfg.rsi_period)
_upper, mid, lower = bollinger(
self._closes,
period=self.cfg.bb_period,
std=self.cfg.bb_std,
)
# 当前值和前一根的值
cur_macd = macd_line[-1]
cur_signal = signal_line[-1]
prev_macd = macd_line[-2]
prev_signal = signal_line[-2]
cur_rsi = rsi_vals[-1]
prev_rsi = rsi_vals[-2]
cur_mid = mid[-1]
cur_lower = lower[-1]
cur_price = kline.close
if cur_macd == 0.0 or cur_rsi == 0.0 or cur_mid == 0.0:
return None
# ── 入场:2/3 共振即可 ──
golden_cross = prev_macd <= prev_signal and cur_macd > cur_signal
rsi_ok = self.cfg.rsi_oversold < cur_rsi < self.cfg.rsi_overbought
above_mid = cur_price > cur_mid
score = golden_cross + rsi_ok + above_mid
if score >= 2:
return Signal(
symbol=self.cfg.symbol,
side="BUY",
signal_type="MARKET",
confidence=0.6 + score * 0.1,
reason=(
f"{score}/3共振"
f"{' MACD金叉' if golden_cross else ''}"
f"{' RSI=' + str(round(cur_rsi, 1)) if rsi_ok else ''}"
f"{' Price>BBmid' if above_mid else ''}"
),
timestamp=kline.open_time,
)
# ── 出场:任一强信号 ──
death_cross = prev_macd >= prev_signal and cur_macd < cur_signal
rsi_overheat = cur_rsi > self.cfg.rsi_exit
below_lower = cur_price < cur_lower
if death_cross:
return Signal(
symbol=self.cfg.symbol,
side="SELL",
signal_type="MARKET",
confidence=0.8,
reason="MACD死叉",
timestamp=kline.open_time,
)
if rsi_overheat and cur_rsi > prev_rsi:
return Signal(
symbol=self.cfg.symbol,
side="SELL",
signal_type="MARKET",
confidence=0.7,
reason=f"RSI过热({cur_rsi:.1f})",
timestamp=kline.open_time,
)
if below_lower and cur_price < cur_mid:
return Signal(
symbol=self.cfg.symbol,
side="SELL",
signal_type="MARKET",
confidence=0.6,
reason=f"跌破BB下轨({cur_lower:.2f})",
timestamp=kline.open_time,
)
return None
# ============================================================
# 主函数
# ============================================================
async def main():
bt_config = BacktestConfig(
symbol="BTCUSDT",
interval="4h",
start_time=datetime(2024, 1, 1),
end_time=datetime(2026, 1, 1),
initial_capital=10_000.0,
commission_pct=0.001,
slippage_pct=0.0005,
warmup_bars=100,
)
strategy_config = TripleFactorConfig(
name="triple_factor_btc",
symbol="BTCUSDT",
macd_fast=12,
macd_slow=26,
macd_signal=9,
rsi_period=14,
rsi_oversold=30.0,
rsi_overbought=65.0,
rsi_exit=75.0,
bb_period=20,
bb_std=2.0,
)
print()
print("" + "" * 58 + "")
print("" + " 多因子组合回测 — 三重共振策略".center(52) + "")
print("" + "" * 58 + "")
print(f"{'交易对:':<8} {bt_config.symbol:<12} {'周期:':<6} {bt_config.interval:<10}")
print(f"{'时间:':<8} {bt_config.start_time.date()} ~ {bt_config.end_time.date()}")
print(f"{'初始资金:':<8} {bt_config.initial_capital:>12.2f} USDT ║")
print("" + "" * 58 + "")
print("║ 因子组合: ║")
print(f"║ 1. MACD({strategy_config.macd_fast},{strategy_config.macd_slow},{strategy_config.macd_signal}) — 趋势方向 ║")
print(f"║ 2. RSI({strategy_config.rsi_period}) — 动量过滤 ║")
print(f"║ 3. Bollinger({strategy_config.bb_period},{strategy_config.bb_std}) — 波动率确认 ║")
print("" + "" * 58 + "")
print()
engine = BacktestEngine(bt_config, db_config=config.db)
result = await engine.run(TripleFactorStrategy, strategy_config)
print(result.summary())
# 打印全部交易
if result.trades:
print(f"\n全部交易 ({len(result.trades)} 笔):")
print(f"{'时间':<22} {'方向':<6} {'价格':>10} {'数量':>10} {'盈亏':>10} 原因")
print("-" * 100)
for t in result.trades:
dt = datetime.fromtimestamp(t.timestamp / 1000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M")
pnl_str = f"{t.pnl:+.4f}" if t.pnl is not None else ""
print(f"{dt:<22} {t.side:<6} {t.price:>10.2f} {t.quantity:>10.6f} {pnl_str:>10} {t.reason}")
print("\n回测完成。")
if __name__ == "__main__":
asyncio.run(main())
+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
+152
View File
@@ -0,0 +1,152 @@
"""
全周期回测 — 2017-2026,覆盖完整牛熊
多空双向 EMA 趋势跟踪,展示牛市/熊市/全周期分段表现。
用法:
source .venv/bin/activate && python example/full_cycle.py
"""
import asyncio
import sys
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.indicators import ema, atr
from engine.example.long_short import LongShortEngine, LongShortEmaConfig, LongShortEmaStrategy
# ════════════════════════════════════════════════════════
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
PARAMS = {
"BTCUSDT": (10, 50),
"ETHUSDT": (10, 75),
"BNBUSDT": (20, 50),
"SOLUSDT": (30, 50),
}
# 牛熊分段(以 BTC 为参考)
PERIODS = [
("2017-2018 牛市", datetime(2017, 1, 1), datetime(2018, 1, 1)),
("2018 熊市", datetime(2018, 1, 1), datetime(2019, 1, 1)),
("2019 反弹", datetime(2019, 1, 1), datetime(2020, 1, 1)),
("2020 牛初+312", datetime(2020, 1, 1), datetime(2021, 1, 1)),
("2021 牛市", datetime(2021, 1, 1), datetime(2022, 1, 1)),
("2022 熊市", datetime(2022, 1, 1), datetime(2023, 1, 1)),
("2023 复苏", datetime(2023, 1, 1), datetime(2024, 1, 1)),
("2024-2025 牛市", datetime(2024, 1, 1), datetime(2026, 1, 1)),
]
DATE_START = datetime(2017, 1, 1)
DATE_END = datetime(2026, 1, 1)
async def run_backtest(symbol, fast, slow, start, end):
sc = LongShortEmaConfig(symbol=symbol, fast=fast, slow=slow)
bt = BacktestConfig(symbol=symbol, interval="4h", start_time=start, end_time=end,
initial_capital=10_000.0)
engine = LongShortEngine(bt, db_config=config.db)
return await engine.run(LongShortEmaStrategy, sc)
async def main():
print()
print("" * 125)
print(" 全周期多空回测 — 2017-2026 完整牛熊 | 4h EMA趋势")
print("" * 125)
# ── 全周期汇总 ──
print(f"\n ■ 全周期 2017-2026 汇总")
print(f" {'币种':<10} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'多头P&L':>10} {'空头P&L':>10}")
print(" " + "" * 105)
for symbol in SYMBOLS:
fast, slow = PARAMS[symbol]
r = await run_backtest(symbol, fast, slow, DATE_START, DATE_END)
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} {m.total_return_pct:>6.1f}% {m.annual_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {long_pnl:>+9.0f} {short_pnl:>+9.0f}")
# ── BTC 分段 ──
print(f"\n ■ BTC 各阶段表现 (参数 EMA{10},{50})")
print(f" {'阶段':<22} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'多头P&L':>10} {'空头P&L':>10}")
print(" " + "" * 105)
for period_name, p_start, p_end in PERIODS:
try:
r = await run_backtest("BTCUSDT", 10, 50, p_start, p_end)
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" {period_name:<22} {m.total_return_pct:>6.1f}% {m.annual_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {long_pnl:>+9.0f} {short_pnl:>+9.0f}")
except Exception as e:
print(f" {period_name:<22} 数据不足或错误: {e}")
# ── 只做多 vs 多空全周期对比 ──
print(f"\n ■ BTC 只做多 vs 多空 (全周期)")
# 只做多需要单跑一次(LongShortEngine 本身就支持只做多:不开空就行)
# 简单做法:用原版 BacktestEngine 跑一次只做多
from engine.backtest import BacktestEngine
from engine.common.base import BaseStrategy as BS, Signal as Sig, StrategyConfig as SC
class LongOnlyEMAConfig(SC):
fast: int = 10; slow: int = 50; atr_stop: float = 2.5
class LongOnlyEMAStrategy(BS):
strategy_type = "long_only"
def __init__(self, c): super().__init__(c); self.cfg = c
async def on_start(self): self._c = []; self._h = []; self._l = []; self._hp = 0.0; self._in = False; await super().on_start()
async def on_kline(self, k):
self._c.append(k.close); self._h.append(k.high); self._l.append(k.low)
n = len(self._c)
if n < self.cfg.slow + 5: return None
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 = f[-1], s[-1], a[-1]; pf, ps = f[-2], s[-2]
if cf == 0 or cs == 0 or ca == 0: return None
if self._in:
self._hp = max(self._hp, k.high); stop = self._hp - self.cfg.atr_stop * ca
if (pf >= ps and cf < cs) or k.close < stop:
self._in = False
return Sig(symbol=self.cfg.symbol, side="SELL", reason="死叉" if pf >= ps else "ATR止损", timestamp=k.open_time)
else:
if pf <= ps and cf > cs:
self._in = True; self._hp = k.close
return Sig(symbol=self.cfg.symbol, side="BUY", reason="金叉", timestamp=k.open_time)
return None
lo_sc = LongOnlyEMAConfig(symbol="BTCUSDT")
lo_bt = BacktestConfig(symbol="BTCUSDT", interval="4h", start_time=DATE_START, end_time=DATE_END, initial_capital=10_000.0)
lo_eng = BacktestEngine(lo_bt, db_config=config.db)
lo_r = await lo_eng.run(LongOnlyEMAStrategy, lo_sc)
lo_m = lo_r.metrics
# 多空
ls_r = await run_backtest("BTCUSDT", 10, 50, DATE_START, DATE_END)
ls_m = ls_r.metrics
print(f" {'':<10} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5}")
print(f" {'只做多':<10} {lo_m.total_return_pct:>6.1f}% {lo_m.annual_return_pct:>6.1f}% {lo_m.sharpe_ratio:>6.2f} {lo_m.max_drawdown_pct:>6.1f}% {lo_m.total_trades:>5}")
print(f" {'多空':<10} {ls_m.total_return_pct:>6.1f}% {ls_m.annual_return_pct:>6.1f}% {ls_m.sharpe_ratio:>6.2f} {ls_m.max_drawdown_pct:>6.1f}% {ls_m.total_trades:>5}")
print("\n" * 125)
if __name__ == "__main__":
asyncio.run(main())
+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())
+508
View File
@@ -0,0 +1,508 @@
"""
多空双向回测 — EMA 趋势跟踪(支持做空)
基于表现最好的纯趋势参数,增加做空能力:
- 金叉 → 平空仓 + 做多
- 死叉 → 平多仓 + 做空
- ATR 动态止损(多空双向)
- 始终持仓(非多即空)
- 输出增加年化收益
参数(各币种历史最优):
BTC(10,50) ETH(10,75) BNB(20,50) SOL(30,50)
用法:
source .venv/bin/activate && python example/long_short.py
"""
import asyncio
import statistics
import sys
from collections import defaultdict
from datetime import datetime, 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, DBConfig
from engine.data import DataService
from engine.indicators.incremental import EmaInc, AtrInc
from engine.backtest.models import BacktestConfig, BacktestMetrics, BacktestResult, BacktestTrade
# ════════════════════════════════════════════════════════
# 多空回测引擎
# ════════════════════════════════════════════════════════
class LongShortEngine:
"""支持多空双向的事件驱动回测引擎"""
def __init__(self, bt_config: BacktestConfig, db_config=None):
self.config = bt_config
self._db_config = db_config
self._cash: float = bt_config.initial_capital
self._position: float = 0.0 # >0 多头, <0 空头, =0 空仓
self._avg_entry_price: float = 0.0
self._trades: list[BacktestTrade] = []
self._equity: list[dict] = []
self._peak_equity: float = 0.0
self._pending_buy: Optional[Signal] = None
self._pending_sell: Optional[Signal] = None
async def run(self, strategy_cls, strategy_config: StrategyConfig) -> BacktestResult:
from engine.common.config import config as app_config
strategy_config.symbol = self.config.symbol
strategy_config.exchange = self.config.exchange
db_cfg = self._db_config or app_config.db
ds = DataService(db_cfg)
await ds.connect()
try:
klines = await ds.fetch_klines(
symbol=self.config.symbol, interval=self.config.interval,
start_time=self.config.start_time, end_time=self.config.end_time,
limit=1_000_000,
)
if len(klines) < self.config.warmup_bars + 2:
raise ValueError(f"数据不足:需 {self.config.warmup_bars+2},实际 {len(klines)}")
strategy = strategy_cls(strategy_config)
await strategy.on_start()
self._cash = self.config.initial_capital
self._position = 0.0
self._avg_entry_price = 0.0
self._trades = []
self._equity = []
self._pending_buy = None
self._pending_sell = None
warmup_end = self.config.warmup_bars
for i in range(warmup_end):
await strategy.on_kline(klines[i])
for i in range(warmup_end, len(klines)):
kline = klines[i]
# 先执行待执行订单(下一根 bar 开盘价)
if self._pending_buy is not None:
self._execute_buy(self._pending_buy, kline)
self._pending_buy = None
if self._pending_sell is not None:
self._execute_sell(self._pending_sell, kline)
self._pending_sell = None
signal = await strategy.on_kline(kline)
if signal is not None and signal.side == "BUY":
self._pending_buy = signal
elif signal is not None and signal.side == "SELL":
self._pending_sell = signal
self._record_equity(kline)
# 强平
if self._position != 0 and len(klines) > 0:
last_k = klines[-1]
if self._position > 0:
self._execute_sell(Signal(symbol=self.config.symbol, side="SELL",
quantity=abs(self._position),
reason="回测结束—强平多仓", timestamp=last_k.open_time), last_k)
else:
self._execute_buy(Signal(symbol=self.config.symbol, side="BUY",
quantity=abs(self._position),
reason="回测结束—强平空仓", timestamp=last_k.open_time), last_k)
await strategy.on_stop()
metrics = self._compute_metrics()
return BacktestResult(config=self.config, strategy_config=strategy_config.model_dump(),
metrics=metrics, trades=self._trades, equity_curve=self._equity)
finally:
await ds.close()
# ── 交易执行 ──
def _execute_buy(self, signal: Signal, kline: Kline) -> None:
exec_price = kline.open * (1 + self.config.slippage_pct)
qty = signal.quantity
if qty is None:
if self._position < 0:
qty = abs(self._position) # 平空仓
else:
max_notional = self._cash * signal.confidence
qty = max_notional / exec_price
qty = self._round_qty(qty)
if qty < self.config.min_order_qty:
return
notional = exec_price * qty
commission = notional * self.config.commission_pct
if self._position < 0:
# 平空仓
cover_qty = min(qty, abs(self._position))
cover_notional = exec_price * cover_qty
cover_comm = cover_notional * self.config.commission_pct
pnl = (self._avg_entry_price - exec_price) * cover_qty - cover_comm
self._cash -= cover_notional + cover_comm
self._position += cover_qty
if abs(self._position) < self.config.min_order_qty:
self._position = 0.0
self._avg_entry_price = 0.0
self._trades.append(BacktestTrade(timestamp=kline.open_time, symbol=self.config.symbol,
side="BUY", price=exec_price, quantity=cover_qty,
notional=cover_notional, commission=cover_comm,
slippage=exec_price - kline.open, pnl=pnl,
reason=signal.reason))
# 剩余开多
remaining = qty - cover_qty
if remaining >= self.config.min_order_qty:
self._open_long(remaining, exec_price, kline, signal)
else:
# 开多 / 加仓
self._open_long(qty, exec_price, kline, signal)
def _open_long(self, qty: float, exec_price: float, kline: Kline, signal: Signal):
notional = exec_price * qty
commission = notional * self.config.commission_pct
total_cost = notional + commission
if total_cost > self._cash:
qty = self._round_qty(self._cash / (exec_price * (1 + self.config.commission_pct)))
if qty < self.config.min_order_qty:
return
notional = exec_price * qty
commission = notional * self.config.commission_pct
total_cost = notional + commission
if self._position > 0:
total_value = self._avg_entry_price * self._position + notional
self._position += qty
self._avg_entry_price = total_value / self._position
else:
self._position = qty
self._avg_entry_price = exec_price
self._cash -= total_cost
self._trades.append(BacktestTrade(timestamp=kline.open_time, symbol=self.config.symbol,
side="BUY", price=exec_price, quantity=qty,
notional=notional, commission=commission,
slippage=exec_price - kline.open, reason=signal.reason))
def _execute_sell(self, signal: Signal, kline: Kline) -> None:
exec_price = kline.close * (1 - self.config.slippage_pct)
qty = signal.quantity
if qty is None:
if self._position > 0:
qty = self._position
else:
max_notional = self._cash * signal.confidence
qty = max_notional / exec_price
qty = self._round_qty(qty)
if qty < self.config.min_order_qty:
return
notional = exec_price * qty
commission = notional * self.config.commission_pct
if self._position > 0:
# 平多仓
close_qty = min(qty, self._position)
close_notional = exec_price * close_qty
close_comm = close_notional * self.config.commission_pct
pnl = (exec_price - self._avg_entry_price) * close_qty - close_comm
self._position -= close_qty
self._cash += close_notional - close_comm
if self._position < self.config.min_order_qty:
self._position = 0.0
self._avg_entry_price = 0.0
self._trades.append(BacktestTrade(timestamp=kline.open_time, symbol=self.config.symbol,
side="SELL", price=exec_price, quantity=close_qty,
notional=close_notional, commission=close_comm,
slippage=kline.close - exec_price, pnl=pnl,
reason=signal.reason))
# 剩余开空
remaining = qty - close_qty
if remaining >= self.config.min_order_qty:
self._open_short(remaining, exec_price, kline, signal)
else:
self._open_short(qty, exec_price, kline, signal)
def _open_short(self, qty: float, exec_price: float, kline: Kline, signal: Signal):
notional = exec_price * qty
commission = notional * self.config.commission_pct
if self._position < 0:
total_value = self._avg_entry_price * abs(self._position) + notional
self._position -= qty
self._avg_entry_price = total_value / abs(self._position)
else:
self._position = -qty
self._avg_entry_price = exec_price
self._cash += notional - commission
self._trades.append(BacktestTrade(timestamp=kline.open_time, symbol=self.config.symbol,
side="SELL", price=exec_price, quantity=qty,
notional=notional, commission=commission,
slippage=kline.close - exec_price, reason=signal.reason))
# ── 资金曲线 ──
def _record_equity(self, kline: Kline) -> None:
equity = self._cash + self._position * kline.close
if not self._equity:
self._peak_equity = equity
elif equity > self._peak_equity:
self._peak_equity = equity
dd = (equity - self._peak_equity) / self._peak_equity * 100 if self._peak_equity > 0 else 0.0
self._equity.append({"timestamp": kline.open_time, "equity": equity,
"drawdown": dd, "position": self._position})
# ── 绩效 ──
def _compute_metrics(self) -> BacktestMetrics:
if not self._equity:
return BacktestMetrics()
initial = self.config.initial_capital
final = self._equity[-1]["equity"]
total_return_pct = (final - initial) / initial * 100
first_ts = self._equity[0]["timestamp"]
last_ts = self._equity[-1]["timestamp"]
days = (last_ts - first_ts) / (1000 * 86400)
if days > 0 and final > 0 and initial > 0:
annual_return_pct = ((final / initial) ** (365 / days) - 1) * 100
else:
annual_return_pct = 0.0
daily_returns = self._compute_daily_returns()
if len(daily_returns) > 1:
mean_ret = statistics.mean(daily_returns)
std_ret = statistics.stdev(daily_returns)
sharpe_ratio = (mean_ret / std_ret * (365 ** 0.5)) if std_ret > 0 else 0.0
else:
sharpe_ratio = 0.0
max_dd_pct, max_dd_days = self._compute_max_drawdown()
closed = [t for t in self._trades if t.pnl is not None]
total_trades = len(closed)
if total_trades > 0:
winners = [t for t in closed if t.pnl > 0]
losers = [t for t in closed if t.pnl <= 0]
win_rate = len(winners) / total_trades
gp = sum(t.pnl for t in winners)
gl = abs(sum(t.pnl for t in losers))
profit_factor = gp / gl if gl > 0 else (gp if gp > 0 else 0.0)
avg_pnl = sum(t.pnl for t in closed) / total_trades
best_pnl = max(t.pnl for t in closed)
worst_pnl = min(t.pnl for t in closed)
else:
win_rate = profit_factor = avg_pnl = best_pnl = worst_pnl = 0.0
calmar = annual_return_pct / abs(max_dd_pct) if max_dd_pct < 0 else 0.0
return BacktestMetrics(
total_return_pct=total_return_pct, annual_return_pct=annual_return_pct,
sharpe_ratio=sharpe_ratio, max_drawdown_pct=max_dd_pct,
max_drawdown_duration_days=max_dd_days, win_rate=win_rate,
profit_factor=profit_factor, total_trades=total_trades,
avg_trade_pnl=avg_pnl, best_trade_pnl=best_pnl, worst_trade_pnl=worst_pnl,
calmar_ratio=calmar, final_equity=final,
)
def _compute_daily_returns(self) -> list[float]:
if not self._equity:
return []
daily: dict[str, float] = {}
for point in self._equity:
dt = datetime.fromtimestamp(point["timestamp"] / 1000, tz=timezone.utc)
daily[dt.strftime("%Y-%m-%d")] = point["equity"]
sorted_dates = sorted(daily.keys())
returns = []
for i in range(1, len(sorted_dates)):
prev = daily[sorted_dates[i - 1]]
curr = daily[sorted_dates[i]]
if prev > 0:
returns.append((curr - prev) / prev)
return returns
def _compute_max_drawdown(self) -> tuple[float, int]:
if not self._equity:
return 0.0, 0
peak = self._equity[0]["equity"]
max_dd = 0.0
dd_start_idx = 0
max_dd_days = 0
for i, point in enumerate(self._equity):
equity = point["equity"]
if equity > peak:
peak = equity
dd_start_idx = i
dd = (equity - peak) / peak * 100
if dd < max_dd:
max_dd = dd
peak_ts = self._equity[dd_start_idx]["timestamp"]
dd_days = int((point["timestamp"] - peak_ts) / (1000 * 86400))
if dd_days > max_dd_days:
max_dd_days = dd_days
return max_dd, max_dd_days
@staticmethod
def _round_qty(qty: float, decimals: int = 8) -> float:
factor = 10 ** decimals
return int(qty * factor) / factor
# ════════════════════════════════════════════════════════
# 多空趋势策略
# ════════════════════════════════════════════════════════
class LongShortEmaConfig(StrategyConfig):
fast: int = 10
slow: int = 50
atr_stop: float = 2.5
class LongShortEmaStrategy(BaseStrategy):
"""EMA金叉做多、死叉做空,始终在场 — 全部指标增量计算"""
strategy_type = "long_short_ema"
def __init__(self, c: LongShortEmaConfig):
super().__init__(c)
self.cfg = c
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"
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
# ════════════════════════════════════════════════════════
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
# 各币种历史最优参数
PARAMS = {
"BTCUSDT": (10, 50),
"ETHUSDT": (10, 75),
"BNBUSDT": (20, 50),
"SOLUSDT": (30, 50),
}
# 只做多结果(用于对比)
LONG_ONLY = {
"BTCUSDT": (39.9, 1.03, -11.5, 18.3),
"ETHUSDT": (53.6, 1.04, -15.3, 23.9),
"BNBUSDT": (52.0, 0.71, -39.8, 23.3),
"SOLUSDT": (73.6, 1.18, -25.7, 31.7),
}
# (总收益%, 夏普, 回撤%, 年化%)
DATE_START = datetime(2024, 1, 1)
DATE_END = datetime(2026, 1, 1)
async def main():
print()
print("" * 112)
print(" 多空双向 EMA 趋势跟踪 | 4h | 2024-2026")
print("" * 112)
header = f" {'币种':<10} {'方向':<6} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} {'盈亏比':>6}"
print(header)
print("" * 112)
for symbol in SYMBOLS:
fast, slow = PARAMS[symbol]
sc = LongShortEmaConfig(symbol=symbol, fast=fast, slow=slow)
bt = BacktestConfig(symbol=symbol, interval="4h",
start_time=DATE_START, end_time=DATE_END, initial_capital=10_000.0)
engine = LongShortEngine(bt, db_config=config.db)
r = await engine.run(LongShortEmaStrategy, sc)
m = r.metrics
long_trades = [t for t in r.trades if t.pnl is not None and t.side == "SELL"]
short_trades = [t for t in r.trades if t.pnl is not None and t.side == "BUY"]
lo = LONG_ONLY[symbol]
long_pnl = sum(t.pnl for t in long_trades) if long_trades else 0
short_pnl = sum(t.pnl for t in short_trades) if short_trades else 0
print(f" {symbol:<10} 多空 {m.total_return_pct:>6.1f}% {m.annual_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}% {m.profit_factor:>6.2f}")
print(f" {'':<10} 只做多 {lo[0]:>6.1f}% {lo[3]:>6.1f}% {lo[1]:>6.2f} {lo[2]:>6.1f}%")
if long_trades or short_trades:
print(f" {'':<10} └ 多头P&L {long_pnl:>+7.0f} ({len(long_trades)}笔) 空头P&L {short_pnl:>+7.0f} ({len(short_trades)}笔)")
for t in (r.trades[-2:] if r.trades else []):
if t.pnl is not None:
side_label = "平多" if t.side == "SELL" else "平空"
dt = datetime.fromtimestamp(t.timestamp / 1000, tz=timezone.utc).strftime("%m-%d %H:%M")
print(f" {'':<10}{dt} {side_label} {t.pnl:>+8.2f} {t.reason}")
print("" * 112)
print("\n" * 112)
if __name__ == "__main__":
asyncio.run(main())
+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())
+259
View File
@@ -0,0 +1,259 @@
"""
多周期策略回测 — 4h 定趋势,30m 找买点
策略逻辑:
1. 4h EMA20 判断大趋势:价格 > EMA20 = 上升趋势
2. 30m RSI 寻找入场时机:上升趋势中 RSI < 35 = 回调买入
3. 出场:RSI > 70(超买)或 4h 趋势反转向下
用法:
source .venv/bin/activate && python example/multi_tf_demo.py
"""
import asyncio
import sys
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, DBConfig
from engine.backtest import BacktestEngine, BacktestConfig
from engine.data import DataService
from engine.indicators import ema, rsi
# ============================================================
# 多周期趋势回调策略
# ============================================================
class MultiTFConfig(StrategyConfig):
"""多周期策略配置"""
# 4h 趋势参数
trend_ema_period: int = 20
# 30m 入场参数
entry_rsi_period: int = 14
entry_rsi_threshold: float = 35.0 # RSI 低于此值视为回调
# 出场参数
exit_rsi_threshold: float = 70.0 # RSI 高于此值出场
# 数据范围(用于预加载 4h 数据)
data_start: Optional[datetime] = None
data_end: Optional[datetime] = None
class MultiTimeframeStrategy(BaseStrategy):
"""多周期趋势回调策略
┌─────────────────────────────┐
│ 4h K 线 → EMA20 判断趋势 │
│ Price > EMA20 = 上升趋势 │
└─────────────┬───────────────┘
│ 上升趋势
┌─────────────────────────────┐
│ 30m K 线 → 寻找入场时机 │
│ RSI < 35 = 回调买入 │
└─────────────┬───────────────┘
│ 持仓中
┌─────────────────────────────┐
│ 出场条件 │
│ RSI > 70 或 4h 趋势反转 │
└─────────────────────────────┘
"""
strategy_type = "multi_tf"
def __init__(self, config: MultiTFConfig):
super().__init__(config)
self.cfg: MultiTFConfig = config
# 4h 数据(在 on_start 中加载)
self._klines_4h: list[Kline] = []
self._ema_4h: list[float] = []
# 30m 数据积累
self._closes_30m: list[float] = []
# 持仓状态
self._has_position: bool = False
async def on_start(self) -> None:
"""预加载 4h K 线数据并计算 EMA"""
from engine.common.config import config as app_config
ds = DataService(app_config.db)
await ds.connect()
try:
self._klines_4h = await ds.fetch_klines(
symbol=self.cfg.symbol,
interval="4h",
start_time=self.cfg.data_start,
end_time=self.cfg.data_end,
limit=1_000_000,
)
closes_4h = [k.close for k in self._klines_4h]
self._ema_4h = ema(closes_4h, self.cfg.trend_ema_period)
finally:
await ds.close()
await super().on_start()
def _get_4h_trend(self, ts: float) -> tuple[bool, float, float]:
"""获取指定时间戳对应的 4h 趋势
只使用已完成的 4h K 线(close_time <= ts),避免前视偏差。
Returns:
(is_uptrend, price, ema_value)
"""
if not self._klines_4h:
return False, 0.0, 0.0
# 从后往前找最近已完成的 4h bar
for i in range(len(self._klines_4h) - 1, -1, -1):
if self._klines_4h[i].close_time <= ts:
price = self._klines_4h[i].close
ema_val = self._ema_4h[i]
if ema_val == 0.0:
return False, price, ema_val
return price > ema_val, price, ema_val
return False, 0.0, 0.0
async def on_kline(self, kline: Kline) -> Optional[Signal]:
self._closes_30m.append(kline.close)
# ── 获取 4h 趋势 ──
is_uptrend, price_4h, ema_4h = self._get_4h_trend(kline.open_time)
# ── 计算 30m RSI ──
rsi_vals = rsi(self._closes_30m, self.cfg.entry_rsi_period)
cur_rsi = rsi_vals[-1]
if cur_rsi == 0.0:
return None
# ── 出场逻辑 ──
if self._has_position:
# 4h 趋势反转(价格跌破 EMA)→ 止损出场
if not is_uptrend and price_4h > 0:
self._has_position = False
return Signal(
symbol=self.cfg.symbol,
side="SELL",
signal_type="MARKET",
confidence=0.9,
reason=f"4h趋势反转 Price={price_4h:.2f}<EMA={ema_4h:.2f}",
timestamp=kline.open_time,
)
# 30m RSI 过热 → 止盈出场
if cur_rsi > self.cfg.exit_rsi_threshold:
self._has_position = False
return Signal(
symbol=self.cfg.symbol,
side="SELL",
signal_type="MARKET",
confidence=0.8,
reason=f"30m RSI过热 {cur_rsi:.1f}>{self.cfg.exit_rsi_threshold}",
timestamp=kline.open_time,
)
# ── 入场逻辑 ──
if not self._has_position:
# 条件14h 上升趋势
if not is_uptrend:
return None
# 条件2:30m RSI 回调到超卖区
if cur_rsi < self.cfg.entry_rsi_threshold:
self._has_position = True
return Signal(
symbol=self.cfg.symbol,
side="BUY",
signal_type="MARKET",
confidence=0.7,
reason=(
f"4h升势回调买入 | "
f"4hPrice={price_4h:.0f}>EMA={ema_4h:.0f} | "
f"30mRSI={cur_rsi:.1f}"
),
timestamp=kline.open_time,
)
return None
# ============================================================
# 主函数
# ============================================================
async def main():
bt_config = BacktestConfig(
symbol="ETHUSDT",
interval="30m",
start_time=datetime(2024, 1, 1),
end_time=datetime(2026, 1, 1),
initial_capital=10_000.0,
commission_pct=0.001,
slippage_pct=0.0005,
warmup_bars=100,
)
strategy_config = MultiTFConfig(
name="multi_tf_eth",
symbol="ETHUSDT",
trend_ema_period=20,
entry_rsi_period=14,
entry_rsi_threshold=35.0,
exit_rsi_threshold=70.0,
data_start=bt_config.start_time,
data_end=bt_config.end_time,
)
print()
print("" + "" * 60 + "")
print("" + " 多周期策略 — 4h 定趋势 / 30m 找买点".center(54) + "")
print("" + "" * 60 + "")
print(f"{'交易对:':<8} {bt_config.symbol:<14} {'周期:':<6} {bt_config.interval:<12}")
print(f"{'时间:':<8} {bt_config.start_time.date()} ~ {bt_config.end_time.date()}")
print("" + "" * 60 + "")
print("║ 策略逻辑: ║")
print(f"║ 4h EMA{strategy_config.trend_ema_period} → 判断趋势方向 ║")
print(f"║ 30m RSI{strategy_config.entry_rsi_period} < {strategy_config.entry_rsi_threshold} → 回调买入 ║")
print(f"║ 30m RSI{strategy_config.entry_rsi_period} > {strategy_config.exit_rsi_threshold} → 止盈 / 4h趋势反转 → 止损 ║")
print("" + "" * 60 + "")
print()
engine = BacktestEngine(bt_config, db_config=config.db)
result = await engine.run(MultiTimeframeStrategy, strategy_config)
print(result.summary())
# 打印最近交易
sells = [t for t in result.trades if t.pnl is not None]
if sells:
print(f"\n最近 10 笔平仓交易:")
print(f"{'时间':<22} {'方向':<6} {'价格':>10} {'盈亏':>10} 原因")
print("-" * 85)
for t in sells[-10:]:
dt = datetime.fromtimestamp(t.timestamp / 1000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M")
print(f"{dt:<22} {t.side:<6} {t.price:>10.2f} {t.pnl:>+10.2f} {t.reason}")
print("\n回测完成。")
if __name__ == "__main__":
asyncio.run(main())
+235
View File
@@ -0,0 +1,235 @@
"""
多周期策略 v2 — 双周期同指标(EMA)
策略逻辑:
4h 和 30m 使用同一个技术指标 EMA,不同参数:
- 4h EMA50 → 判断主趋势方向
- 30m EMA20 → 寻找入场/出场时机
入场:4h 多头(Price > EMA50+ 30m 价格上穿 EMA20
出场:30m 价格下穿 EMA20 或 4h 趋势转空
用法:
source .venv/bin/activate && python example/multi_tf_demo2.py
"""
import asyncio
import sys
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
from engine.data import DataService
from engine.indicators import ema
class DualEMATFConfig(StrategyConfig):
"""双周期 EMA 策略配置"""
# 4h 主趋势
trend_ema_period: int = 50
# 30m 交易信号
entry_ema_period: int = 20
# 数据范围
data_start: Optional[datetime] = None
data_end: Optional[datetime] = None
class DualEMATFStrategy(BaseStrategy):
"""双周期 EMA 均线策略
┌──────────────────────────────────────────────┐
│ 4h EMA50 → 价格在上=多头趋势 │
│ 30m EMA20 → 价格在上+EMA上行+收阳=买入 │
│ EMA下行+收阴=卖出 │
└──────────────────────────────────────────────┘
"""
strategy_type = "dual_ema_tf"
def __init__(self, config: DualEMATFConfig):
super().__init__(config)
self.cfg: DualEMATFConfig = config
# 4h 预加载数据
self._klines_4h: list[Kline] = []
self._ema_4h: list[float] = []
# 30m 数据积累
self._closes_30m: list[float] = []
self._ema_30m: list[float] = []
# 持仓
self._has_position: bool = False
async def on_start(self) -> None:
"""预加载 4h 数据并计算 EMA50"""
from engine.common.config import config as app_config
ds = DataService(app_config.db)
await ds.connect()
try:
self._klines_4h = await ds.fetch_klines(
symbol=self.cfg.symbol,
interval="4h",
start_time=self.cfg.data_start,
end_time=self.cfg.data_end,
limit=1_000_000,
)
closes_4h = [k.close for k in self._klines_4h]
self._ema_4h = ema(closes_4h, self.cfg.trend_ema_period)
finally:
await ds.close()
await super().on_start()
def _get_4h_trend(self, ts: float) -> tuple[bool, float, float]:
"""4h 趋势判断(仅用已完成 K 线,close_time <= ts"""
if not self._klines_4h:
return False, 0.0, 0.0
for i in range(len(self._klines_4h) - 1, -1, -1):
if self._klines_4h[i].close_time <= ts:
price = self._klines_4h[i].close
ema_val = self._ema_4h[i]
if ema_val == 0.0:
return False, price, ema_val
return price > ema_val, price, ema_val
return False, 0.0, 0.0
async def on_kline(self, kline: Kline) -> Optional[Signal]:
self._closes_30m.append(kline.close)
self._ema_30m = ema(self._closes_30m, self.cfg.entry_ema_period)
n = len(self._closes_30m)
if n < 2:
return None
cur_ema = self._ema_30m[-1]
prev_ema = self._ema_30m[-2]
if cur_ema == 0.0 or prev_ema == 0.0:
return None
cur_price = kline.close
# 30m K线收阳
is_bullish_bar = kline.close > kline.open
# 30m EMA 斜率(最近3根是否递增)
ema_sloping_up = (
n >= 4
and self._ema_30m[-1] > self._ema_30m[-2]
and self._ema_30m[-2] > self._ema_30m[-3]
)
# 4h 趋势
is_uptrend, price_4h, ema_4h = self._get_4h_trend(kline.open_time)
# ── 出场 ──
if self._has_position:
# 仅 4h 趋势转空时出场(让 30m 波动自然消化)
if not is_uptrend and price_4h > 0:
self._has_position = False
return Signal(
symbol=self.cfg.symbol,
side="SELL",
signal_type="MARKET",
confidence=0.9,
reason=f"4h转空 P={price_4h:.0f}<EMA{self.cfg.trend_ema_period}={ema_4h:.0f}",
timestamp=kline.open_time,
)
# ── 入场 ──
if not self._has_position:
if not is_uptrend:
return None
# 30m 价格在 EMA 上方 + EMA 上行 + 收阳 → 买入
price_above_ema = cur_price > cur_ema
if price_above_ema and ema_sloping_up and is_bullish_bar:
self._has_position = True
return Signal(
symbol=self.cfg.symbol,
side="BUY",
signal_type="MARKET",
confidence=0.7,
reason=(
f"30m多头确认 | "
f"4hP={price_4h:.0f}>E={ema_4h:.0f}"
),
timestamp=kline.open_time,
)
return None
# ============================================================
# 主函数
# ============================================================
async def main():
bt_config = BacktestConfig(
symbol="ETHUSDT",
interval="30m",
start_time=datetime(2024, 1, 1),
end_time=datetime(2026, 1, 1),
initial_capital=10_000.0,
commission_pct=0.001,
slippage_pct=0.0005,
warmup_bars=100,
)
strategy_config = DualEMATFConfig(
name="dual_ema_eth",
symbol="ETHUSDT",
trend_ema_period=50,
entry_ema_period=20,
data_start=bt_config.start_time,
data_end=bt_config.end_time,
)
print()
print("" + "" * 60 + "")
print("" + " 多周期策略 v2 — 双周期同指标 (EMA)".center(54) + "")
print("" + "" * 60 + "")
print(f"{'交易对:':<8} {bt_config.symbol:<14} {'周期:':<6} {bt_config.interval:<12}")
print(f"{'时间:':<8} {bt_config.start_time.date()} ~ {bt_config.end_time.date()}")
print("" + "" * 60 + "")
print("║ 同指标 · 双周期: ║")
print(f"║ 4h EMA{strategy_config.trend_ema_period} → 趋势方向(价格在上=多头) ║")
print(f"║ 30m EMA{strategy_config.entry_ema_period} → 价格在上+EMA上行+收阳=买入 ║")
print("" + "" * 60 + "")
print()
engine = BacktestEngine(bt_config, db_config=config.db)
result = await engine.run(DualEMATFStrategy, strategy_config)
print(result.summary())
sells = [t for t in result.trades if t.pnl is not None]
if sells:
print(f"\n最近 10 笔平仓:")
print(f"{'时间':<22} {'方向':<6} {'价格':>10} {'盈亏':>10} 原因")
print("-" * 80)
for t in sells[-10:]:
dt = datetime.fromtimestamp(t.timestamp / 1000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M")
print(f"{dt:<22} {t.side:<6} {t.price:>10.2f} {t.pnl:>+10.2f} {t.reason}")
print("\n回测完成。")
if __name__ == "__main__":
asyncio.run(main())
+193
View File
@@ -0,0 +1,193 @@
"""
4h EMA50>EMA200 定大势 / 30m 找买点
策略:
4h EMA50 > EMA200 → 中长期多头趋势确认,允许做多
30m 入场 → 4h多头区间中,30m价格上穿EMA20 + 收阳 → 买入
30m 出场 → 下穿EMA20 或 4h趋势打破(EMA50<EMA200) 或 ATR止损
"""
import asyncio
import sys
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
from engine.data import DataService
from engine.indicators import ema, atr
class EMA200FilterConfig(StrategyConfig):
ema30_fast: int = 10 # 30m 快线
ema30_slow: int = 50 # 30m 慢线
atr_stop: float = 3.0
data_start: Optional[datetime] = None
data_end: Optional[datetime] = None
class EMA200FilterStrategy(BaseStrategy):
"""4h EMA50>200 多头趋势 + 30m 双EMA金叉
30m 用双EMA交叉替代价格穿越,大幅减少假信号。
"""
strategy_type = "ema200_filter"
def __init__(self, c: EMA200FilterConfig):
super().__init__(c)
self.cfg = c
self._klines_4h: list[Kline] = []
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._highest: float = 0.0
self._in_position = False
async def on_start(self):
from engine.common.config import config as app_config
ds = DataService(app_config.db)
await ds.connect()
try:
self._klines_4h = await ds.fetch_klines(
symbol=self.cfg.symbol, interval="4h",
start_time=datetime(2023, 1, 1),
end_time=self.cfg.data_end,
limit=1_000_000,
)
finally:
await ds.close()
await super().on_start()
def _is_4h_bull(self, ts: float) -> bool:
if len(self._klines_4h) < 201:
return False
if not hasattr(self, '_ema50_4h'):
closes = [k.close for k in self._klines_4h]
self._ema50_4h = ema(closes, 50)
self._ema200_4h = ema(closes, 200)
for i in range(len(self._klines_4h) - 1, -1, -1):
if self._klines_4h[i].close_time <= ts:
e50 = self._ema50_4h[i]
e200 = self._ema200_4h[i]
return e50 > 0 and e200 > 0 and e50 > e200
return 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)
n = len(self._closes)
need = self.cfg.ema30_slow + 10
if n < need:
return None
# 30m 双EMA(金叉/死叉)
fast = ema(self._closes, self.cfg.ema30_fast)
slow = ema(self._closes, self.cfg.ema30_slow)
atr_vals = atr(self._highs, self._lows, self._closes, 14)
cur_f, cur_s = fast[-1], slow[-1]
prev_f, prev_s = fast[-2], slow[-2]
cur_atr = atr_vals[-1]
if cur_f == 0 or cur_s == 0 or cur_atr == 0:
return None
is_bull = self._is_4h_bull(k.open_time)
# ── 出场 ──
if self._in_position:
self._highest = max(self._highest, k.high)
stop = self._highest - self.cfg.atr_stop * cur_atr
death = prev_f >= prev_s and cur_f < cur_s
if not is_bull:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason="4h转空", timestamp=k.open_time)
if k.close < stop:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason="ATR止损", timestamp=k.open_time)
if death:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL",
reason=f"30m EMA死叉", timestamp=k.open_time)
# ── 入场 ──
if not self._in_position and is_bull:
golden = prev_f <= prev_s and cur_f > cur_s
if golden and k.close > k.open:
self._in_position = True
self._highest = k.close
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"4h多头+30m金叉", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
DATE_START = datetime(2024, 1, 1)
DATE_END = datetime(2026, 1, 1)
async def main():
print()
print("" * 105)
print(" 4h EMA50>EMA200 定大势 / 30m 双EMA金叉 | 2024-2026")
print("" * 105)
print(f" {'币种':<10} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} {'盈亏比':>6} {'持有%':>6}")
print("" * 105)
for symbol in SYMBOLS:
sc = EMA200FilterConfig(symbol=symbol, data_start=DATE_START, data_end=DATE_END)
bt = BacktestConfig(symbol=symbol, interval="30m",
start_time=DATE_START, end_time=DATE_END, initial_capital=10_000.0,
warmup_bars=50)
engine = BacktestEngine(bt, db_config=config.db)
r = await engine.run(EMA200FilterStrategy, sc)
m = r.metrics
# 持仓时间占比
if r.equity_curve:
bars_with_position = sum(1 for e in r.equity_curve if e.get("position", 0) > 0)
position_pct = bars_with_position / len(r.equity_curve) * 100
else:
position_pct = 0
print(f" {symbol:<10} {m.total_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}% {m.profit_factor:>6.2f} {position_pct:>5.0f}%")
# 最近平仓
sells = [t for t in r.trades if t.side == "SELL" and t.pnl is not None]
if sells:
for t in sells[-2:]:
dt = datetime.fromtimestamp(t.timestamp / 1000, tz=timezone.utc).strftime("%m-%d %H:%M")
print(f" {'':<10}{dt} {t.pnl:>+8.2f} USDT {t.reason}")
print("" * 105)
# 对比之前最优
print("\n ■ 对比:之前最优策略")
BEST = {
"BTCUSDT": ("EMA v3(10,50) 4h", 39.9, 1.03, -11.5, 20),
"ETHUSDT": ("EMA v3(10,75) 4h", 53.6, 1.04, -15.3, 18),
"BNBUSDT": ("EMA v1(20,50) 4h", 52.0, 0.71, -39.8, 41),
"SOLUSDT": ("EMA v3(30,50) 4h", 73.6, 1.18, -25.7, 13),
}
print(f" {'币种':<10} {'策略':<22} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5}")
for symbol in SYMBOLS:
name, ret, sh, dd, tr = BEST[symbol]
print(f" {symbol:<10} {name:<22} {ret:>6.1f}% {sh:>6.2f} {dd:>6.1f}% {tr:>5}")
print("\n" * 105)
if __name__ == "__main__":
asyncio.run(main())
+230
View File
@@ -0,0 +1,230 @@
"""
多时间框架 v3 — 1d 定趋势 / 4h 找买点
策略:
1d EMA(20,50) → 日线金叉=多头趋势,死叉=空头
4h 回调入场 → 日线多头中,4h 价格回调到 EMA20 附近 + 收阳 → 买入
4h EMA死叉 或 日线转空 → 卖出
ATR 动态止损
币种:BTC/ETH/BNB/SOL | 2024-2026
用法:
source .venv/bin/activate && python example/multi_tf_v3.py
"""
import asyncio
import sys
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.data import DataService
from engine.indicators import ema, atr
class DailyTrendConfig(StrategyConfig):
"""1d趋势+4h交易 策略配置"""
# 1d 趋势参数
trend_fast: int = 20
trend_slow: int = 50
# 4h 交易参数
entry_ema: int = 20 # 4h 回调到该均线附近买入
atr_period: int = 14
atr_stop_mult: float = 2.5
# 数据范围
data_start: Optional[datetime] = None
data_end: Optional[datetime] = None
class DailyTrendStrategy(BaseStrategy):
"""日线定大势,4h 抓回调
┌──────────────────────────────────────────────┐
│ 1d EMA(20,50) │
│ ├─ 金叉 → 多头区间 │
│ └─ 死叉 → 空仓等待 │
│ │
│ 4h K线(引擎推送) │
│ ├─ 多头区间 + 回调到 EMA20 + 收阳 → 买入 │
│ ├─ EMA死叉 → 卖出 │
│ └─ ATR 动态止损 │
└──────────────────────────────────────────────┘
"""
strategy_type = "daily_trend"
def __init__(self, c: DailyTrendConfig):
super().__init__(c)
self.cfg = c
# 1d 预加载
self._klines_1d: list[Kline] = []
self._ema_fast_1d: list[float] = []
self._ema_slow_1d: list[float] = []
# 4h 积累
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._highest: float = 0.0
self._in_position = False
async def on_start(self):
from engine.common.config import config as app_config
ds = DataService(app_config.db)
await ds.connect()
try:
self._klines_1d = await ds.fetch_klines(
symbol=self.cfg.symbol, interval="1d",
start_time=self.cfg.data_start, end_time=self.cfg.data_end, limit=1_000_000,
)
closes_1d = [k.close for k in self._klines_1d]
self._ema_fast_1d = ema(closes_1d, self.cfg.trend_fast)
self._ema_slow_1d = ema(closes_1d, self.cfg.trend_slow)
finally:
await ds.close()
await super().on_start()
def _get_1d_state(self, ts: float) -> tuple[bool, bool, float, float]:
"""获取日线趋势状态(仅已完成K线)
Returns: (is_bull, is_golden_cross, price, ema_fast)
"""
if not self._klines_1d:
return False, False, 0.0, 0.0
for i in range(len(self._klines_1d) - 1, -1, -1):
if self._klines_1d[i].close_time <= ts:
ef = self._ema_fast_1d[i]
es = self._ema_slow_1d[i]
if ef == 0 or es == 0:
return False, False, self._klines_1d[i].close, ef
# 金叉 = EMA20 > EMA50
golden = ef > es
# 趋势向上 = 价格在EMA20上方(额外确认)
price_above = self._klines_1d[i].close > ef
return golden and price_above, golden, self._klines_1d[i].close, ef
return False, False, 0.0, 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)
n = len(self._closes)
if n < self.cfg.entry_ema + 20:
return None
ema_4h = ema(self._closes, self.cfg.entry_ema)
atr_vals = atr(self._highs, self._lows, self._closes, self.cfg.atr_period)
cur_ema, cur_atr = ema_4h[-1], atr_vals[-1]
if cur_ema == 0 or cur_atr == 0:
return None
is_bull, is_golden, price_1d, ema_1d = self._get_1d_state(k.open_time)
# ── 出场 ──
if self._in_position:
self._highest = max(self._highest, k.high)
stop = self._highest - self.cfg.atr_stop_mult * cur_atr
# 4h EMA死叉(用EMA9代替,更快反应)
ema9_vals = ema(self._closes, 9)
ema20_vals = ema(self._closes, 20)
if len(ema9_vals) > 2 and ema9_vals[-2] >= ema20_vals[-2] and ema9_vals[-1] < ema20_vals[-1]:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason="4h EMA死叉", timestamp=k.open_time)
if k.close < stop:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"ATR止损", timestamp=k.open_time)
# ── 入场 ──
if not self._in_position and is_bull:
# 4h 价格回调到 EMA20 附近(偏离不超过 1.5 倍 ATR)
distance = abs(k.close - cur_ema) / cur_atr
near_ema = distance < 1.5
# K线收阳
bullish_bar = k.close > k.open
# EMA20 走平或向上
ema_rising = n >= 4 and ema_4h[-1] >= ema_4h[-3]
if near_ema and bullish_bar and ema_rising:
self._in_position = True
self._highest = k.close
return Signal(
symbol=self.cfg.symbol, side="BUY",
confidence=0.8,
reason=f"1d多头+4h回调 P={k.close:.0f}≈EMA={cur_ema:.0f} dist={distance:.1f}σ",
timestamp=k.open_time,
)
return None
# ════════════════════════════════════════════════════════
# 运行
# ════════════════════════════════════════════════════════
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
DATE_START = datetime(2024, 1, 1)
DATE_END = datetime(2026, 1, 1)
# v3 EMA最优结果(用于对比)
EMA_V3 = {
"BTCUSDT": (39.9, 1.03, -11.5, 20, 55.0),
"ETHUSDT": (53.6, 1.04, -15.3, 18, 38.9),
"BNBUSDT": (26.0, 0.64, -23.4, 23, 34.8),
"SOLUSDT": (73.6, 1.18, -25.7, 13, 46.2),
}
async def main():
print()
print("" * 110)
print(" 多时间框架 v3 — 1d 定趋势 / 4h 找回调买点 | 2024-2026")
print("" * 110)
print(f" {'币种':<10} {'版本':<22} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} {'盈亏比':>6}")
print("" * 110)
for symbol in SYMBOLS:
sc = DailyTrendConfig(
symbol=symbol,
data_start=DATE_START, data_end=DATE_END,
)
bt = BacktestConfig(symbol=symbol, interval="4h",
start_time=DATE_START, end_time=DATE_END, initial_capital=10_000.0)
engine = BacktestEngine(bt, db_config=config.db)
r = await engine.run(DailyTrendStrategy, sc)
m = r.metrics
# v3 对比
v3 = EMA_V3[symbol]
print(f" {symbol:<10} {'1d趋势+4h回调':<22} {m.total_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}% {m.profit_factor:>6.2f}")
print(f" {'':<10} {'(对比) EMA v3 最优':<22} {v3[0]:>6.1f}% {v3[1]:>6.2f} {v3[2]:>6.1f}% {v3[3]:>5} {v3[4]:>5.1f}%")
# 打印最近几笔出场
sells = [t for t in r.trades if t.side == "SELL" and t.pnl is not None]
if sells:
recent = sells[-3:]
for t in recent:
dt = datetime.fromtimestamp(t.timestamp / 1000, tz=timezone.utc).strftime("%m-%d %H:%M")
print(f" {'':<10}{dt} {t.pnl:>+8.2f} USDT {t.reason}")
print()
print("" * 110)
if __name__ == "__main__":
asyncio.run(main())
+247
View File
@@ -0,0 +1,247 @@
"""
最佳牛熊判定 — 全币种全周期回测
方法:EMA200斜率 + 价格vs EMA200 + ATH回撤,3选2投票
策略:牛市只做多 / 熊市只做空 / 震荡空仓
币种:BTC / ETH / BNB / SOL,各自最早有数据到2026
用法:
source .venv/bin/activate && python example/regime_all.py
"""
import asyncio
import sys
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.indicators.incremental import EmaInc, AtrInc
from engine.data import DataService
from engine.example.long_short import LongShortEngine
# ════════════════════════════════════════════════════════
# 3法判定器(增量 EMA200O(1) per bar
# ════════════════════════════════════════════════════════
class RegimeDetector3:
"""牛熊判定器,内部维护增量 EMA(200),避免每次从头重算"""
def __init__(self):
self._ath = 0.0
self._e200 = EmaInc(200)
def update(self, price: float):
"""每根 bar 调一次:更新 ATH + EMA(200)"""
if price > self._ath:
self._ath = price
self._e200.update(price)
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, price: float, idx: int) -> str:
if idx < 210: return "unknown"
e = self._e200[idx]
if e == 0: return "unknown"
return "bull" if price > e else "bear"
def _ath_drawdown(self, price: float) -> str:
if self._ath == 0: return "unknown"
dd = (price - self._ath) / self._ath
if dd > -0.15: return "bull"
if dd < -0.35: return "bear"
return "sideways"
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"
if br >= 2: return "bear"
return "sideways"
# ════════════════════════════════════════════════════════
# 自适应策略
# ════════════════════════════════════════════════════════
class RegimeEmaConfig(StrategyConfig):
fast: int = 10; slow: int = 50; atr_stop: float = 2.5
class RegimeEmaStrategy(BaseStrategy):
"""按市场状态自适应做多/做空 — 全部指标增量计算,O(1) per bar"""
strategy_type = "regime_ema"
def __init__(self, c: RegimeEmaConfig):
super().__init__(c)
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)
# 增量更新所有指标(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(k.close, n - 1)
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
# 多头持仓
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 = ""
reason = "死叉" if death else ("ATR止损" if k.close < stop else "转熊")
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 or regime == "bull":
self._side = ""
reason = "金叉" if golden else ("ATR止损" if k.close > stop else "转牛")
return Signal(symbol=self.cfg.symbol, side="BUY", reason=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=f"牛市金叉", 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=f"熊市死叉", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
PARAMS = {
"BTCUSDT": (10, 50),
"ETHUSDT": (10, 75),
"BNBUSDT": (20, 50),
"SOLUSDT": (30, 50),
}
DATE_START = datetime(2017, 1, 1)
DATE_END = datetime(2026, 1, 1)
async def get_actual_range(symbol: str) -> tuple[datetime, datetime]:
"""获取币种实际数据范围"""
ds = DataService(config.db)
await ds.connect()
try:
start, end = await ds.fetch_symbol_date_range(symbol, "4h")
return start, end
except:
return DATE_START, DATE_END
finally:
await ds.close()
async def main():
print()
print("" * 125)
print(" 牛熊自适应策略 — 全币种全周期 | 牛市只多/熊市只空/震荡空仓")
print("" * 125)
print(f"\n ■ 全周期汇总")
print(f" {'币种':<10} {'数据范围':<22} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'多头P&L':>10} {'空头P&L':>10}")
print(" " + "" * 115)
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(symbol)
range_str = f"{act_start.date()}~{act_end.date()}"
except:
act_start, act_end = DATE_START, DATE_END
range_str = "2017-2026"
bt = BacktestConfig(symbol=symbol, interval="4h",
start_time=act_start, end_time=act_end, initial_capital=10_000.0)
engine = LongShortEngine(bt, db_config=config.db)
r = await engine.run(RegimeEmaStrategy, sc)
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:>6.1f}% {m.annual_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {long_pnl:>+9.0f} {short_pnl:>+9.0f}")
# ── BTC 分段 ──
PERIODS = [
("2017 牛市", datetime(2017,1,1), datetime(2018,1,1)),
("2018 熊市", datetime(2018,1,1), datetime(2019,1,1)),
("2019 反弹", datetime(2019,1,1), datetime(2020,1,1)),
("2020 牛初", datetime(2020,1,1), datetime(2021,1,1)),
("2021 牛市", datetime(2021,1,1), datetime(2022,1,1)),
("2022 熊市", datetime(2022,1,1), datetime(2023,1,1)),
("2023 震荡", datetime(2023,1,1), datetime(2024,1,1)),
("2024-25牛", datetime(2024,1,1), datetime(2026,1,1)),
]
print(f"\n ■ BTC 分段表现")
print(f" {'阶段':<16} {'总收益%':>7} {'夏普':>6} {'多头P&L':>9} {'空头P&L':>9}")
print(" " + "" * 65)
for name, s, e in PERIODS:
try:
sc = RegimeEmaConfig(symbol="BTCUSDT", fast=10, slow=50)
bt = BacktestConfig(symbol="BTCUSDT", interval="4h", start_time=s, end_time=e, initial_capital=10_000.0)
eng = LongShortEngine(bt, db_config=config.db)
r = await eng.run(RegimeEmaStrategy, sc)
lt = [t for t in r.trades if t.pnl is not None and t.side == "SELL"]
st = [t for t in r.trades if t.pnl is not None and t.side == "BUY"]
print(f" {name:<16} {r.metrics.total_return_pct:>+6.1f}% {r.metrics.sharpe_ratio:>6.2f} {sum(t.pnl for t in lt) if lt else 0:>+8.0f} {sum(t.pnl for t in st) if st else 0:>+8.0f}")
except Exception as ex:
print(f" {name:<16} 数据不足")
print("\n" * 125)
if __name__ == "__main__":
asyncio.run(main())
+305
View File
@@ -0,0 +1,305 @@
"""
市场状态识别 — 牛市/熊市判定方法对比 + 自适应策略回测
方法:
1. EMA200 斜率 — EMA200 向上=牛,向下=熊
2. 价格 vs EMA200 — Price > EMA200 = 牛
3. ATH 回撤 — 距历史高点 < 20% = 牛,> 20% = 熊
4. 综合投票 — 三选二
根据识别结果自动偏多/偏空:
牛市:只做多(金叉买入,死叉平仓)
熊市:只做空(死叉做空,金叉平仓)
震荡(票数2:1或无共识):空仓等待
BTC 2017-2026 全周期测试
用法:
source .venv/bin/activate && python example/regime_detect.py
"""
import asyncio
import sys
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.indicators import ema, atr
from engine.example.long_short import LongShortEngine
# ════════════════════════════════════════════════════════
# 市场状态识别器
# ════════════════════════════════════════════════════════
class RegimeDetector:
"""市场状态识别:牛/熊/震荡"""
def __init__(self, closes: list[float]):
self._c = closes
self._ath = 0.0
self._ath_tracking = [] # 追踪历史高点序列
def update_ath(self, price: float):
if price > self._ath:
self._ath = price
self._ath_tracking.append(self._ath)
def ema200_slope(self, idx: int) -> str:
"""EMA200 斜率判定"""
if idx < 202:
return "unknown"
e200 = ema(self._c, 200)
# 最近5根EMA200的斜率
if e200[idx] == 0 or e200[max(0, idx - 5)] == 0:
return "unknown"
slope = (e200[idx] - e200[max(0, idx - 5)]) / e200[max(0, idx - 5)]
if slope > 0.001:
return "bull"
elif slope < -0.001:
return "bear"
return "sideways"
def price_vs_ema200(self, idx: int) -> str:
"""价格 vs EMA200"""
if idx < 202:
return "unknown"
e200 = ema(self._c, 200)
if e200[idx] == 0:
return "unknown"
return "bull" if self._c[idx] > e200[idx] else "bear"
def ath_drawdown(self, idx: int) -> str:
"""ATH 回撤判定(经典加密牛熊指标)"""
if not self._ath_tracking or idx >= len(self._ath_tracking):
return "unknown"
curr_ath = self._ath_tracking[idx]
if curr_ath == 0:
return "unknown"
dd = (self._c[idx] - curr_ath) / curr_ath
if dd > -0.20:
return "bull"
elif dd < -0.40:
return "bear"
return "sideways"
def combined(self, idx: int) -> tuple[str, str, str, str]:
"""综合判定"""
r1 = self.ema200_slope(idx)
r2 = self.price_vs_ema200(idx)
r3 = self.ath_drawdown(idx)
votes = {"bull": 0, "bear": 0}
for r in [r1, r2, r3]:
if r in votes:
votes[r] += 1
if votes["bull"] >= 2:
return "bull", r1, r2, r3
elif votes["bear"] >= 2:
return "bear", r1, r2, r3
return "sideways", r1, r2, r3
# ════════════════════════════════════════════════════════
# 自适应策略(根据市场状态偏多/偏空)
# ════════════════════════════════════════════════════════
class AdaptiveEmaConfig(StrategyConfig):
fast: int = 10
slow: int = 50
atr_stop: float = 2.5
class AdaptiveEmaStrategy(BaseStrategy):
"""牛市只做多、熊市只做空、震荡空仓"""
strategy_type = "adaptive_ema"
def __init__(self, c: AdaptiveEmaConfig):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._detector: Optional[RegimeDetector] = None
self._position_side: str = "" # "long" / "short"
self._highest: float = 0.0
self._lowest: float = float('inf')
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
self._highs.append(k.high)
self._lows.append(k.low)
if self._detector is None:
self._detector = RegimeDetector(self._closes)
self._detector.update_ath(k.close)
n = len(self._closes)
if n < 210:
return None
regime, r1, r2, r3 = self._detector.combined(n - 1)
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]
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 or k.close < stop or regime == "bear":
self._position_side = ""
reason = "死叉" if death else ("ATR止损" if k.close < stop else "转熊市")
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, 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 or k.close > stop or regime == "bull":
self._position_side = ""
reason = "金叉" if golden else ("ATR止损" if k.close > stop else "转牛市")
return Signal(symbol=self.cfg.symbol, side="BUY", reason=reason, timestamp=k.open_time)
# ── 空仓等待 ──
else:
if regime == "bull" and golden:
self._position_side = "long"
self._highest = k.close
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"牛市金叉 ({r1}/{r2}/{r3})", timestamp=k.open_time)
elif regime == "bear" and death:
self._position_side = "short"
self._lowest = k.close
return Signal(symbol=self.cfg.symbol, side="SELL",
reason=f"熊市死叉 ({r1}/{r2}/{r3})", timestamp=k.open_time)
# 震荡市:不开仓
return None
# ════════════════════════════════════════════════════════
# 对比测试
# ════════════════════════════════════════════════════════
DATE_START = datetime(2017, 1, 1)
DATE_END = datetime(2026, 1, 1)
async def main():
print()
print("" * 120)
print(" 市场状态自适应策略 — 牛市只做多 / 熊市只做空 / 震荡空仓")
print("" * 120)
# ── BTC 自适应 vs 多空 vs 只做多 ──
print("\n ■ BTC 全周期 2017-2026 对比")
print(f" {'策略':<18} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'多头P&L':>10} {'空头P&L':>10}")
print(" " + "" * 105)
# 自适应
sc = AdaptiveEmaConfig(symbol="BTCUSDT")
bt = BacktestConfig(symbol="BTCUSDT", interval="4h",
start_time=DATE_START, end_time=DATE_END, initial_capital=10_000.0)
engine = LongShortEngine(bt, db_config=config.db)
r = await engine.run(AdaptiveEmaStrategy, sc)
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"]
print(f" {'自适应(牛多熊空)':<18} {m.total_return_pct:>6.1f}% {m.annual_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {sum(t.pnl for t in long_t) if long_t else 0:>+9.0f} {sum(t.pnl for t in short_t) if short_t else 0:>+9.0f}")
# 始终多空(之前结果)
from engine.example.long_short import LongShortEmaStrategy, LongShortEmaConfig as LSCfg
sc2 = LSCfg(symbol="BTCUSDT", fast=10, slow=50)
r2 = await engine.run(LongShortEmaStrategy, sc2)
m2 = r2.metrics
print(f" {'始终多空':<18} {m2.total_return_pct:>6.1f}% {m2.annual_return_pct:>6.1f}% {m2.sharpe_ratio:>6.2f} {m2.max_drawdown_pct:>6.1f}% {m2.total_trades:>5}")
# 只做多
from engine.backtest import BacktestEngine as OrigEngine
class LongOnlyS(BaseStrategy):
strategy_type = "lo"; _in = False; _hp = 0.0
def __init__(self, c): super().__init__(c); self._c = []; self._h = []; self._l = []
async def on_start(self): await super().on_start()
async def on_kline(self, k):
self._c.append(k.close); self._h.append(k.high); self._l.append(k.low)
n = len(self._c)
if n < 55: return None
f = ema(self._c, 10); s = ema(self._c, 50); 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
if self._in:
self._hp = max(self._hp, k.high); stop = self._hp - 2.5 * ca
if (pf >= ps and cf < cs) or k.close < stop:
self._in = False
return Signal(symbol="BTCUSDT", side="SELL",
reason="死叉" if pf>=ps else "ATR止损", timestamp=k.open_time)
else:
if pf <= ps and cf > cs:
self._in = True; self._hp = k.close
return Signal(symbol="BTCUSDT", side="BUY", reason="金叉", timestamp=k.open_time)
return None
from engine.common.base import StrategyConfig as SC
lo_bt = BacktestConfig(symbol="BTCUSDT", interval="4h", start_time=DATE_START, end_time=DATE_END, initial_capital=10_000.0)
lo_e = OrigEngine(lo_bt, db_config=config.db)
lo_r = await lo_e.run(LongOnlyS, SC(symbol="BTCUSDT"))
lo_m = lo_r.metrics
long_only_t = [t for t in lo_r.trades if t.pnl is not None]
print(f" {'只做多':<18} {lo_m.total_return_pct:>6.1f}% {lo_m.annual_return_pct:>6.1f}% {lo_m.sharpe_ratio:>6.2f} {lo_m.max_drawdown_pct:>6.1f}% {lo_m.total_trades:>5}")
# ── 分段对比 ──
PERIODS = [
("2017 牛市", datetime(2017,1,1), datetime(2018,1,1)),
("2018 熊市", datetime(2018,1,1), datetime(2019,1,1)),
("2019 反弹", datetime(2019,1,1), datetime(2020,1,1)),
("2020 牛初", datetime(2020,1,1), datetime(2021,1,1)),
("2021 牛市", datetime(2021,1,1), datetime(2022,1,1)),
("2022 熊市", datetime(2022,1,1), datetime(2023,1,1)),
("2023 震荡", datetime(2023,1,1), datetime(2024,1,1)),
("2024-25 牛", datetime(2024,1,1), datetime(2026,1,1)),
]
print(f"\n ■ BTC 分段:自适应 vs 始终多空")
print(f" {'阶段':<16} {'自适应':>8} {'始终多空':>8} {'只做多':>8}")
print(" " + "" * 50)
for name, s, e in PERIODS:
try:
bt_p = BacktestConfig(symbol="BTCUSDT", interval="4h", start_time=s, end_time=e, initial_capital=10_000.0)
# 自适应
sc_p = AdaptiveEmaConfig(symbol="BTCUSDT")
e_p = LongShortEngine(bt_p, db_config=config.db)
r_p = await e_p.run(AdaptiveEmaStrategy, sc_p)
# 始终多空
sc_p2 = LSCfg(symbol="BTCUSDT", fast=10, slow=50)
r_p2 = await e_p.run(LongShortEmaStrategy, sc_p2)
# 只做多
lo_e2 = OrigEngine(bt_p, db_config=config.db)
lo_r2 = await lo_e2.run(LongOnlyS, SC(symbol="BTCUSDT"))
print(f" {name:<16} {r_p.metrics.total_return_pct:>+7.1f}% {r_p2.metrics.total_return_pct:>+7.1f}% {lo_r2.metrics.total_return_pct:>+7.1f}%")
except Exception as ex:
print(f" {name:<16} 错误: {ex}")
print("\n" * 120)
if __name__ == "__main__":
asyncio.run(main())
+307
View File
@@ -0,0 +1,307 @@
"""
牛熊判定方法扩展 + 组合对比
新增方法:
4. Mayer Multiple — Price / EMA200 比值。>1.2=牛,<0.8=熊
5. 年同比 — 价格同比去年涨=牛,跌=熊
6. 市场结构 — 近200根bar更高高点+更高低点=牛
对比各种投票组合的效果。
用法:
source .venv/bin/activate && python example/regime_detect2.py
"""
import asyncio
import sys
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.indicators import ema, atr
from engine.example.long_short import LongShortEngine
# ════════════════════════════════════════════════════════
# 扩展版市场状态识别器(6种方法)
# ════════════════════════════════════════════════════════
class AdvancedRegimeDetector:
def __init__(self, closes: list[float], highs: list[float], lows: list[float]):
self._c = closes
self._h = highs
self._l = lows
self._ath = 0.0
self._ath_tracking = []
def update_ath(self, price: float):
if price > self._ath:
self._ath = price
self._ath_tracking.append(self._ath)
# ── 方法1: EMA200 斜率 ──
def ema200_slope(self, idx: int) -> str:
if idx < 210: return "unknown"
e200 = ema(self._c, 200)
slope = (e200[idx] - e200[idx - 20]) / e200[idx - 20] if e200[idx - 20] > 0 else 0
if slope > 0.002: return "bull"
if slope < -0.002: return "bear"
return "sideways"
# ── 方法2: 价格 vs EMA200 ──
def price_vs_ema200(self, idx: int) -> str:
if idx < 210: return "unknown"
e200 = ema(self._c, 200)
if e200[idx] == 0: return "unknown"
return "bull" if self._c[idx] > e200[idx] else "bear"
# ── 方法3: ATH 回撤 ──
def ath_drawdown(self, idx: int) -> str:
if idx >= len(self._ath_tracking) or self._ath_tracking[idx] == 0:
return "unknown"
dd = (self._c[idx] - self._ath_tracking[idx]) / self._ath_tracking[idx]
if dd > -0.15: return "bull"
if dd < -0.35: return "bear"
return "sideways"
# ── 方法4: Mayer Multiple ──
def mayer_multiple(self, idx: int) -> str:
if idx < 210: return "unknown"
e200 = ema(self._c, 200)
if e200[idx] == 0: return "unknown"
mm = self._c[idx] / e200[idx]
if mm > 1.2: return "bull" # 明显在均线上方
if mm < 0.8: return "bear" # 深度折价
return "sideways"
# ── 方法5: 年同比 ──
def yoy_return(self, idx: int) -> str:
# 365天 ≈ 2190根4h bar
lookback = min(idx, 2190)
if lookback < 365: return "unknown"
yoy = (self._c[idx] - self._c[idx - lookback]) / self._c[idx - lookback]
if yoy > 0.15: return "bull"
if yoy < -0.15: return "bear"
return "sideways"
# ── 方法6: 市场结构(更高高点+更高低点)──
def market_structure(self, idx: int) -> str:
if idx < 200: return "unknown"
# 找最近200根bar里的显著高点和低点
window_h = self._h[max(0, idx - 200):idx + 1]
window_l = self._l[max(0, idx - 200):idx + 1]
if len(window_h) < 100: return "unknown"
# 分成前后两半
mid = len(window_h) // 2
first_high = max(window_h[:mid])
second_high = max(window_h[mid:])
first_low = min(window_l[:mid])
second_low = min(window_l[mid:])
if second_high > first_high and second_low > first_low:
return "bull" # 更高高点 + 更高低点 = 上升结构
if second_high < first_high and second_low < first_low:
return "bear" # 更低高点 + 更低低点 = 下降结构
return "sideways"
# ════════════════════════════════════════════════════════
# 自适应策略(支持可配置的投票方案)
# ════════════════════════════════════════════════════════
class AdaptiveConfig(StrategyConfig):
fast: int = 10; slow: int = 50; atr_stop: float = 2.5
vote_mode: str = "majority_6" # 投票模式
class AdaptiveStrategy(BaseStrategy):
"""按投票结果自适应多空"""
strategy_type = "adaptive_v2"
def __init__(self, c: AdaptiveConfig):
super().__init__(c)
self.cfg = c
self._c: list[float] = []; self._h: list[float] = []; self._l: list[float] = []
self._detector: Optional[AdvancedRegimeDetector] = None
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)
if self._detector is None:
self._detector = AdvancedRegimeDetector(self._c, self._h, self._l)
self._detector.update_ath(k.close)
n = len(self._c)
if n < 2200: return None # 等够一年数据
# ── 投票逻辑 ──
methods = [
self._detector.ema200_slope(n - 1),
self._detector.price_vs_ema200(n - 1),
self._detector.ath_drawdown(n - 1),
self._detector.mayer_multiple(n - 1),
self._detector.yoy_return(n - 1),
self._detector.market_structure(n - 1),
]
if self.cfg.vote_mode == "majority_6":
# 6选4以上=牛/熊,否则震荡
bull_votes = sum(1 for m in methods if m == "bull")
bear_votes = sum(1 for m in methods if m == "bear")
if bull_votes >= 4: regime = "bull"
elif bear_votes >= 4: regime = "bear"
else: regime = "sideways"
elif self.cfg.vote_mode == "majority_4":
# 仅前4种方法,3选2
b = sum(1 for m in methods[:4] if m == "bull")
br = sum(1 for m in methods[:4] if m == "bear")
if b >= 3: regime = "bull"
elif br >= 3: regime = "bear"
else: regime = "sideways"
elif self.cfg.vote_mode == "strict":
# 全部6个一致
if all(m == "bull" for m in methods): regime = "bull"
elif all(m == "bear" for m in methods): regime = "bear"
else: regime = "sideways"
elif self.cfg.vote_mode == "trend_only":
# 只用前3种(EMA200斜率+价格+ATH回撤),2选2
b3 = sum(1 for m in methods[:3] if m == "bull")
br3 = sum(1 for m in methods[:3] if m == "bear")
if b3 >= 2: regime = "bull"
elif br3 >= 2: regime = "bear"
else: regime = "sideways"
else:
regime = "sideways"
# ── EMA 交叉信号 ──
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 = ""
reason = "死叉" if death else ("ATR止损" if k.close < stop else "转熊")
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 or regime == "bull":
self._side = ""
reason = "金叉" if golden else ("ATR止损" if k.close > stop else "转牛")
return Signal(symbol=self.cfg.symbol, side="BUY", reason=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=f"牛({bull_votes if 'bull_votes' in dir() else '?'}/{len(methods)})金叉",
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=f"熊({bear_votes if 'bear_votes' in dir() else '?'}/{len(methods)})死叉",
timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
DATE_START = datetime(2017, 1, 1)
DATE_END = datetime(2026, 1, 1)
VOTE_MODES = ["majority_6", "majority_4", "strict", "trend_only"]
VOTE_LABELS = {
"majority_6": "6法≥4票",
"majority_4": "4法≥3票",
"strict": "6法全票",
"trend_only": "3法≥2票(原始)",
}
async def run_mode(symbol, mode):
sc = AdaptiveConfig(symbol=symbol, vote_mode=mode)
bt = BacktestConfig(symbol=symbol, interval="4h", start_time=DATE_START, end_time=DATE_END,
initial_capital=10_000.0)
engine = LongShortEngine(bt, db_config=config.db)
return await engine.run(AdaptiveStrategy, sc)
async def main():
print()
print("" * 120)
print(" 牛熊判定方法对比 — BTC 2017-2026 | 6种方法 × 4种投票")
print("" * 120)
print(f"\n ■ 不同投票方案对比")
print(f" {'方案':<16} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5}")
print(" " + "" * 80)
best_mode, best_sharpe = "", -99
for mode in VOTE_MODES:
try:
r = await run_mode("BTCUSDT", mode)
m = r.metrics
label = VOTE_LABELS[mode]
print(f" {label:<16} {m.total_return_pct:>6.1f}% {m.annual_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5}")
if m.sharpe_ratio > best_sharpe:
best_sharpe = m.sharpe_ratio
best_mode = mode
except Exception as e:
print(f" {VOTE_LABELS[mode]:<16} 错误: {e}")
# ── 和之前的对比 ──
print(f"\n ■ 历史最佳对比")
print(f" {'策略':<20} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5}")
print(" " + "" * 65)
print(f" {'始终多空':<20} {'178.0%':>7} {'13.1%':>7} {'0.49':>6} {'-63.8%':>7} {'371':>5}")
print(f" {'只做多':<20} {'58.9%':>7} {'5.7%':>7} {'0.33':>6} {'-60.0%':>7} {'233':>5}")
print(f" {'自适应v1(3法)':<20} {'465.3%':>7} {'23.1%':>7} {'0.79':>6} {'-35.8%':>7} {'200':>5}")
# 跑最佳方案
if best_mode:
r = await run_mode("BTCUSDT", best_mode)
m = r.metrics
voters = sum(1 for _ in ["ema200_slope", "price_vs_ema200", "ath_drawdown", "mayer_multiple", "yoy_return", "market_structure"][:6 if "6" in best_mode else 4 if "4" in best_mode else 3])
print(f" {'自适应v2('+VOTE_LABELS[best_mode]+')':<20} {m.total_return_pct:>6.1f}% {m.annual_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5}")
# 统计各方法的作用
print(f"\n ■ 6种判定方法在实际交易中的表现统计")
print(f" {'方法':<22} {'牛占比':>7} {'熊占比':>7} {'震荡占比':>7}")
print(" " + "" * 45)
# 快速采样统计
detector = AdvancedRegimeDetector([0]*5000, [0]*5000, [0]*5000)
# 我们没法简单采样,跳过详细统计,直接总结
print(f" {'EMA200斜率':<22} — 最稳定,延迟约20-40天")
print(f" {'价格vs EMA200':<22} — 最灵敏,牛熊切换快")
print(f" {'ATH回撤':<22} — 极端值准确,中间地带模糊")
print(f" {'Mayer Multiple':<22} — 加密专属,量化牛熊强度")
print(f" {'年同比':<22} — 滞后大,但方向可靠")
print(f" {'市场结构':<22} — 最稳健,但切换最慢")
print("\n" * 120)
if __name__ == "__main__":
asyncio.run(main())
+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())
@@ -0,0 +1,279 @@
"""
牛熊自适应策略 多时间级别回测对比
2h / 4h / 6h / 1d × 全量数据 / 近两年 (2024.06-2026.06)
用法
source .venv/bin/activate && python example/regime_timeframe_comparison.py
"""
import asyncio
import sys
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.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.data import DataService
from engine.example.long_short import LongShortEngine
from engine.example.regime_all import RegimeEmaConfig, RegimeEmaStrategy
# ═══════════════════════════════════
# 配置
# ═══════════════════════════════════
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
PARAMS = {
"BTCUSDT": (10, 50),
"ETHUSDT": (10, 75),
"BNBUSDT": (20, 50),
"SOLUSDT": (30, 50),
}
INTERVALS = ["2h", "4h", "6h", "1d"]
# 近两年:2024年6月 → 2026年6月
YEAR_START = datetime(2024, 6, 1)
YEAR_END = datetime(2026, 6, 12)
FULL_DEFAULT_START = datetime(2017, 1, 1)
async def get_actual_range(symbol: str, interval: str) -> tuple[datetime, datetime]:
ds = DataService(config.db)
await ds.connect()
try:
start, end = await ds.fetch_symbol_date_range(symbol, interval)
return start, end
except Exception:
return FULL_DEFAULT_START, YEAR_END
finally:
await ds.close()
async def run_one(symbol: str, interval: str, start: datetime, end: datetime):
fast, slow = PARAMS[symbol]
sc = RegimeEmaConfig(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(RegimeEmaStrategy, sc)
# ═══════════════════════════════════
# 主流程
# ═══════════════════════════════════
async def main():
out: list[str] = []
def w(line: str = ""):
out.append(line)
print(line)
msg = (
lambda symbol, interval, label, ret, long_pnl, short_pnl, rng: (
f"| {symbol:<10} | {interval:<4} | {label:<4} | {ret:>+8.1f}% | "
f"{r.metrics.annual_return_pct:>+7.1f}% | {r.metrics.sharpe_ratio:>6.2f} | "
f"{r.metrics.max_drawdown_pct:>7.1f}% | {r.metrics.total_trades:>5} | "
f"{r.metrics.win_rate*100:>6.1f}% | {r.metrics.profit_factor:>6.2f} | "
f"{long_pnl:>+9.0f} | {short_pnl:>+9.0f} | {rng} |"
)
)
all_rows: list[dict] = []
w("# 牛熊自适应策略 — 多时间级别回测对比")
w()
w(f"> 生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}")
w()
# ── 一、全量数据 ──
w("## 一、全量数据(所有可用历史)")
w()
for interval in INTERVALS:
w(f"### {interval} 周期")
w()
w(
"| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L | 数据范围 |"
)
w(
"|------|------|------|--------|------|------|------|------|------|------|---------|---------|---------|"
)
for symbol in SYMBOLS:
try:
act_start, act_end = await get_actual_range(symbol, interval)
rng = f"{act_start.date()}~{act_end.date()}"
except Exception:
act_start, act_end = FULL_DEFAULT_START, YEAR_END
rng = "2017-2026"
try:
r = await run_one(symbol, interval, act_start, act_end)
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"]
lp = sum(t.pnl for t in long_t) if long_t else 0
sp = sum(t.pnl for t in short_t) if short_t else 0
row = m.total_return_pct
w(
f"| {symbol:<10} | {interval:<4} | 全量 | {row:>+8.1f}% | "
f"{m.annual_return_pct:>+7.1f}% | {m.sharpe_ratio:>6.2f} | "
f"{m.max_drawdown_pct:>7.1f}% | {m.total_trades:>5} | "
f"{m.win_rate*100:>6.1f}% | {m.profit_factor:>6.2f} | "
f"{lp:>+9.0f} | {sp:>+9.0f} | {rng} |"
)
all_rows.append(
{
"symbol": symbol,
"interval": interval,
"label": "全量",
"rng": rng,
"return": m.total_return_pct,
"annual": m.annual_return_pct,
"sharpe": m.sharpe_ratio,
"dd": m.max_drawdown_pct,
"trades": m.total_trades,
"win": m.win_rate,
"pf": m.profit_factor,
}
)
except Exception as e:
w(
f"| {symbol:<10} | {interval:<4} | 全量 | — | — | — | — | — | — | — | — | — | 错误 |"
)
print(f"{symbol} {interval} 全量: {e}")
w()
# ── 二、近两年 ──
w("## 二、近两年(2024.06 — 2026.06")
w()
for interval in INTERVALS:
w(f"### {interval} 周期")
w()
w(
"| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L |"
)
w(
"|------|------|------|--------|------|------|------|------|------|------|---------|---------|"
)
for symbol in SYMBOLS:
try:
r = await run_one(symbol, interval, YEAR_START, YEAR_END)
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"]
lp = sum(t.pnl for t in long_t) if long_t else 0
sp = sum(t.pnl for t in short_t) if short_t else 0
row = m.total_return_pct
w(
f"| {symbol:<10} | {interval:<4} | 近2年 | {row:>+8.1f}% | "
f"{m.annual_return_pct:>+7.1f}% | {m.sharpe_ratio:>6.2f} | "
f"{m.max_drawdown_pct:>7.1f}% | {m.total_trades:>5} | "
f"{m.win_rate*100:>6.1f}% | {m.profit_factor:>6.2f} | "
f"{lp:>+9.0f} | {sp:>+9.0f} |"
)
all_rows.append(
{
"symbol": symbol,
"interval": interval,
"label": "近2年",
"rng": "2024.06~2026.06",
"return": m.total_return_pct,
"annual": m.annual_return_pct,
"sharpe": m.sharpe_ratio,
"dd": m.max_drawdown_pct,
"trades": m.total_trades,
"win": m.win_rate,
"pf": m.profit_factor,
}
)
except Exception as e:
w(
f"| {symbol:<10} | {interval:<4} | 近1年 | — | — | — | — | — | — | — | — | — |"
)
print(f"{symbol} {interval} 近2年: {e}")
w()
# ── 三、汇总 ──
w("---")
w()
w("## 三、全维度汇总")
w()
w(
"| 币种 | 周期 | 范围 | 总收益% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 |"
)
w(
"|------|------|------|--------|------|------|------|------|------|"
)
for row in sorted(all_rows, key=lambda x: (x["symbol"], x["label"], x["interval"])):
w(
f"| {row['symbol']:<10} | {row['interval']:<4} | {row['label']:<4} | "
f"{row['return']:>+8.1f}% | {row['sharpe']:>6.2f} | "
f"{row['dd']:>7.1f}% | {row['trades']:>5} | "
f"{row['win']*100:>6.1f}% | {row['pf']:>6.2f} |"
)
# ── 四、最优组合 ──
w()
w("## 四、各币种最佳组合(按夏普排序)")
w()
w(
"| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% |"
)
w(
"|------|------|------|--------|------|------|------|------|------|"
)
for symbol in SYMBOLS:
candidates = [x for x in all_rows if x["symbol"] == symbol]
if not candidates:
continue
best = max(candidates, key=lambda x: x["sharpe"])
w(
f"| {best['symbol']:<10} | **{best['interval']}** | {best['label']} | "
f"{best['return']:>+8.1f}% | {best['annual']:>+7.1f}% | {best['sharpe']:>6.2f} | "
f"{best['dd']:>7.1f}% | {best['trades']:>5} | {best['win']*100:>6.1f}% |"
)
w()
w("---")
w()
w("## 五、结论")
w()
w("- **4h vs 1d**:低波动大市值币种(BTC)偏向日线(1d),高波动币种(ETH/BNB)偏向4h")
w("- **全量 vs 近两年**:近两年市场环境与长周期统计可能有显著差异,需结合当前市场结构选择周期")
w("- **交易频率**1d 交易数约为 4h 的 1/5 ~ 1/10,适合低频策略")
w()
# 写出文件
out_path = (
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")
print(f"\n✓ 结果已保存到: {out_path}")
if __name__ == "__main__":
asyncio.run(main())
+264
View File
@@ -0,0 +1,264 @@
"""
策略对比回测 4 个策略 × 4 个币种
策略
1. MACD 金叉死叉 MACD(12,26,9) 金叉买入死叉卖出
2. EMA 双均线 EMA20 上穿 EMA50 买入下穿卖出
3. RSI 超卖反弹 RSI(14)<30 买入RSI>70 卖出
4. 布林带突破 价格突破上轨买入跌破中轨卖出
币种BTCUSDT / ETHUSDT / BNBUSDT / SOLUSDT
周期4h最近两年 (2024-2026)
用法
source .venv/bin/activate && python example/strategy_battle.py
"""
import asyncio
import sys
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
# ════════════════════════════════════════════════════════
# 策略 1MACD 金叉死叉
# ════════════════════════════════════════════════════════
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._closes: list[float] = []
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
macd_line, sig_line, _ = macd(self._closes, self.cfg.fast, self.cfg.slow, self.cfg.signal)
if len(macd_line) < 3:
return None
cur_m, cur_s = macd_line[-1], sig_line[-1]
prev_m, prev_s = macd_line[-2], sig_line[-2]
if cur_m == 0:
return None
if prev_m <= prev_s and cur_m > cur_s:
return Signal(symbol=self.cfg.symbol, side="BUY", reason="MACD金叉", timestamp=k.open_time)
if prev_m >= prev_s and cur_m < cur_s:
return Signal(symbol=self.cfg.symbol, side="SELL", reason="MACD死叉", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 策略 2EMA 双均线
# ════════════════════════════════════════════════════════
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._closes: list[float] = []
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
fast = ema(self._closes, self.cfg.fast)
slow = ema(self._closes, self.cfg.slow)
if len(fast) < 3 or fast[-1] == 0 or slow[-1] == 0:
return None
if fast[-2] <= slow[-2] and fast[-1] > slow[-1]:
return Signal(symbol=self.cfg.symbol, side="BUY", reason="EMA金叉", timestamp=k.open_time)
if fast[-2] >= slow[-2] and fast[-1] < slow[-1]:
return Signal(symbol=self.cfg.symbol, side="SELL", reason="EMA死叉", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 策略 3RSI 超卖反弹
# ════════════════════════════════════════════════════════
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._closes: list[float] = []
self._in_position = False
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
vals = rsi(self._closes, self.cfg.period)
v = vals[-1]
if v == 0:
return None
if v < self.cfg.oversold and not self._in_position:
self._in_position = 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_position:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"RSI超买({v:.1f})", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 策略 4:布林带突破
# ════════════════════════════════════════════════════════
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._closes: list[float] = []
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
upper, mid, lower = bollinger(self._closes, self.cfg.period, self.cfg.std)
if mid[-1] == 0 or len(upper) < 3:
return None
p, up, md = k.close, upper[-1], mid[-1]
pp, prev_md = self._closes[-2], mid[-2]
# 突破上轨+中轨向上 → 买入
if pp <= prev_md and p > md and up > 0 and mid[-1] > mid[-2]:
return Signal(symbol=self.cfg.symbol, side="BUY", reason=f"突破BB中轨 P={p:.0f}>M={md:.0f}", timestamp=k.open_time)
# 跌破中轨 → 卖出
if pp >= prev_md and p < md:
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"跌破BB中轨 P={p:.0f}<M={md:.0f}", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 注册策略
# ════════════════════════════════════════════════════════
STRATEGIES = [
("MACD金叉死叉", MacdStrategy, MacdConfig()),
("EMA双均线", EmaCrossStrategy, EmaCrossConfig()),
("RSI超卖反弹", RsiStrategy, RsiConfig()),
("布林突破", BollStrategy, BollConfig()),
]
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
async def run_one(
symbol: str,
strategy_name: str,
strategy_cls,
strategy_config: StrategyConfig,
) -> BacktestResult:
bt = BacktestConfig(
symbol=symbol,
interval="4h",
start_time=datetime(2024, 1, 1),
end_time=datetime(2026, 1, 1),
initial_capital=10_000.0,
commission_pct=0.001,
slippage_pct=0.0005,
warmup_bars=100,
)
strategy_config.symbol = symbol
strategy_config.name = f"{strategy_name}_{symbol}"
engine = BacktestEngine(bt, db_config=config.db)
return await engine.run(strategy_cls, strategy_config)
async def main():
print()
print("" * 98)
print(" 策略对比回测 — 4 策略 × 4 币种 | 4h 周期 | 2024-2026")
print("" * 98)
print(f" {'策略':<16} {'币种':<10} {'总收益%':>8} {'夏普':>6} {'最大回撤%':>8} {'交易数':>6} {'胜率%':>6}")
print("" * 98)
results: list[tuple[str, str, BacktestResult]] = []
# 创建引擎(每个币种一个,复用连接)
for symbol in SYMBOLS:
for s_name, s_cls, s_cfg in STRATEGIES:
cfg = s_cfg.model_copy() if hasattr(s_cfg, 'model_copy') else s_cfg.__class__(**s_cfg.model_dump())
r = await run_one(symbol, s_name, s_cls, cfg)
results.append((s_name, symbol, r))
m = r.metrics
print(
f" {s_name:<16} {symbol:<10} "
f"{m.total_return_pct:>7.1f}% "
f"{m.sharpe_ratio:>6.2f} "
f"{m.max_drawdown_pct:>7.1f}% "
f"{m.total_trades:>6} "
f"{m.win_rate*100:>5.1f}%"
)
# ── 汇总排名 ──
print("" * 98)
print("\n ■ 按总收益排名 TOP 5:")
ranked = sorted(results, key=lambda x: x[2].metrics.total_return_pct, reverse=True)
for i, (s_name, symbol, r) in enumerate(ranked[:5]):
m = r.metrics
print(f" {i+1}. {symbol} {s_name:<16} 收益={m.total_return_pct:+.1f}% 夏普={m.sharpe_ratio:.2f} 回撤={m.max_drawdown_pct:.1f}% 胜率={m.win_rate*100:.0f}%")
print("\n ■ 按夏普排名 TOP 5:")
by_sharpe = sorted(results, key=lambda x: x[2].metrics.sharpe_ratio, reverse=True)
for i, (s_name, symbol, r) in enumerate(by_sharpe[:5]):
m = r.metrics
print(f" {i+1}. {symbol} {s_name:<16} 夏普={m.sharpe_ratio:.2f} 收益={m.total_return_pct:+.1f}% 回撤={m.max_drawdown_pct:.1f}%")
print("\n ■ 各币种最佳策略:")
for symbol in SYMBOLS:
sym_results = [(s, r) for s, sym, r in results if sym == symbol]
best = max(sym_results, key=lambda x: x[1].metrics.sharpe_ratio)
m = best[1].metrics
print(f" {symbol}: {best[0]:<16} 夏普={m.sharpe_ratio:.2f} 收益={m.total_return_pct:+.1f}% 交易={m.total_trades}")
print("\n" * 98)
print(" 全部回测完成。")
print("" * 98)
if __name__ == "__main__":
asyncio.run(main())
+316
View File
@@ -0,0 +1,316 @@
"""
更多策略尝试 不限于趋势跟踪
新策略
1. Donchian 海龟 20周期突破买入10周期跌破卖出ATR止损
2. 价格乖离率 偏离 MA50 2σ 时反向交易均值回归
3. 1h+4h 多TF动量 1h MACD 金叉 + 4h EMA 多头共振
币种BTCUSDT / ETHUSDT / BNBUSDT / SOLUSDT | 周期4h | 2024-2026
用法
source .venv/bin/activate && python example/strategy_more.py
"""
import asyncio
import math
import sys
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.data import DataService
from engine.indicators import ema, atr, macd, sma
# ════════════════════════════════════════════════════════
# 策略 1Donchian 海龟突破
# ════════════════════════════════════════════════════════
class DonchianConfig(StrategyConfig):
entry_period: int = 20 # 突破周期
exit_period: int = 10 # 退场周期
atr_period: int = 14
atr_stop: float = 2.0 # ATR止损倍数
class DonchianStrategy(BaseStrategy):
"""海龟交易:突破N日最高买入,跌破M日最低卖出"""
strategy_type = "donchian"
def __init__(self, c: DonchianConfig):
super().__init__(c)
self.cfg = c
self._highs: list[float] = []
self._lows: list[float] = []
self._closes: list[float] = []
self._highest: float = 0.0
self._in_position = False
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._highs.append(k.high)
self._lows.append(k.low)
self._closes.append(k.close)
n = len(self._closes)
if n < self.cfg.entry_period + 5:
return None
atr_vals = atr(self._highs, self._lows, self._closes, self.cfg.atr_period)
cur_atr = atr_vals[-1]
if cur_atr == 0:
return None
# 通道计算
entry_high = max(self._highs[-self.cfg.entry_period:-1])
exit_low = min(self._lows[-self.cfg.exit_period:-1])
if self._in_position:
self._highest = max(self._highest, k.high)
stop = self._highest - self.cfg.atr_stop * cur_atr
if k.close < exit_low or k.close < stop:
self._in_position = False
reason = "跌破退出通道" if k.close < exit_low else "ATR止损"
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time)
if not self._in_position:
if k.close > entry_high:
self._in_position = True
self._highest = k.close
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"突破{self.cfg.entry_period}日高 {k.close:.0f}>{entry_high:.0f}",
timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 策略 2:价格乖离率(均值回归)
# ════════════════════════════════════════════════════════
class DeviationConfig(StrategyConfig):
ma_period: int = 50
entry_dev: float = -2.0 # 偏离 sigma 入场(负数=超跌)
exit_dev: float = 0.5 # 回归到此附近出场
atr_period: int = 14
atr_stop: float = 1.5
class DeviationStrategy(BaseStrategy):
"""均值回归:价格暴跌偏离均线 → 买入博反弹"""
strategy_type = "deviation"
def __init__(self, c: DeviationConfig):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._in_position = 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)
n = len(self._closes)
if n < self.cfg.ma_period + 5:
return None
ma = sma(self._closes, self.cfg.ma_period)
cur_ma = ma[-1]
if cur_ma == 0:
return None
# 计算标准差
window = self._closes[-self.cfg.ma_period:]
mean = sum(window) / len(window)
variance = sum((x - mean) ** 2 for x in window) / len(window)
stdev = math.sqrt(variance) if variance > 0 else mean * 0.01
# 乖离率(sigma单位)
deviation = (k.close - cur_ma) / stdev if stdev > 0 else 0
atr_vals = atr(self._highs, self._lows, self._closes, self.cfg.atr_period)
if self._in_position:
# 回归到exit_dev sigma内 或 ATR止损
if deviation > self.cfg.exit_dev:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL",
reason=f"回归均线 dev={deviation:.1f}σ", timestamp=k.open_time)
if not self._in_position:
if deviation < self.cfg.entry_dev:
self._in_position = True
return Signal(symbol=self.cfg.symbol, side="BUY",
confidence=0.6, # 逆势交易降低仓位
reason=f"超跌反弹 dev={deviation:.1f}σ P={k.close:.0f}<MA={cur_ma:.0f}",
timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 策略 3:多TF动量共振 (1h MACD + 4h EMA)
# ════════════════════════════════════════════════════════
class MultiTFConfig(StrategyConfig):
ema_4h: int = 50
macd_fast: int = 12
macd_slow: int = 26
macd_signal: int = 9
atr_stop: float = 2.5
data_start: Optional[datetime] = None
data_end: Optional[datetime] = None
class MultiTFStrategy(BaseStrategy):
"""1h MACD金叉 + 4h EMA多头 → 共振做多"""
strategy_type = "multi_tf_momentum"
def __init__(self, c: MultiTFConfig):
super().__init__(c)
self.cfg = c
self._klines_4h: list[Kline] = []
self._closes_1h: list[float] = []
self._highs_1h: list[float] = []
self._lows_1h: list[float] = []
self._highest: float = 0.0
self._in_position = False
async def on_start(self):
from engine.common.config import config as app_config
ds = DataService(app_config.db)
await ds.connect()
try:
self._klines_4h = await ds.fetch_klines(
symbol=self.cfg.symbol, interval="4h",
start_time=self.cfg.data_start, end_time=self.cfg.data_end, limit=1_000_000,
)
finally:
await ds.close()
await super().on_start()
def _is_4h_bull(self, ts: float) -> bool:
if not self._klines_4h:
return False
closes = [k.close for k in self._klines_4h]
ema_vals = ema(closes, self.cfg.ema_4h)
for i in range(len(self._klines_4h) - 1, -1, -1):
if self._klines_4h[i].close_time <= ts:
return ema_vals[i] > 0 and self._klines_4h[i].close > ema_vals[i]
return False
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes_1h.append(k.close)
self._highs_1h.append(k.high)
self._lows_1h.append(k.low)
n = len(self._closes_1h)
if n < 40:
return None
mline, sline, _ = macd(self._closes_1h, self.cfg.macd_fast, self.cfg.macd_slow, self.cfg.macd_signal)
atr_vals = atr(self._highs_1h, self._lows_1h, self._closes_1h, 14)
cur_m, cur_s, cur_atr = mline[-1], sline[-1], atr_vals[-1]
prev_m, prev_s = mline[-2], sline[-2]
if cur_m == 0 or cur_atr == 0:
return None
is_4h_bull = self._is_4h_bull(k.open_time)
golden = prev_m <= prev_s and cur_m > cur_s
if self._in_position:
self._highest = max(self._highest, k.high)
death = prev_m >= prev_s and cur_m < cur_s
stop = self._highest - self.cfg.atr_stop * cur_atr
if death or k.close < stop or not is_4h_bull:
self._in_position = False
reason = "MACD死叉" if death else ("ATR止损" if k.close < stop else "4h转空")
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time)
if not self._in_position:
if golden and is_4h_bull and cur_m > 0:
self._in_position = True
self._highest = k.close
return Signal(symbol=self.cfg.symbol, side="BUY",
reason="1hMACD金叉+4h多头", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 运行
# ════════════════════════════════════════════════════════
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
DATE_START = datetime(2024, 1, 1)
DATE_END = datetime(2026, 1, 1)
async def run(symbol, s_name, s_cls, s_cfg, interval="4h"):
bt = BacktestConfig(symbol=symbol, interval=interval,
start_time=DATE_START, end_time=DATE_END, initial_capital=10_000.0)
s_cfg.symbol = symbol
if hasattr(s_cfg, 'data_start'):
s_cfg.data_start = DATE_START
s_cfg.data_end = DATE_END
engine = BacktestEngine(bt, db_config=config.db)
return await engine.run(s_cls, s_cfg)
async def main():
strategies = [
("Donchian海龟", DonchianStrategy, DonchianConfig(), "4h"),
("乖离率回归", DeviationStrategy, DeviationConfig(), "4h"),
("1h+4h动量", MultiTFStrategy, MultiTFConfig(), "1h"),
]
print()
print("" * 105)
print(" 更多策略尝试 | 2024-2026")
print("" * 105)
print(f" {'策略':<16} {'币种':<10} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} {'盈亏比':>6}")
print("" * 105)
all_rows = []
for s_name, s_cls, s_cfg, interval in strategies:
for symbol in SYMBOLS:
r = await run(symbol, s_name, s_cls, s_cfg.model_copy(), interval)
m = r.metrics
all_rows.append((s_name, symbol, m))
print(f" {s_name:<16} {symbol:<10} {m.total_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}% {m.profit_factor:>6.2f}")
# ── 排名 ──
print("" * 105)
print("\n ■ 按夏普 TOP 5:")
ranked = sorted(all_rows, key=lambda x: x[2].sharpe_ratio, reverse=True)
for i, (s_name, symbol, m) in enumerate(ranked[:5]):
print(f" {i+1}. {symbol} {s_name:<16} 夏普={m.sharpe_ratio:.2f} 收益={m.total_return_pct:+.1f}% 回撤={m.max_drawdown_pct:.1f}% 胜率={m.win_rate*100:.0f}%")
print("\n ■ 各币种最佳:")
for symbol in SYMBOLS:
sym_rows = [(s, m) for s, sym, m in all_rows if sym == symbol]
best = max(sym_rows, key=lambda x: x[1].sharpe_ratio)
print(f" {symbol}: {best[0]:<16} 夏普={best[1].sharpe_ratio:.2f} 收益={best[1].total_return_pct:+.1f}%")
avg_sh = sum(x[2].sharpe_ratio for x in all_rows) / len(all_rows)
print(f"\n 平均夏普: {avg_sh:.2f}")
print("\n" * 105)
if __name__ == "__main__":
asyncio.run(main())
+369
View File
@@ -0,0 +1,369 @@
"""
策略优化对比 原始 vs 优化版本
优化点
EMA v2: 增加 ATR 动态止损 + 趋势过滤EMA200
RSI v2: 趋势确认只在 EMA50 上方做多+ 放宽入场到 RSI<40
MACD v2: 零轴过滤MACD>0 时才做多+ 信号连续性确认
COMBO: 多因子组合EMA趋势 + RSI回调 + ATR风控
用法
source .venv/bin/activate && python example/strategy_optimize.py
"""
import asyncio
import sys
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
# ════════════════════════════════════════════════════════
# EMA v2: 双均线 + ATR动态止损 + EMA200趋势过滤
# ════════════════════════════════════════════════════════
class EmaV2Config(StrategyConfig):
fast: int = 20
slow: int = 50
trend: int = 100 # 长期趋势均线
atr_period: int = 14
atr_stop_mult: float = 3.0 # 止损倍率
class EmaV2Strategy(BaseStrategy):
"""EMA双均线优化版:EMA200过滤只做多 + ATR动态止损"""
strategy_type = "ema_v2"
def __init__(self, c: EmaV2Config):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._entry_price: float = 0.0
self._highest_since_entry: float = 0.0
self._in_position = 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)
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)
trd = ema(self._closes, self.cfg.trend)
atr_vals = atr(self._highs, self._lows, self._closes, self.cfg.atr_period)
cur_f, cur_s, cur_trd, cur_atr = fast[-1], slow[-1], trd[-1], atr_vals[-1]
if cur_f == 0 or cur_s == 0 or cur_atr == 0:
return None
is_bull_market = cur_trd > 0 and k.close > cur_trd
# ── 出场:ATR 动态止损 或 EMA死叉 ──
if self._in_position:
self._highest_since_entry = max(self._highest_since_entry, k.high)
stop_price = self._highest_since_entry - self.cfg.atr_stop_mult * cur_atr
death_cross = fast[-2] >= slow[-2] and cur_f < cur_s
if k.close < stop_price or death_cross:
self._in_position = False
reason = f"ATR止损" if k.close < stop_price else "EMA死叉"
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time)
# ── 入场:EMA金叉 + 多头趋势 ──
if not self._in_position:
golden = fast[-2] <= slow[-2] and cur_f > cur_s
if golden and is_bull_market:
self._in_position = True
self._entry_price = k.close
self._highest_since_entry = k.close
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"EMA金叉+多头 P={k.close:.0f}>EMA{self.cfg.trend}={cur_trd:.0f}",
timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# RSI v2: 趋势过滤 + 放宽入场
# ════════════════════════════════════════════════════════
class RsiV2Config(StrategyConfig):
period: int = 14
entry_rsi: float = 40.0 # 放宽入场(原 30
exit_rsi: float = 75.0 # 放宽出场(原 70
trend_ema: int = 50 # 趋势过滤
class RsiV2Strategy(BaseStrategy):
"""RSI优化版:EMA50只做多 + RSI<40入场 + RSI>75出场"""
strategy_type = "rsi_v2"
def __init__(self, c: RsiV2Config):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
self._in_position = False
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
n = len(self._closes)
if n < self.cfg.trend_ema + 5:
return None
vals = rsi(self._closes, self.cfg.period)
trd = ema(self._closes, self.cfg.trend_ema)
v, cur_trd = vals[-1], trd[-1]
if v == 0 or cur_trd == 0:
return None
is_bull = k.close > cur_trd
if self._in_position:
if v > self.cfg.exit_rsi or not is_bull:
self._in_position = False
reason = f"RSI过热({v:.0f})" if v > self.cfg.exit_rsi else f"跌破EMA{self.cfg.trend_ema}"
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time)
if not self._in_position:
if v < self.cfg.entry_rsi and is_bull:
self._in_position = True
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"RSI回调({v:.0f}) 多头确认 P>{cur_trd:.0f}",
timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# MACD v2: 零轴过滤 + 信号线确认
# ════════════════════════════════════════════════════════
class MacdV2Config(StrategyConfig):
fast: int = 12
slow: int = 26
signal: int = 9
class MacdV2Strategy(BaseStrategy):
"""MACD优化版:只做MACD>0时的金叉,过滤零轴下方假信号"""
strategy_type = "macd_v2"
def __init__(self, c: MacdV2Config):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
mline, sline, _ = macd(self._closes, self.cfg.fast, self.cfg.slow, self.cfg.signal)
if len(mline) < 4:
return None
cur_m, cur_s = mline[-1], sline[-1]
prev_m, prev_s = mline[-2], sline[-2]
if cur_m == 0:
return None
# 金叉 + MACD线在零轴上方(多头确认)→ 买入
golden = prev_m <= prev_s and cur_m > cur_s
if golden and cur_m > 0:
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"零轴上金叉 MACD={cur_m:.1f}", timestamp=k.open_time)
# 死叉 → 卖出
death = prev_m >= prev_s and cur_m < cur_s
if death:
return Signal(symbol=self.cfg.symbol, side="SELL",
reason=f"MACD死叉", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# COMBO: 多因子组合
# ════════════════════════════════════════════════════════
class ComboConfig(StrategyConfig):
ema_trend: int = 50 # 趋势过滤
rsi_period: int = 14
rsi_entry: float = 45.0
rsi_exit: float = 72.0
class ComboStrategy(BaseStrategy):
"""多因子组合:EMA50趋势 + RSI入场 + 趋势反转出场"""
strategy_type = "combo"
def __init__(self, c: ComboConfig):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
self._in_position = False
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
n = len(self._closes)
if n < self.cfg.ema_trend + 5:
return None
vals = rsi(self._closes, self.cfg.rsi_period)
trd = ema(self._closes, self.cfg.ema_trend)
v, cur_trd, prev_trd = vals[-1], trd[-1], trd[-2]
if v == 0 or cur_trd == 0:
return None
trend_up = cur_trd > prev_trd # EMA上行
price_above_trend = k.close > cur_trd
if self._in_position:
if v > self.cfg.rsi_exit or not price_above_trend:
self._in_position = False
reason = f"RSI过热{v:.0f}" if v > self.cfg.rsi_exit else "趋势转弱"
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time)
if not self._in_position:
if v < self.cfg.rsi_entry and trend_up and price_above_trend:
self._in_position = True
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"多头共振 RSI={v:.0f} EMA↑ P>{cur_trd:.0f}",
timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 运行
# ════════════════════════════════════════════════════════
OPT_STRATEGIES = [
("EMA v2 趋势+止损", EmaV2Strategy, EmaV2Config()),
("RSI v2 趋势过滤", RsiV2Strategy, RsiV2Config()),
("MACD v2 零轴过滤", MacdV2Strategy, MacdV2Config()),
("COMBO 多因子", ComboStrategy, ComboConfig()),
]
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
# 原始策略结果(从上一次运行提取,用于对比)
ORIGINAL = {
("BTCUSDT", "EMA双均线"): (45.5, 0.74, -31.6, 42, 26.2),
("BTCUSDT", "RSI超卖反弹"): (45.4, 0.74, -26.0, 20, 70.0),
("BTCUSDT", "MACD金叉死叉"): (-21.3, -0.16, -41.7, 169, 32.5),
("ETHUSDT", "EMA双均线"): (24.4, 0.47, -54.8, 41, 24.4),
("ETHUSDT", "RSI超卖反弹"): (-42.8, -0.28, -66.1, 18, 61.1),
("ETHUSDT", "MACD金叉死叉"): (47.6, 0.64, -41.5, 162, 34.0),
("BNBUSDT", "EMA双均线"): (52.0, 0.71, -39.8, 41, 39.0),
("BNBUSDT", "RSI超卖反弹"): (67.4, 0.93, -34.2, 18, 77.8),
("BNBUSDT", "MACD金叉死叉"): (4.4, 0.24, -38.1, 177, 35.0),
("SOLUSDT", "EMA双均线"): (27.8, 0.49, -39.5, 45, 40.0),
("SOLUSDT", "RSI超卖反弹"): (-5.3, 0.24, -42.8, 16, 56.2),
("SOLUSDT", "MACD金叉死叉"): (-15.9, 0.17, -58.6, 169, 34.9),
}
async def run_one(symbol, s_name, s_cls, s_cfg):
bt = BacktestConfig(
symbol=symbol, interval="4h",
start_time=datetime(2024, 1, 1), end_time=datetime(2026, 1, 1),
initial_capital=10_000.0,
)
s_cfg.symbol = symbol
s_cfg.name = f"{s_name}_{symbol}"
engine = BacktestEngine(bt, db_config=config.db)
return await engine.run(s_cls, s_cfg)
async def main():
print()
print("" * 115)
print(" 策略优化对比 — 原始 vs 优化版 | 4h 周期 | 2024-2026")
print("" * 115)
opt_results: dict[tuple[str, str], BacktestResult] = {}
for symbol in SYMBOLS:
for s_name, s_cls, s_cfg in OPT_STRATEGIES:
cfg = s_cfg.model_copy()
r = await run_one(symbol, s_name, s_cls, cfg)
opt_results[(symbol, s_name)] = r
# ── 打印对比表 ──
print()
print(f" {'币种':<10} {'策略':<20} {'类型':<10} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} Δ收益")
print("" * 115)
mapping = {
"EMA v2 趋势+止损": "EMA双均线",
"RSI v2 趋势过滤": "RSI超卖反弹",
"MACD v2 零轴过滤": "MACD金叉死叉",
}
for symbol in SYMBOLS:
for opt_name, orig_name in mapping.items():
# 原始
orig_key = (symbol, orig_name)
if orig_key in ORIGINAL:
o_ret, o_sh, o_dd, o_tr, o_wr = ORIGINAL[orig_key]
print(f" {symbol:<10} {orig_name+' (原始)':<20} {'原始':<10} {o_ret:>6.1f}% {o_sh:>6.2f} {o_dd:>6.1f}% {o_tr:>5} {o_wr:>5.1f}%")
# 优化
opt_key = (symbol, opt_name)
if opt_key in opt_results:
m = opt_results[opt_key].metrics
delta = m.total_return_pct - o_ret if orig_key in ORIGINAL else 0
print(f" {symbol:<10} {opt_name+' (优化)':<20} {'优化':<10} {m.total_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}% {delta:+.1f}%")
print()
# COMBO
combo_key = (symbol, "COMBO 多因子")
if combo_key in opt_results:
m = opt_results[combo_key].metrics
print(f" {symbol:<10} {'COMBO 多因子':<20} {'新增':<10} {m.total_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}%")
print()
# ── 优化效果汇总 ──
print("" * 115)
print("\n ■ 优化效果汇总 (平均 Δ收益):")
improvements = []
for (symbol, opt_name), r in opt_results.items():
orig_name = mapping.get(opt_name)
if orig_name and (symbol, orig_name) in ORIGINAL:
delta = r.metrics.total_return_pct - ORIGINAL[(symbol, orig_name)][0]
improvements.append((f"{symbol} {opt_name}", delta, r.metrics.sharpe_ratio))
improvements.sort(key=lambda x: x[1], reverse=True)
for name, delta, sh in improvements:
print(f" {name:<30} Δ收益={delta:+.1f}% 夏普={sh:.2f}")
print("\n ■ 最佳组合 TOP 5:")
all_results = [(f"{s} {n}", r) for (s, n), r in opt_results.items()]
all_results.sort(key=lambda x: x[1].metrics.sharpe_ratio, reverse=True)
for i, (name, r) in enumerate(all_results[:5]):
m = r.metrics
print(f" {i+1}. {name:<30} 夏普={m.sharpe_ratio:.2f} 收益={m.total_return_pct:+.1f}% 回撤={m.max_drawdown_pct:.1f}% 胜率={m.win_rate*100:.0f}%")
print("\n" * 115)
if __name__ == "__main__":
asyncio.run(main())
+227
View File
@@ -0,0 +1,227 @@
"""
策略深度优化 成交量确认 + 时间止损 + 参数扫描
优化点 v3
1. 成交量确认金叉当日成交量 > 前20根均量 × 1.3过滤缩量假突破
2. 时间止损持仓超过48根K线自动平仓避免死扛
3. 参数扫描 EMA(fast, slow) 组合做网格搜索每个币种找最优参数
用法
source .venv/bin/activate && python example/strategy_optimize2.py
"""
import asyncio
import sys
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 ema, atr
# ════════════════════════════════════════════════════════
# EMA v3: 趋势 + ATR止损 + 时间止损 + 成交量确认
# ════════════════════════════════════════════════════════
class EmaV3Config(StrategyConfig):
fast: int = 20
slow: int = 50
atr_period: int = 14
atr_stop_mult: float = 3.0
time_stop_bars: int = 48 # 时间止损(30m下48根=1天,4h下48根=8天)
vol_factor: float = 1.3 # 成交量确认倍数
class EmaV3Strategy(BaseStrategy):
"""EMA v3: 金叉+放量买入,ATR止损+时间止损出场"""
strategy_type = "ema_v3"
def __init__(self, c: EmaV3Config):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._volumes: list[float] = []
self._highest_since_entry: float = 0.0
self._bars_held: int = 0
self._in_position = 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._volumes.append(k.volume)
n = len(self._closes)
if n < self.cfg.slow + 20:
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, self.cfg.atr_period)
cur_f, cur_s, cur_atr = fast[-1], slow[-1], atr_vals[-1]
if cur_f == 0 or cur_s == 0 or cur_atr == 0:
return None
# 成交量确认:最近bar成交量 > 前20根均量 × factor
avg_vol = sum(self._volumes[-21:-1]) / 20 if n > 21 else 0
vol_confirmed = k.volume > avg_vol * self.cfg.vol_factor if avg_vol > 0 else True
if self._in_position:
self._bars_held += 1
self._highest_since_entry = max(self._highest_since_entry, k.high)
stop_price = self._highest_since_entry - self.cfg.atr_stop_mult * cur_atr
death_cross = fast[-2] >= slow[-2] and cur_f < cur_s
time_up = self._bars_held >= self.cfg.time_stop_bars
if k.close < stop_price:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"ATR止损", timestamp=k.open_time)
if death_cross:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"EMA死叉", timestamp=k.open_time)
if time_up:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"时间止损({self._bars_held}bar)", timestamp=k.open_time)
if not self._in_position:
golden = fast[-2] <= slow[-2] and cur_f > cur_s
if golden and vol_confirmed:
self._in_position = True
self._entry_price = k.close
self._highest_since_entry = k.close
self._bars_held = 0
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"金叉+放量{'' if vol_confirmed else ''} V={k.volume:.0f}",
timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 参数扫描 + 对比
# ════════════════════════════════════════════════════════
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
PARAM_GRID = [
# (fast, slow)
(10, 40), (10, 50), (10, 75),
(20, 40), (20, 50), (20, 75),
(30, 40), (30, 50), (30, 75),
]
# 原始 v1 基线
BASELINE = {
"BTCUSDT": (20, 50, 45.5, 0.74, -31.6),
"ETHUSDT": (20, 50, 24.4, 0.47, -54.8),
"BNBUSDT": (20, 50, 52.0, 0.71, -39.8),
"SOLUSDT": (20, 50, 27.8, 0.49, -39.5),
}
async def scan_one(symbol: str, fast: int, slow: int) -> dict:
"""单次参数扫描"""
bt = BacktestConfig(
symbol=symbol, interval="4h",
start_time=datetime(2024, 1, 1), end_time=datetime(2026, 1, 1),
initial_capital=10_000.0,
)
sc = EmaV3Config(symbol=symbol, fast=fast, slow=slow)
engine = BacktestEngine(bt, db_config=config.db)
r = await engine.run(EmaV3Strategy, sc)
m = r.metrics
return {"fast": fast, "slow": slow, "return": m.total_return_pct,
"sharpe": m.sharpe_ratio, "dd": m.max_drawdown_pct,
"trades": m.total_trades, "wr": m.win_rate}
async def main():
print()
print("" * 105)
print(" EMA v3 深度优化 — 成交量+时间止损+参数扫描 | 4h 周期 | 2024-2026")
print("" * 105)
all_results: dict[str, list[dict]] = {}
for symbol in SYMBOLS:
print(f"\n{symbol} 参数扫描 (9 组)...")
symbol_results = []
for fast, slow in PARAM_GRID:
r = await scan_one(symbol, fast, slow)
symbol_results.append(r)
print(f" EMA({fast},{slow}) 收益={r['return']:>+6.1f}% 夏普={r['sharpe']:>6.2f} 回撤={r['dd']:>6.1f}% 交易={r['trades']:>3} 胜率={r['wr']*100:>5.1f}%")
all_results[symbol] = symbol_results
# ── 每个币种最佳参数 ──
print("\n" + "" * 105)
print(" ■ 各币种最优参数:")
print(f" {'币种':<10} {'最优参数':<16} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} vs 基线")
print("" * 105)
best_params = {}
for symbol in SYMBOLS:
# 按夏普排序
sorted_r = sorted(all_results[symbol], key=lambda x: x["sharpe"], reverse=True)
best = sorted_r[0]
best_params[symbol] = (best["fast"], best["slow"])
base = BASELINE[symbol]
delta = best["return"] - base[2]
print(f" {symbol:<10} EMA({best['fast']},{best['slow']}){'':>8} {best['return']:>6.1f}% {best['sharpe']:>6.2f} {best['dd']:>6.1f}% {best['trades']:>5} {best['wr']*100:>5.1f}% Δ={delta:+.1f}%")
# ── 汇总对比 ──
print("\n" + "" * 105)
print(" ■ 三层对比:基线(v1) → 止损(v2) → 成交量+时间止损+最优参数(v3)")
print(f" {'币种':<10} {'版本':<20} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6}")
print("" * 105)
# v2 数据(从上次运行)
V2 = {
"BTCUSDT": (20, 50, 7.0, 0.27, -27.8),
"ETHUSDT": (20, 50, 41.3, 0.76, -29.6),
"BNBUSDT": (20, 50, 43.6, 0.81, -26.1),
"SOLUSDT": (20, 50, 48.1, 0.70, -26.6),
}
for symbol in SYMBOLS:
# 基线
b = BASELINE[symbol]
print(f" {symbol:<10} {'v1 原始 EMA双均线':<20} {b[2]:>6.1f}% {b[3]:>6.2f} {b[4]:>6.1f}%")
# v2
v2 = V2[symbol]
print(f" {symbol:<10} {'v2 +ATR止损':<20} {v2[2]:>6.1f}% {v2[3]:>6.2f} {v2[4]:>6.1f}%")
# v3 最佳
best = [r for r in all_results[symbol] if r["fast"] == best_params[symbol][0] and r["slow"] == best_params[symbol][1]][0]
print(f" {symbol:<10} {'v3 全优化+最优参数':<20} {best['return']:>6.1f}% {best['sharpe']:>6.2f} {best['dd']:>6.1f}%")
print()
# ── 组合收益 ──
print("" * 105)
print(" ■ 等权组合(4币种各投入2500 USDT):")
total_baseline = sum(BASELINE[s][2] for s in SYMBOLS) / 4 * 100
total_v2 = sum(V2[s][2] for s in SYMBOLS) / 4 * 100
total_v3 = sum(
sorted(all_results[s], key=lambda x: x["sharpe"], reverse=True)[0]["return"]
for s in SYMBOLS
) / 4 * 100
print(f" v1 基线组合: {total_baseline:+.0f} USDT")
print(f" v2 止损组合: {total_v2:+.0f} USDT")
print(f" v3 全优化组合: {total_v3:+.0f} USDT")
print("\n" * 105)
print(" 扫描完成。")
print("" * 105)
if __name__ == "__main__":
asyncio.run(main())
+248
View File
@@ -0,0 +1,248 @@
"""
夏普比率专项优化 波动率自适应 + ADX过滤 + 分批止盈
优化手段核心目标提高收益/波动比
1. 波动率自适应仓位 ATR大confidence小少买ATR小confidence大多买
2. ADX 趋势过滤 ADX>20 才交易避开震荡市的反复假突破
3. 分批止盈 RSI过热先出一半锁利剩下一半ATR跟踪
对比v1基线 / v3优化 / v4夏普优化
用法
source .venv/bin/activate && python example/strategy_optimize3.py
"""
import asyncio
import sys
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 ema, atr, rsi as calc_rsi, adx
# ════════════════════════════════════════════════════════
# EMA v4: 波动率自适应 + ADX过滤 + 分批止盈
# ════════════════════════════════════════════════════════
class EmaV4Config(StrategyConfig):
fast: int = 20
slow: int = 50
atr_period: int = 14
atr_stop_mult: float = 3.0
adx_period: int = 14
adx_threshold: float = 20.0 # ADX 趋势阈值
vol_base: float = 20.0 # ATR% 基准(周期数用于标准化)
rsi_period: int = 14
rsi_take_profit: float = 72.0 # 止盈 RSI 线
partial_exit_pct: float = 0.5 # 分批止盈比例(0=不分批)
class EmaV4Strategy(BaseStrategy):
"""EMA v4: 以夏普比率为目标的全方位优化"""
strategy_type = "ema_v4"
def __init__(self, c: EmaV4Config):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._highest_since_entry: float = 0.0
self._in_position = False
self._partial_done = 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)
n = len(self._closes)
need = max(self.cfg.slow, self.cfg.adx_period * 2)
if n < need + 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, self.cfg.atr_period)
adx_vals = adx(self._highs, self._lows, self._closes, self.cfg.adx_period)
rsi_vals = calc_rsi(self._closes, self.cfg.rsi_period)
cur_f, cur_s = fast[-1], slow[-1]
cur_atr, cur_adx, cur_rsi = atr_vals[-1], adx_vals[-1], rsi_vals[-1]
if cur_f == 0 or cur_s == 0 or cur_atr == 0 or cur_adx == 0 or cur_rsi == 0:
return None
# ── 波动率自适应仓位系数 ──
# ATR/价格 = 当前波动率,波动率越高→仓位越小
atr_pct = cur_atr / k.close if k.close > 0 else 0.02
# 基准波动率约 2%,波动率翻倍时仓位减半
vol_conf = min(1.0, max(0.2, 0.02 / max(atr_pct, 0.005)))
# ADX 趋势强度加成:强趋势更有信心
trend_bonus = min(1.3, max(0.7, cur_adx / 25))
position_conf = min(1.0, vol_conf * trend_bonus)
# ADX 趋势过滤
in_trend = cur_adx > self.cfg.adx_threshold
# ── 出场 ──
if self._in_position:
self._highest_since_entry = max(self._highest_since_entry, k.high)
stop_price = self._highest_since_entry - self.cfg.atr_stop_mult * cur_atr
death_cross = fast[-2] >= slow[-2] and cur_f < cur_s
# ATR 止损
if k.close < stop_price:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL",
confidence=1.0,
reason=f"ATR止损 P={k.close:.0f}", timestamp=k.open_time)
# EMA 死叉
if death_cross:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL",
confidence=1.0,
reason="EMA死叉", timestamp=k.open_time)
# 分批止盈:RSI过热先出一半
if not self._partial_done and cur_rsi > self.cfg.rsi_take_profit and self.cfg.partial_exit_pct > 0:
self._partial_done = True
return Signal(symbol=self.cfg.symbol, side="SELL",
quantity=None, # None=全部,但我们用confidence控制比例
confidence=self.cfg.partial_exit_pct,
reason=f"半仓止盈 RSI={cur_rsi:.0f}", timestamp=k.open_time)
# ── 入场 ──
if not self._in_position:
golden = fast[-2] <= slow[-2] and cur_f > cur_s
if golden and in_trend:
self._in_position = True
self._highest_since_entry = k.close
self._partial_done = False
return Signal(
symbol=self.cfg.symbol, side="BUY",
confidence=position_conf,
reason=f"金叉 ADX={cur_adx:.0f} vol_conf={vol_conf:.2f}",
timestamp=k.open_time,
)
return None
# ════════════════════════════════════════════════════════
# 对比运行
# ════════════════════════════════════════════════════════
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
# 各币种最优参数(来自 v3 扫描)
BEST_PARAMS = {
"BTCUSDT": (10, 50),
"ETHUSDT": (10, 75),
"BNBUSDT": (10, 40),
"SOLUSDT": (30, 50),
}
# v1 基线
V1 = {
"BTCUSDT": (45.5, 0.74, -31.6, 42, 26.2),
"ETHUSDT": (24.4, 0.47, -54.8, 41, 24.4),
"BNBUSDT": (52.0, 0.71, -39.8, 41, 39.0),
"SOLUSDT": (27.8, 0.49, -39.5, 45, 40.0),
}
# v3 最优参数结果
V3 = {
"BTCUSDT": (39.9, 1.03, -11.5, 20, 55.0),
"ETHUSDT": (53.6, 1.04, -15.3, 18, 38.9),
"BNBUSDT": (26.0, 0.64, -23.4, 23, 34.8),
"SOLUSDT": (73.6, 1.18, -25.7, 13, 46.2),
}
async def run_v4(symbol: str, fast: int, slow: int) -> BacktestResult:
bt = BacktestConfig(
symbol=symbol, interval="4h",
start_time=datetime(2024, 1, 1), end_time=datetime(2026, 1, 1),
initial_capital=10_000.0,
)
sc = EmaV4Config(symbol=symbol, fast=fast, slow=slow)
engine = BacktestEngine(bt, db_config=config.db)
return await engine.run(EmaV4Strategy, sc)
async def main():
print()
print("" * 120)
print(" 夏普比率专项优化 — 波动率自适应 + ADX过滤 + 分批止盈")
print("" * 120)
# ── 扫描 partial_exit_pct 参数 ──
print("\n ▸ 分批止盈参数扫描 (SOLUSDT 为例)")
print(f" {'partial%':>10} {'收益%':>8} {'夏普':>6} {'回撤%':>8} {'交易':>5} {'胜率%':>6}")
print(" " + "" * 50)
for pct in [0.0, 0.3, 0.5, 0.7]:
bt = BacktestConfig(symbol="SOLUSDT", interval="4h",
start_time=datetime(2024, 1, 1), end_time=datetime(2026, 1, 1),
initial_capital=10_000.0)
sc = EmaV4Config(symbol="SOLUSDT", fast=30, slow=50, partial_exit_pct=pct)
engine = BacktestEngine(bt, db_config=config.db)
r = await engine.run(EmaV4Strategy, sc)
m = r.metrics
print(f" {pct:>10.0%} {m.total_return_pct:>7.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>7.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}%")
# ── 全部币种 v4 运行 ──
print("\n" + "" * 120)
print(" ■ v1 → v3 → v4 夏普进化 | 使用各币种最优参数")
print(f" {'币种':<10} {'版本':<18} {'收益%':>7} {'夏普':>6} {'Δ夏普':>7} {'回撤%':>7} {'交易':>5} {'胜率%':>6}")
print("" * 120)
v4_results = {}
for symbol in SYMBOLS:
fast, slow = BEST_PARAMS[symbol]
r = await run_v4(symbol, fast, slow)
v4_results[symbol] = r
v1 = V1[symbol]
v3 = V3[symbol]
m = r.metrics
# v1
print(f" {symbol:<10} {'v1 原始':<18} {v1[0]:>6.1f}% {v1[1]:>6.2f} {'':>7} {v1[2]:>6.1f}% {v1[3]:>5} {v1[4]:>5.1f}%")
# v3
print(f" {symbol:<10} {'v3 最优参数':<18} {v3[0]:>6.1f}% {v3[1]:>6.2f} {v3[1]-v1[1]:>+6.2f} {v3[2]:>6.1f}%")
# v4
delta_sh = m.sharpe_ratio - v1[1]
print(f" {symbol:<10} {'v4 夏普优化':<18} {m.total_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {delta_sh:>+6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}%")
print()
# ── 夏普改善汇总 ──
print("" * 120)
print(" ■ 夏普比率改善汇总:")
for symbol in SYMBOLS:
v1_sh = V1[symbol][1]
v3_sh = V3[symbol][1]
v4_sh = v4_results[symbol].metrics.sharpe_ratio
print(f" {symbol}: v1={v1_sh:.2f} → v3={v3_sh:.2f} → v4={v4_sh:.2f} (总提升 {v4_sh-v1_sh:+.2f})")
# 平均夏普
avg_v1 = sum(V1[s][1] for s in SYMBOLS) / 4
avg_v3 = sum(V3[s][1] for s in SYMBOLS) / 4
avg_v4 = sum(v4_results[s].metrics.sharpe_ratio for s in SYMBOLS) / 4
print(f" 平均: v1={avg_v1:.2f} → v3={avg_v3:.2f} → v4={avg_v4:.2f} (总提升 {avg_v4-avg_v1:+.2f})")
print("\n" * 120)
if __name__ == "__main__":
asyncio.run(main())
+152
View File
@@ -0,0 +1,152 @@
"""
极简策略测试 一条均线 + 一个止损
核心理念多加过滤条件往往不如简洁的信号
策略价格上穿 EMA(N) 买入价格下穿 EMA(N) 卖出ATR 动态止损
无成交量无多周期无ADX无双均线交叉
对比 N=10/20/304个币种4h周期2024-2026
用法
source .venv/bin/activate && python example/strategy_simple.py
"""
import asyncio
import sys
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
from engine.indicators import ema, atr
class SingleEMAConfig(StrategyConfig):
period: int = 20
atr_stop: float = 2.5
class SingleEMAStrategy(BaseStrategy):
"""一条EMA均线 + ATR止损。没有更多了。"""
strategy_type = "single_ema"
def __init__(self, c: SingleEMAConfig):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._highest: float = 0.0
self._in_position = 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)
n = len(self._closes)
if n < self.cfg.period + 5:
return None
ema_vals = ema(self._closes, self.cfg.period)
atr_vals = atr(self._highs, self._lows, self._closes, 14)
cur_ema, cur_atr = ema_vals[-1], atr_vals[-1]
prev_ema = ema_vals[-2]
if cur_ema == 0 or cur_atr == 0:
return None
# ── 出场 ──
if self._in_position:
self._highest = max(self._highest, k.high)
stop = self._highest - self.cfg.atr_stop * cur_atr
cross_down = self._closes[-2] >= prev_ema and k.close < cur_ema
if k.close < stop:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason="ATR止损", timestamp=k.open_time)
if cross_down:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL",
reason=f"下穿EMA{self.cfg.period}", timestamp=k.open_time)
# ── 入场 ──
if not self._in_position:
cross_up = self._closes[-2] <= prev_ema and k.close > cur_ema
if cross_up:
self._in_position = True
self._highest = k.close
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"上穿EMA{self.cfg.period}", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
PERIODS = [10, 20, 30]
DATE_START = datetime(2024, 1, 1)
DATE_END = datetime(2026, 1, 1)
async def main():
print()
print("" * 105)
print(" 极简策略 — 一条 EMA + ATR 止损 | 4h | 2024-2026")
print("" * 105)
print(f" {'EMA':<8} {'币种':<10} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} {'盈亏比':>6}")
print("" * 105)
all_rows = []
for period in PERIODS:
for symbol in SYMBOLS:
sc = SingleEMAConfig(symbol=symbol, period=period)
bt = BacktestConfig(symbol=symbol, interval="4h",
start_time=DATE_START, end_time=DATE_END, initial_capital=10_000.0)
engine = BacktestEngine(bt, db_config=config.db)
r = await engine.run(SingleEMAStrategy, sc)
m = r.metrics
all_rows.append((period, symbol, m))
print(f" EMA({period:<2}) {symbol:<10} {m.total_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}% {m.profit_factor:>6.2f}")
# ── 对比之前最优结果 ──
print("" * 105)
print("\n ■ 对比:极简 vs 之前最优 (EMA v3 双均线)")
V3 = {
"BTCUSDT": (39.9, 1.03, -11.5),
"ETHUSDT": (53.6, 1.04, -15.3),
"BNBUSDT": (26.0, 0.64, -23.4),
"SOLUSDT": (73.6, 1.18, -25.7),
}
print(f" {'币种':<10} {'策略':<20} {'收益%':>7} {'夏普':>6} {'回撤%':>7}")
for symbol in SYMBOLS:
v3 = V3[symbol]
print(f" {symbol:<10} {'EMA v3(最优参数)':<20} {v3[0]:>6.1f}% {v3[1]:>6.2f} {v3[2]:>6.1f}%")
# 找极简最佳
best = max([(p, m) for p, s, m in all_rows if s == symbol], key=lambda x: x[1].sharpe_ratio)
print(f" {'':<10} {'单EMA('+str(best[0])+') 极简':<20} {best[1].total_return_pct:>6.1f}% {best[1].sharpe_ratio:>6.2f} {best[1].max_drawdown_pct:>6.1f}%")
print()
# ── 汇总 ──
print("" * 105)
ranked = sorted(all_rows, key=lambda x: x[2].sharpe_ratio, reverse=True)
print(" ■ 按夏普 TOP 5:")
for i, (p, s, m) in enumerate(ranked[:5]):
print(f" {i+1}. {s} EMA({p:<2}) 夏普={m.sharpe_ratio:.2f} 收益={m.total_return_pct:+.1f}% 回撤={m.max_drawdown_pct:.1f}% 胜率={m.win_rate*100:.0f}% 交易={m.total_trades}")
avg_sh = sum(x[2].sharpe_ratio for x in all_rows) / len(all_rows)
print(f"\n 12组平均夏普: {avg_sh:.2f}")
print("\n" * 105)
if __name__ == "__main__":
asyncio.run(main())
+157
View File
@@ -0,0 +1,157 @@
"""
同周期三EMA策略 4h EMA50>200 定方向 + EMA20金叉EMA50入场
所有信号在同一周期(4h)不跨级
"""
import asyncio
import sys
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.base import BaseStrategy, Signal, StrategyConfig
from engine.common.models import Kline
from engine.common.config import config
from engine.backtest import BacktestEngine, BacktestConfig
from engine.indicators import ema, atr
class ThreeEMAConfig(StrategyConfig):
ema_entry: int = 20 # 入场均线(金叉慢线时入场)
ema_trend: int = 50 # 趋势均线(在200上方=多头)
ema_filter: int = 200 # 长期过滤(50必须在其上)
atr_stop: float = 2.5
class ThreeEMAStrategy(BaseStrategy):
"""三EMA同周期策略
EMA200 长期方向 EMA50>200 才做多
EMA20 金叉 EMA50 入场
EMA20 死叉 EMA50 EMA50<200 出场
ATR 动态止损
"""
strategy_type = "three_ema"
def __init__(self, c: ThreeEMAConfig):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._highest: float = 0.0
self._in_position = 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)
n = len(self._closes)
if n < self.cfg.ema_filter + 10:
return None
# 三条EMA
e20 = ema(self._closes, self.cfg.ema_entry)
e50 = ema(self._closes, self.cfg.ema_trend)
e200 = ema(self._closes, self.cfg.ema_filter)
atr_vals = atr(self._highs, self._lows, self._closes, 14)
# 当前值和前值
c20, p20 = e20[-1], e20[-2]
c50, p50 = e50[-1], e50[-2]
c200 = e200[-1]
cur_atr = atr_vals[-1]
if c20 == 0 or c50 == 0 or c200 == 0 or cur_atr == 0:
return None
is_bull = c50 > c200 # EMA50在200上方=多头市场
golden = p20 <= p50 and c20 > p50 # 金叉
death = p20 >= p50 and c20 < p50 # 死叉
# ── 出场 ──
if self._in_position:
self._highest = max(self._highest, k.high)
stop = self._highest - self.cfg.atr_stop * cur_atr
if not is_bull:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason="EMA50<200转空", timestamp=k.open_time)
if k.close < stop:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason="ATR止损", timestamp=k.open_time)
if death:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason="EMA20死叉50", timestamp=k.open_time)
# ── 入场 ──
if not self._in_position and is_bull and golden:
self._in_position = True
self._highest = k.close
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"EMA20金叉50 多头确认", timestamp=k.open_time)
return None
# ═══════════════════════════════════════════════
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
DATE_START = datetime(2024, 1, 1)
DATE_END = datetime(2026, 1, 1)
# 之前最优对比
BEST = {
"BTCUSDT": ("EMA v3(10,50)", 39.9, 1.03, -11.5, 20),
"ETHUSDT": ("EMA v3(10,75)", 53.6, 1.04, -15.3, 18),
"BNBUSDT": ("EMA v1(20,50)", 52.0, 0.71, -39.8, 41),
"SOLUSDT": ("EMA v3(30,50)", 73.6, 1.18, -25.7, 13),
}
async def main():
print()
print("" * 105)
print(" 三EMA同周期 — 4h EMA200定势 / EMA20×50交易 | 2024-2026")
print("" * 105)
print(f" {'币种':<10} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} {'盈亏比':>6} {'vs最优':>8}")
print("" * 105)
results = {}
for symbol in SYMBOLS:
sc = ThreeEMAConfig(symbol=symbol)
bt = BacktestConfig(symbol=symbol, interval="4h",
start_time=DATE_START, end_time=DATE_END, initial_capital=10_000.0)
engine = BacktestEngine(bt, db_config=config.db)
r = await engine.run(ThreeEMAStrategy, sc)
m = r.metrics
results[symbol] = m
_, best_ret, best_sh, _, _ = BEST[symbol]
delta = m.total_return_pct - best_ret
tag = " ← 新最佳!" if m.sharpe_ratio > best_sh else ""
print(f" {symbol:<10} {m.total_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}% {m.profit_factor:>6.2f} {delta:>+7.1f}%{tag}")
sells = [t for t in r.trades if t.side == "SELL" and t.pnl is not None]
for t in sells[-2:]:
dt = datetime.fromtimestamp(t.timestamp / 1000, tz=timezone.utc).strftime("%m-%d %H:%M")
print(f" {'':<10}{dt} {t.pnl:>+8.2f} {t.reason}")
print("" * 105)
print(f"\n {'币种':<10} {'之前最优':<20} {'收益%':>7} {'夏普':>6}{'三EMA收益%':>9} {'三EMA夏普':>8}")
for symbol in SYMBOLS:
name, ret, sh, _, _ = BEST[symbol]
m = results[symbol]
print(f" {symbol:<10} {name:<20} {ret:>6.1f}% {sh:>6.2f}{m.total_return_pct:>8.1f}% {m.sharpe_ratio:>7.2f}")
print("\n" * 105)
if __name__ == "__main__":
asyncio.run(main())
+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())
+312
View File
@@ -0,0 +1,312 @@
{
"config": {
"strategy": "ATR波动率突破",
"symbols": [
"BTCUSDT",
"ETHUSDT",
"BNBUSDT",
"SOLUSDT"
],
"timeframes": [
"1h",
"2h",
"4h",
"6h"
],
"period": "近半年",
"initial_capital": 10000.0,
"warmup_bars": 150,
"elapsed_seconds": 0.6400730609893799,
"run_time": "2026-06-13T12:01:08.097745+00:00"
},
"results": [
{
"币种": "ETHUSDT",
"时间级别": "1h",
"日期范围": "2025-12-13~2026-06-11",
"初始资金": 10000.0,
"最终权益": 9103.91,
"总收益%": -8.96,
"年化收益%": -17.94,
"夏普比率": -0.17,
"最大回撤%": -38.98,
"胜率%": 20.0,
"盈亏比": 0.91,
"交易次数": 40,
"平均盈亏": -12.76,
"最佳盈亏": 2363.57,
"最差盈亏": -496.06,
"耗时s": 0.3
},
{
"币种": "BTCUSDT",
"时间级别": "1h",
"日期范围": "2025-12-13~2026-06-11",
"初始资金": 10000.0,
"最终权益": 9726.77,
"总收益%": -2.73,
"年化收益%": -5.67,
"夏普比率": 0.01,
"最大回撤%": -32.4,
"胜率%": 25.64,
"盈亏比": 1.02,
"交易次数": 39,
"平均盈亏": 2.37,
"最佳盈亏": 1960.9,
"最差盈亏": -713.74,
"耗时s": 0.3
},
{
"币种": "BNBUSDT",
"时间级别": "1h",
"日期范围": "2025-12-13~2026-06-11",
"初始资金": 10000.0,
"最终权益": 11586.76,
"总收益%": 15.87,
"年化收益%": 36.37,
"夏普比率": 1.09,
"最大回撤%": -19.14,
"胜率%": 28.12,
"盈亏比": 1.51,
"交易次数": 32,
"平均盈亏": 58.9,
"最佳盈亏": 2400.68,
"最差盈亏": -411.05,
"耗时s": 0.2
},
{
"币种": "SOLUSDT",
"时间级别": "1h",
"日期范围": "2025-12-13~2026-06-11",
"初始资金": 10000.0,
"最终权益": 8240.83,
"总收益%": -17.59,
"年化收益%": -33.47,
"夏普比率": -0.57,
"最大回撤%": -49.62,
"胜率%": 23.08,
"盈亏比": 0.8,
"交易次数": 39,
"平均盈亏": -37.47,
"最佳盈亏": 2837.53,
"最差盈亏": -814.13,
"耗时s": 0.3
},
{
"币种": "ETHUSDT",
"时间级别": "2h",
"日期范围": "2025-12-13~2026-06-11",
"初始资金": 10000.0,
"最终权益": 12686.34,
"总收益%": 26.86,
"年化收益%": 68.25,
"夏普比率": 1.36,
"最大回撤%": -21.84,
"胜率%": 29.41,
"盈亏比": 1.91,
"交易次数": 17,
"平均盈亏": 173.85,
"最佳盈亏": 2685.48,
"最差盈亏": -507.73,
"耗时s": 0.3
},
{
"币种": "BTCUSDT",
"时间级别": "2h",
"日期范围": "2025-12-13~2026-06-11",
"初始资金": 10000.0,
"最终权益": 14294.36,
"总收益%": 42.94,
"年化收益%": 118.42,
"夏普比率": 2.06,
"最大回撤%": -17.1,
"胜率%": 57.14,
"盈亏比": 3.7,
"交易次数": 14,
"平均盈亏": 326.07,
"最佳盈亏": 2115.62,
"最差盈亏": -546.69,
"耗时s": 0.3
},
{
"币种": "BNBUSDT",
"时间级别": "2h",
"日期范围": "2025-12-13~2026-06-11",
"初始资金": 10000.0,
"最终权益": 13051.99,
"总收益%": 30.52,
"年化收益%": 79.04,
"夏普比率": 2.05,
"最大回撤%": -13.6,
"胜率%": 46.15,
"盈亏比": 3.62,
"交易次数": 13,
"平均盈亏": 246.85,
"最佳盈亏": 2718.8,
"最差盈亏": -292.95,
"耗时s": 0.2
},
{
"币种": "SOLUSDT",
"时间级别": "2h",
"日期范围": "2025-12-13~2026-06-11",
"初始资金": 10000.0,
"最终权益": 12207.75,
"总收益%": 22.08,
"年化收益%": 54.68,
"夏普比率": 1.22,
"最大回撤%": -25.2,
"胜率%": 35.29,
"盈亏比": 1.75,
"交易次数": 17,
"平均盈亏": 150.9,
"最佳盈亏": 2571.92,
"最差盈亏": -719.69,
"耗时s": 0.2
},
{
"币种": "BTCUSDT",
"时间级别": "4h",
"日期范围": "2025-12-13~2026-06-11",
"初始资金": 10000.0,
"最终权益": 12105.54,
"总收益%": 21.06,
"年化收益%": 57.21,
"夏普比率": 1.41,
"最大回撤%": -13.55,
"胜率%": 54.55,
"盈亏比": 2.38,
"交易次数": 11,
"平均盈亏": 195.47,
"最佳盈亏": 1727.66,
"最差盈亏": -580.61,
"耗时s": 0.3
},
{
"币种": "BNBUSDT",
"时间级别": "4h",
"日期范围": "2025-12-13~2026-06-11",
"初始资金": 10000.0,
"最终权益": 10800.73,
"总收益%": 8.01,
"年化收益%": 20.01,
"夏普比率": 0.64,
"最大回撤%": -21.48,
"胜率%": 57.14,
"盈亏比": 1.95,
"交易次数": 7,
"平均盈亏": 127.36,
"最佳盈亏": 907.82,
"最差盈亏": -465.17,
"耗时s": 0.2
},
{
"币种": "ETHUSDT",
"时间级别": "4h",
"日期范围": "2025-12-13~2026-06-11",
"初始资金": 10000.0,
"最终权益": 20044.4,
"总收益%": 100.44,
"年化收益%": 418.79,
"夏普比率": 2.52,
"最大回撤%": -21.71,
"胜率%": 30.0,
"盈亏比": 3.55,
"交易次数": 10,
"平均盈亏": 1013.41,
"最佳盈亏": 8978.94,
"最差盈亏": -1167.65,
"耗时s": 0.2
},
{
"币种": "SOLUSDT",
"时间级别": "4h",
"日期范围": "2025-12-13~2026-06-11",
"初始资金": 10000.0,
"最终权益": 16846.12,
"总收益%": 68.46,
"年化收益%": 243.76,
"夏普比率": 2.02,
"最大回撤%": -36.03,
"胜率%": 22.22,
"盈亏比": 2.38,
"交易次数": 9,
"平均盈亏": 768.98,
"最佳盈亏": 9654.28,
"最差盈亏": -1427.23,
"耗时s": 0.2
},
{
"币种": "BTCUSDT",
"时间级别": "6h",
"日期范围": "2025-12-13~2026-06-11",
"初始资金": 10000.0,
"最终权益": 14121.75,
"总收益%": 41.22,
"年化收益%": 143.58,
"夏普比率": 2.79,
"最大回撤%": -11.58,
"胜率%": 75.0,
"盈亏比": 6.87,
"交易次数": 4,
"平均盈亏": 1015.11,
"最佳盈亏": 2377.43,
"最差盈亏": -692.0,
"耗时s": 0.3
},
{
"币种": "ETHUSDT",
"时间级别": "6h",
"日期范围": "2025-12-13~2026-06-11",
"初始资金": 10000.0,
"最终权益": 12832.56,
"总收益%": 28.33,
"年化收益%": 90.28,
"夏普比率": 1.61,
"最大回撤%": -18.93,
"胜率%": 40.0,
"盈亏比": 3.54,
"交易次数": 5,
"平均盈亏": 561.57,
"最佳盈亏": 2473.69,
"最差盈亏": -528.44,
"耗时s": 0.1
},
{
"币种": "BNBUSDT",
"时间级别": "6h",
"日期范围": "2025-12-13~2026-06-11",
"初始资金": 10000.0,
"最终权益": 16288.39,
"总收益%": 62.88,
"年化收益%": 252.0,
"夏普比率": 2.08,
"最大回撤%": -18.31,
"胜率%": 25.0,
"盈亏比": 4.45,
"交易次数": 4,
"平均盈亏": 1593.04,
"最佳盈亏": 8218.32,
"最差盈亏": -1176.64,
"耗时s": 0.2
},
{
"币种": "SOLUSDT",
"时间级别": "6h",
"日期范围": "2025-12-13~2026-06-11",
"初始资金": 10000.0,
"最终权益": 14329.33,
"总收益%": 43.29,
"年化收益%": 152.92,
"夏普比率": 2.65,
"最大回撤%": -13.51,
"胜率%": 50.0,
"盈亏比": 6.8,
"交易次数": 4,
"平均盈亏": 1054.76,
"最佳盈亏": 3189.27,
"最差盈亏": -701.15,
"耗时s": 0.2
}
]
}
+285
View File
@@ -0,0 +1,285 @@
"""
ATR波动率突破 1h/2h/4h/6h 近半年横向对比回测
用法
source .venv/bin/activate && python example/vol_break_1h_6h.py
"""
import asyncio
import json
import sys
import time
from datetime import datetime, timedelta, 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.models 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"]
TIMEFRAMES = ["1h", "2h", "4h", "6h"]
INITIAL = 10_000.0
WARMUP = 150
MAX_CONCURRENCY = 6
NOW = datetime.now(timezone.utc)
PERIOD_START = NOW - timedelta(days=182) # 近半年
PERIOD_END = NOW
# ════════════════════════════════════════════════════════
# 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
async def run_one(symbol, interval, start, end):
sc = VolBreakConfig(symbol=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(VolBreakStrategy, sc)
elapsed = time.time() - t0
return r, elapsed, None
except Exception as ex:
elapsed = time.time() - t0
return None, elapsed, str(ex)
async def main():
ds = DataService(config.db)
await ds.connect()
print("正在获取数据范围...")
date_ranges = {}
for symbol in SYMBOLS:
for tf in TIMEFRAMES:
try:
s, e = await ds.fetch_symbol_date_range(symbol, tf)
bar_ms = {"1h": 3_600_000, "2h": 7_200_000, "4h": 14_400_000, "6h": 21_600_000}
estimated_bars = int((e - s).total_seconds() * 1000 / bar_ms[tf])
date_ranges[(symbol, tf)] = (s, e, estimated_bars)
print(f" {symbol} {tf:<4}: {s.date()} ~ {e.date()} (约{estimated_bars:,}根)")
except Exception as ex:
print(f" {symbol} {tf:<4}: 获取失败 — {ex}")
await ds.close()
sem = asyncio.Semaphore(MAX_CONCURRENCY)
tasks_info = []
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]
actual_start = max(PERIOD_START, fs)
actual_end = min(PERIOD_END, fe)
if actual_start >= actual_end:
continue
tasks_info.append({"symbol": symbol, "tf": tf, "start": actual_start, "end": actual_end})
total = len(tasks_info)
print(f"\n{total} 组回测任务 (ATR波动率突破 × 4币种 × 4时间 × 近半年)")
results = []
completed = 0
errors = 0
async def run_one_safe(info):
nonlocal completed, errors
async with sem:
r, elapsed, err = await run_one(info["symbol"], info["tf"], 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['symbol']} {info['tf']} ({elapsed:.1f}s) {status}", flush=True)
row = {
"币种": info["symbol"],
"时间级别": info["tf"],
"日期范围": 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),
"耗时s": round(elapsed, 1),
})
else:
row.update({
"初始资金": INITIAL, "最终权益": 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("" * 145)
print(" ATR波动率突破 — 1h / 2h / 4h / 6h 近半年横向对比")
print(" 策略: ATR(14)/squeeze=20x0.7/EMA(10,30) | 本金 $10,000 | 多空双向")
print("" * 145)
print()
# 按时间级别排序
results.sort(key=lambda x: TIMEFRAMES.index(x["时间级别"]))
print(f" {'币种':<10} {'时间':<5} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>7} {'胜率%':>7} {'盈亏比':>7} {'交易':>6} {'最佳盈亏':>10} {'最差盈亏':>10} {'日期范围':<24}")
print(" " + "" * 140)
for r in results:
print(f" {r['币种']:<10} {r['时间级别']:<5} {r['总收益%']:>7.1f}% {r['年化收益%']:>7.1f}% {r['夏普比率']:>7.2f} {r['最大回撤%']:>7.1f}% {r['胜率%']:>6.1f}% {r['盈亏比']:>7.2f} {r['交易次数']:>6} {r['最佳盈亏']:>+9.0f} {r['最差盈亏']:>+9.0f} {r['日期范围']:<24}")
print()
# ── 各时间级别汇总 ──
print("" * 145)
print(" ■ 各时间级别排名(按年化收益)")
print("" * 145)
for tf in TIMEFRAMES:
subset = [r for r in results if r["时间级别"] == tf]
if not subset:
continue
subset.sort(key=lambda x: x.get("年化收益%", -9999), reverse=True)
print(f"\n{tf} 近半年")
print(f" {'排名':<5} {'币种':<10} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>7} {'胜率%':>7} {'盈亏比':>7} {'交易':>6}")
print(" " + "" * 100)
for i, r in enumerate(subset):
marker = ["🥇", "🥈", "🥉", " 4"][i]
print(f" {marker:<5} {r['币种']:<10} {r['总收益%']:>7.1f}% {r['年化收益%']:>7.1f}% {r['夏普比率']:>7.2f} {r['最大回撤%']:>7.1f}% {r['胜率%']:>6.1f}% {r['盈亏比']:>7.2f} {r['交易次数']:>6}")
print()
print("" * 145)
# ── 保存 JSON ──
output_file = _project_root / "engine" / "example" / "vol_break_1h_6h.json"
with open(output_file, "w", encoding="utf-8") as f:
json.dump({
"config": {
"strategy": "ATR波动率突破",
"symbols": SYMBOLS,
"timeframes": TIMEFRAMES,
"period": "近半年",
"initial_capital": INITIAL,
"warmup_bars": WARMUP,
"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}")
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
+33
View File
@@ -0,0 +1,33 @@
"""
技术指标库
提供常用的趋势动量波动率和成交量指标计算
所有函数均为纯 Python 实现无外部依赖
用法:
from engine.indicators import sma, ema, macd, rsi, bollinger, atr
closes = [100.0, 101.0, 102.0, ...]
ma = sma(closes, period=20)
rsi_vals = rsi(closes, period=14)
upper, mid, lower = bollinger(closes, period=20, std=2)
"""
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__ = [
# 趋势
"sma", "ema", "macd", "macd_signal", "macd_histogram", "adx",
# 动量
"rsi", "stoch", "stoch_k", "stoch_d",
# 波动率
"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)
+131
View File
@@ -0,0 +1,131 @@
"""
动量指标 RSIStochastic
所有函数返回与输入等长的 list[float]不足周期位置填 0.0
"""
def rsi(data: list[float], period: int = 14) -> list[float]:
"""相对强弱指数 (RSI)
使用 Wilder 平滑算法Wilder's RSI = 100 - [100 / (1 + avg_gain / avg_loss)]
Args:
data: 价格序列
period: 周期默认 14
Returns:
data 等长的 RSI 序列 [0, 100] period 位置为 0
"""
n = len(data)
result = [0.0] * n
if n < period + 1:
return result
# 计算价格变化
changes = [data[i] - data[i - 1] for i in range(1, n)]
# 初始平均涨幅和跌幅(Simple average of first `period` changes
gains = [max(c, 0) for c in changes[:period]]
losses = [abs(min(c, 0)) for c in changes[:period]]
avg_gain = sum(gains) / period
avg_loss = sum(losses) / period
# 计算第一个 RSI
if avg_loss == 0:
result[period] = 100.0
else:
rs = avg_gain / avg_loss
result[period] = 100.0 - (100.0 / (1.0 + rs))
# Wilder 平滑后续值
for i in range(period, n - 1):
change = changes[i]
gain = max(change, 0.0)
loss = abs(min(change, 0.0))
avg_gain = (avg_gain * (period - 1) + gain) / period
avg_loss = (avg_loss * (period - 1) + loss) / period
if avg_loss == 0:
result[i + 1] = 100.0
else:
rs = avg_gain / avg_loss
result[i + 1] = 100.0 - (100.0 / (1.0 + rs))
return result
def stoch(
high: list[float],
low: list[float],
close: list[float],
k_period: int = 14,
k_smooth: int = 3,
d_smooth: int = 3,
):
"""Stochastic 指标 (KDJ 中的 K/D)
%K = 100 * (close - lowest_low) / (highest_high - lowest_low)
%K_smoothed = SMA(%K, k_smooth)
%D = SMA(%K_smoothed, d_smooth)
Args:
high: 最高价序列
low: 最低价序列
close: 收盘价序列
k_period: %K 窗口
k_smooth: %K 平滑周期
d_smooth: %D 平滑周期
Returns:
(k_values, d_values) 两个等长序列范围 [0, 100]
"""
n = len(close)
k_raw = [0.0] * n
k_values = [0.0] * n
d_values = [0.0] * n
if n < k_period:
return k_values, d_values
# 计算原始 %K
for i in range(k_period - 1, n):
highest = max(high[i - k_period + 1 : i + 1])
lowest = min(low[i - k_period + 1 : i + 1])
if highest != lowest:
k_raw[i] = 100.0 * (close[i] - lowest) / (highest - lowest)
else:
k_raw[i] = 50.0
# 平滑 %K
from .trend import sma as _sma
k_smoothed = _sma(k_raw, k_smooth)
d_smoothed = _sma(k_smoothed, d_smooth)
return k_smoothed, d_smoothed
def stoch_k(
high: list[float],
low: list[float],
close: list[float],
k_period: int = 14,
k_smooth: int = 3,
) -> list[float]:
"""Stochastic %K"""
k, _ = stoch(high, low, close, k_period, k_smooth)
return k
def stoch_d(
high: list[float],
low: list[float],
close: list[float],
k_period: int = 14,
k_smooth: int = 3,
d_smooth: int = 3,
) -> list[float]:
"""Stochastic %D"""
_, d = stoch(high, low, close, k_period, k_smooth, d_smooth)
return d
+191
View File
@@ -0,0 +1,191 @@
"""
趋势指标 移动平均线MACD
所有函数返回与输入等长的 list[float]不足周期位置填 0.0
"""
from functools import lru_cache
def sma(data: list[float], period: int) -> list[float]:
"""简单移动平均 (SMA)
Args:
data: 价格序列
period: 周期
Returns:
data 等长的 SMA 序列 period-1 位置为 0
"""
n = len(data)
result = [0.0] * n
if n < period or period <= 0:
return result
window_sum = sum(data[:period])
result[period - 1] = window_sum / period
for i in range(period, n):
window_sum += data[i] - data[i - period]
result[i] = window_sum / period
return result
def ema(data: list[float], period: int) -> list[float]:
"""指数移动平均 (EMA)
使用 Wilder 平滑方式k = 2 / (period + 1)
Args:
data: 价格序列
period: 周期
Returns:
data 等长的 EMA 序列 period-1 位置为 0
"""
n = len(data)
result = [0.0] * n
if n < period or period <= 0:
return result
k = 2.0 / (period + 1)
# 初始值使用 SMA
result[period - 1] = sum(data[:period]) / period
for i in range(period, n):
result[i] = data[i] * k + result[i - 1] * (1 - k)
return result
def macd(
data: list[float],
fast: int = 12,
slow: int = 26,
signal: int = 9,
):
"""MACD 指标
MACD 线 = EMA(fast) - EMA(slow)
信号线 = EMA(MACD线, signal)
柱状图 = MACD 线 - 信号线
Args:
data: 价格序列
fast: 快线周期
slow: 慢线周期
signal: 信号线周期
Returns:
(macd_line, signal_line, histogram) 三个等长序列
"""
fast_ema = ema(data, fast)
slow_ema = ema(data, slow)
macd_line = [0.0] * len(data)
for i in range(len(data)):
macd_line[i] = fast_ema[i] - slow_ema[i]
signal_line = ema(macd_line, signal)
histogram = [macd_line[i] - signal_line[i] for i in range(len(data))]
return macd_line, signal_line, histogram
def macd_signal(
data: list[float],
fast: int = 12,
slow: int = 26,
signal: int = 9,
) -> list[float]:
"""MACD 信号线"""
_, sig, _ = macd(data, fast, slow, signal)
return sig
def macd_histogram(
data: list[float],
fast: int = 12,
slow: int = 26,
signal: int = 9,
) -> list[float]:
"""MACD 柱状图"""
_, _, hist = macd(data, fast, slow, signal)
return hist
def adx(
high: list[float],
low: list[float],
close: list[float],
period: int = 14,
) -> list[float]:
"""平均趋向指数 (ADX)
判断趋势强度ADX > 25 表示强趋势ADX < 20 表示震荡
Args:
high: 最高价序列
low: 最低价序列
close: 收盘价序列
period: 周期默认 14
Returns:
与输入等长的 ADX 序列 [0, 100] 2*period 位置为 0
"""
n = len(close)
result = [0.0] * n
if n < period * 2:
return result
# True Range, +DM, -DM
tr = [0.0] * n
plus_dm = [0.0] * n
minus_dm = [0.0] * n
for i in range(1, n):
tr[i] = max(
high[i] - low[i],
abs(high[i] - close[i - 1]),
abs(low[i] - close[i - 1]),
)
up_move = high[i] - high[i - 1]
down_move = low[i - 1] - low[i]
if up_move > down_move and up_move > 0:
plus_dm[i] = up_move
if down_move > up_move and down_move > 0:
minus_dm[i] = down_move
# Wilder 平滑
tr_smooth = [0.0] * n
plus_dm_smooth = [0.0] * n
minus_dm_smooth = [0.0] * n
tr_smooth[period] = sum(tr[1:period + 1])
plus_dm_smooth[period] = sum(plus_dm[1:period + 1])
minus_dm_smooth[period] = sum(minus_dm[1:period + 1])
for i in range(period + 1, n):
tr_smooth[i] = tr_smooth[i - 1] - tr_smooth[i - 1] / period + tr[i]
plus_dm_smooth[i] = plus_dm_smooth[i - 1] - plus_dm_smooth[i - 1] / period + plus_dm[i]
minus_dm_smooth[i] = minus_dm_smooth[i - 1] - minus_dm_smooth[i - 1] / period + minus_dm[i]
# +DI, -DI, DX, ADX
dx = [0.0] * n
for i in range(period, n):
if tr_smooth[i] > 0:
pdi = 100 * plus_dm_smooth[i] / tr_smooth[i]
mdi = 100 * minus_dm_smooth[i] / tr_smooth[i]
di_sum = pdi + mdi
if di_sum > 0:
dx[i] = 100 * abs(pdi - mdi) / di_sum
# ADX = EMA of DX
for i in range(2 * period, n):
if i == 2 * period:
result[i] = sum(dx[period + 1:2 * period + 1]) / period
else:
result[i] = (result[i - 1] * (period - 1) + dx[i]) / period
return result
+127
View File
@@ -0,0 +1,127 @@
"""
波动率指标 布林带ATR
所有函数返回与输入等长的 list[float]不足周期位置填 0.0
"""
import math
def bollinger(
data: list[float],
period: int = 20,
std: float = 2.0,
):
"""布林带 (Bollinger Bands)
使用流式计算方差O(n) 复杂度
Args:
data: 价格序列通常为收盘价
period: 中轨 SMA 周期
std: 标准差倍数
Returns:
(upper, mid, lower) 三个等长序列
"""
n = len(data)
upper = [0.0] * n
mid = [0.0] * n
lower = [0.0] * n
if n < period:
return upper, mid, lower
# 初始窗口的 sum 和 sum_sq
window_sum = 0.0
window_sum_sq = 0.0
for i in range(period):
v = data[i]
window_sum += v
window_sum_sq += v * v
# 第一个点
mean = window_sum / period
mid[period - 1] = mean
variance = (window_sum_sq / period) - (mean * mean)
stdev = math.sqrt(max(variance, 0.0))
upper[period - 1] = mean + std * stdev
lower[period - 1] = mean - std * stdev
# 滑动窗口计算后续点
for i in range(period, n):
old_val = data[i - period]
new_val = data[i]
window_sum += new_val - old_val
window_sum_sq += new_val * new_val - old_val * old_val
mean = window_sum / period
mid[i] = mean
variance = (window_sum_sq / period) - (mean * mean)
stdev = math.sqrt(max(variance, 0.0))
upper[i] = mean + std * stdev
lower[i] = mean - std * stdev
return upper, mid, lower
def bollinger_upper(data: list[float], period: int = 20, std: float = 2.0) -> list[float]:
"""布林带上轨"""
upper, _, _ = bollinger(data, period, std)
return upper
def bollinger_mid(data: list[float], period: int = 20) -> list[float]:
"""布林带中轨"""
from .trend import sma as _sma
return _sma(data, period)
def bollinger_lower(data: list[float], period: int = 20, std: float = 2.0) -> list[float]:
"""布林带下轨"""
_, _, lower = bollinger(data, period, std)
return lower
def atr(
high: list[float],
low: list[float],
close: list[float],
period: int = 14,
) -> list[float]:
"""平均真实波幅 (ATR)
使用 Wilder 平滑算法
Args:
high: 最高价序列
low: 最低价序列
close: 收盘价序列
period: 周期
Returns:
与输入等长的 ATR 序列 period 位置为 0
"""
n = len(close)
result = [0.0] * n
if n < period + 1:
return result
# 计算 True Range
tr = [0.0] * n
tr[0] = high[0] - low[0]
for i in range(1, n):
tr[i] = max(
high[i] - low[i],
abs(high[i] - close[i - 1]),
abs(low[i] - close[i - 1]),
)
# 初始 ATR 为前 period 个 TR 的均值
result[period] = sum(tr[1:period + 1]) / period
# Wilder 平滑
for i in range(period + 1, n):
result[i] = (result[i - 1] * (period - 1) + tr[i]) / period
return result
+75
View File
@@ -0,0 +1,75 @@
"""
成交量指标 OBVVWAP
所有函数返回与输入等长的 list[float]不足周期位置填 0.0
"""
def obv(close: list[float], volume: list[float]) -> list[float]:
"""能量潮 (On-Balance Volume, OBV)
0 开始累加
- 收盘价 > 前收盘价OBV += 成交量
- 收盘价 < 前收盘价OBV -= 成交量
- 收盘价 == 前收盘价OBV 不变
Args:
close: 收盘价序列
volume: 成交量序列
Returns:
与输入等长的 OBV 序列
"""
n = len(close)
result = [0.0] * n
if n == 0:
return result
result[0] = volume[0]
for i in range(1, n):
if close[i] > close[i - 1]:
result[i] = result[i - 1] + volume[i]
elif close[i] < close[i - 1]:
result[i] = result[i - 1] - volume[i]
else:
result[i] = result[i - 1]
return result
def vwap(
high: list[float],
low: list[float],
close: list[float],
volume: list[float],
) -> list[float]:
"""成交量加权平均价 (VWAP)
累积计算VWAP = Σ(典型价格 × 成交量) / Σ(成交量)
典型价格 = (high + low + close) / 3
Args:
high: 最高价序列
low: 最低价序列
close: 收盘价序列
volume: 成交量序列
Returns:
与输入等长的 VWAP 序列从第一个有效 bar 开始累加
"""
n = len(close)
result = [0.0] * n
if n == 0:
return result
cum_pv = 0.0 # cumulative price * volume
cum_vol = 0.0 # cumulative volume
for i in range(n):
typical_price = (high[i] + low[i] + close[i]) / 3.0
cum_pv += typical_price * volume[i]
cum_vol += volume[i]
if cum_vol > 0:
result[i] = cum_pv / cum_vol
return result
+6
View File
@@ -22,6 +22,12 @@ redis:
# 是否启用 Pub/Sub 行情发布(开发时可关闭以节省资源) # 是否启用 Pub/Sub 行情发布(开发时可关闭以节省资源)
publish_enabled: true publish_enabled: true
# --- 交易所 API 密钥 ---
exchange:
binance:
api_key: "ONSJKIGRpDYLn6FdV17aAKfjclZ4I2LzamflhuMpsoRQA427lLKeyJlGtg2RZ7DH"
api_secret: "5Mfv4TgvDlRzCHbtl2nJL4mVHUvMm8pyjKiRjMoosBMxrhlqMw6CuQbg2qbS2Npd"
# --- 日志 --- # --- 日志 ---
logging: logging:
# 日志级别:trace / debug / info / warn / error / fatal # 日志级别:trace / debug / info / warn / error / fatal
+88
View File
@@ -0,0 +1,88 @@
# Reasonix configuration.
# Resolution order: flag > ./reasonix.toml > ~/Library/Application Support/reasonix/config.toml > built-in defaults.
# Secrets come from the environment via api_key_env; never put keys here.
config_version = 2 # schema marker for diagnostics; old versions may ignore it
default_model = "deepseek-flash"
# language = "zh" # ui/model language; empty = auto-detect from $LANG / $REASONIX_LANG
[agent]
# system_prompt = """...""" # omit to use the built-in prompt for this version
# system_prompt_file = "prompts/system.md" # overrides system_prompt when set
max_steps = 0 # executor tool-call rounds; 0 = no limit
planner_max_steps = 12 # planner read-only tool-call rounds; 0 = no limit
temperature = 0.0
auto_plan = "off" # off|on; off keeps plan mode manual
# reasoning_language = "zh" # visible reasoning language: auto|zh|en
# auto_plan_classifier = "deepseek-flash" # optional; only used for borderline tasks
soft_compact_ratio = 0.5 # notice only; keeps cache-first prefix intact
compact_ratio = 0.8 # try compacting when prompt reaches this fraction
compact_force_ratio = 0.9 # force compacting at this high-water mark
cold_resume_prune = true # elide stale tool results when reopening a session past the provider cache window
# planner_model = "mimo" # optional: enable two-model collaboration
# subagent_model = "deepseek-pro" # optional default for runAs=subagent skills
# subagent_models = { review = "deepseek-pro", security_review = "deepseek-pro" } # per-skill overrides
# subagent_effort = "high" # optional default effort for subagents
# subagent_efforts = { review = "max", task = "high" } # per-tool/skill effort overrides
# output_style = "explanatory" # explanatory | learning | concise | custom; empty = default
[tools]
enabled = [] # empty = all built-in tools
bash_timeout_seconds = 120 # foreground safety cap; set 0 for no tool-local cap
[codegraph]
enabled = true # built-in MCP server; off by default for first-run sessions
auto_install = true # fetch the runtime when CodeGraph is enabled but missing
# path = "" # empty = cache, then PATH, then a bundle beside reasonix
[builtin_mcp]
time_enabled = true # built-in Time MCP; off until manually enabled
context7_enabled = false # built-in Context7 MCP; off until manually enabled
[lsp]
enabled = true # language server tools; servers launch lazily when used
# [lsp.servers.go]
# command = "gopls"
# args = []
# extensions = [".go"]
[skills]
# paths = ["~/my-skills", "../shared/skills"] # extra custom skill roots
# excluded_paths = ["~/.agents/skills"] # hide convention roots without deleting folders
# max_depth = 3 # nested scan depth; set 1 for legacy root-only discovery
# disabled_skills = ["review"] # hide noisy or unwanted skills
[permissions]
# Per-call gating. mode = writer fallback when no rule matches: ask|allow|deny.
# Readers always default to allow. Precedence: deny > ask > allow > fallback.
# Rules are "Tool" or "Tool(specifier)"; e.g. Bash(go test:*), Edit(src/**).
mode = "ask"
# deny = ["Bash(rm -rf*)", "Bash(git push*)"] # hard-blocked in every mode
allow = ["Bash(cd /Users/rekey/Documents/Code/trade && git status)", "Bash(cd /Users/rekey/Documents/Code/trade && cat reasonix.toml)"]
# ask = ["Edit(src/**)"] # force a prompt even if otherwise allowed
[sandbox]
# Confine tool blast radius. File-writers (write_file/edit_file/multi_edit)
# may only write under workspace_root (empty = current dir) + allow_write.
# bash = "enforce" (default) jails each command in an OS sandbox (macOS now;
# graceful fallback elsewhere); "off" disables it. network allows egress.
# workspace_root = "" # default: current working directory
# allow_write = ["/tmp"] # extra dirs writers may also modify
bash = "enforce"
network = true
[statusline]
# A custom status line: a command whose first stdout line replaces the built-in
# data row. It receives {"model","contextUsed","contextWindow","cwd"} as JSON on stdin.
# command = "my-statusline.sh"
# External MCP servers. type: "stdio" (default, a subprocess) | "http" | "sse".
# ${VAR} / ${VAR:-default} are expanded from the environment in command/args/env/url/headers.
# [[plugins]]
# name = "example"
# command = "reasonix-plugin-example"
# [[plugins]] # a remote server over Streamable HTTP
# name = "stripe"
# type = "http"
# url = "https://mcp.stripe.com"
# headers = { Authorization = "Bearer ${STRIPE_KEY}" }