Compare commits
10 Commits
039bfb5075
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6708abaf56 | |||
| 9351dec226 | |||
| a9c45cce39 | |||
| d5ec69217e | |||
| 0cd2cbbb79 | |||
| edc50e8809 | |||
| b5cdb41993 | |||
| 515e61c517 | |||
| 4da520c14b | |||
| 212f6fedad |
@@ -7,3 +7,5 @@ db/pgsql/
|
||||
# Python
|
||||
__pycache__/
|
||||
.venv/
|
||||
|
||||
.codegraph
|
||||
@@ -0,0 +1,4 @@
|
||||
# 项目级 Reasonix 配置 — 优先级高于全局 config.toml
|
||||
# 参考: ~/Library/Application Support/reasonix/config.toml
|
||||
[agent]
|
||||
reasoning_language = "zh" # 强制推理过程使用中文
|
||||
@@ -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` 的类型定义。新增周期需同步。
|
||||
@@ -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`。
|
||||
@@ -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 类型检查
|
||||
```
|
||||
@@ -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`。
|
||||
@@ -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 要求分区列必须在主键中)。
|
||||
@@ -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 转换。
|
||||
@@ -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 配置文件。
|
||||
@@ -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` 的类型定义。新增周期需同步。
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
| 层 | 语言 | 职责 |
|
||||
|---|------|------|
|
||||
| **数据层** | TypeScript (Node.js) | 行情采集、WebSocket 连接管理、K 线合成、数据写入 |
|
||||
| **数据层** | TypeScript (Bun) | 行情采集、WebSocket 连接管理、K 线合成、数据写入 |
|
||||
| **业务层** | Python 3.10+ | 策略引擎、回测、风控、交易执行 |
|
||||
| **接口层** | 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+** | 策略引擎、回测、风控逻辑 |
|
||||
| **时序数据库** | **TimescaleDB (推荐)** | K 线数据存储,基于 PostgreSQL 扩展 |
|
||||
| 关系型数据库 | PostgreSQL 16+ | 订单、策略配置、用户数据等(TimescaleDB 基于 PG,可共用)|
|
||||
@@ -87,7 +87,7 @@
|
||||
|------|------|
|
||||
| Node.js WebSocket 性能优异,天然适合高并发连接 | 需要维护两套技术栈 |
|
||||
| TypeScript 类型系统在数据管道中减少运行时错误 | 跨语言调试稍复杂 |
|
||||
| **共享类型**:前后端可用同一套 TypeScript 类型定义 | 部署需要 Node.js + Python 双运行时 |
|
||||
| **共享类型**:前后端可用同一套 TypeScript 类型定义 | 部署需要 Bun + Python 双运行时 |
|
||||
| 事件驱动模型与行情数据流天然匹配 | 团队需要双语言能力 |
|
||||
| npm 生态有成熟的交易 SDK(ccxt 等) | |
|
||||
|
||||
@@ -940,7 +940,7 @@ export class TimescaleDBStorage {
|
||||
##### 6. Python 读取实现(策略引擎侧)
|
||||
|
||||
```python
|
||||
# common/storage.py
|
||||
# engine/common/storage.py
|
||||
import asyncpg
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
@@ -1160,6 +1160,7 @@ volumes:
|
||||
|
||||
| 子模块 | 职责 | 关键技术点 |
|
||||
| -------------- | ---------------------------------------------- | -------------------------------------- |
|
||||
| **通用模块** | 策略基类、数据模型、日志、配置 | `engine/common/` 目录,基础类型定义 |
|
||||
| **策略管理器** | 策略注册、启动、停止、热加载 | 插件化架构、动态导入、`importlib` |
|
||||
| **信号分发器** | 将策略产生的交易信号分发到交易执行模块 | 事件总线、消息队列 |
|
||||
| **回测引擎** | 使用历史数据模拟策略执行,评估收益、回撤等指标 | `vectorbt` / `backtrader`、事件驱动 |
|
||||
@@ -1168,27 +1169,15 @@ volumes:
|
||||
#### 策略基类设计
|
||||
|
||||
```python
|
||||
class BaseStrategy(ABC):
|
||||
"""所有策略的基类"""
|
||||
from common import BaseStrategy, StrategyConfig, Signal
|
||||
from common.models import Kline, Ticker, OrderBook
|
||||
|
||||
def __init__(self, config: StrategyConfig):
|
||||
self.config = config
|
||||
self.position = 0.0
|
||||
self.pnl = 0.0
|
||||
class MyStrategy(BaseStrategy):
|
||||
"""策略示例 — 所有策略继承 BaseStrategy"""
|
||||
strategy_type = "my_strategy"
|
||||
|
||||
@abstractmethod
|
||||
async def on_ticker(self, ticker: Ticker) -> Signal | None:
|
||||
"""处理 ticker 数据,返回交易信号"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
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.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.5 | 实现 TS 行情采集器,WebSocket 订阅实时 ticker 和 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.3 | 实现信号分发器(`engine/signals.py`) | 事件总线,策略到执行器的信号传递 |
|
||||
| 2.4 | 实现回测引擎(`engine/backtest.py`) | 历史数据回测,收益曲线、回撤等指标 |
|
||||
@@ -1590,7 +1579,12 @@ trade/
|
||||
│
|
||||
├── engine/ # 🐍 Python 策略引擎
|
||||
│ ├── __init__.py
|
||||
│ ├── base.py # BaseStrategy 基类
|
||||
│ ├── env.yaml # 引擎环境配置
|
||||
│ ├── common/ # 引擎通用模块
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── base.py # BaseStrategy 基类
|
||||
│ │ ├── models.py # 数据模型(Kline/Ticker/Trade/OrderBook)
|
||||
│ │ └── logger.py # 结构化日志
|
||||
│ ├── manager.py # 策略管理器
|
||||
│ ├── signals.py # 信号分发器
|
||||
│ ├── backtest.py # 回测引擎
|
||||
@@ -1632,10 +1626,9 @@ trade/
|
||||
│ ├── grid_trading.py # 网格交易策略
|
||||
│ └── arbitrage.py # 套利策略(可选)
|
||||
│
|
||||
├── common/ # 🐍 Python 公共工具模块
|
||||
├── common/ # 🐍 Python 公共基础设施(跨模块共享配置、工具等)
|
||||
│ ├── __init__.py
|
||||
│ ├── logger.py # 日志配置
|
||||
│ ├── models.py # 数据模型(Pydantic,与 TS types 对应)
|
||||
│ ├── config.py # 全局配置
|
||||
│ ├── constants.py # 常量定义
|
||||
│ └── utils.py # 工具函数
|
||||
│
|
||||
@@ -1662,7 +1655,7 @@ trade/
|
||||
```
|
||||
┌──────────────────┐ ┌──────────────┐ ┌─────────────┐
|
||||
│ TS 数据模块 │ │ PostgreSQL │ │ Grafana │
|
||||
│ (Node.js 进程) │ │ (业务数据) │ │ (可视化) │
|
||||
│ (Bun 进程) │ │ (业务数据) │ │ (可视化) │
|
||||
└──────┬───────────┘ └──────────────┘ └──────┬──────┘
|
||||
│ │
|
||||
│ ┌──────────────┐ │
|
||||
|
||||
+26
-5
@@ -11,7 +11,7 @@
|
||||
// const ds = new DataSource({ ...pgsql });
|
||||
// const redisClient = new Redis(redis.url);
|
||||
//
|
||||
// 配置文件位置:<project_root>/env.yaml
|
||||
// 配置文件位置:data/env.yaml(软链接 → 项目根目录 env.yaml)
|
||||
// TypeScript / Python 模块共享同一份配置。
|
||||
// ============================================================
|
||||
|
||||
@@ -39,12 +39,14 @@ function getProjectRoot(): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从项目根目录读取 env.yaml 并解析为原始对象。
|
||||
* 读取 data/env.yaml(软链接指向项目根目录 env.yaml)并解析为原始对象。
|
||||
* 文件不存在时抛出明确错误,不做静默降级。
|
||||
*/
|
||||
function loadYamlConfig(): unknown {
|
||||
const root = getProjectRoot();
|
||||
const yamlPath = resolve(root, "env.yaml");
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
// config/index.ts → config/ → data/env.yaml
|
||||
const yamlPath = resolve(__dirname, "../env.yaml");
|
||||
|
||||
let content: string;
|
||||
try {
|
||||
@@ -52,7 +54,7 @@ function loadYamlConfig(): unknown {
|
||||
} catch {
|
||||
throw new Error(
|
||||
`[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",
|
||||
} 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. 工具:运行时打印配置概要(不含敏感信息)
|
||||
// ============================================================
|
||||
@@ -150,6 +165,12 @@ export function printConfigSummary(): void {
|
||||
url: redis.url.replace(/\/\/.*@/, "//***@"), // 隐藏密码
|
||||
publishEnabled: redis.publishEnabled,
|
||||
},
|
||||
exchange: {
|
||||
binance: {
|
||||
apiKey: exchange.binance.apiKey.slice(0, 6) + "***",
|
||||
apiSecret: "***",
|
||||
},
|
||||
},
|
||||
logging: {
|
||||
level: logging.level,
|
||||
nodeEnv: logging.nodeEnv,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
export interface EnvConfig {
|
||||
db: DbConfig;
|
||||
redis: RedisConfig;
|
||||
exchange: ExchangeConfig;
|
||||
logging: LoggingConfig;
|
||||
}
|
||||
|
||||
@@ -31,6 +32,18 @@ export interface RedisConfig {
|
||||
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 {
|
||||
level: "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
||||
node_env: "development" | "production" | "test";
|
||||
@@ -77,6 +90,21 @@ export function validateConfig(raw: unknown): EnvConfig {
|
||||
const redisUrl = assertString(redisObj["url"], "redis.url");
|
||||
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 ---
|
||||
const logging = obj["logging"];
|
||||
if (typeof logging !== "object" || logging === null) {
|
||||
@@ -99,6 +127,12 @@ export function validateConfig(raw: unknown): EnvConfig {
|
||||
url: redisUrl,
|
||||
publish_enabled: redisPublishEnabled,
|
||||
},
|
||||
exchange: {
|
||||
binance: {
|
||||
api_key: binanceApiKey,
|
||||
api_secret: binanceApiSecret,
|
||||
},
|
||||
},
|
||||
logging: {
|
||||
level: logLevel,
|
||||
node_env: nodeEnv,
|
||||
|
||||
@@ -13,12 +13,18 @@
|
||||
-- - klines 基表由 02-init-tables.sql 创建为 TimescaleDB hypertable
|
||||
-- - 连续聚合视图由 03-continuous-aggregates.sql 创建
|
||||
-- - TypeORM 的 synchronize:true 与 SQL 脚本互为 fallback(开发/生产双路径)
|
||||
-- - 本脚本为 init-db 链的第一环,仅负责扩展启用
|
||||
-- - 本脚本为 init-db 链的第一环,负责扩展启用(TimescaleDB / pg_prewarm / pg_stat_statements)
|
||||
-- ============================================================
|
||||
|
||||
-- 启用 TimescaleDB 扩展(必须最先执行)
|
||||
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 $$
|
||||
BEGIN
|
||||
|
||||
@@ -100,7 +100,7 @@ CREATE TABLE IF NOT EXISTS trading_pairs (
|
||||
kline_interval VARCHAR(100) NOT NULL DEFAULT '1m',
|
||||
|
||||
-- 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 起始,
|
||||
-- 新交易对从 epoch 起始时间开始全量补拉。
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
-- 03-continuous-aggregates.sql — K 线分层连续聚合视图
|
||||
-- ============================================================
|
||||
-- 从 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 创建)
|
||||
@@ -36,6 +36,37 @@
|
||||
-- 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 基表聚合)
|
||||
-- ============================================================
|
||||
@@ -160,6 +191,37 @@ WITH NO DATA;
|
||||
-- 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 聚合,分层链)
|
||||
-- ============================================================
|
||||
@@ -191,6 +253,68 @@ WITH NO DATA;
|
||||
-- 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 聚合,分层链)
|
||||
-- ============================================================
|
||||
@@ -253,25 +377,75 @@ WITH NO DATA;
|
||||
-- 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 的查询
|
||||
-- ============================================================
|
||||
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_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_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_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_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_15m', NULL, NULL);
|
||||
-- CALL refresh_continuous_aggregate('klines_30m', 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_6h', NULL, NULL);
|
||||
-- CALL refresh_continuous_aggregate('klines_8h', NULL, NULL);
|
||||
-- CALL refresh_continuous_aggregate('klines_1d', NULL, NULL);
|
||||
-- CALL refresh_continuous_aggregate('klines_1w', NULL, NULL);
|
||||
-- CALL refresh_continuous_aggregate('klines_1mon', NULL, NULL);
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../env.yaml
|
||||
@@ -1,19 +1,25 @@
|
||||
import { MainClient, type Kline as BinanceRestKline } from "binance";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
import { exchange } from "../config";
|
||||
import { BaseRestClient } from './base_rest';
|
||||
import type { KlineInterval, Kline, MarketInfo } from '../types';
|
||||
|
||||
/** K 线周期 → 毫秒数映射(用于时间桶计算) */
|
||||
export const KLINE_INTERVAL_MS: Record<KlineInterval, number> = {
|
||||
"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,
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
@@ -102,8 +108,8 @@ async function fetchBinanceKlines(
|
||||
limit = 500,
|
||||
): Promise<Kline[]> {
|
||||
const client = new MainClient({
|
||||
api_key: 'ONSJKIGRpDYLn6FdV17aAKfjclZ4I2LzamflhuMpsoRQA427lLKeyJlGtg2RZ7DH',
|
||||
api_secret: '5Mfv4TgvDlRzCHbtl2nJL4mVHUvMm8pyjKiRjMoosBMxrhlqMw6CuQbg2qbS2Npd',
|
||||
api_key: exchange.binance.apiKey,
|
||||
api_secret: exchange.binance.apiSecret,
|
||||
}, {
|
||||
timeout: 3000,
|
||||
});
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
// ============================================================
|
||||
// 用途:
|
||||
// 按月份粒度逐月刷新 klines 分层连续聚合物化视图链:
|
||||
// 5m → 15m → 30m → 1h → 4h → 1d → 1w
|
||||
// 每层依赖下一层的数据,因此严格按从低到高顺序刷新。
|
||||
// 基表 1m → 3m / 5m → 15m → 30m → 1h → 2h / 4h / 6h → 8h / 1d → 1w / 1mon
|
||||
// 严格按依赖顺序从低到高刷新。
|
||||
//
|
||||
// 使用方式:
|
||||
// # 仅生成 SQL 不执行(dry-run,默认)
|
||||
@@ -36,29 +36,41 @@ import { logger } from "../utils/logger";
|
||||
/**
|
||||
* 分层聚合视图链(按依赖顺序:低层级 → 高层级)
|
||||
*
|
||||
* 刷新顺序至关重要:
|
||||
* klines_5m 源数据来自 klines(1m 基表)
|
||||
* klines_15m 源数据来自 klines_5m
|
||||
* klines_30m 源数据来自 klines_15m
|
||||
* klines_1h 源数据来自 klines_30m
|
||||
* klines_4h 源数据来自 klines_1h
|
||||
* klines_1d 源数据来自 klines_4h
|
||||
* klines_1w 源数据来自 klines_1d
|
||||
* 实际依赖关系(来自于 03-continuous-aggregates.sql):
|
||||
* 基表 1m
|
||||
* ├── 3m (直接聚合 1m)
|
||||
* └── 5m (直接聚合 1m)→ 15m → 30m → 1h ──→ 2h
|
||||
* ├── 4h ──→ 8h
|
||||
* │ └── 1d ──→ 1w
|
||||
* │ └── 1mon
|
||||
* └── 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 = [
|
||||
"klines_3m",
|
||||
"klines_5m",
|
||||
"klines_15m",
|
||||
"klines_30m",
|
||||
"klines_1h",
|
||||
"klines_2h",
|
||||
"klines_4h",
|
||||
"klines_6h",
|
||||
"klines_8h",
|
||||
"klines_1d",
|
||||
"klines_1w",
|
||||
"klines_1mon",
|
||||
] 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
|
||||
|
||||
|
||||
@@ -42,3 +42,12 @@ for (const pair of allPairs) {
|
||||
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);
|
||||
@@ -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
@@ -1,10 +1,15 @@
|
||||
/** K 线周期枚举 */
|
||||
export type KlineInterval =
|
||||
| "1m"
|
||||
| "3m"
|
||||
| "5m"
|
||||
| "15m"
|
||||
| "30m"
|
||||
| "1h"
|
||||
| "2h"
|
||||
| "4h"
|
||||
| "6h"
|
||||
| "8h"
|
||||
| "1d"
|
||||
| "1w";
|
||||
| "1w"
|
||||
| "1mon";
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -0,0 +1,178 @@
|
||||
# 牛熊自适应趋势跟踪策略
|
||||
|
||||
## 概述
|
||||
|
||||
通过识别市场所处的牛熊状态,自适应地选择做多或做空方向,在震荡市中空仓等待。
|
||||
|
||||
核心思想:**牛市不逆势做空,熊市不逆势做多。**
|
||||
|
||||
---
|
||||
|
||||
## 市场状态判定(3 法投票)
|
||||
|
||||
每根 4h K 线收盘后,用以下三种方法独立判定当前市场状态:
|
||||
|
||||
### 方法 1:EMA200 斜率
|
||||
|
||||
```
|
||||
计算:EMA200 近 20 根 K 线的变化率
|
||||
判定:斜率 > +0.2% → 牛
|
||||
斜率 < -0.2% → 熊
|
||||
其他 → 震荡
|
||||
```
|
||||
|
||||
EMA200 向上倾斜说明长期趋势向上,向下倾斜说明长期趋势向下。
|
||||
|
||||
### 方法 2:价格 vs EMA200
|
||||
|
||||
```
|
||||
判定:当前收盘价 > EMA200 → 牛
|
||||
当前收盘价 < EMA200 → 熊
|
||||
```
|
||||
|
||||
最直接的趋势判定——价格在年线上方就是多头市场。
|
||||
|
||||
### 方法 3:ATH 回撤
|
||||
|
||||
```
|
||||
追踪历史最高价 (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)
|
||||
@@ -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,适合低频策略
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -14,7 +14,7 @@ from pydantic import BaseModel, Field, field_validator
|
||||
# K 线周期类型
|
||||
# ============================================================
|
||||
|
||||
KlineInterval = Literal["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w"]
|
||||
KlineInterval = Literal["1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "1d", "1w", "1mon"]
|
||||
|
||||
|
||||
# ============================================================
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# engine.data — K 线数据服务
|
||||
|
||||
from .service import DataService
|
||||
|
||||
__all__ = ["DataService"]
|
||||
@@ -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)),
|
||||
)
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 策略 2:RSI 超买超卖
|
||||
# ============================================================
|
||||
|
||||
|
||||
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, "策略 2:RSI 超买超卖 (30/70)")
|
||||
|
||||
print("\n全部回测完成。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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/9,2h 胜 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/9,2h 胜 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/9,2h 胜 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/9,2h 胜 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/9,2h 胜 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/9,2h 胜 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/9,2h 胜 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/9,2h 胜 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/9,2h 胜 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/9,2h 胜 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/9,2h 胜 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/9,2h 胜 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/9,2h 胜 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/9,2h 胜 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/9,2h 胜 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/9,2h 胜 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
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 策略 3:MACD 金叉死叉
|
||||
# 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
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 策略 6:RSI均值回归 (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
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 策略 7:ATR波动率突破 (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
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 策略 8:EMA双均线多空 (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
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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:
|
||||
# 条件1:4h 上升趋势
|
||||
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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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法判定器(增量 EMA200,O(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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 策略 1: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._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
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 策略 2: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._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
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 策略 3: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._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())
|
||||
@@ -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
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 策略 1:Donchian 海龟突破
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
极简策略测试 — 一条均线 + 一个止损
|
||||
|
||||
核心理念:多加过滤条件往往不如简洁的信号。
|
||||
|
||||
策略:价格上穿 EMA(N) → 买入,价格下穿 EMA(N) → 卖出,ATR 动态止损。
|
||||
无成交量、无多周期、无ADX、无双均线交叉。
|
||||
|
||||
对比 N=10/20/30,4个币种,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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 策略 3:MACD 金叉死叉
|
||||
# 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
@@ -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",
|
||||
]
|
||||
@@ -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:
|
||||
"""增量 ATR(Wilder 平滑)
|
||||
|
||||
内部维护完整序列,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:
|
||||
"""增量 RSI(Wilder 平滑)
|
||||
|
||||
内部维护完整序列,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)
|
||||
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
动量指标 — RSI、Stochastic
|
||||
|
||||
所有函数返回与输入等长的 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
成交量指标 — OBV、VWAP
|
||||
|
||||
所有函数返回与输入等长的 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
|
||||
@@ -22,6 +22,12 @@ redis:
|
||||
# 是否启用 Pub/Sub 行情发布(开发时可关闭以节省资源)
|
||||
publish_enabled: true
|
||||
|
||||
# --- 交易所 API 密钥 ---
|
||||
exchange:
|
||||
binance:
|
||||
api_key: "ONSJKIGRpDYLn6FdV17aAKfjclZ4I2LzamflhuMpsoRQA427lLKeyJlGtg2RZ7DH"
|
||||
api_secret: "5Mfv4TgvDlRzCHbtl2nJL4mVHUvMm8pyjKiRjMoosBMxrhlqMw6CuQbg2qbS2Npd"
|
||||
|
||||
# --- 日志 ---
|
||||
logging:
|
||||
# 日志级别:trace / debug / info / warn / error / fatal
|
||||
|
||||
@@ -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}" }
|
||||
Reference in New Issue
Block a user