feat(engine): 新增 Python 策略引擎模块
- config/settings.py:Pydantic 解析 env.yaml - data/db.py:asyncpg 连接池管理 - data/reader.py:KlineReader 只读查询 TimescaleDB - data/models.py:KlineRecord 等 Pydantic 模型,镜像 TypeORM 实体 - example/test_db.py:数据库查询验证示例 - README.md:引擎架构文档
This commit is contained in:
+1152
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
|||||||
|
from engine.config.settings import ( # noqa: F401
|
||||||
|
DB_DSN,
|
||||||
|
DbConfig,
|
||||||
|
EnvConfig,
|
||||||
|
LoggingConfig,
|
||||||
|
RedisConfig,
|
||||||
|
db,
|
||||||
|
logging,
|
||||||
|
print_config_summary,
|
||||||
|
redis,
|
||||||
|
)
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,81 @@
|
|||||||
|
"""中心化配置模块 —— 读取项目根目录 env.yaml,Pydantic 校验导出强类型配置对象。"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class DbConfig(BaseModel):
|
||||||
|
"""TimescaleDB / PostgreSQL 连接参数"""
|
||||||
|
|
||||||
|
host: str = "localhost"
|
||||||
|
port: int = Field(default=5432, ge=1, le=65535)
|
||||||
|
name: str
|
||||||
|
user: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class RedisConfig(BaseModel):
|
||||||
|
"""Redis 连接配置"""
|
||||||
|
|
||||||
|
url: str = "redis://localhost:6379"
|
||||||
|
publish_enabled: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingConfig(BaseModel):
|
||||||
|
"""日志配置"""
|
||||||
|
|
||||||
|
level: Literal["trace", "debug", "info", "warn", "error", "fatal"] = "info"
|
||||||
|
node_env: Literal["development", "production"] = "development"
|
||||||
|
|
||||||
|
|
||||||
|
class EnvConfig(BaseModel):
|
||||||
|
"""env.yaml 顶层结构"""
|
||||||
|
|
||||||
|
db: DbConfig
|
||||||
|
redis: RedisConfig
|
||||||
|
logging: LoggingConfig
|
||||||
|
|
||||||
|
|
||||||
|
def _get_project_root() -> Path:
|
||||||
|
return Path(__file__).resolve().parent.parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def _load_yaml_config() -> dict:
|
||||||
|
root = _get_project_root()
|
||||||
|
yaml_path = root / "env.yaml"
|
||||||
|
|
||||||
|
if not yaml_path.exists():
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"[config] 无法读取配置文件: {yaml_path}\n"
|
||||||
|
f"请确保项目根目录存在 env.yaml。"
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(yaml_path, "r", encoding="utf-8") as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
raise ValueError(f"[config] env.yaml 解析结果为空: {yaml_path}")
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
_raw = _load_yaml_config()
|
||||||
|
_config = EnvConfig.model_validate(_raw)
|
||||||
|
|
||||||
|
db = _config.db
|
||||||
|
redis = _config.redis
|
||||||
|
logging = _config.logging
|
||||||
|
|
||||||
|
DB_DSN = f"postgresql://{db.user}:{db.password}@{db.host}:{db.port}/{db.name}"
|
||||||
|
|
||||||
|
|
||||||
|
def print_config_summary() -> None:
|
||||||
|
"""打印脱敏后的配置概要(不含密码明文)"""
|
||||||
|
print(f"[config] TimescaleDB: {db.user}@{db.host}:{db.port}/{db.name}")
|
||||||
|
print(
|
||||||
|
f"[config] Redis: {redis.url.replace('//', '//***@') if '@' in redis.url else redis.url}"
|
||||||
|
)
|
||||||
|
print(f"[config] Logging: level={logging.level}, env={logging.node_env}")
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from engine.data.db import close_pool, get_pool # noqa: F401
|
||||||
|
from engine.data.models import KlineRecord, Signal, TradingPairInfo # noqa: F401
|
||||||
|
from engine.data.reader import KlineReader # noqa: F401
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,28 @@
|
|||||||
|
"""asyncpg 连接池管理 —— engine 模块对 TimescaleDB 的唯一切入点。"""
|
||||||
|
|
||||||
|
import asyncpg
|
||||||
|
from engine.config.settings import DB_DSN, db as db_config
|
||||||
|
|
||||||
|
_pool: asyncpg.Pool | None = None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_pool() -> asyncpg.Pool:
|
||||||
|
"""获取或创建 asyncpg 连接池(懒初始化,复用)。"""
|
||||||
|
global _pool
|
||||||
|
if _pool is None:
|
||||||
|
_pool = await asyncpg.create_pool(
|
||||||
|
dsn=DB_DSN,
|
||||||
|
min_size=2,
|
||||||
|
max_size=10,
|
||||||
|
command_timeout=30,
|
||||||
|
server_settings={"default_transaction_read_only": "on"},
|
||||||
|
)
|
||||||
|
return _pool
|
||||||
|
|
||||||
|
|
||||||
|
async def close_pool() -> None:
|
||||||
|
"""关闭连接池(应用退出时调用)"""
|
||||||
|
global _pool
|
||||||
|
if _pool:
|
||||||
|
await _pool.close()
|
||||||
|
_pool = None
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
"""
|
||||||
|
Python 数据模型 —— 镜像 data/db/entities/ 中的 TypeORM 实体。
|
||||||
|
|
||||||
|
命名约定:
|
||||||
|
- 类名与 TypeORM 实体类名一致(Kline, Exchange, TradingPair)
|
||||||
|
- 字段名使用 Python snake_case,对应 TypeORM 的 camelCase / snake_case
|
||||||
|
- 所有模型使用 Pydantic,提供运行时校验 + IDE 智能提示
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class KlineRecord(BaseModel):
|
||||||
|
"""
|
||||||
|
K 线数据记录。
|
||||||
|
|
||||||
|
映射关系:
|
||||||
|
TypeORM Kline.time → KlineRecord.time
|
||||||
|
TypeORM Kline.exchange → KlineRecord.exchange
|
||||||
|
TypeORM Kline.symbol → KlineRecord.symbol
|
||||||
|
TypeORM Kline.interval → KlineRecord.interval
|
||||||
|
TypeORM Kline.open → KlineRecord.open
|
||||||
|
TypeORM Kline.high → KlineRecord.high
|
||||||
|
TypeORM Kline.low → KlineRecord.low
|
||||||
|
TypeORM Kline.close → KlineRecord.close
|
||||||
|
TypeORM Kline.volume → KlineRecord.volume
|
||||||
|
TypeORM Kline.quote_volume → KlineRecord.quote_volume
|
||||||
|
TypeORM Kline.taker_buy_base_vol → KlineRecord.taker_buy_base_vol
|
||||||
|
TypeORM Kline.taker_buy_quote_vol→ KlineRecord.taker_buy_quote_vol
|
||||||
|
TypeORM Kline.trade_count → KlineRecord.trade_count
|
||||||
|
TypeORM Kline.is_closed → KlineRecord.is_closed
|
||||||
|
"""
|
||||||
|
|
||||||
|
time: datetime
|
||||||
|
exchange: str
|
||||||
|
symbol: str
|
||||||
|
interval: str
|
||||||
|
|
||||||
|
open: float
|
||||||
|
high: float
|
||||||
|
low: float
|
||||||
|
close: float
|
||||||
|
volume: float
|
||||||
|
|
||||||
|
quote_volume: Optional[float] = None
|
||||||
|
taker_buy_base_vol: Optional[float] = None
|
||||||
|
taker_buy_quote_vol: Optional[float] = None
|
||||||
|
trade_count: Optional[int] = None
|
||||||
|
is_closed: bool = True
|
||||||
|
created_at: Optional[datetime] = None
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_record(cls, record) -> "KlineRecord":
|
||||||
|
return cls(
|
||||||
|
time=record["time"],
|
||||||
|
exchange=record["exchange"],
|
||||||
|
symbol=record["symbol"],
|
||||||
|
interval=record["interval"],
|
||||||
|
open=float(record["open"]),
|
||||||
|
high=float(record["high"]),
|
||||||
|
low=float(record["low"]),
|
||||||
|
close=float(record["close"]),
|
||||||
|
volume=float(record["volume"]),
|
||||||
|
quote_volume=float(record["quote_volume"]) if record["quote_volume"] is not None else None,
|
||||||
|
taker_buy_base_vol=float(record["taker_buy_base_vol"]) if record["taker_buy_base_vol"] is not None else None,
|
||||||
|
taker_buy_quote_vol=float(record["taker_buy_quote_vol"]) if record["taker_buy_quote_vol"] is not None else None,
|
||||||
|
trade_count=int(record["trade_count"]) if record["trade_count"] is not None else None,
|
||||||
|
is_closed=bool(record["is_closed"]),
|
||||||
|
created_at=record["created_at"] if "created_at" in record else None,
|
||||||
|
updated_at=record["updated_at"] if "updated_at" in record else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TradingPairInfo(BaseModel):
|
||||||
|
"""交易对配置信息(轻量版,仅包含策略决策所需的字段)"""
|
||||||
|
|
||||||
|
symbol: str
|
||||||
|
exchange: str
|
||||||
|
base_asset: str
|
||||||
|
quote_asset: str
|
||||||
|
price_precision: int
|
||||||
|
quantity_precision: int
|
||||||
|
min_qty: Optional[float] = None
|
||||||
|
min_notional: Optional[float] = None
|
||||||
|
active: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class Signal(BaseModel):
|
||||||
|
"""交易信号"""
|
||||||
|
|
||||||
|
symbol: str
|
||||||
|
signal_type: str = Field(..., pattern=r"^(BUY|SELL|HOLD)$")
|
||||||
|
price: Optional[float] = None
|
||||||
|
quantity: Optional[float] = None
|
||||||
|
reason: str = ""
|
||||||
|
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
"""
|
||||||
|
K 线数据读取器 —— 从 TimescaleDB 查询历史 K 线供策略分析和回测使用。
|
||||||
|
|
||||||
|
所有方法均为只读查询,对应 data 模块 Kline 实体的字段结构。
|
||||||
|
参考:data/db/entities/kline.entity.ts
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, Sequence
|
||||||
|
|
||||||
|
import asyncpg
|
||||||
|
|
||||||
|
from engine.data.db import get_pool
|
||||||
|
from engine.data.models import KlineRecord
|
||||||
|
|
||||||
|
|
||||||
|
class KlineReader:
|
||||||
|
"""TimescaleDB K 线只读查询器"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._pool: Optional[asyncpg.Pool] = None
|
||||||
|
|
||||||
|
async def _ensure_pool(self) -> asyncpg.Pool:
|
||||||
|
if self._pool is None:
|
||||||
|
self._pool = await get_pool()
|
||||||
|
return self._pool
|
||||||
|
|
||||||
|
async def get_klines(
|
||||||
|
self,
|
||||||
|
symbol: str,
|
||||||
|
interval: str,
|
||||||
|
start_time: datetime,
|
||||||
|
end_time: datetime,
|
||||||
|
exchange: str = "binance",
|
||||||
|
limit: int = 1000,
|
||||||
|
) -> list[KlineRecord]:
|
||||||
|
"""
|
||||||
|
查询指定时间范围内的 K 线数据,支持任意周期(1m / 5m / 15m / 1h / 4h / 1d 等)。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
symbol: 交易对(如 BTCUSDT)
|
||||||
|
interval: K 线周期(1m / 5m / 15m / 30m / 1h / 4h / 1d 等)
|
||||||
|
start_time: 起始时间(含)
|
||||||
|
end_time: 结束时间(含)
|
||||||
|
exchange: 交易所标识
|
||||||
|
limit: 最大返回条数
|
||||||
|
"""
|
||||||
|
pool = await self._ensure_pool()
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
time, exchange, symbol, interval,
|
||||||
|
open, high, low, close, volume,
|
||||||
|
quote_volume, taker_buy_base_vol, taker_buy_quote_vol,
|
||||||
|
trade_count, is_closed, created_at, updated_at
|
||||||
|
FROM klines
|
||||||
|
WHERE exchange = $1
|
||||||
|
AND symbol = $2
|
||||||
|
AND interval = $3
|
||||||
|
AND time >= $4
|
||||||
|
AND time <= $5
|
||||||
|
ORDER BY time ASC
|
||||||
|
LIMIT $6
|
||||||
|
"""
|
||||||
|
|
||||||
|
rows: Sequence[asyncpg.Record] = await pool.fetch(
|
||||||
|
query,
|
||||||
|
exchange,
|
||||||
|
symbol,
|
||||||
|
interval,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
return [KlineRecord.from_record(row) for row in rows]
|
||||||
|
|
||||||
|
async def get_latest_klines(
|
||||||
|
self,
|
||||||
|
symbol: str,
|
||||||
|
interval: str,
|
||||||
|
exchange: str = "binance",
|
||||||
|
limit: int = 500,
|
||||||
|
) -> list[KlineRecord]:
|
||||||
|
"""
|
||||||
|
获取最近 N 根已闭合的 K 线(策略启动时快速预热)。
|
||||||
|
|
||||||
|
仅查询 is_closed = TRUE 的 K 线,避免使用未闭合的不完整数据。
|
||||||
|
"""
|
||||||
|
pool = await self._ensure_pool()
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
time, exchange, symbol, interval,
|
||||||
|
open, high, low, close, volume,
|
||||||
|
quote_volume, taker_buy_base_vol, taker_buy_quote_vol,
|
||||||
|
trade_count, is_closed, created_at, updated_at
|
||||||
|
FROM klines
|
||||||
|
WHERE exchange = $1
|
||||||
|
AND symbol = $2
|
||||||
|
AND interval = $3
|
||||||
|
AND is_closed = TRUE
|
||||||
|
ORDER BY time DESC
|
||||||
|
LIMIT $4
|
||||||
|
"""
|
||||||
|
|
||||||
|
rows = await pool.fetch(query, exchange, symbol, interval, limit)
|
||||||
|
|
||||||
|
records = [KlineRecord.from_record(row) for row in rows]
|
||||||
|
records.reverse()
|
||||||
|
return records
|
||||||
|
|
||||||
|
async def get_klines_by_count(
|
||||||
|
self,
|
||||||
|
symbol: str,
|
||||||
|
interval: str,
|
||||||
|
count: int = 100,
|
||||||
|
exchange: str = "binance",
|
||||||
|
before_time: Optional[datetime] = None,
|
||||||
|
) -> list[KlineRecord]:
|
||||||
|
"""
|
||||||
|
获取最新的 N 根 K 线(按时间倒序取 N 条再正序返回)。
|
||||||
|
|
||||||
|
适合策略运行时获取最近 N 根 K 线做指标计算,不关心特定时间范围。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
symbol: 交易对
|
||||||
|
interval: K 线周期
|
||||||
|
count: 需要获取的 K 线数量
|
||||||
|
exchange: 交易所标识
|
||||||
|
before_time: 可选,只获取该时间之前的 K 线
|
||||||
|
"""
|
||||||
|
pool = await self._ensure_pool()
|
||||||
|
|
||||||
|
if before_time:
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
time, exchange, symbol, interval,
|
||||||
|
open, high, low, close, volume,
|
||||||
|
quote_volume, taker_buy_base_vol, taker_buy_quote_vol,
|
||||||
|
trade_count, is_closed, created_at, updated_at
|
||||||
|
FROM klines
|
||||||
|
WHERE exchange = $1
|
||||||
|
AND symbol = $2
|
||||||
|
AND interval = $3
|
||||||
|
AND time <= $4
|
||||||
|
ORDER BY time DESC
|
||||||
|
LIMIT $5
|
||||||
|
"""
|
||||||
|
rows = await pool.fetch(query, exchange, symbol, interval, before_time, count)
|
||||||
|
else:
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
time, exchange, symbol, interval,
|
||||||
|
open, high, low, close, volume,
|
||||||
|
quote_volume, taker_buy_base_vol, taker_buy_quote_vol,
|
||||||
|
trade_count, is_closed, created_at, updated_at
|
||||||
|
FROM klines
|
||||||
|
WHERE exchange = $1
|
||||||
|
AND symbol = $2
|
||||||
|
AND interval = $3
|
||||||
|
ORDER BY time DESC
|
||||||
|
LIMIT $4
|
||||||
|
"""
|
||||||
|
rows = await pool.fetch(query, exchange, symbol, interval, count)
|
||||||
|
|
||||||
|
records = [KlineRecord.from_record(row) for row in rows]
|
||||||
|
records.reverse()
|
||||||
|
return records
|
||||||
|
|
||||||
|
async def get_ohlcv_array(
|
||||||
|
self,
|
||||||
|
symbol: str,
|
||||||
|
interval: str,
|
||||||
|
exchange: str = "binance",
|
||||||
|
limit: int = 200,
|
||||||
|
before_time: Optional[datetime] = None,
|
||||||
|
) -> list[tuple[datetime, float, float, float, float, float]]:
|
||||||
|
"""
|
||||||
|
获取 OHLCV 元组数组,直接用于 pandas DataFrame 或 TA-Lib 计算。
|
||||||
|
|
||||||
|
返回格式: [(timestamp, open, high, low, close, volume), ...]
|
||||||
|
时间升序排列。
|
||||||
|
"""
|
||||||
|
pool = await self._ensure_pool()
|
||||||
|
|
||||||
|
if before_time:
|
||||||
|
query = """
|
||||||
|
SELECT time, open, high, low, close, volume
|
||||||
|
FROM klines
|
||||||
|
WHERE exchange = $1
|
||||||
|
AND symbol = $2
|
||||||
|
AND interval = $3
|
||||||
|
AND time <= $4
|
||||||
|
ORDER BY time DESC
|
||||||
|
LIMIT $5
|
||||||
|
"""
|
||||||
|
rows = await pool.fetch(query, exchange, symbol, interval, before_time, limit)
|
||||||
|
else:
|
||||||
|
query = """
|
||||||
|
SELECT time, open, high, low, close, volume
|
||||||
|
FROM klines
|
||||||
|
WHERE exchange = $1
|
||||||
|
AND symbol = $2
|
||||||
|
AND interval = $3
|
||||||
|
ORDER BY time DESC
|
||||||
|
LIMIT $4
|
||||||
|
"""
|
||||||
|
rows = await pool.fetch(query, exchange, symbol, interval, limit)
|
||||||
|
|
||||||
|
return [
|
||||||
|
(row["time"], float(row["open"]), float(row["high"]), float(row["low"]), float(row["close"]), float(row["volume"]))
|
||||||
|
for row in reversed(rows)
|
||||||
|
]
|
||||||
|
|
||||||
|
async def get_available_symbols(
|
||||||
|
self,
|
||||||
|
exchange: str = "binance",
|
||||||
|
) -> list[str]:
|
||||||
|
"""
|
||||||
|
查询已激活的交易对列表。
|
||||||
|
|
||||||
|
从 trading_pairs 配置表读取(仅 active=TRUE 的记录),
|
||||||
|
而非扫描 klines 时序表,避免全表扫描。
|
||||||
|
对应 data/db/entities/trading-pair.entity.ts。
|
||||||
|
"""
|
||||||
|
pool = await self._ensure_pool()
|
||||||
|
|
||||||
|
rows = await pool.fetch(
|
||||||
|
"""
|
||||||
|
SELECT tp.symbol
|
||||||
|
FROM trading_pairs tp
|
||||||
|
JOIN exchanges e ON tp.exchange_id = e.id
|
||||||
|
WHERE e.name = $1
|
||||||
|
AND tp.active = TRUE
|
||||||
|
ORDER BY tp.symbol
|
||||||
|
""",
|
||||||
|
exchange,
|
||||||
|
)
|
||||||
|
|
||||||
|
return [row["symbol"] for row in rows]
|
||||||
|
|
||||||
|
async def get_available_intervals(
|
||||||
|
self,
|
||||||
|
symbol: str,
|
||||||
|
exchange: str = "binance",
|
||||||
|
) -> list[str]:
|
||||||
|
"""
|
||||||
|
查询某交易对配置的 K 线周期列表。
|
||||||
|
|
||||||
|
从 trading_pairs.kline_intervals 读取(逗号分隔字符串),
|
||||||
|
而非扫描 klines 时序表,避免全表扫描。
|
||||||
|
对应 data/db/entities/trading-pair.entity.ts: kline_intervals 字段。
|
||||||
|
"""
|
||||||
|
pool = await self._ensure_pool()
|
||||||
|
|
||||||
|
row = await pool.fetchrow(
|
||||||
|
"""
|
||||||
|
SELECT tp.kline_intervals
|
||||||
|
FROM trading_pairs tp
|
||||||
|
JOIN exchanges e ON tp.exchange_id = e.id
|
||||||
|
WHERE e.name = $1
|
||||||
|
AND tp.symbol = $2
|
||||||
|
AND tp.active = TRUE
|
||||||
|
""",
|
||||||
|
exchange,
|
||||||
|
symbol,
|
||||||
|
)
|
||||||
|
|
||||||
|
if row is None or not row["kline_intervals"]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# kline_intervals 格式: "1m,5m,15m,1h,4h,1d"
|
||||||
|
return [
|
||||||
|
interval.strip()
|
||||||
|
for interval in row["kline_intervals"].split(",")
|
||||||
|
if interval.strip()
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,44 @@
|
|||||||
|
"""
|
||||||
|
数据库模块测试示例。
|
||||||
|
|
||||||
|
运行方式(在项目根目录 trade/ 下):
|
||||||
|
python -m engine.example.test_db
|
||||||
|
|
||||||
|
前提条件:
|
||||||
|
docker compose up -d
|
||||||
|
data 模块已同步过 K 线数据到 TimescaleDB
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
from engine.config import print_config_summary
|
||||||
|
from engine.data import KlineRecord, KlineReader, close_pool
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print("=" * 60)
|
||||||
|
print(" Trade Engine — 数据库模块测试")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
print_config_summary()
|
||||||
|
|
||||||
|
reader = KlineReader()
|
||||||
|
|
||||||
|
# 获取最近 100 根 BTCUSDT 5m K 线
|
||||||
|
klines = await reader.get_klines_by_count(
|
||||||
|
symbol="BTCUSDT",
|
||||||
|
interval="1m",
|
||||||
|
count=5,
|
||||||
|
)
|
||||||
|
print(f"\n查询到 {len(klines)} 根 K 线:")
|
||||||
|
for k in klines[:5]:
|
||||||
|
print(f" {k.time.isoformat()} | O={k.open:.2f} H={k.high:.2f} "
|
||||||
|
f"L={k.low:.2f} C={k.close:.2f} V={k.volume:.4f}")
|
||||||
|
if len(klines) > 5:
|
||||||
|
print(f" ... 剩余 {len(klines) - 5} 根")
|
||||||
|
|
||||||
|
await close_pool()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[project]
|
||||||
|
name = "trade-engine"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "量化交易策略引擎"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"asyncpg>=0.29",
|
||||||
|
"pydantic>=2.5",
|
||||||
|
"pyyaml>=6.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.pyright]
|
||||||
|
venvPath = "."
|
||||||
|
venv = ".venv"
|
||||||
Reference in New Issue
Block a user