chore: 初始化项目骨架 — 数据模块依赖配置、TimescaleDB 建表脚本、Docker Compose 编排
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
*.log
|
||||
db/pgsql/
|
||||
@@ -0,0 +1,42 @@
|
||||
# ============================================================
|
||||
# Trade Data Module — 环境变量配置模板
|
||||
# ============================================================
|
||||
# 复制为 .env 并修改:
|
||||
# cp .env.example .env
|
||||
# ============================================================
|
||||
|
||||
# --- 行情订阅 ---
|
||||
# 逗号分隔的交易对列表(大写)
|
||||
SYMBOLS=BTCUSDT,ETHUSDT
|
||||
|
||||
# --- TimescaleDB 连接 ---
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=trade
|
||||
DB_USER=trader
|
||||
DB_PASSWORD=changeme
|
||||
|
||||
# --- Redis 连接 ---
|
||||
REDIS_URL=redis://localhost:6379
|
||||
# 是否启用 Redis 发布(开发时可关闭)
|
||||
REDIS_PUBLISH_ENABLED=true
|
||||
|
||||
# --- 批量写入 ---
|
||||
# 缓冲区条数阈值(达到后自动刷新)
|
||||
BATCH_SIZE=500
|
||||
# 最大缓冲时间(毫秒),超时后自动刷新
|
||||
FLUSH_INTERVAL_MS=1000
|
||||
|
||||
# --- WebSocket 连接 ---
|
||||
# 断线重连延迟基数(毫秒),指数退避:基数 × 2^attempts
|
||||
WS_RECONNECT_DELAY_MS=3000
|
||||
# 心跳间隔(毫秒)
|
||||
WS_PING_INTERVAL_MS=30000
|
||||
# 最大重连次数
|
||||
WS_MAX_RECONNECT_ATTEMPTS=10
|
||||
|
||||
# --- 日志 ---
|
||||
# 日志级别:trace / debug / info / warn / error / fatal
|
||||
LOG_LEVEL=debug
|
||||
# 生产环境(关闭 pretty print,输出 JSON)
|
||||
NODE_ENV=development
|
||||
@@ -0,0 +1,228 @@
|
||||
-- ============================================================
|
||||
-- 001_init.sql — TimescaleDB 数据初始化
|
||||
--
|
||||
-- Docker Compose 首次启动时自动执行
|
||||
-- 挂载路径:./data/init-db:/docker-entrypoint-initdb.d
|
||||
-- ============================================================
|
||||
|
||||
-- 扩展
|
||||
CREATE EXTENSION IF NOT EXISTS timescaledb;
|
||||
CREATE EXTENSION IF NOT EXISTS timescaledb_toolkit;
|
||||
|
||||
-- ============================================================
|
||||
-- 1. K 线主表
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS klines (
|
||||
-- 时间维度
|
||||
time TIMESTAMPTZ NOT NULL, -- K 线开盘时间(UTC)
|
||||
|
||||
-- 标识维度
|
||||
exchange TEXT NOT NULL, -- 交易所:binance/okx/bybit
|
||||
symbol TEXT NOT NULL, -- 交易对:BTCUSDT/ETHUSDT
|
||||
interval TEXT NOT NULL, -- 周期:1m/5m/15m/1h/4h/1d
|
||||
|
||||
-- OHLCV
|
||||
open NUMERIC(20,8) NOT NULL,
|
||||
high NUMERIC(20,8) NOT NULL,
|
||||
low NUMERIC(20,8) NOT NULL,
|
||||
close NUMERIC(20,8) NOT NULL,
|
||||
volume NUMERIC(20,8) NOT NULL DEFAULT 0, -- 成交量(基准币种)
|
||||
|
||||
-- 扩展字段
|
||||
quote_volume NUMERIC(20,8) DEFAULT 0, -- 成交额(计价币种)
|
||||
taker_buy_base_vol NUMERIC(20,8) DEFAULT 0, -- 主动买入量
|
||||
taker_buy_quote_vol NUMERIC(20,8) DEFAULT 0, -- 主动买入额
|
||||
trade_count INTEGER DEFAULT 0, -- 成交笔数
|
||||
is_closed BOOLEAN DEFAULT TRUE, -- K 线是否已闭合
|
||||
|
||||
-- 元数据
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
-- 唯一约束(同一根 K 线不可重复)
|
||||
UNIQUE (time, exchange, symbol, interval)
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 2. 转换为 hypertable(时序分区)
|
||||
-- ============================================================
|
||||
SELECT create_hypertable(
|
||||
'klines',
|
||||
'time', -- 时间列
|
||||
chunk_time_interval => INTERVAL '1 day', -- 每个 chunk = 1 天数据
|
||||
partitioning_column => 'exchange', -- 空间分区列
|
||||
number_partitions => 4, -- 4 个空间分区
|
||||
if_not_exists => TRUE
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 3. 索引
|
||||
-- ============================================================
|
||||
|
||||
-- 主力查询索引:按交易对+周期+时间范围查(覆盖 95% 查询)
|
||||
CREATE INDEX IF NOT EXISTS idx_klines_lookup
|
||||
ON klines (exchange, symbol, interval, time DESC);
|
||||
|
||||
-- 回测专用索引:按交易对+周期+时间正序
|
||||
CREATE INDEX IF NOT EXISTS idx_klines_backtest
|
||||
ON klines (symbol, interval, time ASC);
|
||||
|
||||
-- 最新 K 线索引(部分索引,仅覆盖已闭合 K 线)
|
||||
CREATE INDEX IF NOT EXISTS idx_klines_latest
|
||||
ON klines (exchange, symbol, interval, time DESC)
|
||||
WHERE is_closed = TRUE;
|
||||
|
||||
-- ============================================================
|
||||
-- 4. 压缩策略
|
||||
-- ============================================================
|
||||
|
||||
-- 启用列式压缩(按 symbol+interval 分组,按 time 排序)
|
||||
ALTER TABLE klines SET (
|
||||
timescaledb.compress,
|
||||
timescaledb.compress_segmentby = 'exchange, symbol, interval',
|
||||
timescaledb.compress_orderby = 'time DESC'
|
||||
);
|
||||
|
||||
-- 自动压缩:K 线闭合 7 天后自动压缩(压缩比约 90%)
|
||||
SELECT add_compression_policy('klines', INTERVAL '7 days', if_not_exists => TRUE);
|
||||
|
||||
-- ============================================================
|
||||
-- 5. 数据保留策略
|
||||
-- ============================================================
|
||||
-- 1m K 线保留 90 天(回测通常用更粗粒度)
|
||||
SELECT add_retention_policy('klines', INTERVAL '90 days', if_not_exists => TRUE);
|
||||
|
||||
-- ============================================================
|
||||
-- 6. 连续聚合(从 1m 自动派生高周期 K 线)
|
||||
-- ============================================================
|
||||
|
||||
-- ---------- 5m K 线 ----------
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_5m
|
||||
WITH (timescaledb.continuous) AS
|
||||
SELECT
|
||||
time_bucket('5 minutes', time) AS time,
|
||||
exchange,
|
||||
symbol,
|
||||
'5m'::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) AS trade_count
|
||||
FROM klines
|
||||
WHERE interval = '1m'
|
||||
GROUP BY time_bucket('5 minutes', time), exchange, symbol;
|
||||
|
||||
SELECT add_continuous_aggregate_policy('klines_5m',
|
||||
start_offset => INTERVAL '1 day',
|
||||
end_offset => INTERVAL '10 minutes',
|
||||
schedule_interval => INTERVAL '1 minute',
|
||||
if_not_exists => TRUE
|
||||
);
|
||||
|
||||
-- ---------- 15m K 线 ----------
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_15m
|
||||
WITH (timescaledb.continuous) AS
|
||||
SELECT
|
||||
time_bucket('15 minutes', time) AS time,
|
||||
exchange,
|
||||
symbol,
|
||||
'15m'::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) AS trade_count
|
||||
FROM klines
|
||||
WHERE interval = '1m'
|
||||
GROUP BY time_bucket('15 minutes', time), exchange, symbol;
|
||||
|
||||
SELECT add_continuous_aggregate_policy('klines_15m',
|
||||
start_offset => INTERVAL '2 days',
|
||||
end_offset => INTERVAL '30 minutes',
|
||||
schedule_interval => INTERVAL '5 minutes',
|
||||
if_not_exists => TRUE
|
||||
);
|
||||
|
||||
-- ---------- 1h K 线 ----------
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_1h
|
||||
WITH (timescaledb.continuous) AS
|
||||
SELECT
|
||||
time_bucket('1 hour', time) AS time,
|
||||
exchange,
|
||||
symbol,
|
||||
'1h'::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) AS trade_count
|
||||
FROM klines
|
||||
WHERE interval = '1m'
|
||||
GROUP BY time_bucket('1 hour', time), exchange, symbol;
|
||||
|
||||
SELECT add_continuous_aggregate_policy('klines_1h',
|
||||
start_offset => INTERVAL '3 days',
|
||||
end_offset => INTERVAL '1 hour',
|
||||
schedule_interval => INTERVAL '5 minutes',
|
||||
if_not_exists => TRUE
|
||||
);
|
||||
|
||||
-- ---------- 1d K 线 ----------
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_1d
|
||||
WITH (timescaledb.continuous) AS
|
||||
SELECT
|
||||
time_bucket('1 day', time) AS time,
|
||||
exchange,
|
||||
symbol,
|
||||
'1d'::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) AS trade_count
|
||||
FROM klines
|
||||
WHERE interval = '1m'
|
||||
GROUP BY time_bucket('1 day', time), exchange, symbol;
|
||||
|
||||
SELECT add_continuous_aggregate_policy('klines_1d',
|
||||
start_offset => INTERVAL '7 days',
|
||||
end_offset => INTERVAL '2 hours',
|
||||
schedule_interval => INTERVAL '1 hour',
|
||||
if_not_exists => TRUE
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 7. 连续聚合的压缩(减少视图存储)
|
||||
-- ============================================================
|
||||
ALTER MATERIALIZED VIEW klines_5m SET (timescaledb.compress = true);
|
||||
ALTER MATERIALIZED VIEW klines_15m SET (timescaledb.compress = true);
|
||||
ALTER MATERIALIZED VIEW klines_1h SET (timescaledb.compress = true);
|
||||
ALTER MATERIALIZED VIEW klines_1d SET (timescaledb.compress = true);
|
||||
|
||||
-- ============================================================
|
||||
-- 初始化完成
|
||||
-- ============================================================
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE 'TimescaleDB initialization complete.';
|
||||
RAISE NOTICE 'Hypertable: klines';
|
||||
RAISE NOTICE 'Continuous aggregates: klines_5m, klines_15m, klines_1h, klines_1d';
|
||||
RAISE NOTICE 'Compression: 7 days delay, 90 days retention';
|
||||
END $$;
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "trade-data",
|
||||
"version": "0.1.0",
|
||||
"description": "数字货币量化交易系统 - TypeScript 数据模块",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"lint": "eslint src/",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"binance": "^3.5.9",
|
||||
"ccxt": "^4.5.56",
|
||||
"ioredis": "^5.11.1",
|
||||
"pg": "^8.21.0",
|
||||
"pino": "^10.3.1",
|
||||
"ws": "^8.21.0",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.9.2",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"eslint": "^10.4.1",
|
||||
"prettier": "^3.8.3",
|
||||
"tsx": "^4.22.4",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^4.1.8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": [
|
||||
"ESNext"
|
||||
],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
"types": [
|
||||
"bun"
|
||||
],
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
services:
|
||||
timescaledb:
|
||||
image: timescale/timescaledb-ha:pg17.10-ts2.27.1
|
||||
container_name: trade-timescaledb
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_DB: trade
|
||||
POSTGRES_USER: trader
|
||||
POSTGRES_PASSWORD: fucketh
|
||||
volumes:
|
||||
- ./db/pgsql:/var/lib/postgresql
|
||||
- ./data/init-db:/docker-entrypoint-initdb.d # 自动执行建表 SQL
|
||||
command: >
|
||||
-c shared_buffers=1GB
|
||||
-c effective_cache_size=3GB
|
||||
-c maintenance_work_mem=256MB
|
||||
-c work_mem=64MB
|
||||
-c wal_buffers=64MB
|
||||
-c random_page_cost=1.1
|
||||
-c effective_io_concurrency=200
|
||||
-c max_connections=50
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U trader -d trade"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
Reference in New Issue
Block a user