chore: 更新 README 架构文档与数据库测试脚本

- README.md: 更新数据层 Node.js→Bun,common→engine/common,同步目录树结构
- db_test.py: TimescaleDB 数据库连接与基础查询测试脚本
This commit is contained in:
Rekey
2026-06-12 10:27:11 +08:00
parent 515e61c517
commit b5cdb41993
2 changed files with 174 additions and 34 deletions
+27 -34
View File
@@ -35,7 +35,7 @@
| 层 | 语言 | 职责 | | 层 | 语言 | 职责 |
|---|------|------| |---|------|------|
| **数据层** | TypeScript (Node.js) | 行情采集、WebSocket 连接管理、K 线合成、数据写入 | | **数据层** | TypeScript (Bun) | 行情采集、WebSocket 连接管理、K 线合成、数据写入 |
| **业务层** | Python 3.10+ | 策略引擎、回测、风控、交易执行 | | **业务层** | Python 3.10+ | 策略引擎、回测、风控、交易执行 |
| **接口层** | TypeScript / Python | FastAPI (Python) 或 NestJS (TS) 提供 REST/WS API | | **接口层** | TypeScript / Python | FastAPI (Python) 或 NestJS (TS) 提供 REST/WS API |
@@ -45,7 +45,7 @@
| 分类 | 技术 / 库 | 说明 | | 分类 | 技术 / 库 | 说明 |
| ------------ | ---------------------------------- | -------------------------------------- | | ------------ | ---------------------------------- | -------------------------------------- |
| **数据层语言** | **TypeScript 5.x (Node.js 20+)** | 行情采集、WebSocket 管理、数据管道 | | **数据层语言** | **TypeScript 5.x (Bun)** | 行情采集、WebSocket 管理、数据管道 |
| **业务层语言** | **Python 3.10+** | 策略引擎、回测、风控逻辑 | | **业务层语言** | **Python 3.10+** | 策略引擎、回测、风控逻辑 |
| **时序数据库** | **TimescaleDB (推荐)** | K 线数据存储,基于 PostgreSQL 扩展 | | **时序数据库** | **TimescaleDB (推荐)** | K 线数据存储,基于 PostgreSQL 扩展 |
| 关系型数据库 | PostgreSQL 16+ | 订单、策略配置、用户数据等(TimescaleDB 基于 PG,可共用)| | 关系型数据库 | PostgreSQL 16+ | 订单、策略配置、用户数据等(TimescaleDB 基于 PG,可共用)|
@@ -87,7 +87,7 @@
|------|------| |------|------|
| Node.js WebSocket 性能优异,天然适合高并发连接 | 需要维护两套技术栈 | | Node.js WebSocket 性能优异,天然适合高并发连接 | 需要维护两套技术栈 |
| TypeScript 类型系统在数据管道中减少运行时错误 | 跨语言调试稍复杂 | | TypeScript 类型系统在数据管道中减少运行时错误 | 跨语言调试稍复杂 |
| **共享类型**:前后端可用同一套 TypeScript 类型定义 | 部署需要 Node.js + Python 双运行时 | | **共享类型**:前后端可用同一套 TypeScript 类型定义 | 部署需要 Bun + Python 双运行时 |
| 事件驱动模型与行情数据流天然匹配 | 团队需要双语言能力 | | 事件驱动模型与行情数据流天然匹配 | 团队需要双语言能力 |
| npm 生态有成熟的交易 SDKccxt 等) | | | npm 生态有成熟的交易 SDKccxt 等) | |
@@ -940,7 +940,7 @@ export class TimescaleDBStorage {
##### 6. Python 读取实现(策略引擎侧) ##### 6. Python 读取实现(策略引擎侧)
```python ```python
# common/storage.py # engine/common/storage.py
import asyncpg import asyncpg
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
@@ -1160,6 +1160,7 @@ volumes:
| 子模块 | 职责 | 关键技术点 | | 子模块 | 职责 | 关键技术点 |
| -------------- | ---------------------------------------------- | -------------------------------------- | | -------------- | ---------------------------------------------- | -------------------------------------- |
| **通用模块** | 策略基类、数据模型、日志、配置 | `engine/common/` 目录,基础类型定义 |
| **策略管理器** | 策略注册、启动、停止、热加载 | 插件化架构、动态导入、`importlib` | | **策略管理器** | 策略注册、启动、停止、热加载 | 插件化架构、动态导入、`importlib` |
| **信号分发器** | 将策略产生的交易信号分发到交易执行模块 | 事件总线、消息队列 | | **信号分发器** | 将策略产生的交易信号分发到交易执行模块 | 事件总线、消息队列 |
| **回测引擎** | 使用历史数据模拟策略执行,评估收益、回撤等指标 | `vectorbt` / `backtrader`、事件驱动 | | **回测引擎** | 使用历史数据模拟策略执行,评估收益、回撤等指标 | `vectorbt` / `backtrader`、事件驱动 |
@@ -1168,27 +1169,15 @@ volumes:
#### 策略基类设计 #### 策略基类设计
```python ```python
class BaseStrategy(ABC): from common import BaseStrategy, StrategyConfig, Signal
"""所有策略的基类""" from common.models import Kline, Ticker, OrderBook
def __init__(self, config: StrategyConfig): class MyStrategy(BaseStrategy):
self.config = config """策略示例 — 所有策略继承 BaseStrategy"""
self.position = 0.0 strategy_type = "my_strategy"
self.pnl = 0.0
@abstractmethod
async def on_ticker(self, ticker: Ticker) -> Signal | None:
"""处理 ticker 数据,返回交易信号"""
...
@abstractmethod
async def on_kline(self, kline: Kline) -> Signal | None: async def on_kline(self, kline: Kline) -> Signal | None:
"""处理 K 线数据,返回交易信号"""
... ...
async def on_orderbook(self, orderbook: OrderBook) -> Signal | None:
"""处理深度数据(可选实现)"""
return None
``` ```
--- ---
@@ -1466,7 +1455,7 @@ async with redis.pubsub() as pubsub:
| ---- | ------------------------------------------------------------ | ----------------------------------- | | ---- | ------------------------------------------------------------ | ----------------------------------- |
| 1.1 | 初始化 TypeScript 数据项目(`data/`),配置 `package.json` | 项目结构,ESLint + Prettier 配置 | | 1.1 | 初始化 TypeScript 数据项目(`data/`),配置 `package.json` | 项目结构,ESLint + Prettier 配置 |
| 1.2 | 初始化 Python 项目,配置 `poetry` / `uv` 依赖管理 | `pyproject.toml`,项目目录结构 | | 1.2 | 初始化 Python 项目,配置 `poetry` / `uv` 依赖管理 | `pyproject.toml`,项目目录结构 |
| 1.3 | 定义共享类型:TypeScript `types/` + Python `common/models.py` | 双端对齐的数据模型 | | 1.3 | 定义共享类型:TypeScript `types/` + Python `engine/common/models.py` | 双端对齐的数据模型 |
| 1.4 | 实现 TS 交易所适配器基类 + Binance 适配器(`data/src/exchanges/` | 统一接口 + WebSocket 连接 | | 1.4 | 实现 TS 交易所适配器基类 + Binance 适配器(`data/src/exchanges/` | 统一接口 + WebSocket 连接 |
| 1.5 | 实现 TS 行情采集器,WebSocket 订阅实时 ticker 和 K 线 | 实时行情流入 | | 1.5 | 实现 TS 行情采集器,WebSocket 订阅实时 ticker 和 K 线 | 实时行情流入 |
| 1.6 | 实现 TS K 线合成器(`data/src/pipeline/kline-synthesizer.ts`) | 多周期 K 线实时合成 | | 1.6 | 实现 TS K 线合成器(`data/src/pipeline/kline-synthesizer.ts`) | 多周期 K 线实时合成 |
@@ -1481,7 +1470,7 @@ async with redis.pubsub() as pubsub:
| 步骤 | 任务 | 产出物 | | 步骤 | 任务 | 产出物 |
| ---- | ------------------------------------------------------------ | -------------------------------- | | ---- | ------------------------------------------------------------ | -------------------------------- |
| 2.1 | 实现策略基类(`engine/base.py` | `BaseStrategy` 抽象基类 | | 2.1 | 实现策略基类(`engine/common/base.py`) | `BaseStrategy` 抽象基类 |
| 2.2 | 实现策略管理器(`engine/manager.py`),支持策略注册和生命周期 | 策略热加载、启动/停止控制 | | 2.2 | 实现策略管理器(`engine/manager.py`),支持策略注册和生命周期 | 策略热加载、启动/停止控制 |
| 2.3 | 实现信号分发器(`engine/signals.py`) | 事件总线,策略到执行器的信号传递 | | 2.3 | 实现信号分发器(`engine/signals.py`) | 事件总线,策略到执行器的信号传递 |
| 2.4 | 实现回测引擎(`engine/backtest.py`) | 历史数据回测,收益曲线、回撤等指标 | | 2.4 | 实现回测引擎(`engine/backtest.py`) | 历史数据回测,收益曲线、回撤等指标 |
@@ -1590,11 +1579,16 @@ trade/
├── engine/ # 🐍 Python 策略引擎 ├── engine/ # 🐍 Python 策略引擎
│ ├── __init__.py │ ├── __init__.py
│ ├── base.py # BaseStrategy 基类 │ ├── env.yaml # 引擎环境配置
│ ├── manager.py # 策略管理器 │ ├── common/ # 引擎通用模块
│ ├── signals.py # 信号分发器 │ ├── __init__.py
│ ├── backtest.py # 回测引擎 │ ├── base.py # BaseStrategy 基类
└── optimizer.py # 参数优化器 │ ├── models.py # 数据模型(Kline/Ticker/Trade/OrderBook
│ │ └── logger.py # 结构化日志
│ ├── manager.py # 策略管理器
│ ├── signals.py # 信号分发器
│ ├── backtest.py # 回测引擎
│ └── optimizer.py # 参数优化器
├── executor/ # 🐍 Python 交易执行模块 ├── executor/ # 🐍 Python 交易执行模块
│ ├── __init__.py │ ├── __init__.py
@@ -1632,12 +1626,11 @@ trade/
│ ├── grid_trading.py # 网格交易策略 │ ├── grid_trading.py # 网格交易策略
│ └── arbitrage.py # 套利策略(可选) │ └── arbitrage.py # 套利策略(可选)
├── common/ # 🐍 Python 公共工具模块 ├── common/ # 🐍 Python 公共基础设施(跨模块共享配置、工具等)
│ ├── __init__.py │ ├── __init__.py
│ ├── logger.py # 日志配置 │ ├── config.py # 全局配置
│ ├── models.py # 数据模型(Pydantic,与 TS types 对应) │ ├── constants.py # 常量定义
── constants.py # 常量定义 ── utils.py # 工具函数
│ └── utils.py # 工具函数
├── tests/ # 🐍 Python 测试 ├── tests/ # 🐍 Python 测试
│ ├── __init__.py │ ├── __init__.py
@@ -1662,7 +1655,7 @@ trade/
``` ```
┌──────────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────────┐ ┌──────────────┐ ┌─────────────┐
│ TS 数据模块 │ │ PostgreSQL │ │ Grafana │ │ TS 数据模块 │ │ PostgreSQL │ │ Grafana │
│ (Node.js 进程) │ │ (业务数据) │ │ (可视化) │ │ (Bun 进程) │ │ (业务数据) │ │ (可视化) │
└──────┬───────────┘ └──────────────┘ └──────┬──────┘ └──────┬───────────┘ └──────────────┘ └──────┬──────┘
│ │ │ │
│ ┌──────────────┐ │ │ ┌──────────────┐ │
+147
View File
@@ -0,0 +1,147 @@
"""
数据库 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",
"5m": "klines_5m",
"15m": "klines_15m",
"30m": "klines_30m",
"1h": "klines_1h",
"4h": "klines_4h",
"1d": "klines_1d",
"1w": "klines_1w",
}
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())