feat(data): 实现 Binance WebSocket 适配器与架构重构

- 新增 exchanges/ 模块:MarketDataFeed 统一接口、BaseExchangeAdapter 抽象基类、
  BinanceAdapter 完整实现(WebSocket + REST)
- WebSocket 层基于 binance 官方 SDK 的 WebsocketClient,自动多路复用与断线重连
- REST 层使用 MainClient(Spot),实现 fetchKlines 自动分页补拉 + fetchMarkets 元数据解析
- 数据标准化:Ticker/Trade/OrderBook/Kline 类型定义与 Binance 原生格式互转
- 引入 RxJS Subject 作为统一事件流管道,按 eventType 运行时路由分发
- 重构 config/:YAML 驱动配置加载 + 零依赖运行时校验(fail-fast)
- 重构 db/:TypeORM DataSource 配置 + TimescaleDB K 线 Hypertable 实体
- 新增 utils/logger.ts:Pino 结构化日志(开发环境 pino-pretty 彩色输出)
- 新增 env.yaml 作为 TS/Python 共享的统一环境配置源
- 删除旧版手写 SQL schema 与散落配置文件,收敛到 TypeORM 实体管理
- 安装 rxjs@7.8.2 依赖
This commit is contained in:
Rekey
2026-06-08 01:24:48 +08:00
parent e91cad79e6
commit 85a0031a78
31 changed files with 4261 additions and 8757 deletions
-38
View File
@@ -1,38 +0,0 @@
# ============================================================
# Trade Data Module — 环境变量配置模板
# ============================================================
# 复制为 .env 并修改:
# cp .env.example .env
# ============================================================
# --- 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
+1348
View File
File diff suppressed because it is too large Load Diff
+985
View File
@@ -0,0 +1,985 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "trade-data",
"dependencies": {
"@timescaledb/typeorm": "^0.0.1",
"binance": "^3.5.9",
"ccxt": "^4.5.56",
"ioredis": "^5.11.1",
"pg": "^8.21.0",
"pino": "^10.3.1",
"rxjs": "^7.8.2",
"typeorm": "^1.0.0",
"yaml": "^2.9.0",
},
"devDependencies": {
"@types/bun": "^1.3.14",
"@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",
},
},
},
"packages": {
"@discoveryjs/json-ext": ["@discoveryjs/json-ext@0.6.3", "", {}, "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ=="],
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.0", "", { "os": "android", "cpu": "arm" }, "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.28.0", "", { "os": "android", "cpu": "x64" }, "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.0", "", { "os": "linux", "cpu": "x64" }, "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.0", "", { "os": "none", "cpu": "x64" }, "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
"@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="],
"@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
"@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.2", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A=="],
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
"@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
"@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="],
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
"@ioredis/commands": ["@ioredis/commands@1.10.0", "", {}, "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q=="],
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
"@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="],
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.3", "", { "os": "android", "cpu": "arm64" }, "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.3", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
"@sqltools/formatter": ["@sqltools/formatter@1.2.5", "", {}, "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@timescaledb/core": ["@timescaledb/core@0.0.1", "", { "dependencies": { "@timescaledb/schemas": "0.0.1", "@timescaledb/utils": "0.0.1" } }, "sha512-T+X8u6qjke6F5pZFSA+L2UJnXkOIKE1isg9LkwyRFgVJR91zmZuLONzf9I3c937czExc0enZapBLreMsJYv2Zg=="],
"@timescaledb/schemas": ["@timescaledb/schemas@0.0.1", "", { "dependencies": { "zod": "^3.24.1" } }, "sha512-DLwJzJkVbgpl43lclvnNYKOslMsGjZMF9KXFCa78tpjLA8ImHV949T5nqDpWvUawnQGDy+PNxoz7eKW2iyefeQ=="],
"@timescaledb/typeorm": ["@timescaledb/typeorm@0.0.1", "", { "dependencies": { "@timescaledb/core": "0.0.1", "@timescaledb/schemas": "0.0.1", "@timescaledb/utils": "0.0.1", "reflect-metadata": "^0.2.2", "typeorm": "^0.3.20" } }, "sha512-aAw9psQAloh/F2yJbLQe12nV25U63ClWFpI2Lkke7F8IYn2qOEb+OYsD1UAmn4U+dQS8YuOqCq32jAxE+DUasg=="],
"@timescaledb/utils": ["@timescaledb/utils@0.0.1", "", { "dependencies": { "@timescaledb/schemas": "0.0.1", "debug": "^4.4.0" } }, "sha512-4adkbhTmK7NAS5ukwlTy5cF9hR+SHC3xY6SPOiWPnpgro+/nIsNpGqFlvGRrAdjJHSTsDPUJ9nHEiyo+obbSpA=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="],
"@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
"@vitest/expect": ["@vitest/expect@4.1.8", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.8", "@vitest/utils": "4.1.8", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ=="],
"@vitest/mocker": ["@vitest/mocker@4.1.8", "", { "dependencies": { "@vitest/spy": "4.1.8", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw=="],
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.8", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA=="],
"@vitest/runner": ["@vitest/runner@4.1.8", "", { "dependencies": { "@vitest/utils": "4.1.8", "pathe": "^2.0.3" } }, "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg=="],
"@vitest/snapshot": ["@vitest/snapshot@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "@vitest/utils": "4.1.8", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ=="],
"@vitest/spy": ["@vitest/spy@4.1.8", "", {}, "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA=="],
"@vitest/utils": ["@vitest/utils@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg=="],
"@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="],
"@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="],
"@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="],
"@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="],
"@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="],
"@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="],
"@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="],
"@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="],
"@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="],
"@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="],
"@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="],
"@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="],
"@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="],
"@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="],
"@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="],
"@webpack-cli/configtest": ["@webpack-cli/configtest@2.1.1", "", { "peerDependencies": { "webpack": "5.x.x", "webpack-cli": "5.x.x" } }, "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw=="],
"@webpack-cli/info": ["@webpack-cli/info@2.0.2", "", { "peerDependencies": { "webpack": "5.x.x", "webpack-cli": "5.x.x" } }, "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A=="],
"@webpack-cli/serve": ["@webpack-cli/serve@2.0.5", "", { "peerDependencies": { "webpack": "5.x.x", "webpack-cli": "5.x.x" } }, "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ=="],
"@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="],
"@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="],
"abab": ["abab@2.0.6", "", {}, "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"acorn-import-phases": ["acorn-import-phases@1.0.4", "", { "peerDependencies": { "acorn": "^8.14.0" } }, "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"acorn-walk": ["acorn-walk@8.3.5", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw=="],
"agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
"ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"ansis": ["ansis@4.3.1", "", {}, "sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA=="],
"app-root-path": ["app-root-path@3.1.0", "", {}, "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA=="],
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
"axios": ["axios@1.17.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw=="],
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.34", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw=="],
"big.js": ["big.js@5.2.2", "", {}, "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ=="],
"binance": ["binance@3.5.9", "", { "dependencies": { "axios": "^1.13.2", "isomorphic-ws": "^4.0.1", "nanoid": "^3.3.11", "ws": "^7.4.0" }, "optionalDependencies": { "source-map-loader": "^2.0.2", "ts-loader": "^8.0.11", "webpack": "^5.102.1", "webpack-bundle-analyzer": "^5.1.1", "webpack-cli": "^5.1.4" } }, "sha512-Jg8w81dKIMpGjVgY3k2s0ltAegmN2kOI3GcBzvAGC7CDqUYpWElasfoSrc0fEffryjGQhgJxf0ubyO1wWhz+rA=="],
"brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"caniuse-lite": ["caniuse-lite@1.0.30001797", "", {}, "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w=="],
"ccxt": ["ccxt@4.5.56", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-4UhGBI0ektkLdBBq8vv9mVhBXoIciqaFaeL6KOhMsQMrEq3h3T8Gt1Z+8Wfx1AbUZI2aAkgqWU42pcLOPhFJrQ=="],
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="],
"cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
"clone-deep": ["clone-deep@4.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", "shallow-clone": "^3.0.0" } }, "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ=="],
"cluster-key-slot": ["cluster-key-slot@1.1.1", "", {}, "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"dayjs": ["dayjs@1.11.21", "", {}, "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.368", "", {}, "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw=="],
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"emojis-list": ["emojis-list@3.0.0", "", {}, "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="],
"enhanced-resolve": ["enhanced-resolve@4.5.0", "", { "dependencies": { "graceful-fs": "^4.1.2", "memory-fs": "^0.5.0", "tapable": "^1.0.0" } }, "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg=="],
"envinfo": ["envinfo@7.21.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow=="],
"errno": ["errno@0.1.8", "", { "dependencies": { "prr": "~1.0.1" }, "bin": { "errno": "cli.js" } }, "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="],
"es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@10.4.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw=="],
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
"eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
"fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"flat": ["flat@5.0.2", "", { "bin": { "flat": "cli.js" } }, "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ=="],
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
"follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="],
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
"html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
"https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"interpret": ["interpret@3.1.1", "", {}, "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ=="],
"ioredis": ["ioredis@5.11.1", "", { "dependencies": { "@ioredis/commands": "1.10.0", "cluster-key-slot": "1.1.1", "debug": "4.4.3", "denque": "2.1.0", "redis-errors": "1.2.0", "redis-parser": "3.0.0", "standard-as-callback": "2.1.0" } }, "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A=="],
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
"is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="],
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
"isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="],
"isomorphic-ws": ["isomorphic-ws@4.0.1", "", { "peerDependencies": { "ws": "*" } }, "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="],
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"loader-runner": ["loader-runner@4.3.2", "", {}, "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w=="],
"loader-utils": ["loader-utils@2.0.4", "", { "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^2.1.2" } }, "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"memory-fs": ["memory-fs@0.5.0", "", { "dependencies": { "errno": "^0.1.3", "readable-stream": "^2.0.1" } }, "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA=="],
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
"node-releases": ["node-releases@2.0.47", "", {}, "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og=="],
"obug": ["obug@2.1.2", "", {}, "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg=="],
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
"opener": ["opener@1.5.2", "", { "bin": { "opener": "bin/opener-bin.js" } }, "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"pg": ["pg@8.21.0", "", { "dependencies": { "pg-connection-string": "^2.13.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.14.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA=="],
"pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="],
"pg-connection-string": ["pg-connection-string@2.13.0", "", {}, "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig=="],
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
"pg-pool": ["pg-pool@3.14.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw=="],
"pg-protocol": ["pg-protocol@1.14.0", "", {}, "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA=="],
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"pino": ["pino@10.3.1", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg=="],
"pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="],
"pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="],
"pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
"process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
"proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
"prr": ["prr@1.0.1", "", {}, "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
"readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
"rechoir": ["rechoir@0.8.0", "", { "dependencies": { "resolve": "^1.20.0" } }, "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ=="],
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
"reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
"resolve-cwd": ["resolve-cwd@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg=="],
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
"rolldown": ["rolldown@1.0.3", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.3", "@rolldown/binding-darwin-arm64": "1.0.3", "@rolldown/binding-darwin-x64": "1.0.3", "@rolldown/binding-freebsd-x64": "1.0.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", "@rolldown/binding-linux-arm64-gnu": "1.0.3", "@rolldown/binding-linux-arm64-musl": "1.0.3", "@rolldown/binding-linux-ppc64-gnu": "1.0.3", "@rolldown/binding-linux-s390x-gnu": "1.0.3", "@rolldown/binding-linux-x64-gnu": "1.0.3", "@rolldown/binding-linux-x64-musl": "1.0.3", "@rolldown/binding-openharmony-arm64": "1.0.3", "@rolldown/binding-wasm32-wasi": "1.0.3", "@rolldown/binding-win32-arm64-msvc": "1.0.3", "@rolldown/binding-win32-x64-msvc": "1.0.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g=="],
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="],
"semver": ["semver@7.8.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ=="],
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
"sha.js": ["sha.js@2.4.12", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" } }, "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w=="],
"shallow-clone": ["shallow-clone@3.0.1", "", { "dependencies": { "kind-of": "^6.0.2" } }, "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
"sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-js": ["source-map-js@0.6.2", "", {}, "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug=="],
"source-map-loader": ["source-map-loader@2.0.2", "", { "dependencies": { "abab": "^2.0.5", "iconv-lite": "^0.6.2", "source-map-js": "^0.6.2" }, "peerDependencies": { "webpack": "^5.0.0" } }, "sha512-yIYkFOsKn+OdOirRJUPQpnZiMkF74raDVQjj5ni3SzbOiA57SabeX80R5zyMQAKpvKySA3Z4a85vFX3bvpC6KQ=="],
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"sql-highlight": ["sql-highlight@6.1.0", "", {}, "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA=="],
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
"std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="],
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
"terser": ["terser@5.48.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q=="],
"terser-webpack-plugin": ["terser-webpack-plugin@5.6.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ=="],
"thread-stream": ["thread-stream@4.2.0", "", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="],
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
"tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="],
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
"to-buffer": ["to-buffer@1.2.2", "", { "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", "typed-array-buffer": "^1.0.3" } }, "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
"ts-loader": ["ts-loader@8.4.0", "", { "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^4.0.0", "loader-utils": "^2.0.0", "micromatch": "^4.0.0", "semver": "^7.3.4" }, "peerDependencies": { "typescript": "*", "webpack": "*" } }, "sha512-6nFY3IZ2//mrPc+ImY3hNWx1vCHyEhl6V+wLmL4CZcm6g1CqX7UKrkc6y0i4FwcfOhxyMPCfaEvh20f4r9GNpw=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tsx": ["tsx@4.22.4", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
"typeorm": ["typeorm@1.0.0", "", { "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^4.2.0", "dayjs": "^1.11.20", "debug": "^4.4.3", "dedent": "^1.7.2", "reflect-metadata": "^0.2.2", "sql-highlight": "^6.1.0", "tinyglobby": "^0.2.16", "tslib": "^2.8.1", "yargs": "^18.0.0" }, "peerDependencies": { "@google-cloud/spanner": "^8.0.0", "@sap/hana-client": "^2.14.22", "better-sqlite3": "^12.0.0", "ioredis": "^5.0.4", "mongodb": "^7.0.0", "mssql": "^12.0.0", "mysql2": "^3.15.3", "oracledb": "^6.3.0", "pg": "^8.5.1", "pg-native": "^3.0.0", "pg-query-stream": "^4.0.0", "redis": "^5.0.0", "sql.js": "^1.4.0", "ts-node": "^10.9.2", "typeorm-aurora-data-api-driver": "^3.0.0" }, "optionalPeers": ["@google-cloud/spanner", "@sap/hana-client", "better-sqlite3", "ioredis", "mongodb", "mssql", "mysql2", "oracledb", "pg", "pg-native", "pg-query-stream", "redis", "sql.js", "ts-node", "typeorm-aurora-data-api-driver"], "bin": { "typeorm": "cli.js", "typeorm-ts-node-esm": "cli-ts-node-esm.js", "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js" } }, "sha512-2mSKNqucP8vo+xQLP59xlHUcqLvG6qajxA7q7tnhJgeZjTrA6lK/Ar7LRyiAxdXhyXmGbIPsArPmcUB9Xg+M7w=="],
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="],
"vite": ["vite@8.0.16", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw=="],
"vitest": ["vitest@4.1.8", "", { "dependencies": { "@vitest/expect": "4.1.8", "@vitest/mocker": "4.1.8", "@vitest/pretty-format": "4.1.8", "@vitest/runner": "4.1.8", "@vitest/snapshot": "4.1.8", "@vitest/spy": "4.1.8", "@vitest/utils": "4.1.8", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.8", "@vitest/browser-preview": "4.1.8", "@vitest/browser-webdriverio": "4.1.8", "@vitest/coverage-istanbul": "4.1.8", "@vitest/coverage-v8": "4.1.8", "@vitest/ui": "4.1.8", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig=="],
"watchpack": ["watchpack@2.5.1", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg=="],
"webpack": ["webpack@5.107.2", "", { "dependencies": { "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.16.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.22.0", "es-module-lexer": "^2.1.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "loader-runner": "^4.3.2", "mime-db": "^1.54.0", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.5.0", "watchpack": "^2.5.1", "webpack-sources": "^3.5.0" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ=="],
"webpack-bundle-analyzer": ["webpack-bundle-analyzer@5.3.0", "", { "dependencies": { "@discoveryjs/json-ext": "^0.6.3", "acorn": "^8.0.4", "acorn-walk": "^8.0.0", "commander": "^14.0.2", "escape-string-regexp": "^5.0.0", "html-escaper": "^3.0.3", "opener": "^1.5.2", "picocolors": "^1.0.0", "sirv": "^3.0.2", "ws": "^8.19.0" }, "bin": { "webpack-bundle-analyzer": "lib/bin/analyzer.js" } }, "sha512-PEhAoqiJ+47d0uLMx/+zo5XOvaU+Vk6N2ZLht7H3n09QLy/fhyvqGNwjdRUHJDgMN8crBR2ZwVHkIswT3Xuawg=="],
"webpack-cli": ["webpack-cli@5.1.4", "", { "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", "@webpack-cli/info": "^2.0.2", "@webpack-cli/serve": "^2.0.5", "colorette": "^2.0.14", "commander": "^10.0.1", "cross-spawn": "^7.0.3", "envinfo": "^7.7.3", "fastest-levenshtein": "^1.0.12", "import-local": "^3.0.2", "interpret": "^3.1.1", "rechoir": "^0.8.0", "webpack-merge": "^5.7.3" }, "peerDependencies": { "webpack": "5.x.x" }, "bin": { "webpack-cli": "bin/cli.js" } }, "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg=="],
"webpack-merge": ["webpack-merge@5.10.0", "", { "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", "wildcard": "^2.0.0" } }, "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA=="],
"webpack-sources": ["webpack-sources@3.5.0", "", {}, "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"which-typed-array": ["which-typed-array@1.1.22", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw=="],
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
"wildcard": ["wildcard@2.0.1", "", {}, "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"ws": ["ws@7.5.11", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA=="],
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="],
"yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
"@timescaledb/typeorm/typeorm": ["typeorm@0.3.30", "", { "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^4.2.0", "app-root-path": "^3.1.0", "buffer": "^6.0.3", "dayjs": "^1.11.20", "debug": "^4.4.3", "dedent": "^1.7.2", "dotenv": "^16.6.1", "glob": "^10.5.0", "reflect-metadata": "^0.2.2", "sha.js": "^2.4.12", "sql-highlight": "^6.1.0", "tslib": "^2.8.1", "uuid": "^11.1.1", "yargs": "^17.7.2" }, "peerDependencies": { "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@sap/hana-client": "^2.14.22", "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", "ioredis": "^5.0.4", "mongodb": "^5.8.0 || ^6.0.0", "mssql": "^9.1.1 || ^10.0.0 || ^11.0.0 || ^12.0.0", "mysql2": "^2.2.5 || ^3.0.1", "oracledb": "^6.3.0", "pg": "^8.5.1", "pg-native": "^3.0.0", "pg-query-stream": "^4.0.0", "redis": "^3.1.1 || ^4.0.0 || ^5.0.14", "sql.js": "^1.4.0", "sqlite3": "^5.0.3", "ts-node": "^10.7.0", "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" }, "optionalPeers": ["@google-cloud/spanner", "@sap/hana-client", "better-sqlite3", "ioredis", "mongodb", "mssql", "mysql2", "oracledb", "pg", "pg-native", "pg-query-stream", "redis", "sql.js", "sqlite3", "ts-node", "typeorm-aurora-data-api-driver"], "bin": { "typeorm": "cli.js", "typeorm-ts-node-esm": "cli-ts-node-esm.js", "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js" } }, "sha512-8T35PzjefOdqc2ZR9mwLQj0pUGp6lQhMbK2EvVMwJVJWlaoHm0v/Q6dThNOZkFchD+0yMg8gwjKM28ePiLSXSQ=="],
"ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
"ajv-keywords/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
"ccxt/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
"enhanced-resolve/tapable": ["tapable@1.1.3", "", {}, "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA=="],
"glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
"postcss/source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
"readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"schema-utils/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="],
"webpack/enhanced-resolve": ["enhanced-resolve@5.23.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA=="],
"webpack/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
"webpack-bundle-analyzer/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"webpack-bundle-analyzer/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
"webpack-cli/@discoveryjs/json-ext": ["@discoveryjs/json-ext@0.5.7", "", {}, "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw=="],
"webpack-cli/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="],
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"@timescaledb/typeorm/typeorm/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="],
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
"schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"webpack/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="],
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@timescaledb/typeorm/typeorm/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"@timescaledb/typeorm/typeorm/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"@timescaledb/typeorm/typeorm/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"@timescaledb/typeorm/typeorm/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@timescaledb/typeorm/typeorm/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"@timescaledb/typeorm/typeorm/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"@timescaledb/typeorm/typeorm/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"@timescaledb/typeorm/typeorm/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@timescaledb/typeorm/typeorm/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
}
}
-184
View File
@@ -1,184 +0,0 @@
// ============================================================
// config.ts — 中心化配置模块(带 Zod 运行时校验)
// ============================================================
// 职责:
// 1. 从 .env 文件加载环境变量(零依赖,手动解析)
// 2. 使用 EnvConfigSchema 校验并类型收窄
// 3. 导出按职责分组的强类型配置对象(pgsql / redis / batch / ws / logging / symbols
//
// 使用方式:
// import { pgsql, redis, batch, ws, logging, symbols } from "./config";
// const pool = new pg.Pool(pgsql);
// const redisClient = new Redis(redis.url);
// ============================================================
import { readFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { EnvConfigSchema, type EnvConfig } from "./db/validators";
// ============================================================
// 1. 加载 .env 文件(零依赖实现)
// ============================================================
/**
* 手动解析 .env 文件为 key-value 对。
* 规则:
* - 忽略空行和 # 开头的注释行
* - 首个 = 分割 key=value
* - 去除首尾空白(不处理引号,值原样保留)
* - 跳过不含 = 的行
*
* 不依赖 dotenv 包,保持依赖精简。
*/
function parseEnvFile(filePath: string): Record<string, string> {
const result: Record<string, string> = {};
try {
const content = readFileSync(filePath, "utf-8");
for (const line of content.split("\n")) {
const trimmed = line.trim();
// 跳过空行和注释
if (trimmed === "" || trimmed.startsWith("#")) {
continue;
}
const eqIdx = trimmed.indexOf("=");
if (eqIdx === -1) {
continue;
}
const key = trimmed.slice(0, eqIdx).trim();
const value = trimmed.slice(eqIdx + 1).trim();
if (key !== "") {
result[key] = value;
}
}
} catch {
// .env 文件不存在时不报错(生产环境变量由容器注入)
}
return result;
}
/**
* 将解析结果注入 process.env(已存在的变量不覆盖)。
* 这使得后续 Zod 的 `z.coerce` 可以从 process.env 读取。
*/
function loadEnvFile(): void {
// __dirname 在 ESM 中不可用,手动计算
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const envPath = resolve(__dirname, ".env");
const parsed = parseEnvFile(envPath);
for (const [key, value] of Object.entries(parsed)) {
// 不覆盖已由系统设置的环境变量(Docker / systemd 注入优先)
if (process.env[key] === undefined) {
process.env[key] = value;
}
}
}
// 模块加载时立即执行
loadEnvFile();
// ============================================================
// 2. Zod 校验 & 类型收窄
// ============================================================
/**
* 经 Zod 校验后的环境变量配置。
* 所有字段均有默认值兜底,即使 .env 缺失也能正常运行。
*/
const rawConfig: EnvConfig = EnvConfigSchema.parse(process.env);
// ============================================================
// 3. 按职责分组的导出配置对象
// ============================================================
/** PostgreSQL / TimescaleDB 连接配置 */
export const pgsql = {
host: rawConfig.DB_HOST,
port: rawConfig.DB_PORT,
database: rawConfig.DB_NAME,
user: rawConfig.DB_USER,
password: rawConfig.DB_PASSWORD,
/** pg.Pool 连接上限,避免连接数暴涨 */
max: 20,
/** 连接空闲超时(毫秒) */
idleTimeoutMillis: 30000,
/** 连接获取超时(毫秒) */
connectionTimeoutMillis: 5000,
} as const;
/** Redis 连接与发布配置 */
export const redis = {
/** Redis 连接 URLioredis 可直接使用) */
url: rawConfig.REDIS_URL,
/** 是否启用 Pub/Sub 发布行情数据(开发环境可关闭以节省资源) */
publishEnabled: rawConfig.REDIS_PUBLISH_ENABLED === "true",
/** 频道前缀,避免多环境 key 冲突 */
channelPrefix: "trade",
/** 重连策略:指数退避基数(毫秒) */
retryDelayBaseMs: 1000,
/** 最大重试次数 */
maxRetries: 10,
} as const;
/** K 线批量写入配置 */
export const batch = {
/** 缓冲区条数阈值(达到后自动刷新) */
size: rawConfig.BATCH_SIZE,
/** 最大缓冲时间(毫秒),超时后即使未达阈值也刷新 */
flushIntervalMs: rawConfig.FLUSH_INTERVAL_MS,
} as const;
/** WebSocket 连接配置(全局默认值,交易所级别可覆盖) */
export const ws = {
/** 断线重连延迟基数(毫秒),指数退避:基数 × 2^attempts */
reconnectDelayMs: rawConfig.WS_RECONNECT_DELAY_MS,
/** 心跳间隔(毫秒) */
pingIntervalMs: rawConfig.WS_PING_INTERVAL_MS,
/** 最大重连次数(超过后标记 error 状态) */
maxReconnectAttempts: rawConfig.WS_MAX_RECONNECT_ATTEMPTS,
} as const;
/** 日志配置 */
export const logging = {
/** 日志级别:trace / debug / info / warn / error / fatal */
level: rawConfig.LOG_LEVEL,
/** 运行环境(production 时 pino 输出 JSON 便于日志采集) */
nodeEnv: rawConfig.NODE_ENV,
/** 是否启用 pino-pretty(开发环境友好输出) */
pretty: rawConfig.NODE_ENV === "development",
} as const;
/** 默认订阅的交易对列表(逗号分隔 → string[]) */
export const symbols: string[] = rawConfig.SYMBOLS.split(",")
.map((s) => s.trim())
.filter(Boolean);
// ============================================================
// 4. 工具:运行时打印配置概要(不含敏感信息)
// ============================================================
/** 打印脱敏后的配置概要,便于启动排查 */
export function printConfigSummary(): void {
const summary = {
pgsql: {
host: pgsql.host,
port: pgsql.port,
database: pgsql.database,
user: pgsql.user,
password: "***",
},
redis: {
url: redis.url.replace(/\/\/.*@/, "//***@"), // 隐藏密码
publishEnabled: redis.publishEnabled,
},
batch,
ws,
logging,
symbols,
};
console.log("[config] 配置概要:", JSON.stringify(summary, null, 2));
}
+160
View File
@@ -0,0 +1,160 @@
// ============================================================
// config.ts — 中心化配置模块(YAML 驱动)
// ============================================================
// 职责:
// 1. 从项目根目录 env.yaml 加载配置
// 2. 使用 validateConfig() 校验并类型收窄
// 3. 导出按职责分组的强类型配置对象(pgsql / redis / logging
//
// 使用方式:
// import { pgsql, redis, logging } from "./config";
// const ds = new DataSource({ ...pgsql });
// const redisClient = new Redis(redis.url);
//
// 配置文件位置:<project_root>/env.yaml
// TypeScript / Python 模块共享同一份配置。
// ============================================================
import { readFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { parse as parseYaml } from "yaml";
import { validateConfig, type EnvConfig } from "./validators";
// ============================================================
// 1. 定位并读取 env.yaml
// ============================================================
/**
* 计算项目根目录的绝对路径。
* data/config.ts → data/ → <project_root>/
*
* 兼容 ESM(无 __dirname)和 Bun 运行时。
*/
function getProjectRoot(): string {
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// config/index.ts → config/ → data/ → <project_root>
return resolve(__dirname, "../..");
}
/**
* 从项目根目录读取 env.yaml 并解析为原始对象。
* 文件不存在时抛出明确错误,不做静默降级。
*/
function loadYamlConfig(): unknown {
const root = getProjectRoot();
const yamlPath = resolve(root, "env.yaml");
let content: string;
try {
content = readFileSync(yamlPath, "utf-8");
} catch {
throw new Error(
`[config] 无法读取配置文件: ${yamlPath}\n` +
`请确保项目根目录存在 env.yaml(可参考 data/.env.example 的结构)。`,
);
}
const parsed = parseYaml(content);
if (parsed === null || parsed === undefined) {
throw new Error(`[config] env.yaml 解析结果为空: ${yamlPath}`);
}
return parsed;
}
// ============================================================
// 2. 加载 & 校验
// ============================================================
/** 经校验后的 env.yaml 配置(强类型) */
const rawConfig: EnvConfig = (() => {
const raw = loadYamlConfig();
return validateConfig(raw);
})();
// ============================================================
// 3. 按职责分组的导出配置对象
// ============================================================
/**
* PostgreSQL / TimescaleDB 连接配置
*
* 连接池参数(max / idleTimeoutMillis / connectionTimeoutMillis
* 为硬编码常量,不在 env.yaml 中暴露,避免误调导致连接耗尽。
*/
export const pgsql = {
host: rawConfig.db.host,
port: rawConfig.db.port,
database: rawConfig.db.name,
user: rawConfig.db.user,
password: rawConfig.db.password,
/** pg.Pool 连接上限,避免连接数暴涨 */
max: 20,
/** 连接空闲超时(毫秒) */
idleTimeoutMillis: 30000,
/** 连接获取超时(毫秒) */
connectionTimeoutMillis: 5000,
} as const;
/**
* Redis 连接与发布配置
*
* channelPrefix / retryDelayBaseMs / maxRetries 为硬编码常量,
* 跨模块保持一致,不需要通过配置文件修改。
*/
export const redis = {
/** Redis 连接 URLioredis 可直接使用) */
url: rawConfig.redis.url,
/** 是否启用 Pub/Sub 发布行情数据(开发环境可关闭以节省资源) */
publishEnabled: rawConfig.redis.publish_enabled,
/** 频道前缀,避免多环境 key 冲突 */
channelPrefix: "trade",
/** 重连策略:指数退避基数(毫秒) */
retryDelayBaseMs: 1000,
/** 最大重试次数 */
maxRetries: 10,
} as const;
/**
* 日志配置
*
* pretty 由 NODE_ENV 自动推导,不在 env.yaml 中独立配置。
*/
export const logging = {
/** 日志级别:trace / debug / info / warn / error / fatal */
level: rawConfig.logging.level,
/** 运行环境(production 时 pino 输出 JSON 便于日志采集) */
nodeEnv: rawConfig.logging.node_env,
/** 是否启用 pino-pretty(开发环境友好输出) */
pretty: rawConfig.logging.node_env === "development",
} as const;
// ============================================================
// 4. 工具:运行时打印配置概要(不含敏感信息)
// ============================================================
/** 打印脱敏后的配置概要,便于启动排查 */
export function printConfigSummary(): void {
const summary = {
projectRoot: getProjectRoot(),
pgsql: {
host: pgsql.host,
port: pgsql.port,
database: pgsql.database,
user: pgsql.user,
password: "***",
},
redis: {
url: redis.url.replace(/\/\/.*@/, "//***@"), // 隐藏密码
publishEnabled: redis.publishEnabled,
},
logging: {
level: logging.level,
nodeEnv: logging.nodeEnv,
pretty: logging.pretty,
},
};
console.log("[config] 配置概要:", JSON.stringify(summary, null, 2));
}
+149
View File
@@ -0,0 +1,149 @@
// ============================================================
// validators.ts — env.yaml 配置类型定义与运行时校验
// ============================================================
// 职责:
// 1. 定义 env.yaml 的 TypeScript 接口
// 2. 提供运行时校验函数(零依赖,手动实现)
// 3. 为 config.ts 提供类型安全的配置读取
//
// 使用方式:
// import { validateConfig, type EnvConfig } from "./db/validators";
// const raw = validateConfig(parsedYaml);
// ============================================================
/** env.yaml 顶层结构 */
export interface EnvConfig {
db: DbConfig;
redis: RedisConfig;
logging: LoggingConfig;
}
export interface DbConfig {
host: string;
port: number;
name: string;
user: string;
password: string;
}
export interface RedisConfig {
url: string;
publish_enabled: boolean;
}
export interface LoggingConfig {
level: "trace" | "debug" | "info" | "warn" | "error" | "fatal";
node_env: "development" | "production" | "test";
}
// ============================================================
// 运行时校验(零依赖)
// ============================================================
const VALID_LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "fatal"] as const;
const VALID_NODE_ENVS = ["development", "production", "test"] as const;
/**
* 校验并返回类型安全的配置对象。
* 校验失败时抛出明确错误信息,遵循 fail-fast 原则。
*/
export function validateConfig(raw: unknown): EnvConfig {
if (typeof raw !== "object" || raw === null) {
throw new Error(`[config] env.yaml 顶层必须为 object,实际: ${typeof raw}`);
}
const obj = raw as Record<string, unknown>;
// --- db ---
const db = obj["db"];
if (typeof db !== "object" || db === null) {
throw new Error("[config] env.yaml 缺少 db 配置段");
}
const dbObj = db as Record<string, unknown>;
const dbHost = assertString(dbObj["host"], "db.host");
const dbPort = assertPort(dbObj["port"], "db.port");
const dbName = assertString(dbObj["name"], "db.name");
const dbUser = assertString(dbObj["user"], "db.user");
const dbPassword = assertString(dbObj["password"], "db.password");
// --- redis ---
const redis = obj["redis"];
if (typeof redis !== "object" || redis === null) {
throw new Error("[config] env.yaml 缺少 redis 配置段");
}
const redisObj = redis as Record<string, unknown>;
const redisUrl = assertString(redisObj["url"], "redis.url");
const redisPublishEnabled = assertBoolean(redisObj["publish_enabled"], "redis.publish_enabled");
// --- logging ---
const logging = obj["logging"];
if (typeof logging !== "object" || logging === null) {
throw new Error("[config] env.yaml 缺少 logging 配置段");
}
const logObj = logging as Record<string, unknown>;
const logLevel = assertEnum(logObj["level"], VALID_LOG_LEVELS, "logging.level");
const nodeEnv = assertEnum(logObj["node_env"], VALID_NODE_ENVS, "logging.node_env");
return {
db: {
host: dbHost,
port: dbPort,
name: dbName,
user: dbUser,
password: dbPassword,
},
redis: {
url: redisUrl,
publish_enabled: redisPublishEnabled,
},
logging: {
level: logLevel,
node_env: nodeEnv,
},
};
}
// ============================================================
// 辅助校验函数
// ============================================================
function assertString(value: unknown, path: string): string {
if (typeof value !== "string" || value.trim() === "") {
throw new Error(`[config] ${path} 必须为非空字符串,实际: ${JSON.stringify(value)}`);
}
return value;
}
function assertPort(value: unknown, path: string): number {
if (typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 65535) {
return value;
}
if (typeof value === "string" && /^\d+$/.test(value)) {
const n = parseInt(value, 10);
if (n > 0 && n <= 65535) return n;
}
throw new Error(`[config] ${path} 必须为有效端口号 (1-65535),实际: ${JSON.stringify(value)}`);
}
function assertBoolean(value: unknown, path: string): boolean {
if (typeof value === "boolean") return value;
if (value === "true" || value === "false") return value === "true";
throw new Error(`[config] ${path} 必须为 boolean,实际: ${JSON.stringify(value)}`);
}
function assertEnum<T extends readonly string[]>(
value: unknown,
allowed: T,
path: string,
): T[number] {
const s = String(value);
if ((allowed as readonly string[]).includes(s)) {
return s as T[number];
}
throw new Error(
`[config] ${path} 必须为 ${allowed.join(" | ")} 之一,实际: ${JSON.stringify(value)}`,
);
}
-530
View File
@@ -1,530 +0,0 @@
// ============================================================
// db/config-crud.ts — 配置表 CRUD 服务层
// ============================================================
// 职责:
// 1. 封装 monitored_symbols / exchange_config / app_config 三张配置表的增删改查
// 2. 所有方法通过 pg.Pool 执行参数化 SQL(防注入)
// 3. 返回类型与 types.ts 严格对应,调用方无需手动断言
// 4. 支持依赖注入:构造函数接收 Pool,便于单元测试 mock
//
// 使用方式:
// import { pool } from "./db";
// import { MonitoredSymbolsRepo, ExchangeConfigRepo, AppConfigRepo } from "./db/config-crud";
//
// const symbolsRepo = new MonitoredSymbolsRepo(pool);
// const all = await symbolsRepo.listAll();
// ============================================================
import type pg from "pg";
import type {
MonitoredSymbolRow,
MonitoredSymbolInsert,
MonitoredSymbolUpdate,
ExchangeConfigRow,
ExchangeConfigInsert,
AppConfigRow,
Exchange,
KlineInterval,
} from "./types";
import {
// monitored_symbols
queryAllMonitoredSymbols,
queryEnabledSymbols,
querySymbolsByExchange,
queryMonitoredSymbolById,
queryMonitoredSymbolByKey,
upsertMonitoredSymbol,
updateMonitoredSymbol,
disableMonitoredSymbol,
deleteMonitoredSymbol,
deleteMonitoredSymbolByKey,
// exchange_config
queryAllExchangeConfigs,
queryEnabledExchanges,
queryExchangeConfig,
queryExchangeConfigById,
upsertExchangeConfig,
updateExchangeConfig,
deleteExchangeConfig,
deleteExchangeConfigByExchange,
// app_config
queryAllAppConfig,
queryAppConfig,
queryAppConfigById,
upsertAppConfig,
updateAppConfig,
deleteAppConfig,
deleteAppConfigById,
} from "./queries";
// ============================================================
// 工具类型:将 Promise 解包的一行或 null
// ============================================================
/** pg query 返回的第一行,不存在则为 null */
type FirstRow<T> = T | null;
// ============================================================
// MonitoredSymbolsRepo — 监控交易对配置 CRUD
// ============================================================
export class MonitoredSymbolsRepo {
constructor(private readonly pool: pg.Pool) {}
// ----------------------------------------------------------
// CREATE / UPSERT
// ----------------------------------------------------------
/**
* 新增或更新监控标的。
* 唯一键冲突时更新 enabled/priority/label/notes 并刷新 updated_at。
*
* @returns 插入或更新后的完整行
*/
async upsert(
insert: MonitoredSymbolInsert,
): Promise<MonitoredSymbolRow> {
const { rows } = await this.pool.query<MonitoredSymbolRow>(
upsertMonitoredSymbol,
[
insert.exchange,
insert.symbol,
insert.interval,
insert.enabled ?? true,
insert.priority ?? 0,
insert.label ?? null,
insert.notes ?? null,
],
);
return rows[0]!;
}
// ----------------------------------------------------------
// READ — 单条
// ----------------------------------------------------------
/** 按主键 ID 查询 */
async findById(id: number): Promise<FirstRow<MonitoredSymbolRow>> {
const { rows } = await this.pool.query<MonitoredSymbolRow>(
queryMonitoredSymbolById,
[id],
);
return rows[0] ?? null;
}
/**
* 按唯一业务键 (exchange, symbol, interval) 查询。
* 这是最常用的精确查找方式。
*/
async findByKey(
exchange: Exchange,
symbol: string,
interval: KlineInterval,
): Promise<FirstRow<MonitoredSymbolRow>> {
const { rows } = await this.pool.query<MonitoredSymbolRow>(
queryMonitoredSymbolByKey,
[exchange, symbol, interval],
);
return rows[0] ?? null;
}
// ----------------------------------------------------------
// READ — 列表
// ----------------------------------------------------------
/** 查询所有监控标的(含已禁用),按优先级降序 */
async listAll(): Promise<MonitoredSymbolRow[]> {
const { rows } = await this.pool.query<MonitoredSymbolRow>(
queryAllMonitoredSymbols,
);
return rows;
}
/** 查询所有启用的监控标的(采集服务启动时调用) */
async listEnabled(): Promise<MonitoredSymbolRow[]> {
const { rows } = await this.pool.query<MonitoredSymbolRow>(
queryEnabledSymbols,
);
return rows;
}
/** 查询指定交易所下所有启用的监控标的 */
async listByExchange(exchange: Exchange): Promise<MonitoredSymbolRow[]> {
const { rows } = await this.pool.query<MonitoredSymbolRow>(
querySymbolsByExchange,
[exchange],
);
return rows;
}
// ----------------------------------------------------------
// UPDATE
// ----------------------------------------------------------
/**
* 按 ID 部分更新监控标的。
* 仅更新传入的非 undefined 字段(COALESCE 语义)。
*
* @returns 更新后的完整行;ID 不存在则返回 null
*/
async update(
id: number,
patch: MonitoredSymbolUpdate,
): Promise<FirstRow<MonitoredSymbolRow>> {
const { rows } = await this.pool.query<MonitoredSymbolRow>(
updateMonitoredSymbol,
[
id,
patch.enabled ?? null,
patch.priority ?? null,
patch.label ?? null,
patch.notes ?? null,
],
);
return rows[0] ?? null;
}
/**
* 禁用指定监控标的(软删除)。
* 不会删除记录,仅将 enabled 设为 FALSE。
*/
async disable(
exchange: Exchange,
symbol: string,
interval: KlineInterval,
): Promise<Pick<MonitoredSymbolRow, "id" | "exchange" | "symbol" | "interval"> | null> {
const { rows } = await this.pool.query<
Pick<MonitoredSymbolRow, "id" | "exchange" | "symbol" | "interval">
>(disableMonitoredSymbol, [exchange, symbol, interval]);
return rows[0] ?? null;
}
// ----------------------------------------------------------
// DELETE(硬删除)
// ----------------------------------------------------------
/** 按 ID 硬删除。返回被删除的 id,不存在则返回 null */
async deleteById(id: number): Promise<number | null> {
const { rows } = await this.pool.query<{ id: number }>(
deleteMonitoredSymbol,
[id],
);
return rows[0]?.id ?? null;
}
/** 按唯一键硬删除。返回被删除的 id,不存在则返回 null */
async deleteByKey(
exchange: Exchange,
symbol: string,
interval: KlineInterval,
): Promise<number | null> {
const { rows } = await this.pool.query<{ id: number }>(
deleteMonitoredSymbolByKey,
[exchange, symbol, interval],
);
return rows[0]?.id ?? null;
}
}
// ============================================================
// ExchangeConfigRepo — 交易所连接配置 CRUD
// ============================================================
export class ExchangeConfigRepo {
constructor(private readonly pool: pg.Pool) {}
// ----------------------------------------------------------
// CREATE / UPSERT
// ----------------------------------------------------------
/**
* 新增或更新交易所配置。
* 唯一键冲突时更新所有连接参数并刷新 updated_at。
*
* @returns 插入或更新后的完整行
*/
async upsert(
insert: ExchangeConfigInsert,
): Promise<ExchangeConfigRow> {
const { rows } = await this.pool.query<ExchangeConfigRow>(
upsertExchangeConfig,
[
insert.exchange,
insert.rest_url ?? null,
insert.ws_url ?? null,
insert.ws_ping_interval_ms ?? 30000,
insert.rate_limit_per_sec ?? 20,
insert.max_reconnect_attempts ?? 10,
insert.reconnect_delay_ms ?? 3000,
insert.enabled ?? true,
insert.notes ?? null,
],
);
return rows[0]!;
}
// ----------------------------------------------------------
// READ — 单条
// ----------------------------------------------------------
/** 按主键 ID 查询 */
async findById(id: number): Promise<FirstRow<ExchangeConfigRow>> {
const { rows } = await this.pool.query<ExchangeConfigRow>(
queryExchangeConfigById,
[id],
);
return rows[0] ?? null;
}
/** 按交易所标识查询(如 "binance" */
async findByExchange(
exchange: Exchange,
): Promise<FirstRow<ExchangeConfigRow>> {
const { rows } = await this.pool.query<ExchangeConfigRow>(
queryExchangeConfig,
[exchange],
);
return rows[0] ?? null;
}
// ----------------------------------------------------------
// READ — 列表
// ----------------------------------------------------------
/** 查询所有交易所配置(含已禁用) */
async listAll(): Promise<ExchangeConfigRow[]> {
const { rows } = await this.pool.query<ExchangeConfigRow>(
queryAllExchangeConfigs,
);
return rows;
}
/** 查询所有启用的交易所配置 */
async listEnabled(): Promise<ExchangeConfigRow[]> {
const { rows } = await this.pool.query<ExchangeConfigRow>(
queryEnabledExchanges,
);
return rows;
}
// ----------------------------------------------------------
// UPDATE
// ----------------------------------------------------------
/**
* 按 ID 部分更新交易所配置。
* 仅更新传入的非 undefined 字段(COALESCE 语义)。
*
* ⚠️ 风险提示:修改限频参数(rate_limit_per_sec)可能触发交易所封禁 IP。
* 务必确认目标交易所的官方限频规则后再调整。
*
* @returns 更新后的完整行;ID 不存在则返回 null
*/
async update(
id: number,
patch: Partial<Omit<ExchangeConfigRow, "id" | "created_at" | "updated_at">>,
): Promise<FirstRow<ExchangeConfigRow>> {
const { rows } = await this.pool.query<ExchangeConfigRow>(
updateExchangeConfig,
[
id,
patch.rest_url ?? null,
patch.ws_url ?? null,
patch.ws_ping_interval_ms ?? null,
patch.rate_limit_per_sec ?? null,
patch.max_reconnect_attempts ?? null,
patch.reconnect_delay_ms ?? null,
patch.enabled ?? null,
patch.notes ?? null,
],
);
return rows[0] ?? null;
}
// ----------------------------------------------------------
// DELETE(硬删除)
// ----------------------------------------------------------
/** 按 ID 硬删除。返回被删除的 id,不存在则返回 null */
async deleteById(id: number): Promise<number | null> {
const { rows } = await this.pool.query<{ id: number }>(
deleteExchangeConfig,
[id],
);
return rows[0]?.id ?? null;
}
/** 按交易所标识硬删除。返回被删除的 id,不存在则返回 null */
async deleteByExchange(exchange: Exchange): Promise<number | null> {
const { rows } = await this.pool.query<{ id: number }>(
deleteExchangeConfigByExchange,
[exchange],
);
return rows[0]?.id ?? null;
}
}
// ============================================================
// AppConfigRepo — 全局应用配置(KVCRUD
// ============================================================
export class AppConfigRepo {
constructor(private readonly pool: pg.Pool) {}
// ----------------------------------------------------------
// CREATE / UPSERT
// ----------------------------------------------------------
/**
* 设置一个配置项(新增或更新)。
*
* @param key — 配置键
* @param value — 配置值(字符串,消费方自行解析类型)
* @param description — 可选说明
* @returns 插入或更新后的完整行
*/
async set(
key: string,
value: string,
description?: string | null,
): Promise<AppConfigRow> {
const { rows } = await this.pool.query<AppConfigRow>(upsertAppConfig, [
key,
value,
description ?? null,
]);
return rows[0]!;
}
// ----------------------------------------------------------
// READ — 单条
// ----------------------------------------------------------
/** 按主键 ID 查询 */
async findById(id: number): Promise<FirstRow<AppConfigRow>> {
const { rows } = await this.pool.query<AppConfigRow>(
queryAppConfigById,
[id],
);
return rows[0] ?? null;
}
/**
* 按 key 查询配置项。
*
* @returns 配置行;不存在则返回 null
*/
async get(key: string): Promise<FirstRow<AppConfigRow>> {
const { rows } = await this.pool.query<AppConfigRow>(queryAppConfig, [key]);
return rows[0] ?? null;
}
/**
* 按 key 获取配置值(字符串)。
* 便捷方法——等价于 (await get(key))?.value ?? defaultValue。
*
* @param key — 配置键
* @param defaultValue — 默认值(key 不存在时返回)
*/
async getValue(key: string, defaultValue = ""): Promise<string> {
const row = await this.get(key);
return row?.value ?? defaultValue;
}
/**
* 按 key 获取配置值并解析为整数。
* 解析失败时返回 defaultValue。
*/
async getIntValue(key: string, defaultValue = 0): Promise<number> {
const raw = await this.getValue(key, String(defaultValue));
const parsed = parseInt(raw, 10);
return Number.isNaN(parsed) ? defaultValue : parsed;
}
/**
* 按 key 获取配置值并解析为布尔。
* 规则:'true' / '1' → true,其余 → false。
*/
async getBoolValue(key: string, defaultValue = false): Promise<boolean> {
const raw = await this.getValue(key, String(defaultValue));
return raw === "true" || raw === "1";
}
// ----------------------------------------------------------
// READ — 列表
// ----------------------------------------------------------
/** 查询所有应用配置 */
async listAll(): Promise<AppConfigRow[]> {
const { rows } = await this.pool.query<AppConfigRow>(queryAllAppConfig);
return rows;
}
/**
* 批量获取多个 key 的值。
* 一次性查询全表后过滤,避免 N+1 问题。
*
* @param keys — 需要获取的 key 列表
* @returns Map<key, value>
*/
async getBatch(keys: string[]): Promise<Map<string, string>> {
const all = await this.listAll();
const map = new Map<string, string>();
const keySet = new Set(keys);
for (const row of all) {
if (keySet.has(row.key)) {
map.set(row.key, row.value);
}
}
// 保证未找到的 key 也有默认值 ""
for (const k of keys) {
if (!map.has(k)) {
map.set(k, "");
}
}
return map;
}
// ----------------------------------------------------------
// UPDATE
// ----------------------------------------------------------
/**
* 按 ID 部分更新应用配置。
*
* @returns 更新后的完整行;ID 不存在则返回 null
*/
async update(
id: number,
value?: string,
description?: string | null,
): Promise<FirstRow<AppConfigRow>> {
const { rows } = await this.pool.query<AppConfigRow>(updateAppConfig, [
id,
value ?? null,
description ?? null,
]);
return rows[0] ?? null;
}
// ----------------------------------------------------------
// DELETE
// ----------------------------------------------------------
/** 按 key 删除配置。返回被删除的 id,不存在则返回 null */
async deleteByKey(key: string): Promise<number | null> {
const { rows } = await this.pool.query<{ id: number }>(deleteAppConfig, [
key,
]);
return rows[0]?.id ?? null;
}
/** 按 ID 删除配置。返回被删除的 id,不存在则返回 null */
async deleteById(id: number): Promise<number | null> {
const { rows } = await this.pool.query<{ id: number }>(
deleteAppConfigById,
[id],
);
return rows[0]?.id ?? null;
}
}
+29
View File
@@ -0,0 +1,29 @@
import { DataSource } from "typeorm";
import { pgsql } from "../config";
import * as entities from "./entities";
export const AppDataSource = new DataSource({
type: "postgres",
host: pgsql.host,
port: pgsql.port,
database: pgsql.database,
username: pgsql.user,
password: pgsql.password,
// 实体注册:关系实体通过 entities/index.ts 统一导出
// TimescaleDB K 线实体后续通过 @timescaledb/typeorm 装饰器注册
entities: [
...Object.values(entities),
],
// 生产环境禁用 synchronize,使用 Migration
synchronize: true,
migrations: [__dirname + "/migrations/*.{ts,js}"],
// 连接池
extra: {
max: pgsql.max, // 最大连接数 20
idleTimeoutMillis: pgsql.idleTimeoutMillis, // 空闲超时 30s
connectionTimeoutMillis: pgsql.connectionTimeoutMillis, // 连接超时 5s
},
logging: process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"],
});
await AppDataSource.initialize();
+32
View File
@@ -0,0 +1,32 @@
// ============================================================
// common.entity.ts — 实体公共基类
// ============================================================
// 所有关系实体(TypeORM 管理域)继承此类,统一:
// - id: UUID 主键
// - created_at: 记录创建时间(自动填充)
// - updated_at: 最后更新时间(自动填充)
//
// TimescaleDB K 线实体(@timescaledb/typeorm 管理域)不继承此类,
// 因为它们需要 @TimeColumn() 等特定装饰器。
// ============================================================
import {
BaseEntity,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
} from "typeorm";
export abstract class CommonBaseEntity extends BaseEntity {
/** UUID 主键(非自增整数,便于分布式场景) */
@PrimaryGeneratedColumn("uuid")
id!: string;
/** 记录创建时间 */
@CreateDateColumn({ type: "timestamptz", name: "created_at" })
createdAt!: Date;
/** 最后更新时间(每次 UPDATE 自动刷新) */
@UpdateDateColumn({ type: "timestamptz", name: "updated_at" })
updatedAt!: Date;
}
+41
View File
@@ -0,0 +1,41 @@
// ============================================================
// exchange.entity.ts — 交易所配置实体
// ============================================================
// 映射到 PostgreSQL exchanges 表,存储已接入的交易所元信息。
// 由 TypeORM 管理(关系数据),不与 TimescaleDB 耦合。
//
// 继承 CommonBaseEntityid (UUID) / created_at / updated_at
// ============================================================
import {
Entity,
Column,
OneToMany,
} from "typeorm";
import { CommonBaseEntity } from "./common.entity";
@Entity("exchanges")
export class Exchange extends CommonBaseEntity {
/** 交易所唯一标识(如 binance / okx / bybit */
@Column("varchar", { length: 50, unique: true })
name!: string;
/** 交易所显示名称(如 Binance / OKX / Bybit */
@Column("varchar", { length: 100 })
label!: string;
/** 是否启用该交易所的数据采集 */
@Column("boolean", { default: true })
enabled!: boolean;
/** 交易所特定配置(JSON:费率、最小下单量、API 限频等) */
@Column("jsonb", { nullable: true })
config?: Record<string, unknown>;
/**
* 该交易所下的所有交易对。
* 使用字符串引用避免循环依赖(TradingPair 也引用 Exchange)。
*/
@OneToMany("TradingPair", "exchange")
tradingPairs!: unknown[];
}
+12
View File
@@ -0,0 +1,12 @@
// ============================================================
// entities/index.ts — 实体统一导出
// ============================================================
// TypeORM DataSource 通过 import * as entities from "../entities"
// 自动注册所有实体,无需手动逐个添加。
// ============================================================
export { CommonBaseEntity } from "./common.entity";
export { Exchange } from "./exchange.entity";
export { TradingPair } from "./trading-pair.entity";
export { Kline } from "./kline.entity";
export type { KlineInterval } from "./kline.entity";
+138
View File
@@ -0,0 +1,138 @@
// ============================================================
// kline.entity.ts — TimescaleDB K 线 Hypertable 实体
// ============================================================
// 映射到 PostgreSQL klines 表(TimescaleDB hypertable)。
// 不继承 CommonBaseEntity — 使用 @timescaledb/typeorm 的
// @Hypertable / @TimeColumn 装饰器管理 TimescaleDB 特性。
//
// 关键 TimescaleDB 特性(由 @Hypertable 装饰器自动配置):
// - 自动按 time 列做时间分区(by_range
// - 列式压缩(compress),7 天后自动执行
// - 通过 ContinuousAggregate 生成高周期 K 线视图
//
// 注意:@timescaledb/typeorm v0.0.1 为实验版本,
// 不支持空间分区(partitioning_column)。
// 若需要空间分区,可通过 db/init-db/ 下的 SQL 脚本手动添加。
// ============================================================
import { Hypertable, TimeColumn } from "@timescaledb/typeorm";
import {
Entity,
PrimaryColumn,
Column,
Index,
CreateDateColumn,
UpdateDateColumn,
} from "typeorm";
/** K 线周期枚举 */
export type KlineInterval =
| "1m"
| "5m"
| "15m"
| "30m"
| "1h"
| "4h"
| "1d"
| "1w";
/**
* 1 分钟 K 线 Hypertable
*
* 存储交易所推送的 OHLCV 数据。写入使用 UPSERT
* ON CONFLICT DO UPDATE),已存在的 K 线只更新
* high/low/close/volume 增量。
*
* 高周期 K 线(5m+)通过 TimescaleDB 连续聚合视图
* 从 1m 表自动生成,无需单独建表。
*/
@Hypertable({
compression: {
compress: true,
compress_orderby: "time DESC",
compress_segmentby: "exchange, symbol, interval",
policy: {
schedule_interval: "365 days", // 365 天后自动压缩
},
},
})
@Index(["exchange", "symbol", "interval", "time"], { unique: true })
@Entity("klines")
export class Kline {
/** K 线开盘时间(UTC)— @timescaledb/typeorm 自动标记为时间分区列 */
@TimeColumn()
@PrimaryColumn("timestamptz")
time!: Date;
/** 交易所标识(binance / okx / bybit */
@Column("text")
exchange!: string;
/** 交易对符号(如 BTCUSDT */
@Column("text")
symbol!: string;
/** K 线周期(1m */
@Column("text")
interval!: KlineInterval;
// ============================================================
// OHLCV 价格数据(NUMERIC(20,8) 精度,与交易所对齐)
// ============================================================
/** 开盘价 */
@Column("numeric", { precision: 20, scale: 8 })
open!: number;
/** 最高价 */
@Column("numeric", { precision: 20, scale: 8 })
high!: number;
/** 最低价 */
@Column("numeric", { precision: 20, scale: 8 })
low!: number;
/** 收盘价 */
@Column("numeric", { precision: 20, scale: 8 })
close!: number;
/** 成交量(base 币种) */
@Column("numeric", { precision: 20, scale: 8 })
volume!: number;
// ============================================================
// 扩展字段(Binance 等交易所提供)
// ============================================================
/** 成交额(quote 币种) */
@Column("numeric", { precision: 20, scale: 8, nullable: true })
quote_volume?: number;
/** 主动买入成交量(base 币种) */
@Column("numeric", { precision: 20, scale: 8, nullable: true })
taker_buy_base_vol?: number;
/** 主动买入成交额(quote 币种) */
@Column("numeric", { precision: 20, scale: 8, nullable: true })
taker_buy_quote_vol?: number;
/** 成交笔数 */
@Column("integer", { nullable: true })
trade_count?: number;
/** K 线是否已关闭(true = 该周期 K 线不再变化) */
@Column("boolean", { default: true })
is_closed!: boolean;
// ============================================================
// 审计字段
// ============================================================
/** 记录首次写入时间 */
@CreateDateColumn({ type: "timestamptz", name: "created_at" })
createdAt!: Date;
/** 记录最后更新时间 */
@UpdateDateColumn({ type: "timestamptz", name: "updated_at" })
updatedAt!: Date;
}
+105
View File
@@ -0,0 +1,105 @@
// ============================================================
// trading-pair.entity.ts — 交易对配置实体
// ============================================================
// 映射到 PostgreSQL trading_pairs 表,存储各交易所的交易对元信息。
// 数据模块启动时从该表读取 active=true 的交易对列表,
// 决定 WebSocket 订阅范围和 K 线合成范围。
//
// 继承 CommonBaseEntityid (UUID) / created_at / updated_at
//
// 与 TimescaleDB klines 表的关系:
// klines.symbol → trading_pairs.symbol(逻辑外键,不做 DB 级约束)
// klines.exchange → exchanges.name(逻辑外键)
// ============================================================
import {
Entity,
Column,
ManyToOne,
JoinColumn,
Index,
} from "typeorm";
import { Exchange } from "./exchange.entity";
import { CommonBaseEntity } from "./common.entity";
@Entity("trading_pairs")
@Index(["exchange", "symbol"], { unique: true }) // 同一交易所下 symbol 唯一
@Index(["active"]) // 按激活状态快速筛选
export class TradingPair extends CommonBaseEntity {
/** 所属交易所 */
@ManyToOne(() => Exchange, { nullable: false })
@JoinColumn({ name: "exchange_id" })
exchange!: Exchange;
/** 交易对符号(如 BTCUSDT / ETHUSDT */
@Column("varchar", { length: 20 })
symbol!: string;
/** 基础币种(如 BTC */
@Column("varchar", { length: 10 })
base_asset!: string;
/** 计价币种(如 USDT */
@Column("varchar", { length: 10 })
quote_asset!: string;
/** 价格精度(小数位数) */
@Column("integer", { default: 10 })
price_precision!: number;
/** 数量精度(小数位数) */
@Column("integer", { default: 10 })
quantity_precision!: number;
/** 最小下单量 */
@Column("numeric", { precision: 32, scale: 8, nullable: true })
min_qty?: number;
/** 下单步长(数量增量) */
@Column("numeric", { precision: 32, scale: 8, nullable: true })
step_size?: number;
/** 最小名义价值(USDT */
@Column("numeric", { precision: 32, scale: 8, nullable: true })
min_notional?: number;
/** 是否激活数据订阅(false 时不采集该交易对行情) */
@Column("boolean", { default: true })
active!: boolean;
/** 是否启用 K 线合成(false 时仅采集原始行情,不合成) */
@Column("boolean", { default: true })
kline_synthesis_enabled!: boolean;
/** K 线合成周期列表(逗号分隔,如 "1m,5m,15m,1h,4h,1d" */
@Column("varchar", { length: 100, default: "1m,5m,15m,1h,4h,1d" })
kline_intervals!: string;
/**
* 历史 K 线最后补全时间(UTC)。
* 记录最近一次 REST 补拉 K 线的结束时间戳,
* 下次启动时从此时间点继续补全,避免重复拉取。
*
* 默认值为 Unix 01970-01-01T00:00:00.000Z),
* 新交易对从 epoch 起始时间开始全量补拉,
* 补全后更新为实际拉取到的最后时间。
*/
@Column("timestamptz", { default: () => "to_timestamp(0)" })
last_backfill_time!: Date;
/** 备注 */
@Column("text", { nullable: true })
notes?: string;
// ============================================================
// 工具方法
// ============================================================
/** 解析 kline_intervals 为周期数组 */
getIntervals(): string[] {
return this.kline_intervals
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}
}
-121
View File
@@ -1,121 +0,0 @@
// ============================================================
// db/index.ts — 统一导出
// ============================================================
// 使用方式:
// import { KlineRow, KlineRawSchema, bulkUpsertKlines } from "./db";
// import { MonitoredSymbolsRepo } from "./db";
// ============================================================
// 类型定义
export type {
Exchange,
KlineInterval,
LogLevel,
KlineRow,
KlineInsert,
AggregatedKlineRow,
MonitoredSymbolRow,
MonitoredSymbolInsert,
MonitoredSymbolUpdate,
ExchangeConfigRow,
ExchangeConfigInsert,
AppConfigRow,
AppConfigKey,
StreamKey,
StreamSubscription,
} from "./types";
// Zod 运行时校验
export {
ExchangeSchema,
KlineIntervalSchema,
LogLevelSchema,
SymbolSchema,
NumericStringSchema,
KlineRawSchema,
KlineBatchSchema,
MonitoredSymbolInsertSchema,
MonitoredSymbolUpdateSchema,
ExchangeConfigInsertSchema,
StreamKeySchema,
EnvConfigSchema,
} from "./validators";
export type {
KlineRaw,
KlineBatch,
MonitoredSymbolInsert as MonitoredSymbolInsertValidated,
MonitoredSymbolUpdate as MonitoredSymbolUpdateValidated,
ExchangeConfigInsert as ExchangeConfigInsertValidated,
StreamKey as StreamKeyValidated,
EnvConfig,
} from "./validators";
// 参数化 SQL 查询
export {
bulkUpsertKlines,
packBulkKlines,
queryKlinesRange,
queryKlinesLatest,
queryAggregatedKlines,
// monitored_symbols
queryAllMonitoredSymbols,
queryEnabledSymbols,
querySymbolsByExchange,
queryMonitoredSymbolById,
queryMonitoredSymbolByKey,
upsertMonitoredSymbol,
updateMonitoredSymbol,
disableMonitoredSymbol,
deleteMonitoredSymbol,
deleteMonitoredSymbolByKey,
// exchange_config
queryAllExchangeConfigs,
queryEnabledExchanges,
queryExchangeConfig,
queryExchangeConfigById,
upsertExchangeConfig,
updateExchangeConfig,
deleteExchangeConfig,
deleteExchangeConfigByExchange,
// app_config
queryAllAppConfig,
queryAppConfig,
queryAppConfigById,
upsertAppConfig,
updateAppConfig,
deleteAppConfig,
deleteAppConfigById,
// 复合查询
queryStreamSubscriptions,
} from "./queries";
export type { BulkKlineParams } from "./queries";
// ============================================================
// Config CRUD 服务层(推荐使用)
// ============================================================
export {
MonitoredSymbolsRepo,
ExchangeConfigRepo,
AppConfigRepo,
} from "./config-crud";
// ============================================================
// PostgreSQL 连接池 & 工具
// ============================================================
export {
pool,
healthCheck,
timescaleVersion,
withTransaction,
closePool,
registerShutdownHandlers,
initSchemaFromFile,
initSchema,
isSchemaInitialized,
} from "./pg";
export { default as defaultPool } from "./pg";
-364
View File
@@ -1,364 +0,0 @@
// ============================================================
// db/pg.ts — PostgreSQL / TimescaleDB 连接池管理
// ============================================================
// 职责:
// 1. 基于 config.ts 的 pgsql 配置创建 pg.Pool 单例
// 2. 连接生命周期事件监听(connect / acquire / remove / error
// 3. 提供健康检查(healthCheck
// 4. 提供事务辅助函数(withTransaction
// 5. 进程退出时优雅关闭(SIGTERM / SIGINT
//
// 使用方式:
// import { pool } from "./db"; // 通过 index.ts 统一导出
// import { healthCheck } from "./db";
// const { rows } = await pool.query("SELECT NOW()");
// ============================================================
import pg from "pg";
import { readFileSync, readdirSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { pgsql } from "../config";
// ============================================================
// 1. 连接池创建(单例)
// ============================================================
/**
* pg.Pool 单例。
* 配置来源于 config.ts → pgsql,已包含连接数上限、超时等参数。
*
* pg.Pool 内部使用懒连接——首次 query 时才建立连接,
* 因此模块加载时不会立即连接数据库。
*/
export const pool = new pg.Pool({
host: pgsql.host,
port: pgsql.port,
database: pgsql.database,
user: pgsql.user,
password: pgsql.password,
max: pgsql.max,
idleTimeoutMillis: pgsql.idleTimeoutMillis,
connectionTimeoutMillis: pgsql.connectionTimeoutMillis,
// TimescaleDB 特有的超时设置:分析查询可能较慢
statement_timeout: 30000, // 单条 SQL 最大执行 30s
// application_name 便于在 pg_stat_activity 中识别
application_name: "trade-data",
});
// ============================================================
// 2. 连接池事件监听(可观测性)
// ============================================================
/** 新客户端连接建立 */
pool.on("connect", (client) => {
// 为每个连接设置 TimescaleDB 优化参数
// 跳过 WAL 日志可加速批量写入(仅在可接受丢失最近几秒数据的场景)
// client.query("SET timescaledb.enable_skip_scan = ON");
console.log(`[pg] 新连接建立 (total: ${pool.totalCount}, idle: ${pool.idleCount})`);
});
/** 从池中获取连接 */
pool.on("acquire", () => {
// 连接池耗尽时会频繁触发,可在此记录高负载信号
});
/** 连接归还池 */
pool.on("remove", () => {
console.log(`[pg] 连接关闭 (total: ${pool.totalCount}, idle: ${pool.idleCount})`);
});
/**
* 空闲客户端出错(如网络中断、PG 重启)。
* pg.Pool 会自动移除问题连接并创建新连接,此处仅记录日志。
*/
pool.on("error", (err: Error) => {
console.error(`[pg] 连接池错误: ${err.message}`);
// 不退出进程——连接池会自动恢复
});
// ============================================================
// 3. 健康检查
// ============================================================
/**
* 数据库连通性检查。
* 执行轻量查询 `SELECT 1`,超时 5 秒。
*
* @returns true 表示数据库可达
*/
export async function healthCheck(): Promise<boolean> {
try {
const client = await pool.connect();
try {
await client.query("SELECT 1");
return true;
} finally {
client.release();
}
} catch {
return false;
}
}
/**
* 深度健康检查:验证 TimescaleDB 扩展是否已安装。
* 仅在首次连接或定期巡检时调用。
*
* @returns TimescaleDB 版本字符串,未安装则返回 null
*/
export async function timescaleVersion(): Promise<string | null> {
try {
const { rows } = await pool.query<{ extversion: string }>(
"SELECT extversion FROM pg_extension WHERE extname = 'timescaledb'",
);
return rows[0]?.extversion ?? null;
} catch {
return null;
}
}
// ============================================================
// 4. 事务辅助
// ============================================================
/**
* 在单连接上执行事务。
* 自动 BEGIN / COMMIT / ROLLBACK,连接用完即释放。
*
* @param fn - 事务体,接收 pg.PoolClient,返回 Promise<T>
* @returns fn 的返回值
* @throws 事务内任何异常都会触发 ROLLBACK 并向上抛出
*
* @example
* const result = await withTransaction(async (client) => {
* await client.query("INSERT INTO ...");
* await client.query("UPDATE ...");
* return { success: true };
* });
*/
export async function withTransaction<T>(
fn: (client: pg.PoolClient) => Promise<T>,
): Promise<T> {
const client = await pool.connect();
try {
await client.query("BEGIN");
const result = await fn(client);
await client.query("COMMIT");
return result;
} catch (err) {
await client.query("ROLLBACK");
throw err;
} finally {
client.release();
}
}
// ============================================================
// 5. 优雅关闭
// ============================================================
/** 是否正在关闭(防止重复调用) */
let shuttingDown = false;
/**
* 关闭连接池。
* 先等待所有进行中的查询完成(drain),再断开所有连接。
*
* 应在进程退出前调用:SIGTERM / SIGINT 处理器中。
*/
export async function closePool(): Promise<void> {
if (shuttingDown) {
return;
}
shuttingDown = true;
console.log("[pg] 正在关闭连接池...");
// pool.end() 等待所有活跃查询完成后关闭
await pool.end();
console.log("[pg] 连接池已关闭");
}
/**
* 注册进程信号处理器——优雅关闭。
* 在应用入口调用一次即可。
*/
export function registerShutdownHandlers(): void {
const shutdown = async (signal: string) => {
console.log(`[pg] 收到 ${signal} 信号,开始优雅关闭...`);
await closePool();
process.exit(0);
};
process.once("SIGTERM", () => shutdown("SIGTERM"));
process.once("SIGINT", () => shutdown("SIGINT"));
}
// ============================================================
// 6. Schema 初始化(基于 data/schema/*.sql
// ============================================================
/**
* 将 SQL 文本按语句拆分为独立命令。
* 规则:
* - 按 `;` 分割
* - 跳过空行和纯注释行
* - 单个语句去除首尾空白后若为空则跳过
*
* 注意:此方法假设 SQL 中 `;` 仅作为语句分隔符,
* 不含存储过程/函数体内的 `;`(schema/*.sql 满足此条件)。
*/
function splitSQLStatements(sql: string): string[] {
const statements: string[] = [];
for (const raw of sql.split(";")) {
const trimmed = raw.trim();
// 跳过空语句
if (trimmed === "") {
continue;
}
// 跳过仅包含注释的行(已在上层过滤,此处兜底)
statements.push(trimmed);
}
return statements;
}
/**
* 从单个 .sql 文件初始化 Schema。
* 读取文件 → 拆分语句 → 逐条执行(同一连接,保证顺序)。
*
* 所有 SQL 均使用 IF NOT EXISTS / ON CONFLICT DO NOTHING
* 因此重复执行安全幂等。
*
* @param filePath - SQL 文件的绝对路径
* @returns 成功执行的语句数
*/
export async function initSchemaFromFile(filePath: string): Promise<number> {
const sql = readFileSync(filePath, "utf-8");
// 预处理:移除纯注释行(以 -- 开头),减少无效语句
const lines = sql
.split("\n")
.filter((line: string) => {
const trimmed = line.trim();
return trimmed !== "" && !trimmed.startsWith("--");
})
.join("\n");
const statements = splitSQLStatements(lines);
if (statements.length === 0) {
return 0;
}
let executed = 0;
// 使用单连接执行所有语句,保证 DDL 顺序(如先建表再建索引)
const client = await pool.connect();
try {
for (const stmt of statements) {
await client.query(stmt);
executed++;
}
} finally {
client.release();
}
return executed;
}
/**
* 从 data/schema/ 目录初始化所有表结构。
*
* 执行顺序(按文件名排序):
* 1. klines.sql — K 线主表(hypertable)、索引、压缩、连续聚合
* 2. config.sql — 配置表(monitored_symbols / exchange_config / app_config+ 预置数据
*
* Docker Compose 首次启动时,001_init.sql 已被自动执行。
* 此函数作为补充,确保以下场景:
* - 裸 PostgreSQL 安装(非 Docker 部署)
* - 版本升级时需要新增表/索引
* - 开发环境快速重置 Schema
*
* @param schemaDir - schema 目录路径,默认 ../schema(相对于本文件)
* @returns 各文件执行结果摘要
*/
export async function initSchema(schemaDir?: string): Promise<{
files: string[];
totalStatements: number;
errors: string[];
}> {
// 计算 schema 目录绝对路径(ESM 中 __dirname 不可用)
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const dir = schemaDir ?? resolve(__dirname, "..", "schema");
const result = { files: [] as string[], totalStatements: 0, errors: [] as string[] };
// 读取目录,按文件名排序保证执行顺序(klines.sql 先于 config.sql
let entries: string[];
try {
entries = readdirSync(dir)
.filter((f: string) => f.endsWith(".sql"))
.sort(); // 字母序
} catch {
result.errors.push(`无法读取 schema 目录: ${dir}`);
return result;
}
// 手动排序:klines.sql 必须在 config.sql 之前(外键/引用依赖)
const klinesFirst = ["klines.sql", "config.sql"];
entries.sort((a, b) => {
const ia = klinesFirst.indexOf(a);
const ib = klinesFirst.indexOf(b);
if (ia !== -1 && ib !== -1) return ia - ib;
if (ia !== -1) return -1;
if (ib !== -1) return 1;
return a.localeCompare(b);
});
for (const entry of entries) {
const filePath = resolve(dir, entry);
try {
const count = await initSchemaFromFile(filePath);
result.files.push(`${entry} (${count} 条)`);
result.totalStatements += count;
} catch (err) {
const msg = `${entry}: ${(err as Error).message}`;
result.errors.push(msg);
console.error(`[pg] Schema 初始化失败 — ${msg}`);
}
}
if (result.errors.length === 0) {
console.log(
`[pg] Schema 初始化完成 — ${result.files.length} 个文件, ${result.totalStatements} 条语句`,
);
}
return result;
}
/**
* 快速判断核心表是否已存在(轻量级检查,不做全量 Schema 验证)。
* 仅在执行 initSchema() 前做一次探测,已存在则跳过。
*
* @returns true 表示 klines 和 monitored_symbols 表均已存在
*/
export async function isSchemaInitialized(): Promise<boolean> {
try {
const { rows } = await pool.query<{ count: string }>(`
SELECT COUNT(*)::TEXT AS count
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name IN ('klines', 'monitored_symbols', 'exchange_config', 'app_config')
`);
// 4 张核心表全部存在
return parseInt(rows[0]?.count ?? "0", 10) >= 4;
} catch {
return false;
}
}
// ============================================================
// 7. 默认导出(便于 import pool from "./db/pg"
// ============================================================
export default pool;
-561
View File
@@ -1,561 +0,0 @@
// ============================================================
// schema/queries.ts — 类型安全的参数化 SQL 查询
// ============================================================
// 每一条 SQL 都使用 $1, $2... 参数化,防止 SQL 注入。
// 返回类型与 types.ts 中的接口严格对应。
//
// 使用方式:
// import { pool } from "../db";
// import { queryEnabledSymbols } from "./schema/queries";
// const result = await pool.query(queryEnabledSymbols, ["binance"]);
// // result.rows 自动推断为 MonitoredSymbolRow[]
// ============================================================
import type {
KlineInsert,
KlineRow,
AggregatedKlineRow,
MonitoredSymbolRow,
MonitoredSymbolInsert,
ExchangeConfigRow,
ExchangeConfigInsert,
AppConfigRow,
Exchange,
KlineInterval,
StreamKey,
} from "./types";
// ============================================================
// K 线查询 — klines
// ============================================================
/**
* 批量插入 K 线(UPSERT — 冲突时更新 OHLCV
*
* 使用 UNNEST 批量写入,单次可插入数千条,性能远优于逐条 INSERT。
* 冲突策略:ON CONFLICT 时更新价格/成交量/闭合状态。
*/
export const bulkUpsertKlines = `
INSERT INTO klines (
time, exchange, symbol, interval,
open, high, low, close, volume,
quote_volume, taker_buy_base_vol, taker_buy_quote_vol,
trade_count, is_closed
)
SELECT * FROM UNNEST(
$1::TIMESTAMPTZ[], -- time[]
$2::TEXT[], -- exchange[]
$3::TEXT[], -- symbol[]
$4::TEXT[], -- interval[]
$5::NUMERIC(20,8)[], -- open[]
$6::NUMERIC(20,8)[], -- high[]
$7::NUMERIC(20,8)[], -- low[]
$8::NUMERIC(20,8)[], -- close[]
$9::NUMERIC(20,8)[], -- volume[]
$10::NUMERIC(20,8)[],-- quote_volume[]
$11::NUMERIC(20,8)[],-- taker_buy_base_vol[]
$12::NUMERIC(20,8)[],-- taker_buy_quote_vol[]
$13::INTEGER[], -- trade_count[]
$14::BOOLEAN[] -- is_closed[]
)
ON CONFLICT (time, exchange, symbol, interval) DO UPDATE SET
open = EXCLUDED.open,
high = EXCLUDED.high,
low = EXCLUDED.low,
close = EXCLUDED.close,
volume = EXCLUDED.volume,
quote_volume = EXCLUDED.quote_volume,
taker_buy_base_vol = EXCLUDED.taker_buy_base_vol,
taker_buy_quote_vol = EXCLUDED.taker_buy_quote_vol,
trade_count = EXCLUDED.trade_count,
is_closed = EXCLUDED.is_closed,
updated_at = NOW()
`;
/** 批量插入的参数类型:每个字段是一个数组 */
export interface BulkKlineParams {
time: Date[];
exchange: Exchange[];
symbol: string[];
interval: KlineInterval[];
open: string[];
high: string[];
low: string[];
close: string[];
volume: string[];
quote_volume: string[];
taker_buy_base_vol: string[];
taker_buy_quote_vol: string[];
trade_count: number[];
is_closed: boolean[];
}
/** 将 KlineInsert[] 拆解为 BulkKlineParams */
export function packBulkKlines(rows: KlineInsert[]): BulkKlineParams {
const len = rows.length;
const params: BulkKlineParams = {
time: new Array(len),
exchange: new Array(len),
symbol: new Array(len),
interval: new Array(len),
open: new Array(len),
high: new Array(len),
low: new Array(len),
close: new Array(len),
volume: new Array(len),
quote_volume: new Array(len),
taker_buy_base_vol: new Array(len),
taker_buy_quote_vol: new Array(len),
trade_count: new Array(len),
is_closed: new Array(len),
};
for (let i = 0; i < len; i++) {
const r = rows[i]!;
params.time[i] = r.time;
params.exchange[i] = r.exchange;
params.symbol[i] = r.symbol;
params.interval[i] = r.interval;
params.open[i] = r.open;
params.high[i] = r.high;
params.low[i] = r.low;
params.close[i] = r.close;
params.volume[i] = r.volume;
params.quote_volume[i] = r.quote_volume ?? "0";
params.taker_buy_base_vol[i] = r.taker_buy_base_vol ?? "0";
params.taker_buy_quote_vol[i] = r.taker_buy_quote_vol ?? "0";
params.trade_count[i] = r.trade_count ?? 0;
params.is_closed[i] = r.is_closed ?? true;
}
return params;
}
/**
* 查询原始 K 线(时间范围)
* 返回类型:KlineRow[]
*/
export const queryKlinesRange = `
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
`;
/**
* 查询最新 N 根 K 线
* 返回类型:KlineRow[]
*/
export const queryKlinesLatest = `
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
`;
// ============================================================
// 聚合 K 线查询 — klines_5m / 15m / 1h / 1d
// ============================================================
/**
* 查询聚合 K 线(动态视图名)
*
* @param viewName — "klines_5m" | "klines_15m" | "klines_1h" | "klines_1d"
*
* 注意:视图名已通过枚举约束,不存在注入风险(不使用用户输入拼接)
*/
export function queryAggregatedKlines(
viewName: "klines_5m" | "klines_15m" | "klines_1h" | "klines_1d",
) {
// 视图名来自代码常量,安全拼接
return `
SELECT time, exchange, symbol, interval,
open, high, low, close, volume,
quote_volume, taker_buy_base_vol, taker_buy_quote_vol, trade_count
FROM ${viewName}
WHERE exchange = $1
AND symbol = $2
AND time >= $3
AND time < $4
ORDER BY time ASC
`;
}
// ============================================================
// 监控交易对查询 — monitored_symbols
// ============================================================
/**
* 查询所有启用的监控标的(采集服务启动时调用)
* 返回类型:MonitoredSymbolRow[]
*/
export const queryEnabledSymbols = `
SELECT id, exchange, symbol, interval,
enabled, priority, label, notes,
created_at, updated_at
FROM monitored_symbols
WHERE enabled = TRUE
ORDER BY exchange, priority DESC, symbol, interval
`;
/**
* 查询指定交易所的监控标的
* 返回类型:MonitoredSymbolRow[]
*/
export const querySymbolsByExchange = `
SELECT id, exchange, symbol, interval,
enabled, priority, label, notes,
created_at, updated_at
FROM monitored_symbols
WHERE exchange = $1
AND enabled = TRUE
ORDER BY priority DESC, symbol, interval
`;
/**
* 插入监控标的(UPSERT
*/
export const upsertMonitoredSymbol = `
INSERT INTO monitored_symbols (exchange, symbol, interval, enabled, priority, label, notes)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (exchange, symbol, interval) DO UPDATE SET
enabled = EXCLUDED.enabled,
priority = EXCLUDED.priority,
label = EXCLUDED.label,
notes = EXCLUDED.notes,
updated_at = NOW()
RETURNING id, exchange, symbol, interval,
enabled, priority, label, notes,
created_at, updated_at
`;
/**
* 禁用监控标的(软删除)
*/
export const disableMonitoredSymbol = `
UPDATE monitored_symbols
SET enabled = FALSE, updated_at = NOW()
WHERE exchange = $1 AND symbol = $2 AND interval = $3
RETURNING id, exchange, symbol, interval
`;
/**
* 按 ID 查询单个监控标的
* 返回类型:MonitoredSymbolRow | null
*/
export const queryMonitoredSymbolById = `
SELECT id, exchange, symbol, interval,
enabled, priority, label, notes,
created_at, updated_at
FROM monitored_symbols
WHERE id = $1
`;
/**
* 查询所有监控标的(含已禁用)
* 返回类型:MonitoredSymbolRow[]
*/
export const queryAllMonitoredSymbols = `
SELECT id, exchange, symbol, interval,
enabled, priority, label, notes,
created_at, updated_at
FROM monitored_symbols
ORDER BY exchange, priority DESC, symbol, interval
`;
/**
* 按唯一键 (exchange, symbol, interval) 查询监控标的
* 返回类型:MonitoredSymbolRow | null
*/
export const queryMonitoredSymbolByKey = `
SELECT id, exchange, symbol, interval,
enabled, priority, label, notes,
created_at, updated_at
FROM monitored_symbols
WHERE exchange = $1 AND symbol = $2 AND interval = $3
`;
/**
* 按 ID 删除监控标的(硬删除)
* 返回被删除记录的 id
*/
export const deleteMonitoredSymbol = `
DELETE FROM monitored_symbols
WHERE id = $1
RETURNING id
`;
/**
* 按唯一键删除监控标的(硬删除)
* 返回被删除记录的 id
*/
export const deleteMonitoredSymbolByKey = `
DELETE FROM monitored_symbols
WHERE exchange = $1 AND symbol = $2 AND interval = $3
RETURNING id
`;
/**
* 更新监控标的(部分字段)
* 仅更新传入的非 NULL 字段,自动更新 updated_at
*/
export const updateMonitoredSymbol = `
UPDATE monitored_symbols
SET enabled = COALESCE($2, enabled),
priority = COALESCE($3, priority),
label = COALESCE($4, label),
notes = COALESCE($5, notes),
updated_at = NOW()
WHERE id = $1
RETURNING id, exchange, symbol, interval,
enabled, priority, label, notes,
created_at, updated_at
`;
// ============================================================
// 交易所配置查询 — exchange_config
// ============================================================
/**
* 查询所有启用的交易所配置
* 返回类型:ExchangeConfigRow[]
*/
export const queryEnabledExchanges = `
SELECT id, exchange,
rest_url, ws_url, ws_ping_interval_ms,
rate_limit_per_sec, max_reconnect_attempts, reconnect_delay_ms,
enabled, notes, created_at, updated_at
FROM exchange_config
WHERE enabled = TRUE
ORDER BY exchange
`;
/**
* 查询单个交易所配置
* 返回类型:ExchangeConfigRow | null
*/
export const queryExchangeConfig = `
SELECT id, exchange,
rest_url, ws_url, ws_ping_interval_ms,
rate_limit_per_sec, max_reconnect_attempts, reconnect_delay_ms,
enabled, notes, created_at, updated_at
FROM exchange_config
WHERE exchange = $1
`;
/**
* 插入/更新交易所配置(UPSERT)
*/
export const upsertExchangeConfig = `
INSERT INTO exchange_config (
exchange, rest_url, ws_url, ws_ping_interval_ms,
rate_limit_per_sec, max_reconnect_attempts, reconnect_delay_ms,
enabled, notes
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (exchange) DO UPDATE SET
rest_url = EXCLUDED.rest_url,
ws_url = EXCLUDED.ws_url,
ws_ping_interval_ms = EXCLUDED.ws_ping_interval_ms,
rate_limit_per_sec = EXCLUDED.rate_limit_per_sec,
max_reconnect_attempts = EXCLUDED.max_reconnect_attempts,
reconnect_delay_ms = EXCLUDED.reconnect_delay_ms,
enabled = EXCLUDED.enabled,
notes = EXCLUDED.notes,
updated_at = NOW()
RETURNING id, exchange,
rest_url, ws_url, ws_ping_interval_ms,
rate_limit_per_sec, max_reconnect_attempts, reconnect_delay_ms,
enabled, notes, created_at, updated_at
`;
/**
* 按 ID 查询交易所配置
* 返回类型:ExchangeConfigRow | null
*/
export const queryExchangeConfigById = `
SELECT id, exchange,
rest_url, ws_url, ws_ping_interval_ms,
rate_limit_per_sec, max_reconnect_attempts, reconnect_delay_ms,
enabled, notes, created_at, updated_at
FROM exchange_config
WHERE id = $1
`;
/**
* 查询所有交易所配置(含已禁用)
* 返回类型:ExchangeConfigRow[]
*/
export const queryAllExchangeConfigs = `
SELECT id, exchange,
rest_url, ws_url, ws_ping_interval_ms,
rate_limit_per_sec, max_reconnect_attempts, reconnect_delay_ms,
enabled, notes, created_at, updated_at
FROM exchange_config
ORDER BY exchange
`;
/**
* 按 ID 删除交易所配置(硬删除)
* 返回被删除记录的 id
*/
export const deleteExchangeConfig = `
DELETE FROM exchange_config
WHERE id = $1
RETURNING id
`;
/**
* 按交易所标识删除配置(硬删除)
* 返回被删除记录的 id
*/
export const deleteExchangeConfigByExchange = `
DELETE FROM exchange_config
WHERE exchange = $1
RETURNING id
`;
/**
* 更新交易所配置(部分字段)
* 仅更新传入的非 NULL 字段,自动更新 updated_at
*/
export const updateExchangeConfig = `
UPDATE exchange_config
SET rest_url = COALESCE($2, rest_url),
ws_url = COALESCE($3, ws_url),
ws_ping_interval_ms = COALESCE($4, ws_ping_interval_ms),
rate_limit_per_sec = COALESCE($5, rate_limit_per_sec),
max_reconnect_attempts = COALESCE($6, max_reconnect_attempts),
reconnect_delay_ms = COALESCE($7, reconnect_delay_ms),
enabled = COALESCE($8, enabled),
notes = COALESCE($9, notes),
updated_at = NOW()
WHERE id = $1
RETURNING id, exchange,
rest_url, ws_url, ws_ping_interval_ms,
rate_limit_per_sec, max_reconnect_attempts, reconnect_delay_ms,
enabled, notes, created_at, updated_at
`;
// ============================================================
// 全局配置查询 — app_config
// ============================================================
/**
* 查询所有应用配置
* 返回类型:AppConfigRow[]
*/
export const queryAllAppConfig = `
SELECT id, key, value, description, updated_at
FROM app_config
ORDER BY key
`;
/**
* 查询单个配置项
* 返回类型:AppConfigRow | null
*/
export const queryAppConfig = `
SELECT id, key, value, description, updated_at
FROM app_config
WHERE key = $1
`;
/**
* 设置配置项(UPSERT
*/
export const upsertAppConfig = `
INSERT INTO app_config (key, value, description)
VALUES ($1, $2, $3)
ON CONFLICT (key) DO UPDATE SET
value = EXCLUDED.value,
description = EXCLUDED.description,
updated_at = NOW()
RETURNING id, key, value, description, updated_at
`;
/**
* 按 ID 查询应用配置
* 返回类型:AppConfigRow | null
*/
export const queryAppConfigById = `
SELECT id, key, value, description, updated_at
FROM app_config
WHERE id = $1
`;
/**
* 按 key 删除应用配置(硬删除)
* 返回被删除记录的 id
*/
export const deleteAppConfig = `
DELETE FROM app_config
WHERE key = $1
RETURNING id
`;
/**
* 按 ID 删除应用配置(硬删除)
* 返回被删除记录的 id
*/
export const deleteAppConfigById = `
DELETE FROM app_config
WHERE id = $1
RETURNING id
`;
/**
* 更新应用配置值(部分字段)
* 仅更新传入的非 NULL 字段,自动更新 updated_at
*/
export const updateAppConfig = `
UPDATE app_config
SET value = COALESCE($2, value),
description = COALESCE($3, description),
updated_at = NOW()
WHERE id = $1
RETURNING id, key, value, description, updated_at
`;
// ============================================================
// 复合查询:采集服务启动加载项
// ============================================================
/**
* 一次性加载所有启动配置:
* 1. 启用的监控标的列表
* 2. 对应交易所的连接配置
*
* 返回两表 JOIN 结果,供采集服务初始化 WebSocket 连接池。
*/
export const queryStreamSubscriptions = `
SELECT
m.exchange,
m.symbol,
m.interval,
m.priority,
e.rest_url,
e.ws_url,
e.ws_ping_interval_ms,
e.rate_limit_per_sec,
e.max_reconnect_attempts,
e.reconnect_delay_ms
FROM monitored_symbols m
JOIN exchange_config e ON m.exchange = e.exchange
WHERE m.enabled = TRUE
AND e.enabled = TRUE
ORDER BY m.exchange, m.priority DESC, m.symbol, m.interval
`;
-249
View File
@@ -1,249 +0,0 @@
// ============================================================
// schema/types.ts — PostgreSQL 表对应的 TypeScript 类型定义
// ============================================================
// 与 data/schema/*.sql 中的表结构一一对应
//
// 类型约定:
// NUMERIC(20,8) → string (保留精度,避免 IEEE 754 浮点误差)
// TIMESTAMPTZ → Date pg 默认解析为 Date
// SERIAL / INT → number
// SMALLINT → number
// REAL → number
// BOOLEAN → boolean
// ============================================================
// ============================================================
// 联合类型:约束 TEXT 字段的合法值
// ============================================================
/** 支持的交易所标识 */
export type Exchange = "binance" | "okx" | "bybit";
/** K 线周期(原始 1m + 聚合派生) */
export type KlineInterval = "1m" | "5m" | "15m" | "1h" | "4h" | "1d";
/** 日志级别 */
export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
// ============================================================
// 1. K 线主表 — klines
// ============================================================
/** klines 表完整行类型 */
export interface KlineRow {
/** K 线开盘时间(UTC */
time: Date;
/** 交易所 */
exchange: Exchange;
/** 交易对,如 BTCUSDT */
symbol: string;
/** K 线周期 */
interval: KlineInterval;
/** 开盘价 */
open: string;
/** 最高价 */
high: string;
/** 最低价 */
low: string;
/** 收盘价 */
close: string;
/** 成交量(基准币种) */
volume: string;
/** 成交额(计价币种) */
quote_volume: string;
/** 主动买入量(基准币种) */
taker_buy_base_vol: string;
/** 主动买入额(计价币种) */
taker_buy_quote_vol: string;
/** 成交笔数 */
trade_count: number;
/** K 线是否已闭合 */
is_closed: boolean;
/** 记录创建时间 */
created_at: Date;
/** 记录更新时间 */
updated_at: Date;
}
/** klines 表插入类型(省略自动生成的元数据列) */
export interface KlineInsert {
time: Date;
exchange: Exchange;
symbol: string;
interval: KlineInterval;
open: string;
high: string;
low: string;
close: string;
volume: string;
quote_volume?: string;
taker_buy_base_vol?: string;
taker_buy_quote_vol?: string;
trade_count?: number;
is_closed?: boolean;
}
// ============================================================
// 2. 连续聚合视图 — klines_5m / klines_15m / klines_1h / klines_1d
// ============================================================
/** 聚合 K 线通用类型(OHLCV,无扩展字段) */
export interface AggregatedKlineRow {
time: Date;
exchange: Exchange;
symbol: string;
interval: KlineInterval;
open: string;
high: string;
low: string;
close: string;
volume: string;
quote_volume: string;
taker_buy_base_vol: string;
taker_buy_quote_vol: string;
trade_count: number;
}
// ============================================================
// 3. 监控交易对配置 — monitored_symbols
// ============================================================
/** monitored_symbols 表完整行类型 */
export interface MonitoredSymbolRow {
/** 自增主键 */
id: number;
/** 交易所 */
exchange: Exchange;
/** 交易对 */
symbol: string;
/** K 线周期 */
interval: KlineInterval;
/** 是否启用采集 */
enabled: boolean;
/** 优先级(0-32767,越大越优先) */
priority: number;
/** 人类可读标签 */
label: string | null;
/** 备注 */
notes: string | null;
/** 创建时间 */
created_at: Date;
/** 更新时间 */
updated_at: Date;
}
/** monitored_symbols 插入类型 */
export interface MonitoredSymbolInsert {
exchange: Exchange;
symbol: string;
interval: KlineInterval;
enabled?: boolean;
priority?: number;
label?: string | null;
notes?: string | null;
}
/** monitored_symbols 更新类型(所有字段可选) */
export interface MonitoredSymbolUpdate {
enabled?: boolean;
priority?: number;
label?: string | null;
notes?: string | null;
}
// ============================================================
// 4. 交易所连接配置 — exchange_config
// ============================================================
/** exchange_config 表完整行类型 */
export interface ExchangeConfigRow {
/** 自增主键 */
id: number;
/** 交易所标识(唯一) */
exchange: Exchange;
/** REST API 基础 URLnull = 使用 SDK 默认值) */
rest_url: string | null;
/** WebSocket 基础 URLnull = 使用 SDK 默认值) */
ws_url: string | null;
/** 心跳间隔(毫秒) */
ws_ping_interval_ms: number;
/** 每秒最大请求数 */
rate_limit_per_sec: number;
/** 最大重连次数 */
max_reconnect_attempts: number;
/** 重连延迟基数(毫秒) */
reconnect_delay_ms: number;
/** 是否启用该交易所 */
enabled: boolean;
/** 备注 */
notes: string | null;
/** 创建时间 */
created_at: Date;
/** 更新时间 */
updated_at: Date;
}
/** exchange_config 插入类型 */
export interface ExchangeConfigInsert {
exchange: Exchange;
rest_url?: string | null;
ws_url?: string | null;
ws_ping_interval_ms?: number;
rate_limit_per_sec?: number;
max_reconnect_attempts?: number;
reconnect_delay_ms?: number;
enabled?: boolean;
notes?: string | null;
}
// ============================================================
// 5. 全局应用配置 — app_config
// ============================================================
/** app_config 表完整行类型 */
export interface AppConfigRow {
/** 自增主键 */
id: number;
/** 配置键 */
key: string;
/** 配置值(统一存储为字符串) */
value: string;
/** 说明 */
description: string | null;
/** 更新时间 */
updated_at: Date;
}
/** 已知的 app_config 键名 */
export type AppConfigKey =
| "batch_size"
| "flush_interval_ms"
| "log_level"
| "redis_publish_enabled";
// ============================================================
// 6. 业务聚合类型
// ============================================================
/**
* 唯一标识一个 K 线流
* 对应 klines / monitored_symbols 的 (exchange, symbol, interval) 组合
*/
export interface StreamKey {
exchange: Exchange;
symbol: string;
interval: KlineInterval;
}
/**
* 采集服务启动时加载的完整订阅配置
* = monitored_symbols JOIN exchange_config
*/
export interface StreamSubscription {
/** 流标识 */
streamKey: StreamKey;
/** 优先级 */
priority: number;
/** 连接配置 */
exchangeConfig: ExchangeConfigRow;
}
-245
View File
@@ -1,245 +0,0 @@
// ============================================================
// schema/validators.ts — Zod 运行时校验 Schema
// ============================================================
// 用途:
// 1. WebSocket 行情数据到达后校验字段完整性再入库
// 2. 配置文件 / 环境变量加载后类型收窄
// 3. API 输入参数校验
//
// 依赖:zod ^4.x(已包含在 data/package.json
// ============================================================
import { z } from "zod";
// ============================================================
// 基础标量 Schema
// ============================================================
/** 交易所枚举 */
export const ExchangeSchema = z.enum(["binance", "okx", "bybit"]);
export type Exchange = z.infer<typeof ExchangeSchema>;
/** K 线周期枚举 */
export const KlineIntervalSchema = z.enum([
"1m",
"5m",
"15m",
"1h",
"4h",
"1d",
]);
export type KlineInterval = z.infer<typeof KlineIntervalSchema>;
/** 日志级别 */
export const LogLevelSchema = z.enum([
"trace",
"debug",
"info",
"warn",
"error",
"fatal",
]);
/** 交易对格式:大写字母 + 大写字母(如 BTCUSDT),3-12 字符 */
export const SymbolSchema = z
.string()
.regex(/^[A-Z0-9]{4,14}$/, "交易对格式无效,示例:BTCUSDT");
/**
* NUMERIC(20,8) 数值字符串
* pg 驱动默认以字符串返回 NUMERIC 以保留精度
*/
export const NumericStringSchema = z
.string()
.regex(/^-?\d+(\.\d+)?$/, "期望 NUMERIC 字符串");
// ============================================================
// 1. Kline 数据校验 — klines 表
// ============================================================
/** WebSocket 原始 OHLCV 消息校验(单条 K 线,入库前) */
export const KlineRawSchema = z.object({
/** K 线开盘时间(UTC),Unix 毫秒时间戳 */
time: z.number().int().positive(),
/** 交易所 */
exchange: ExchangeSchema,
/** 交易对 */
symbol: SymbolSchema,
/** 周期 */
interval: KlineIntervalSchema,
/** 开盘价 */
open: NumericStringSchema,
/** 最高价 */
high: NumericStringSchema,
/** 最低价 */
low: NumericStringSchema,
/** 收盘价 */
close: NumericStringSchema,
/** 成交量 */
volume: NumericStringSchema,
/** 成交额(可选) */
quote_volume: NumericStringSchema.optional().default("0"),
/** 主动买入量(可选) */
taker_buy_base_vol: NumericStringSchema.optional().default("0"),
/** 主动买入额(可选) */
taker_buy_quote_vol: NumericStringSchema.optional().default("0"),
/** 成交笔数(可选) */
trade_count: z.number().int().nonnegative().optional().default(0),
/** K 线是否闭合 */
is_closed: z.boolean().optional().default(true),
});
export type KlineRaw = z.infer<typeof KlineRawSchema>;
/** 批量 K 线消息校验(WebSocket 可能一次推送多根) */
export const KlineBatchSchema = z.array(KlineRawSchema).min(1);
export type KlineBatch = z.infer<typeof KlineBatchSchema>;
// ============================================================
// 2. 监控交易对配置校验 — monitored_symbols
// ============================================================
/** 插入监控标的 */
export const MonitoredSymbolInsertSchema = z.object({
exchange: ExchangeSchema,
symbol: SymbolSchema,
interval: KlineIntervalSchema,
enabled: z.boolean().optional().default(true),
/** 优先级 0-32767SMALLINT 范围) */
priority: z
.number()
.int()
.min(0)
.max(32767)
.optional()
.default(0),
label: z.string().max(200).nullable().optional().default(null),
notes: z.string().max(1000).nullable().optional().default(null),
});
export type MonitoredSymbolInsert = z.infer<
typeof MonitoredSymbolInsertSchema
>;
/** 更新监控标的 */
export const MonitoredSymbolUpdateSchema = z.object({
enabled: z.boolean().optional(),
priority: z.number().int().min(0).max(32767).optional(),
label: z.string().max(200).nullable().optional(),
notes: z.string().max(1000).nullable().optional(),
});
export type MonitoredSymbolUpdate = z.infer<
typeof MonitoredSymbolUpdateSchema
>;
// ============================================================
// 3. 交易所连接配置校验 — exchange_config
// ============================================================
/** 交易所连接配置输入 */
export const ExchangeConfigInsertSchema = z.object({
exchange: ExchangeSchema,
rest_url: z.string().url().nullable().optional().default(null),
ws_url: z.string().url().nullable().optional().default(null),
ws_ping_interval_ms: z
.number()
.int()
.min(5000)
.max(300000)
.optional()
.default(30000),
rate_limit_per_sec: z.number().positive().max(100).optional().default(20),
max_reconnect_attempts: z
.number()
.int()
.min(0)
.max(100)
.optional()
.default(10),
reconnect_delay_ms: z
.number()
.int()
.min(100)
.max(60000)
.optional()
.default(3000),
enabled: z.boolean().optional().default(true),
notes: z.string().max(500).nullable().optional().default(null),
});
export type ExchangeConfigInsert = z.infer<
typeof ExchangeConfigInsertSchema
>;
// ============================================================
// 4. 流标识校验 — StreamKey
// ============================================================
/** (exchange, symbol, interval) 三元组 */
export const StreamKeySchema = z.object({
exchange: ExchangeSchema,
symbol: SymbolSchema,
interval: KlineIntervalSchema,
});
export type StreamKey = z.infer<typeof StreamKeySchema>;
// ============================================================
// 5. 环境变量 / 配置校验
// ============================================================
/** .env 环境变量 schema */
export const EnvConfigSchema = z.object({
/** 逗号分隔的交易对列表 */
SYMBOLS: z
.string()
.optional()
.default("BTCUSDT,ETHUSDT"),
DB_HOST: z.string().optional().default("localhost"),
DB_PORT: z.coerce.number().int().positive().optional().default(5432),
DB_NAME: z.string().optional().default("trade"),
DB_USER: z.string().optional().default("trader"),
DB_PASSWORD: z.string().optional().default("changeme"),
REDIS_URL: z.string().url().optional().default("redis://localhost:6379"),
REDIS_PUBLISH_ENABLED: z
.enum(["true", "false"])
.optional()
.default("true"),
BATCH_SIZE: z.coerce.number().int().positive().optional().default(500),
FLUSH_INTERVAL_MS: z.coerce
.number()
.int()
.positive()
.optional()
.default(1000),
/** WebSocket 断线重连延迟基数(毫秒) */
WS_RECONNECT_DELAY_MS: z.coerce
.number()
.int()
.positive()
.optional()
.default(3000),
/** WebSocket 心跳间隔(毫秒) */
WS_PING_INTERVAL_MS: z.coerce
.number()
.int()
.positive()
.optional()
.default(30000),
/** WebSocket 最大重连次数 */
WS_MAX_RECONNECT_ATTEMPTS: z.coerce
.number()
.int()
.nonnegative()
.optional()
.default(10),
LOG_LEVEL: LogLevelSchema.optional().default("info"),
NODE_ENV: z
.enum(["development", "production", "test"])
.optional()
.default("development"),
});
export type EnvConfig = z.infer<typeof EnvConfigSchema>;
+186
View File
@@ -0,0 +1,186 @@
// ============================================================
// base.ts — 交易所适配器抽象基类
// ============================================================
// 所有交易所适配器(Binance / OKX / Bybit ...)继承此类,
// 复用指数退避重连、连接状态管理、限流等通用逻辑。
//
// 子类只需实现:
// - connect() — 建立 WebSocket/REST 连接
// - disconnect() — 断开连接并清理资源
// - subscribeTicker() / subscribeTrade() / subscribeOrderbook()
// - fetchKlines() — REST 历史 K 线补拉
// - fetchMarkets() — 交易对元数据拉取
// ============================================================
import { Subject, type Observable } from "rxjs";
import { logger } from "../utils/logger";
import type {
MarketDataFeed,
Ticker,
Trade,
OrderBook,
Kline,
KlineInterval,
MarketInfo,
ConnectionState,
AdapterConfig,
} from "./types";
import { DEFAULT_ADAPTER_CONFIG } from "./types";
// ============================================================
// 工具:异步 sleep
// ============================================================
/** 返回一个在 ms 毫秒后 resolve 的 Promise */
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// ============================================================
// BaseExchangeAdapter
// ============================================================
export abstract class BaseExchangeAdapter implements MarketDataFeed {
/** 交易所标识(子类必须覆盖) */
abstract readonly exchange: string;
/** 适配器配置(可在子类构造函数中覆盖默认值) */
protected readonly config: AdapterConfig;
/** 当前连接状态 */
protected _connectionState: ConnectionState = "disconnected";
/** 当前重连尝试次数(成功连接后重置) */
protected reconnectAttempt = 0;
/** Subject 清理注册表 —— disconnect 时统一 complete */
protected activeSubjects = new Set<Subject<unknown>>();
// ============================================================
// 构造函数
// ============================================================
constructor(config: Partial<AdapterConfig> = {}) {
this.config = { ...DEFAULT_ADAPTER_CONFIG, ...config };
}
// ============================================================
// 连接状态(只读暴露)
// ============================================================
get connectionState(): ConnectionState {
return this._connectionState;
}
/** 更新连接状态并记录日志 */
protected setConnectionState(state: ConnectionState): void {
const prev = this._connectionState;
this._connectionState = state;
if (prev !== state) {
logger.info(
{ exchange: this.exchange, from: prev, to: state },
`[${this.exchange}] connection state: ${prev}${state}`,
);
}
}
// ============================================================
// 指数退避重连(所有子类复用)
// ============================================================
/**
* 执行指数退避重连。
*
* 延迟公式:delay = baseDelay × 2^min(attempt, 5)
* - attempt=0: 3s
* - attempt=1: 6s
* - attempt=2: 12s
* - attempt=5: 96s(之后不再翻倍)
*
* 超过 maxReconnectAttempts 后抛出错误。
*
* @throws 达到最大重试次数后抛出
*/
protected async reconnect(): Promise<void> {
const { reconnectBaseDelayMs: baseDelay, maxReconnectAttempts } = this.config;
if (this.reconnectAttempt >= maxReconnectAttempts) {
this.setConnectionState("error");
throw new Error(
`[${this.exchange}] 重连失败:已达最大重试次数 (${maxReconnectAttempts})`,
);
}
const cappedAttempt = Math.min(this.reconnectAttempt, 5);
const delay = baseDelay * Math.pow(2, cappedAttempt);
logger.warn(
{
exchange: this.exchange,
attempt: this.reconnectAttempt + 1,
maxAttempts: maxReconnectAttempts,
delayMs: delay,
},
`[${this.exchange}] WebSocket 重连中...`,
);
await sleep(delay);
this.reconnectAttempt++;
this.setConnectionState("connecting");
await this.connect();
}
/** 成功连接后重置重连计数器 */
protected resetReconnectAttempts(): void {
if (this.reconnectAttempt > 0) {
logger.info(
{ exchange: this.exchange, attempts: this.reconnectAttempt },
`[${this.exchange}] 重连成功,计数器重置`,
);
}
this.reconnectAttempt = 0;
}
// ============================================================
// Subject 管理工具
// ============================================================
/**
* 创建一个受管理的 Subjectdisconnect 时自动 complete。
* 子类在 subscribe* 方法中使用此工具创建 Subject。
*/
protected createManagedSubject<T>(): Subject<T> {
const subject = new Subject<T>();
this.activeSubjects.add(subject as Subject<unknown>);
return subject;
}
/** 完成所有受管理的 Subjectdisconnect 时调用) */
protected completeAllSubjects(): void {
for (const subject of this.activeSubjects) {
subject.complete();
}
this.activeSubjects.clear();
}
// ============================================================
// 抽象方法 —— 子类必须实现
// ============================================================
abstract connect(): Promise<void>;
abstract disconnect(): Promise<void>;
abstract subscribeTicker(symbols: string[]): Observable<Ticker>;
abstract subscribeTrade(symbols: string[]): Observable<Trade>;
abstract subscribeOrderbook(symbol: string, depth?: number): Observable<OrderBook>;
abstract fetchKlines(
symbol: string,
interval: KlineInterval,
startTime: number,
endTime: number,
limit?: number,
): Promise<Kline[]>;
abstract fetchMarkets(): Promise<MarketInfo[]>;
}
export default BaseExchangeAdapter;
+735 -348
View File
File diff suppressed because it is too large Load Diff
+280 -26
View File
@@ -1,17 +1,133 @@
// ============================================================ // ============================================================
// exchanges/types.ts — WebSocket 事件数据类型 // types.ts — 统一行情数据类型定义与 MarketDataFeed 接口
// ============================================================
// 所有交易所适配器共享的数据结构和接口契约。
// 适配器负责将交易所原生数据格式转换为以下标准化类型。
//
// 设计原则:
// - 字段语义与 Binance/OKX/Bybit 通用概念对齐
// - 时间戳统一使用 Unix 毫秒(number),便于排序和计算
// - 价格/数量使用 number 类型(JavaScript 64-bit float),
// 对精度敏感场景(如 orderbook 快照)保留原始字符串
// ============================================================ // ============================================================
/** 由 WebSocket 推送的单根 K 线数据 */ import type { Observable } from "rxjs";
export interface KlineWsData {
// ============================================================
// K 线周期
// ============================================================
/** K 线周期枚举(与 kline.entity.ts 中 KlineInterval 保持一致) */
export type KlineInterval =
| "1m"
| "5m"
| "15m"
| "30m"
| "1h"
| "4h"
| "1d"
| "1w";
/** K 线周期 → 毫秒数映射(用于时间桶计算) */
export const KLINE_INTERVAL_MS: Record<KlineInterval, number> = {
"1m": 60_000,
"5m": 300_000,
"15m": 900_000,
"30m": 1_800_000,
"1h": 3_600_000,
"4h": 14_400_000,
"1d": 86_400_000,
"1w": 604_800_000,
};
// ============================================================
// 标准化行情数据结构
// ============================================================
/** 24 小时滚动 Ticker 统计 */
export interface Ticker {
/** 交易所标识 */ /** 交易所标识 */
exchange: string; exchange: string;
/** 交易对,如 BTCUSDT */ /** 交易对符号(大写,如 BTCUSDT */
symbol: string; symbol: string;
/** K 线周期,如 1m / 1h / 1d */ /** 最新成交价 */
interval: string; lastPrice: number;
/** K 线开盘时间(Unix 毫秒时间戳) */ /** 24h 开盘价 */
time: number; openPrice: number;
/** 24h 最高价 */
highPrice: number;
/** 24h 最低价 */
lowPrice: number;
/** 24h 成交量(base 币种) */
volume: number;
/** 24h 成交额(quote 币种) */
quoteVolume: number;
/** 24h 价格变化 */
priceChange: number;
/** 24h 价格变化百分比(0.05 = 5% */
priceChangePercent: number;
/** 买一价 */
bidPrice: number;
/** 买一量 */
bidQty: number;
/** 卖一价 */
askPrice: number;
/** 卖一量 */
askQty: number;
/** 事件发生时间(Unix ms */
eventTime: number;
/** 交易所收盘时间(Unix ms,用于判断 K 线是否闭合) */
closeTime: number;
}
/** 逐笔成交 */
export interface Trade {
/** 交易所标识 */
exchange: string;
/** 交易对符号 */
symbol: string;
/** 成交价 */
price: number;
/** 成交数量(base 币种) */
amount: number;
/** 成交额(quote 币种 = price × amount */
quoteAmount: number;
/** 成交时间(Unix ms */
timestamp: number;
/** 买方是否为挂单方(true = 主动卖出 / taker sell */
isBuyerMaker: boolean;
/** 交易所成交 ID(可能为字符串,如 Binance tradeId 为 bigint */
tradeId: string;
}
/** 订单簿深度快照 */
export interface OrderBook {
/** 交易所标识 */
exchange: string;
/** 交易对符号 */
symbol: string;
/** 买单列表 [[price, qty], ...],按价格降序(买一在前) */
bids: [number, number][];
/** 卖单列表 [[price, qty], ...],按价格升序(卖一在前) */
asks: [number, number][];
/** 上次更新 ID */
lastUpdateId: number;
/** 事件发生时间(Unix ms */
eventTime: number;
}
/** 标准化 K 线(OHLCV */
export interface Kline {
/** 交易所标识 */
exchange: string;
/** 交易对符号 */
symbol: string;
/** K 线周期 */
interval: KlineInterval;
/** 开盘时间(Unix ms */
openTime: number;
/** 收盘时间(Unix ms */
closeTime: number;
/** 开盘价 */ /** 开盘价 */
open: number; open: number;
/** 最高价 */ /** 最高价 */
@@ -20,26 +136,164 @@ export interface KlineWsData {
low: number; low: number;
/** 收盘价 */ /** 收盘价 */
close: number; close: number;
/** 成交量(基准币种) */ /** 成交量(base 币种) */
volume: number; volume: number;
/** 成交额(quote 币种) */
quoteVolume: number;
/** 主动买入成交量(base 币种) */
takerBuyBaseVol: number;
/** 主动买入成交额(quote 币种) */
takerBuyQuoteVol: number;
/** 成交笔数 */
tradeCount: number;
/** 该 K 线是否已关闭(不再更新) */
isClosed: boolean;
} }
/** ExchangeWs 构造参数 */ /** K 线增量更新(仅推送最新一根 OHLCV 变化) */
export interface ExchangeWsConfig { export interface KlineDelta {
/** 交易所 ID,ccxt 支持的所有交易所标识 */ exchange: string;
exchangeId: string; symbol: string;
/** 要订阅的交易对列表,如 ['BTCUSDT', 'ETHUSDT'] */ interval: KlineInterval;
symbols: string[]; openTime: number;
/** K 线周期,默认 '1m' */ closeTime: number;
interval?: string; open: number;
/** 传递给 ccxt.pro 交易所实例的额外选项(如 agent、apiKey 等) */ high: number;
ccxtOptions?: Record<string, unknown>; low: number;
close: number;
volume: number;
isClosed: boolean;
} }
/** ExchangeWs 连接状态 */ // ============================================================
export type WsConnectionState = // WebSocket 连接状态
| "idle" // 尚未启动 // ============================================================
| "connecting" // 正在连接 WebSocket
| "connected" // 已连接,正在接收数据 /** 连接状态枚举 */
| "disconnected" // 已断开 export type ConnectionState =
| "error"; // 错误状态 | "disconnected"
| "connecting"
| "connected"
| "error";
// ============================================================
// 适配器配置
// ============================================================
/** 交易所适配器通用配置 */
export interface AdapterConfig {
/** 指数退避重连基数(毫秒),默认 3000 */
reconnectBaseDelayMs: number;
/** 最大重连次数,默认 10 */
maxReconnectAttempts: number;
/** REST API 请求冷却时间(毫秒),默认 200 */
restRateLimitMs: number;
}
/** 默认适配器配置 */
export const DEFAULT_ADAPTER_CONFIG: AdapterConfig = {
reconnectBaseDelayMs: 3000,
maxReconnectAttempts: 10,
restRateLimitMs: 200,
};
// ============================================================
// MarketDataFeed 接口 —— 所有交易所适配器必须实现
// ============================================================
/**
* 统一行情数据源接口。
*
* 每个交易所适配器实现此接口,向上层管道暴露标准化数据流。
* 使用 RxJS Observable 作为统一推送机制,pipeline 层可自由
* 组合、过滤、分流各交易所数据。
*/
export interface MarketDataFeed {
/** 交易所标识(如 "binance" */
readonly exchange: string;
/** 当前连接状态 */
readonly connectionState: ConnectionState;
/** 建立 WebSocket 连接 */
connect(): Promise<void>;
/** 断开连接 */
disconnect(): Promise<void>;
/**
* 订阅 24h 滚动 Ticker 流。
* 每笔成交触发推送(Binance: <symbol>@ticker)。
*/
subscribeTicker(symbols: string[]): Observable<Ticker>;
/**
* 订阅逐笔成交流。
* 实时推送每笔撮合成交(Binance: <symbol>@trade)。
*/
subscribeTrade(symbols: string[]): Observable<Trade>;
/**
* 订阅订单簿深度。
* depth 参数指定档位(如 5/10/20),默认 20。
*/
subscribeOrderbook(symbol: string, depth?: number): Observable<OrderBook>;
/**
* REST 拉取历史 K 线(用于补齐缺失数据或回测)。
*
* @param symbol - 交易对符号
* @param interval - K 线周期
* @param startTime - 起始时间(Unix ms
* @param endTime - 结束时间(Unix ms
* @param limit - 最大返回条数(默认 500)
* @returns 标准化 K 线数组,按时间升序
*/
fetchKlines(
symbol: string,
interval: KlineInterval,
startTime: number,
endTime: number,
limit?: number,
): Promise<Kline[]>;
/**
* 获取交易所交易对信息(用于自动注册到 trading_pairs 表)。
* 返回标准化后的交易对元数据。
*/
fetchMarkets(): Promise<MarketInfo[]>;
}
/** 交易对元信息(从交易所 REST API 获取) */
export interface MarketInfo {
symbol: string;
baseAsset: string;
quoteAsset: string;
pricePrecision: number;
quantityPrecision: number;
minQty?: number;
stepSize?: number;
minNotional?: number;
}
// ============================================================
// 工具类型
// ============================================================
/** Binance WebSocket 原始 K 线数据(kline 事件中的 k 字段) */
export interface BinanceRawKline {
t: number; // K 线开始时间
T: number; // K 线结束时间
s: string; // 交易对
i: string; // 周期
o: string; // 开盘价
h: string; // 最高价
l: string; // 最低价
c: string; // 收盘价
v: string; // 成交量
n: number; // 成交笔数
x: boolean; // 是否已关闭
q: string; // 成交额
V: string; // 主动买入成交量
Q: string; // 主动买入成交额
}
-228
View File
@@ -1,228 +0,0 @@
-- ============================================================
-- 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 '001_init.sql — 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 $$;
-136
View File
@@ -1,136 +0,0 @@
-- ============================================================
-- 002_config.sql — 配置表初始化
-- ============================================================
-- Docker Compose 首次启动时在 001_init.sql 之后自动执行
-- 挂载路径:./data/init-db:/docker-entrypoint-initdb.d
-- 执行顺序:按文件名排序(001 → 002)
-- ============================================================
-- ============================================================
-- 1. monitored_symbols — 监控交易对配置
-- ============================================================
-- 用途:声明数据采集模块需要订阅哪些交易对的 K 线流
-- 消费方:WebSocket 行情采集服务启动时读取此表决定订阅列表
CREATE TABLE IF NOT EXISTS monitored_symbols (
id SERIAL PRIMARY KEY,
-- ---- 标识维度(与 klines 表对齐) ----
exchange TEXT NOT NULL, -- 交易所:binance / okx / bybit
symbol TEXT NOT NULL, -- 交易对:BTCUSDT / ETHUSDT
interval TEXT NOT NULL, -- K 线周期:1m / 5m / 15m / 1h / 4h / 1d
-- ---- 控制字段 ----
enabled BOOLEAN NOT NULL DEFAULT TRUE, -- 是否启用采集
priority SMALLINT NOT NULL DEFAULT 0, -- 优先级(数值越大越优先,用于限频时取舍)
-- ---- 备注 ----
label TEXT, -- 人类可读标签,如 "BTC/USDT 1分钟线"
notes TEXT, -- 备注说明
-- ---- 元数据 ----
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- 同一 (exchange, symbol, interval) 组合不可重复
UNIQUE (exchange, symbol, interval)
);
-- 索引:按启用状态快速筛选
CREATE INDEX IF NOT EXISTS idx_monitored_symbols_enabled
ON monitored_symbols (enabled, exchange, priority DESC);
-- 索引:按交易对查找所有周期
CREATE INDEX IF NOT EXISTS idx_monitored_symbols_symbol
ON monitored_symbols (symbol, interval);
-- ============================================================
-- 2. exchange_config — 交易所连接配置
-- ============================================================
-- 用途:存储各交易所的 API 端点、限频参数等连接级配置
-- ⚠️ 安全提醒:API Key/Secret 不应明文存储于此表,
-- 建议通过环境变量或 Vault 注入,此表仅存非敏感参数
CREATE TABLE IF NOT EXISTS exchange_config (
id SERIAL PRIMARY KEY,
-- ---- 交易所标识 ----
exchange TEXT NOT NULL UNIQUE, -- 交易所:binance / okx / bybit
-- ---- 连接参数 ----
rest_url TEXT, -- REST API 基础 URL(留空则用 SDK 默认值)
ws_url TEXT, -- WebSocket 基础 URL(留空则用 SDK 默认值)
ws_ping_interval_ms INTEGER NOT NULL DEFAULT 30000, -- 心跳间隔(毫秒)
-- ---- 限频控制 ----
rate_limit_per_sec REAL NOT NULL DEFAULT 20.0, -- 每秒最大请求数
max_reconnect_attempts INT NOT NULL DEFAULT 10, -- 最大重连次数
reconnect_delay_ms INT NOT NULL DEFAULT 3000, -- 重连延迟基数(指数退避)
-- ---- 开关 ----
enabled BOOLEAN NOT NULL DEFAULT TRUE, -- 是否启用该交易所
-- ---- 备注 ----
notes TEXT,
-- ---- 元数据 ----
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ============================================================
-- 3. app_config — 全局应用配置(Key-Value
-- ============================================================
-- 用途:存储不适合硬编码的运行时参数,如批量写入阈值
CREATE TABLE IF NOT EXISTS app_config (
id SERIAL PRIMARY KEY,
key TEXT NOT NULL UNIQUE, -- 配置键
value TEXT NOT NULL, -- 配置值(统一存为文本,消费方自行解析类型)
description TEXT, -- 说明
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 默认配置项(仅首次插入,已存在则跳过)
INSERT INTO app_config (key, value, description) VALUES
('batch_size', '500', '批量写入缓冲区条数阈值'),
('flush_interval_ms', '1000', '缓冲区最大等待时间(毫秒)'),
('log_level', 'info', '日志级别:trace / debug / info / warn / error'),
('redis_publish_enabled', 'true', '是否启用 Redis 发布')
ON CONFLICT (key) DO NOTHING;
-- ============================================================
-- 4. 预置种子数据
-- ============================================================
-- 预置 Binance 主力交易对监控配置(仅首次插入)
INSERT INTO monitored_symbols (exchange, symbol, interval, enabled, priority, label) VALUES
-- Binance 主力交易对 — 1m
('binance', 'BTCUSDT', '1m', TRUE, 10, 'BTC/USDT 1分钟线'),
('binance', 'ETHUSDT', '1m', TRUE, 9, 'ETH/USDT 1分钟线'),
('binance', 'SOLUSDT', '1m', TRUE, 8, 'SOL/USDT 1分钟线'),
('binance', 'BNBUSDT', '1m', TRUE, 7, 'BNB/USDT 1分钟线'),
-- Binance 主力交易对 — 1h(策略用)
('binance', 'BTCUSDT', '1h', TRUE, 10, 'BTC/USDT 1小时线'),
('binance', 'ETHUSDT', '1h', TRUE, 9, 'ETH/USDT 1小时线'),
('binance', 'SOLUSDT', '1h', TRUE, 8, 'SOL/USDT 1小时线'),
-- Binance 主力交易对 — 1d(日线)
('binance', 'BTCUSDT', '1d', TRUE, 10, 'BTC/USDT 日线'),
('binance', 'ETHUSDT', '1d', TRUE, 9, 'ETH/USDT 日线')
ON CONFLICT (exchange, symbol, interval) DO NOTHING;
-- 预置交易所默认连接配置(仅首次插入)
INSERT INTO exchange_config (exchange, rate_limit_per_sec, notes) VALUES
('binance', 20.0, 'Binance 现货 — 默认权重限频 1200/min'),
('okx', 10.0, 'OKX 现货 — 默认限频 10/s'),
('bybit', 10.0, 'Bybit 现货 — 默认限频 10/s')
ON CONFLICT (exchange) DO NOTHING;
-- ============================================================
-- 初始化完成
-- ============================================================
DO $$
BEGIN
RAISE NOTICE '002_config.sql — Config tables initialized.';
RAISE NOTICE 'Tables: monitored_symbols, exchange_config, app_config';
RAISE NOTICE 'Seed data: 9 symbols (Binance), 3 exchanges';
END $$;
-5320
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -13,13 +13,15 @@
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"@timescaledb/typeorm": "^0.0.1",
"binance": "^3.5.9", "binance": "^3.5.9",
"ccxt": "^4.5.56", "ccxt": "^4.5.56",
"ioredis": "^5.11.1", "ioredis": "^5.11.1",
"pg": "^8.21.0", "pg": "^8.21.0",
"pino": "^10.3.1", "pino": "^10.3.1",
"ws": "^8.21.0", "rxjs": "^7.8.2",
"zod": "^4.4.3" "typeorm": "^1.0.0",
"yaml": "^2.9.0"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "^1.3.14", "@types/bun": "^1.3.14",
-19
View File
@@ -1,19 +0,0 @@
import ccxt from "ccxt";
import { ExchangeWs } from '../exchanges/binance';
const exchange = new ExchangeWs({
exchangeId: 'binance',
symbols: ['BTCUSDT'],
interval: '1m',
});
// exchange.on('kline', (data) => {
// console.log(data);
// });
exchange.on('ready', () => {
console.log(exchange.getState());
console.log(exchange.getReadySymbols());
});
const ccxtClient = new ccxt.binance();
-177
View File
@@ -1,177 +0,0 @@
-- ============================================================
-- schema/config.sql — 配置表 DDL(参考副本)
-- ============================================================
-- 数据库:TimescaleDB (PostgreSQL 17)
-- 说明:管理系统配置、监控标的、交易所连接参数
--
-- ⚠️ 权威初始化脚本已迁移至:data/init-db/001_init.sql
-- 本文件保留作为 pg.initSchema() 非 Docker 部署的回退方案和文档参考。
-- 修改表结构时请同步更新 001_init.sql。
-- ============================================================
-- ============================================================
-- 1. monitored_symbols — 监控交易对配置
-- ============================================================
-- 用途:声明数据采集模块需要订阅哪些交易对的 K 线流
-- 消费方:WebSocket 行情采集服务启动时读取此表决定订阅列表
-- ============================================================
CREATE TABLE IF NOT EXISTS monitored_symbols (
id SERIAL PRIMARY KEY,
-- ---- 标识维度(与 klines 表对齐) ----
exchange TEXT NOT NULL, -- 交易所:binance / okx / bybit
symbol TEXT NOT NULL, -- 交易对:BTCUSDT / ETHUSDT
interval TEXT NOT NULL, -- K 线周期:1m / 5m / 15m / 1h / 4h / 1d
-- ---- 控制字段 ----
enabled BOOLEAN NOT NULL DEFAULT TRUE, -- 是否启用采集
priority SMALLINT NOT NULL DEFAULT 0, -- 优先级(数值越大越优先,用于限频时取舍)
-- ---- 备注 ----
label TEXT, -- 人类可读标签,如 "BTC/USDT 1分钟线"
notes TEXT, -- 备注说明
-- ---- 元数据 ----
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- 同一 (exchange, symbol, interval) 组合不可重复
UNIQUE (exchange, symbol, interval)
);
-- 索引:按启用状态快速筛选
CREATE INDEX IF NOT EXISTS idx_monitored_symbols_enabled
ON monitored_symbols (enabled, exchange, priority DESC);
-- 索引:按交易对查找所有周期
CREATE INDEX IF NOT EXISTS idx_monitored_symbols_symbol
ON monitored_symbols (symbol, interval);
COMMENT ON TABLE monitored_symbols IS '监控交易对配置表:声明哪些 (交易所,交易对,周期) 需要采集 K 线数据';
COMMENT ON COLUMN monitored_symbols.exchange IS '交易所标识:binance / okx / bybit';
COMMENT ON COLUMN monitored_symbols.symbol IS '交易对:BTCUSDT / ETHUSDT';
COMMENT ON COLUMN monitored_symbols.interval IS 'K 线周期:1m / 5m / 15m / 1h / 4h / 1d';
COMMENT ON COLUMN monitored_symbols.enabled IS '是否启用 WebSocket 订阅';
COMMENT ON COLUMN monitored_symbols.priority IS '优先级(0-32767),限频时高优先级交易对优先保留';
-- ============================================================
-- 2. exchange_config — 交易所连接配置
-- ============================================================
-- 用途:存储各交易所的 API 端点、限频参数等连接级配置
-- 安全提醒:API Key/Secret 不应明文存储于此表,
-- 建议通过环境变量或 Vault 注入,此表仅存非敏感参数
-- ============================================================
CREATE TABLE IF NOT EXISTS exchange_config (
id SERIAL PRIMARY KEY,
-- ---- 交易所标识 ----
exchange TEXT NOT NULL UNIQUE, -- 交易所:binance / okx / bybit
-- ---- 连接参数 ----
rest_url TEXT, -- REST API 基础 URL(留空则用 SDK 默认值)
ws_url TEXT, -- WebSocket 基础 URL(留空则用 SDK 默认值)
ws_ping_interval_ms INTEGER NOT NULL DEFAULT 30000, -- 心跳间隔(毫秒)
-- ---- 限频控制 ----
rate_limit_per_sec REAL NOT NULL DEFAULT 20.0, -- 每秒最大请求数
max_reconnect_attempts INT NOT NULL DEFAULT 10, -- 最大重连次数
reconnect_delay_ms INT NOT NULL DEFAULT 3000, -- 重连延迟基数(指数退避)
-- ---- 开关 ----
enabled BOOLEAN NOT NULL DEFAULT TRUE, -- 是否启用该交易所
-- ---- 备注 ----
notes TEXT,
-- ---- 元数据 ----
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE exchange_config IS '交易所连接配置表:REST/WS 端点、限频、重连策略';
COMMENT ON COLUMN exchange_config.rest_url IS 'REST API 地址,空则使用 SDK 默认';
COMMENT ON COLUMN exchange_config.ws_url IS 'WebSocket 地址,空则使用 SDK 默认';
COMMENT ON COLUMN exchange_config.rate_limit_per_sec IS '每秒最大请求数(Binance 默认 20/s';
COMMENT ON COLUMN exchange_config.max_reconnect_attempts IS 'WebSocket 断线最大重连次数';
COMMENT ON COLUMN exchange_config.reconnect_delay_ms IS '重连退避基数(实际延迟 = 基数 × 2^attempts';
-- ============================================================
-- 3. app_config — 全局应用配置(Key-Value
-- ============================================================
-- 用途:存储不适合硬编码的运行时参数,如批量写入阈值
-- ============================================================
CREATE TABLE IF NOT EXISTS app_config (
id SERIAL PRIMARY KEY,
key TEXT NOT NULL UNIQUE, -- 配置键
value TEXT NOT NULL, -- 配置值(统一存为文本,消费方自行解析类型)
description TEXT, -- 说明
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE app_config IS '全局应用配置(KV 结构),运行时参数集中管理';
-- 默认配置项
INSERT INTO app_config (key, value, description) VALUES
('batch_size', '500', '批量写入缓冲区条数阈值'),
('flush_interval_ms', '1000', '缓冲区最大等待时间(毫秒)'),
('log_level', 'info', '日志级别:trace / debug / info / warn / error'),
('redis_publish_enabled', 'true', '是否启用 Redis 发布')
ON CONFLICT (key) DO NOTHING;
-- ============================================================
-- 4. 初始数据:预置常见交易对的监控配置
-- ============================================================
-- 以下为建议的默认监控列表,可根据实际需求增删
INSERT INTO monitored_symbols (exchange, symbol, interval, enabled, priority, label) VALUES
-- Binance 主力交易对 — 1m
('binance', 'BTCUSDT', '1m', TRUE, 10, 'BTC/USDT 1分钟线'),
('binance', 'ETHUSDT', '1m', TRUE, 9, 'ETH/USDT 1分钟线'),
('binance', 'SOLUSDT', '1m', TRUE, 8, 'SOL/USDT 1分钟线'),
('binance', 'BNBUSDT', '1m', TRUE, 7, 'BNB/USDT 1分钟线'),
-- Binance 主力交易对 — 1h(策略用)
('binance', 'BTCUSDT', '1h', TRUE, 10, 'BTC/USDT 1小时线'),
('binance', 'ETHUSDT', '1h', TRUE, 9, 'ETH/USDT 1小时线'),
('binance', 'SOLUSDT', '1h', TRUE, 8, 'SOL/USDT 1小时线'),
-- Binance 主力交易对 — 1d(日线)
('binance', 'BTCUSDT', '1d', TRUE, 10, 'BTC/USDT 日线'),
('binance', 'ETHUSDT', '1d', TRUE, 9, 'ETH/USDT 日线')
ON CONFLICT (exchange, symbol, interval) DO NOTHING;
-- 预置交易所默认连接配置
INSERT INTO exchange_config (exchange, rate_limit_per_sec, notes) VALUES
('binance', 20.0, 'Binance 现货 — 默认权重限频 1200/min'),
('okx', 10.0, 'OKX 现货 — 默认限频 10/s'),
('bybit', 10.0, 'Bybit 现货 — 默认限频 10/s')
ON CONFLICT (exchange) DO NOTHING;
-- ============================================================
-- 5. 常用查询示例
-- ============================================================
-- 查询所有启用的监控标的(采集服务启动时使用)
-- SELECT exchange, symbol, interval, priority
-- FROM monitored_symbols
-- WHERE enabled = TRUE
-- ORDER BY exchange, priority DESC, symbol, interval;
-- 查询某交易所下所有交易对
-- SELECT DISTINCT symbol
-- FROM monitored_symbols
-- WHERE exchange = 'binance' AND enabled = TRUE
-- ORDER BY symbol;
-- 禁用某个交易对的采集
-- UPDATE monitored_symbols SET enabled = FALSE, updated_at = NOW()
-- WHERE exchange = 'binance' AND symbol = 'BTCUSDT' AND interval = '1m';
-- 新增监控标的(动态添加,无需重启)
-- INSERT INTO monitored_symbols (exchange, symbol, interval, enabled, priority, label)
-- VALUES ('binance', 'DOGEUSDT', '1m', TRUE, 5, 'DOGE/USDT 1分钟线')
-- ON CONFLICT (exchange, symbol, interval) DO UPDATE
-- SET enabled = TRUE, updated_at = NOW();
-206
View File
@@ -1,206 +0,0 @@
-- ============================================================
-- schema/klines.sql — K 线表 DDL(参考副本)
-- ============================================================
-- 数据库:TimescaleDB (PostgreSQL 17 + timescaledb 2.x)
-- 说明:存储全交易所 OHLCV 数据,按时间自动分区压缩
--
-- ⚠️ 权威初始化脚本:data/init-db/001_init.sql
-- 本文件保留作为 pg.initSchema() 非 Docker 部署的回退方案和文档参考。
-- 修改表结构时请同步更新 001_init.sql。
-- ============================================================
-- ============================================================
-- 1. klines — K 线主表(hypertable
-- ============================================================
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. 转换为 TimescaleDB hypertable
-- ============================================================
-- 按 time 列做 1 天分区,按 exchange 做 4 空间分区
SELECT create_hypertable(
'klines',
'time',
chunk_time_interval => INTERVAL '1 day',
partitioning_column => 'exchange',
number_partitions => 4,
if_not_exists => TRUE
);
-- ============================================================
-- 3. 索引设计
-- ============================================================
-- 主力查询索引(覆盖 95% 查询场景)
-- 用途:按交易对+周期+时间范围查询最新 K 线
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. 压缩策略(列式压缩,压缩比约 90%)
-- ============================================================
ALTER TABLE klines SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'exchange, symbol, interval',
timescaledb.compress_orderby = 'time DESC'
);
-- K 线闭合 7 天后自动触发压缩
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;
-- ---- 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;
-- ---- 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;
-- ---- 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;
-- 连续聚合视图也启用压缩
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);
-- ============================================================
-- 7. 常用查询示例
-- ============================================================
-- 查询最新 N 根 1h K 线
-- SELECT time, open, high, low, close, volume
-- FROM klines_1h
-- WHERE exchange = 'binance' AND symbol = 'BTCUSDT'
-- ORDER BY time DESC LIMIT 100;
-- 查询某个时间范围内的原始 1m K 线
-- SELECT time, open, high, low, close, volume
-- FROM klines
-- WHERE exchange = 'binance' AND symbol = 'ETHUSDT' AND interval = '1m'
-- AND time BETWEEN '2026-06-01' AND '2026-06-06'
-- ORDER BY time ASC;
+4
View File
@@ -17,6 +17,10 @@
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"noEmit": true, "noEmit": true,
// TypeORM decorator support
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictPropertyInitialization": false,
// Best practices // Best practices
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
+20
View File
@@ -0,0 +1,20 @@
// data/src/logger.ts
import pino from "pino";
import { logging } from "../config";
export const logger = pino({
level: logging.level,
// 开发环境:使用 pino-pretty 彩色输出
// 生产环境:JSON 格式,便于 ELK / Loki 采集
...(logging.pretty
? { transport: { target: "pino-pretty", options: { colorize: true } } }
: {}),
// 自动注入模块名
base: { module: "trade-data" },
// 序列化 Error 对象
serializers: {
err: pino.stdSerializers.err,
},
});
export default logger;
+30
View File
@@ -0,0 +1,30 @@
# ============================================================
# Trade System — 统一环境配置(YAML
# ============================================================
# 使用方式:
# data 模块通过 data/config.ts 读取此文件
# Python 模块通过 yaml.safe_load 读取此文件
#
# 修改后重启服务即可生效,无需重新构建。
# ============================================================
# --- TimescaleDB / PostgreSQL 连接 ---
db:
host: localhost
port: 5432
name: trade
user: trader
password: fucketh
# --- Redis 连接 ---
redis:
url: redis://localhost:6379
# 是否启用 Pub/Sub 行情发布(开发时可关闭以节省资源)
publish_enabled: true
# --- 日志 ---
logging:
# 日志级别:trace / debug / info / warn / error / fatal
level: debug
# 运行环境:development 启用 pretty printproduction 输出 JSON
node_env: development