Bläddra i källkod

feat: initial adaptive grid strategy

helium3@sina.com 2 månader sedan
incheckning
e3f2ade13f
73 ändrade filer med 13109 tillägg och 0 borttagningar
  1. 13 0
      .env.example
  2. 22 0
      .eslintrc.cjs
  3. 50 0
      .github/pull_request_template.md
  4. 6 0
      .gitignore
  5. 36 0
      CONTRIBUTING.md
  6. 63 0
      README.md
  7. 139 0
      apps/runner/src/index.js
  8. 832 0
      apps/runner/src/index.ts
  9. 118 0
      config/config.example.yaml
  10. 106 0
      config/config.yaml
  11. 43 0
      config/grid.example.yaml
  12. 153 0
      docs/API_CONNECTOR_SPEC.md
  13. 48 0
      docs/API_SPEC_Pacifica_Signing.md
  14. 268 0
      docs/ARCHITECTURE_DESIGN.md
  15. 269 0
      docs/CODE_DELIVERY_PLAN.md
  16. 29 0
      docs/CONFIG_GUIDE.md
  17. 309 0
      docs/CONFIG_REFERENCE.md
  18. 701 0
      docs/GRID_IMPLEMENTATION_PLAN.md
  19. 557 0
      docs/GRID_STRATEGY_DESIGN.md
  20. 82 0
      docs/IMPLEMENTATION_PLAN.md
  21. 421 0
      docs/MODULE_INTERFACES.md
  22. 205 0
      docs/OPERATIONS_PLAYBOOK.md
  23. 189 0
      docs/PRD_Pacifica_DeltaNeutral_Scalping.md
  24. 198 0
      docs/SEQUENCE_FLOW.md
  25. 174 0
      docs/TESTING_PLAN.md
  26. 505 0
      docs/合规_dex_perp_做市_剥头皮_执行架构(type_script).md
  27. 37 0
      package.json
  28. 531 0
      packages/connectors/pacifica/src/adapter.ts
  29. 72 0
      packages/connectors/pacifica/src/adapterRegistry.ts
  30. 93 0
      packages/connectors/pacifica/src/errors.ts
  31. 9 0
      packages/connectors/pacifica/src/helper.ts
  32. 29 0
      packages/connectors/pacifica/src/metrics.ts
  33. 50 0
      packages/connectors/pacifica/src/rateLimiter.ts
  34. 158 0
      packages/connectors/pacifica/src/signing.ts
  35. 307 0
      packages/connectors/pacifica/src/wsClient.ts
  36. 163 0
      packages/connectors/pacifica/src/wsOrderGateway.ts
  37. 76 0
      packages/domain/src/types.ts
  38. 157 0
      packages/execution/src/globalOrderCoordinator.ts
  39. 164 0
      packages/execution/src/orderRouter.ts
  40. 64 0
      packages/hedge/src/fundingRateMonitor.ts
  41. 25 0
      packages/hedge/src/hedgeEngine.ts
  42. 36 0
      packages/portfolio/src/positionManager.ts
  43. 4 0
      packages/registry/src/index.ts
  44. 50 0
      packages/registry/src/riskAllocator.ts
  45. 131 0
      packages/registry/src/symbolRegistry.ts
  46. 97 0
      packages/registry/src/symbolScorer.ts
  47. 37 0
      packages/registry/src/types.ts
  48. 169 0
      packages/risk/src/riskEngine.ts
  49. 372 0
      packages/strategies/__tests__/gridMaker.test.ts
  50. 766 0
      packages/strategies/src/gridMaker.ts
  51. 26 0
      packages/strategies/src/marketMaker.ts
  52. 27 0
      packages/strategies/src/microScalper.ts
  53. 27 0
      packages/telemetry/src/gridMetrics.ts
  54. 46 0
      packages/telemetry/src/metrics.ts
  55. 95 0
      packages/utils/src/marketDataAdapter.ts
  56. 198 0
      packages/utils/src/shadowBook.ts
  57. 95 0
      packages/utils/src/volatilityEstimator.ts
  58. 2560 0
      pnpm-lock.yaml
  59. 3 0
      pnpm-workspace.yaml
  60. 83 0
      tests/adapterRegistry.test.ts
  61. 97 0
      tests/globalOrderCoordinator.test.ts
  62. 44 0
      tests/marketDataAdapter.test.ts
  63. 95 0
      tests/orderRouter.test.ts
  64. 131 0
      tests/pacificaWsClient.test.ts
  65. 19 0
      tests/rateLimiter.test.ts
  66. 76 0
      tests/riskAllocatorAllocation.test.ts
  67. 72 0
      tests/riskEngine.test.ts
  68. 51 0
      tests/signing.test.ts
  69. 102 0
      tests/symbolRegistry.test.ts
  70. 41 0
      tests/volatilityEstimator.test.ts
  71. 61 0
      tests/wsOrderGateway.test.ts
  72. 18 0
      tsconfig.base.json
  73. 9 0
      vitest.config.ts

+ 13 - 0
.env.example

@@ -0,0 +1,13 @@
+PACIFICA_API_BASE=https://api.pacifica.fi/api/v1
+PACIFICA_TEST_API_BASE=https://test-api.pacifica.fi/api/v1
+PACIFICA_MAKER_ADDRESS=
+PACIFICA_MAKER_PRIVATE_KEY= # Ed25519 private key (base58/base64 as required by Pacifica)
+PACIFICA_HEDGER_ADDRESS=
+PACIFICA_HEDGER_PRIVATE_KEY=
+# Optional global fallback values (used if per-role variables are absent)
+PACIFICA_ACCOUNT_ADDRESS=
+PACIFICA_ACCOUNT_PRIVATE_KEY=
+PACIFICA_SUBACCOUNT=main
+# Legacy names for backward compatibility
+PACIFICA_API_KEY=
+PACIFICA_API_SECRET=

+ 22 - 0
.eslintrc.cjs

@@ -0,0 +1,22 @@
+module.exports = {
+  root: true,
+  env: {
+    node: true,
+    es2022: true
+  },
+  parser: '@typescript-eslint/parser',
+  parserOptions: {
+    ecmaVersion: 2022,
+    sourceType: 'module'
+  },
+  plugins: ['@typescript-eslint'],
+  extends: [
+    'eslint:recommended',
+    'plugin:@typescript-eslint/recommended'
+  ],
+  ignorePatterns: ['dist', 'node_modules'],
+  rules: {
+    '@typescript-eslint/no-explicit-any': 'off',
+    '@typescript-eslint/explicit-module-boundary-types': 'off'
+  }
+};

+ 50 - 0
.github/pull_request_template.md

@@ -0,0 +1,50 @@
+## 概要
+- 说明变更内容与影响面(引用文档章节)。
+
+## 文档引用
+- [ ] `docs/ARCHITECTURE_DESIGN.md`
+- [ ] `docs/IMPLEMENTATION_PLAN.md`
+- [ ] `docs/CODE_DELIVERY_PLAN.md`
+- [ ] `docs/API_CONNECTOR_SPEC.md`
+- [ ] `docs/MODULE_INTERFACES.md`
+- [ ] `docs/SEQUENCE_FLOW.md`
+- [ ] `docs/CONFIG_REFERENCE.md`
+- [ ] `docs/TESTING_PLAN.md`
+- [ ] `docs/OPERATIONS_PLAYBOOK.md`
+- (如适用)`docs/PRD_Pacifica_DeltaNeutral_Scalping.md`
+- **引用章节(填写文件与行号)**:
+  - `docs/ARCHITECTURE_DESIGN.md#L`
+  - `docs/IMPLEMENTATION_PLAN.md#L`
+  - `docs/CODE_DELIVERY_PLAN.md#L`
+  - `docs/API_CONNECTOR_SPEC.md#L`
+  - `docs/MODULE_INTERFACES.md#L`
+  - `docs/SEQUENCE_FLOW.md#L`
+  - `docs/CONFIG_REFERENCE.md#L`
+  - `docs/TESTING_PLAN.md#L`
+  - `docs/OPERATIONS_PLAYBOOK.md#L`
+
+> 若本次改动导致文档更新,请在此列出更新内容,并附“Revision History”记录。
+
+## Codex 准则对齐
+- [ ] 已根据 README 中“Codex 准则”逐条自检(若有未满足项请注明原因)  
+  - 暗猜接口?→ 否  
+  - 模糊执行?→ 否  
+  - 盲想业务?→ 否  
+  - 创造接口?→ 否  
+  - 跳过验证?→ 否  
+  - 破坏架构?→ 否  
+  - 假装理解?→ 否  
+  - 盲目修改?→ 否
+
+## 测试 / 回测
+- [ ] `pnpm lint`
+- [ ] 测试/回测命令:``
+- 结果摘要:
+
+## 检查清单
+- [ ] 已在 Issue/任务中引用相关文档行号
+- [ ] 已更新或确认文档与实现一致
+- [ ] 确认无自成交/合规风险漏洞
+- [ ] 变更内容可追溯(日志/指标/审计)
+
+请确保提交前所有勾选项均为 ✅,否则说明原因。

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+node_modules/
+logs/
+*.log
+.DS_Store
+tmp/
+.idea/

+ 36 - 0
CONTRIBUTING.md

@@ -0,0 +1,36 @@
+# Contributing
+
+文档优先是本仓库的核心原则。任何改动都必须明确对应的文档章节,并在偏离前先更新文档。
+
+## 必读文档
+在编写代码或提出 Issue 之前,请先阅读:
+- `docs/ARCHITECTURE_DESIGN.md`
+- `docs/IMPLEMENTATION_PLAN.md`
+- `docs/CODE_DELIVERY_PLAN.md`
+- (如与策略细节相关)`docs/PRD_Pacifica_DeltaNeutral_Scalping.md`
+
+## 工作流程
+1. **任务定义**:创建 Issue/任务时写明影响范围,并引用上述文档的章节或行号;若计划修改架构或里程碑,先提交文档更新。  
+2. **开发前检查**:确认本次改动与现有文档一致;若发现不一致,优先更新文档并在 PR 中说明。  
+3. **开发执行**:遵循 `docs/CODE_DELIVERY_PLAN.md` 的阶段排布与质量要求,必要时补充自测或回测记录。  
+4. **提交前自检**:
+   - 运行 `pnpm lint` 与相关测试/回测;
+   - 更新或新增文档(如有变更);
+   - 整理引用的文档章节,准备填写 PR 模板。  
+5. **PR 要求**:
+   - 在描述中列出引用的文档章节(格式示例:`docs/ARCHITECTURE_DESIGN.md#L40`);
+   - 若文档已更新,说明更新原因与影响范围;
+   - 附带测试/回测结果;
+   - 勾选 PR 模板中的所有检查项。
+
+## 文档更新规范
+- 修改架构、计划或流程时,务必同步更新对应文档,并在文档底部添加“Revision History”。
+- 若新增模块,请在 `docs/ARCHITECTURE_DESIGN.md` 中描述设计,并在 `docs/CODE_DELIVERY_PLAN.md` 中加入执行计划。
+- 避免在代码中引入与文档不一致的行为;若为临时方案,需注明 TODO 并附引用。
+
+## 沟通节奏
+- 建议每周例行同步一次文档与实现差异;
+- 重大决策需在 Issue/PR 中讨论并记录,确保历史可追溯;
+- 所有讨论结论需回写到文档对应位置。
+
+感谢遵守文档优先的协作方式,让策略实现始终与设计保持一致。

+ 63 - 0
README.md

@@ -0,0 +1,63 @@
+# Pacifica Delta-Neutral + Dual-Sided Scalping — TS Skeleton
+
+## Overview
+本仓库提供一个面向 Pacifica 永续合约的多账户 Delta 中性策略骨架,涵盖连接器、执行路由、风控、策略、行情管线和监控模块,便于快速验证网格/剥头皮等中性策略。
+
+核心模块:
+- **connectors**:Pacifica REST/WebSocket 适配器、签名、限频/指标、账户注册表
+- **utils**:ShadowBook、MarketDataAdapter 等行情聚合工具
+- **execution**:OrderRouter(滑点、post-only、节流、STP)与 GlobalOrderCoordinator
+- **risk / portfolio / hedge**:RiskEngine(kill-switch)、PositionManager、HedgeEngine、FundingRateMonitor
+- **strategies**:GridMaker、MarketMaker、MicroScalper
+- **apps/runner**:加载配置、注册账户、启动策略与风险/行情管线
+
+## Quick Start
+```bash
+pnpm install
+cp .env.example .env
+cp config/config.example.yaml config/config.yaml
+pnpm dev
+# 常用检查
+pnpm lint
+pnpm test
+pnpm typecheck
+```
+
+## Logging
+- Runner 默认使用 `pino` 结构化日志,除标准输出外,还会写入 `logs/runner-<timestamp>.log`(目录自动创建)。启动时会在控制台打印实际 log 文件路径,便于后续复盘。
+- 可用环境变量定制行为:
+  - `LOG_DIR`:日志目录(默认 `logs`,仅在 `LOG_FILE` 未指定时生效)
+  - `LOG_FILE`:自定义日志文件(接受相对或绝对路径)
+  - `LOG_FILE_LEVEL`:写入文件的最低级别(默认跟随 `LOG_LEVEL`,例如设置 `debug` 捕获全量明细)
+- 结构化日志可以直接供 Codex 或外部工具重放,建议在本地调试时保留原始 JSON,必要时再结合 `pino-pretty` 做人类可读打印。
+
+## Docs-first Workflow
+- 开发前务必查阅并遵循:`docs/ARCHITECTURE_DESIGN.md`, `docs/IMPLEMENTATION_PLAN.md`, `docs/CODE_DELIVERY_PLAN.md`, `docs/API_CONNECTOR_SPEC.md`, `docs/MODULE_INTERFACES.md`, `docs/SEQUENCE_FLOW.md`, `docs/CONFIG_REFERENCE.md`, `docs/TESTING_PLAN.md`, `docs/OPERATIONS_PLAYBOOK.md`
+- 若实现与文档不一致,先更新文档再提交代码,并在 PR 描述说明差异。
+- PR 模板要求列出引用章节与测试结果,CI 应覆盖 `pnpm lint` / `pnpm test` / `pnpm typecheck`。
+
+## Codex 准则
+1. 以暗猜接口为耻,以认真查阅为荣  
+2. 以模糊执行为耻,以寻求确认为荣  
+3. 以盲想业务为耻,以人类确认为荣  
+4. 以创造接口为耻,以复用现有为荣  
+5. 以跳过验证为耻,以主动测试为荣  
+6. 以破坏架构为耻,以遵循规范为荣  
+7. 以假装理解为耻,以诚实无知为荣  
+8. 以盲目修改为耻,以谨慎重构为荣
+
+## Reference Documentation
+- **架构与规划**:`docs/ARCHITECTURE_DESIGN.md`, `docs/IMPLEMENTATION_PLAN.md`, `docs/CODE_DELIVERY_PLAN.md`
+- **接口规范**:`docs/API_CONNECTOR_SPEC.md`, `docs/MODULE_INTERFACES.md`
+- **流程与配置**:`docs/SEQUENCE_FLOW.md`, `docs/CONFIG_REFERENCE.md`
+- **质量保障与运维**:`docs/TESTING_PLAN.md`, `docs/OPERATIONS_PLAYBOOK.md`
+
+## Tests
+- 单元测试位于 `tests/` 目录,使用 Vitest (`pnpm test`)
+- `pnpm lint` 运行 ESLint (`@typescript-eslint`),`pnpm typecheck` 执行 `tsc --noEmit`
+- 建议在 PR 中附测试日志与关键指标(REST 请求指标、delta_abs、kill-switch 状态等)
+
+## Notes
+- 运行前需在 `.env` 中填写 Pacifica API 密钥,`config/config.yaml` 例子说明账户、策略、风险、执行等参数
+- `AdapterRegistry` 支持 maker/hedger 多账户,Residence of funding 监控通过 `FundingRateMonitor`
+- TODO(见计划):接入真实 WS 行情、完善资金费率管线、补充更多单测/集成测试

+ 139 - 0
apps/runner/src/index.js

@@ -0,0 +1,139 @@
+"use strict";
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+    return new (P || (P = Promise))(function (resolve, reject) {
+        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+        step((generator = generator.apply(thisArg, _arguments || [])).next());
+    });
+};
+var __generator = (this && this.__generator) || function (thisArg, body) {
+    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
+    return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
+    function verb(n) { return function (v) { return step([n, v]); }; }
+    function step(op) {
+        if (f) throw new TypeError("Generator is already executing.");
+        while (g && (g = 0, op[0] && (_ = 0)), _) try {
+            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
+            if (y = 0, t) op = [op[0] & 2, t.value];
+            switch (op[0]) {
+                case 0: case 1: t = op; break;
+                case 4: _.label++; return { value: op[1], done: false };
+                case 5: _.label++; y = op[1]; op = [0]; continue;
+                case 7: op = _.ops.pop(); _.trys.pop(); continue;
+                default:
+                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
+                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
+                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
+                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
+                    if (t[2]) _.ops.pop();
+                    _.trys.pop(); continue;
+            }
+            op = body.call(thisArg, _);
+        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
+        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
+    }
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+require("dotenv/config");
+var node_fs_1 = require("node:fs");
+var yaml_1 = require("yaml");
+var pino_1 = require("pino");
+var shadowBook_1 = require("../../packages/utils/src/shadowBook");
+var adapter_1 = require("../../packages/connectors/pacifica/src/adapter");
+var orderRouter_1 = require("../../packages/execution/src/orderRouter");
+var marketMaker_1 = require("../../packages/strategies/src/marketMaker");
+var microScalper_1 = require("../../packages/strategies/src/microScalper");
+var gridMaker_1 = require("../../packages/strategies/src/gridMaker");
+var hedgeEngine_1 = require("../../packages/hedge/src/hedgeEngine");
+var logger = (0, pino_1.default)({ level: process.env.LOG_LEVEL || "info" });
+var cfg = (0, yaml_1.parse)((0, node_fs_1.readFileSync)("config/config.yaml", "utf8"));
+var baseUrl = process.env.PACIFICA_API_BASE || cfg.api_base;
+var adapter = new adapter_1.PacificaAdapter({
+    baseUrl: baseUrl,
+    apiKey: process.env.PACIFICA_API_KEY,
+    secret: process.env.PACIFICA_API_SECRET,
+    subaccount: process.env.PACIFICA_SUBACCOUNT || "main",
+});
+var shadow = new shadowBook_1.ShadowBook();
+function refreshBook(symbol) {
+    return __awaiter(this, void 0, void 0, function () {
+        var _a, _b, e_1;
+        return __generator(this, function (_c) {
+            switch (_c.label) {
+                case 0:
+                    _c.trys.push([0, 2, , 3]);
+                    _b = (_a = shadow).set;
+                    return [4 /*yield*/, adapter.getBook(symbol)];
+                case 1:
+                    _b.apply(_a, [_c.sent()]);
+                    return [3 /*break*/, 3];
+                case 2:
+                    e_1 = _c.sent();
+                    logger.error({ err: e_1 }, "book refresh failed");
+                    return [3 /*break*/, 3];
+                case 3: return [2 /*return*/];
+            }
+        });
+    });
+}
+var router = new orderRouter_1.OrderRouter(function (o) { return adapter.place(o); }, function () { return shadow.get(); }, { maxBps: 5 });
+// Initialize HedgeEngine (需要对冲账户 adapter,这里暂用同一个 adapter)
+var hedgeEngine = new hedgeEngine_1.HedgeEngine(cfg.hedge || { kp: 0.6, ki: 0.05, Qmax: 0.4, minIntervalMs: 200 }, function (o) { return adapter.place(o); }, function () { return shadow.mid(); });
+function main() {
+    return __awaiter(this, void 0, void 0, function () {
+        var strategyMode, symbol, gridConfig, gridMaker_2, mm_1, scalp_1;
+        return __generator(this, function (_a) {
+            switch (_a.label) {
+                case 0:
+                    strategyMode = cfg.strategy_mode || 'scalper';
+                    symbol = cfg.symbols[0] || "BTC";
+                    logger.info({ symbol: symbol, baseUrl: baseUrl, strategyMode: strategyMode }, "runner start");
+                    // 行情刷新
+                    setInterval(function () { return refreshBook(symbol); }, 250);
+                    if (!(strategyMode === 'grid' || strategyMode === 'both')) return [3 /*break*/, 2];
+                    logger.info({ gridConfig: cfg.grid }, 'Starting Grid strategy');
+                    gridConfig = {
+                        symbol: cfg.grid.symbol || symbol,
+                        gridStepBps: cfg.grid.grid_step_bps,
+                        gridRangeBps: cfg.grid.grid_range_bps,
+                        baseClipUsd: cfg.grid.base_clip_usd,
+                        maxLayers: cfg.grid.max_layers,
+                        hedgeThresholdBase: cfg.grid.hedge_threshold_base
+                    };
+                    gridMaker_2 = new gridMaker_1.GridMaker(gridConfig, router, hedgeEngine, shadow);
+                    // 初始化网格
+                    return [4 /*yield*/, gridMaker_2.initialize()];
+                case 1:
+                    // 初始化网格
+                    _a.sent();
+                    // 模拟 Fill 事件监听(实际需要从 adapter 或 WebSocket 获取)
+                    // adapter.onFill((fill: Fill) => gridMaker.onFill(fill));
+                    // 定期输出状态
+                    setInterval(function () {
+                        var status = gridMaker_2.getStatus();
+                        logger.info({ status: status }, 'Grid status');
+                    }, 30000); // 每 30 秒
+                    _a.label = 2;
+                case 2:
+                    if (strategyMode === 'scalper' || strategyMode === 'both') {
+                        logger.info('Starting MarketMaker + Scalper strategies');
+                        mm_1 = new marketMaker_1.MarketMaker({ symbol: symbol, tickSz: 0.5, clipSz: 0.001, spreadBps: cfg.mm.spread_bps }, router, function () { return shadow.get(); });
+                        scalp_1 = new microScalper_1.MicroScalper({
+                            symbol: symbol,
+                            clipSz: 0.001,
+                            triggerSpreadBps: cfg.scalper.trigger.spread_bps,
+                            tpBps: cfg.scalper.tp_bps,
+                            slBps: cfg.scalper.sl_bps,
+                            cooldownMs: cfg.scalper.trigger.min_cooldown_ms
+                        }, router, function () { return shadow.get(); });
+                        setInterval(function () { return mm_1.onTick(); }, cfg.mm.reprice_ms);
+                        setInterval(function () { return scalp_1.onBook(); }, 150);
+                    }
+                    return [2 /*return*/];
+            }
+        });
+    });
+}
+main().catch(function (e) { logger.error(e); process.exit(1); });

+ 832 - 0
apps/runner/src/index.ts

@@ -0,0 +1,832 @@
+import 'dotenv/config';
+import { readFileSync, mkdirSync } from "node:fs";
+import { dirname, join, resolve } from "node:path";
+import { parse } from "yaml";
+import pino from "pino";
+import { ShadowBook } from "../../../packages/utils/src/shadowBook";
+import { MarketDataAdapter } from "../../../packages/utils/src/marketDataAdapter";
+import { AdapterRegistry } from "../../../packages/connectors/pacifica/src/adapterRegistry";
+import { OrderRouter } from "../../../packages/execution/src/orderRouter";
+import { GlobalOrderCoordinator } from "../../../packages/execution/src/globalOrderCoordinator";
+import { MarketMaker } from "../../../packages/strategies/src/marketMaker";
+import { MicroScalper } from "../../../packages/strategies/src/microScalper";
+import { GridMaker, type AdaptiveGridConfig } from "../../../packages/strategies/src/gridMaker";
+import { HedgeEngine } from "../../../packages/hedge/src/hedgeEngine";
+import { PositionManager } from "../../../packages/portfolio/src/positionManager";
+import { RiskEngine } from "../../../packages/risk/src/riskEngine";
+import { PacificaWebSocket } from "../../../packages/connectors/pacifica/src/wsClient";
+import { PacificaWsOrderGateway } from "../../../packages/connectors/pacifica/src/wsOrderGateway";
+import type { Order, Fill } from "../../../packages/domain/src/types";
+import type { KillSwitchConfig } from "../../../packages/risk/src/riskEngine";
+
+const logger = createRunnerLogger();
+const cfg = parse(readFileSync("config/config.yaml","utf8"));
+
+const baseUrl = cfg.api_base || process.env.PACIFICA_API_BASE || "https://api.pacifica.fi/api/v1";
+
+type AccountConfigEntry = {
+  api_base?: string;
+  address?: string;
+  private_key?: string;
+  api_key?: string;
+  secret?: string;
+  subaccount?: string;
+  role?: string;
+};
+
+type MarketMakerRawConfig = {
+  tick_sz?: number;
+  clip_sz?: number;
+  spread_bps?: number;
+  reprice_ms?: number;
+};
+
+type ScalperRawConfig = {
+  clip_sz?: number;
+  tp_bps?: number;
+  sl_bps?: number;
+  on_book_interval_ms?: number;
+  trigger?: {
+    spread_bps?: number;
+    min_cooldown_ms?: number;
+  };
+};
+
+const adapterRegistry = new AdapterRegistry();
+
+const accountsConfig: Record<string, AccountConfigEntry> = cfg.accounts || {};
+const accountEntries = Object.entries(accountsConfig);
+const wsGatewayConfigs: Array<{ id: string; address?: string; privateKey?: string }> = [];
+const addressToAccountId = new Map<string, string>();
+const fillHandlers = new Map<string, Array<(fill: Fill) => Promise<void>>>();
+let killSwitchActive = false;
+let gridMakerInstance: GridMaker | undefined;
+let marketMakerInstance: MarketMaker | undefined;
+let scalperInstance: MicroScalper | undefined;
+let gridStatusTimer: NodeJS.Timeout | undefined;
+let gridTickTimer: NodeJS.Timeout | undefined;
+let marketMakerTimer: NodeJS.Timeout | undefined;
+let scalperTimer: NodeJS.Timeout | undefined;
+
+function registerFillHandler(accountId: string | undefined, handler: (fill: Fill) => Promise<void>): void {
+  if (!accountId || killSwitchActive) return;
+  const existing = fillHandlers.get(accountId);
+  if (existing) {
+    existing.push(handler);
+    return;
+  }
+  fillHandlers.set(accountId, [handler]);
+}
+
+const fallbackAddress =
+  process.env.PACIFICA_ACCOUNT_ADDRESS ||
+  process.env.PACIFICA_API_KEY;
+const fallbackPrivateKey =
+  process.env.PACIFICA_ACCOUNT_PRIVATE_KEY ||
+  process.env.PACIFICA_API_SECRET;
+const fallbackSubaccount = process.env.PACIFICA_SUBACCOUNT || "main";
+
+if (accountEntries.length === 0) {
+  adapterRegistry.register("maker", {
+    baseUrl,
+    apiKey: fallbackAddress,
+    secret: fallbackPrivateKey,
+    subaccount: fallbackSubaccount
+  }, "maker");
+  if (fallbackAddress && fallbackPrivateKey) {
+    wsGatewayConfigs.push({ id: "maker", address: fallbackAddress, privateKey: fallbackPrivateKey });
+    addressToAccountId.set(fallbackAddress, "maker");
+  } else {
+    logger.warn("WS gateway disabled for maker account: missing address/private key");
+  }
+} else {
+  for (const [id, value] of accountEntries) {
+    if (typeof value !== "object" || value === null) continue;
+    const upper = id.toUpperCase();
+    const address =
+      value.address ||
+      value.api_key ||
+      process.env[`${upper}_ADDRESS`] ||
+      process.env[`${upper}_API_KEY`] ||
+      fallbackAddress;
+    const privateKey =
+      value.private_key ||
+      value.secret ||
+      process.env[`${upper}_PRIVATE_KEY`] ||
+      process.env[`${upper}_API_SECRET`] ||
+      fallbackPrivateKey;
+    const subaccount =
+      value.subaccount ||
+      process.env[`${upper}_SUBACCOUNT`] ||
+      fallbackSubaccount;
+    adapterRegistry.register(
+      id,
+      {
+        baseUrl: value.api_base || baseUrl,
+        apiKey: address,
+        secret: privateKey,
+        subaccount
+      },
+      value.role || id
+    );
+    if (address && privateKey) {
+      wsGatewayConfigs.push({ id, address, privateKey });
+      addressToAccountId.set(address, id);
+    } else {
+      logger.warn({ accountId: id }, "WS gateway disabled: missing address/private_key");
+    }
+  }
+}
+
+const makerEntry = adapterRegistry.findEntryByRole("maker") ?? adapterRegistry.list()[0];
+if (!makerEntry) {
+  throw new Error("No Pacifica adapters configured");
+}
+const makerAccountId = makerEntry.id;
+const makerAdapter = makerEntry.adapter;
+const hedgerAccountId = adapterRegistry.findEntryByRole("hedger")?.id ?? makerAccountId;
+
+const shadow = new ShadowBook();
+
+async function main(){
+  const strategyMode = cfg.strategy_mode || 'scalper';
+const primarySymbol = Array.isArray(cfg.symbols) && cfg.symbols.length ? cfg.symbols[0] : "BTC";
+const defaultGridSymbol = cfg.grid?.symbol || primarySymbol;
+  const marketMakerCfg: MarketMakerRawConfig = cfg.mm || {};
+  const scalperCfg: ScalperRawConfig = cfg.scalper || {
+    trigger: { spread_bps: 2, min_cooldown_ms: 500 },
+    tp_bps: 3,
+    sl_bps: 6,
+    clip_sz: 0.001
+  };
+
+  logger.info({ symbol: primarySymbol, baseUrl, strategyMode }, "runner start");
+
+  const marketData = new MarketDataAdapter({
+    symbols: [primarySymbol],
+    shadowBook: shadow,
+    fetchSnapshot: (symbol: string) => makerAdapter.getOrderBook(symbol),
+    pollIntervalMs: cfg.market_data?.poll_interval_ms ?? 1000
+  });
+
+  await marketData.start();
+  marketData.on('error', payload => {
+    logger.warn({ symbol: payload.symbol, error: payload.error }, 'market data error');
+  });
+
+  const positionManager = new PositionManager();
+  const globalCoordinator = new GlobalOrderCoordinator();
+  const riskEngine = new RiskEngine(
+    {
+      maxBaseAbs: cfg.risk?.max_base_abs ?? 0,
+      maxNotionalAbs: cfg.risk?.max_notional_abs ?? 0,
+      maxOrderSz: cfg.risk?.max_order_sz ?? 0
+    },
+    mapKillSwitch(cfg.risk?.kill_switch)
+  );
+
+  const triggerKillSwitch = async (source?: string) => {
+    if (killSwitchActive) return;
+    killSwitchActive = true;
+    const status = riskEngine.getStatus();
+    const gridStatusSnapshot = gridMakerInstance?.getStatus();
+    logger.error({ status, gridStatus: gridStatusSnapshot, source }, 'Kill-switch activated; halting strategies');
+
+    const hookUrl = process.env.KILL_SWITCH_WEBHOOK;
+    if (hookUrl && typeof fetch === 'function') {
+      try {
+        await fetch(hookUrl, {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify({
+            timestamp: new Date().toISOString(),
+            source,
+            status,
+            gridStatus: gridStatusSnapshot
+          })
+        });
+      } catch (error) {
+        logger.warn({ error }, 'Failed to notify kill-switch webhook');
+      }
+    }
+
+    if (gridStatusTimer) {
+      clearInterval(gridStatusTimer);
+      gridStatusTimer = undefined;
+    }
+    if (gridTickTimer) {
+      clearInterval(gridTickTimer);
+      gridTickTimer = undefined;
+    }
+    if (marketMakerTimer) {
+      clearInterval(marketMakerTimer);
+      marketMakerTimer = undefined;
+    }
+    if (scalperTimer) {
+      clearInterval(scalperTimer);
+      scalperTimer = undefined;
+    }
+
+    if (gridMakerInstance) {
+      await gridMakerInstance.shutdown().catch(error => {
+        logger.error({ error }, 'Failed to shutdown GridMaker');
+      });
+      gridMakerInstance = undefined;
+    }
+
+    marketMakerInstance = undefined;
+    scalperInstance = undefined;
+    fillHandlers.clear();
+
+    const outstanding = globalCoordinator.list();
+    await Promise.allSettled(outstanding.map(async snapshot => {
+      try {
+        const adapter = adapterRegistry.get(snapshot.accountId);
+        const symbol = snapshot.symbol ?? defaultGridSymbol;
+        if (snapshot.clientOrderId) {
+          await adapter.cancelByClientId(snapshot.clientOrderId, symbol);
+        } else {
+          await adapter.cancel(snapshot.orderId, symbol);
+        }
+        globalCoordinator.release(snapshot.orderId);
+      } catch (error) {
+        logger.error({ orderId: snapshot.orderId, clientOrderId: snapshot.clientOrderId, error }, 'Failed to cancel outstanding order');
+      }
+    }));
+  };
+
+  const maybeTriggerKillSwitch = async (source?: string) => {
+    if (killSwitchActive) return;
+    if (!riskEngine.shouldHalt()) return;
+    await triggerKillSwitch(source);
+  };
+
+  const refreshRisk = async (symbol: string) => {
+    try {
+      const snapshots = await adapterRegistry.collectPositions(symbol);
+      const aggregated = await positionManager.snapshot(symbol, snapshots);
+      const mid = shadow.mid(symbol) ?? aggregated.accounts[0]?.entryPx ?? 0;
+      riskEngine.updateDeltaAbs(aggregated.base);
+      const equity = aggregated.quote + aggregated.base * mid;
+      riskEngine.updateEquity(equity);
+      await maybeTriggerKillSwitch('risk_refresh');
+    } catch (error) {
+      logger.error({ symbol, error }, 'Failed to refresh risk');
+    }
+  };
+
+  const dispatchOrder = async (order: Order): Promise<{ id: string }> => {
+    const accountId = order.accountId ?? makerAccountId;
+    order.accountId = accountId;
+
+    globalCoordinator.validate({
+      accountId,
+      symbol: order.symbol,
+      side: order.side,
+      price: order.px
+    });
+
+    const snapshots = await adapterRegistry.collectPositions(order.symbol);
+    const aggregated = await positionManager.snapshot(order.symbol, snapshots);
+    const mid = shadow.mid(order.symbol) ?? order.px;
+
+    riskEngine.updateDeltaAbs(aggregated.base);
+    const equity = aggregated.quote + aggregated.base * mid;
+    riskEngine.updateEquity(equity);
+    riskEngine.preCheck(order, aggregated, mid);
+    if (killSwitchActive || riskEngine.shouldHalt()) {
+      await maybeTriggerKillSwitch('pre_check');
+      throw new Error('kill-switch active');
+    }
+
+    const adapter = adapterRegistry.get(accountId);
+    const { id } = await adapter.place(order);
+
+    globalCoordinator.register({
+      orderId: id,
+      clientOrderId: order.clientId,
+      accountId,
+      symbol: order.symbol,
+      side: order.side,
+      price: order.px,
+      timestamp: Date.now()
+    });
+
+    return { id };
+  };
+
+  const router = new OrderRouter(
+    async (order: Order) => dispatchOrder(order),
+    (sym: string) => shadow.snapshot(sym),
+    {
+      maxBps: cfg.execution?.max_slippage_bps ?? 5,
+      minIntervalMs: cfg.execution?.min_order_interval_ms ?? 0
+    }
+  );
+
+  const routeOrderUpdate = (accountId: string, update: any) => {
+    const orderId = update?.order_id ?? update?.orderId;
+    if (!orderId) return;
+    const statusRaw = (update?.status ?? update?.state ?? '').toString().toLowerCase();
+    if (statusRaw === 'filled' || statusRaw === 'canceled' || statusRaw === 'cancelled' || statusRaw === 'expired' || statusRaw === 'rejected') {
+      globalCoordinator.release(orderId);
+    }
+  };
+
+ const routeFill = async (accountId: string, fill: Fill) => {
+    try {
+      const handlers = fillHandlers.get(accountId);
+      if (handlers) {
+        for (const handler of handlers) {
+          await handler(fill);
+        }
+      }
+      globalCoordinator.release(fill.orderId);
+      await refreshRisk(fill.symbol);
+      if (accountId === hedgerAccountId) {
+        riskEngine.recordHedgeSuccess();
+      }
+      await maybeTriggerKillSwitch('fill');
+    } catch (error) {
+      logger.error({ accountId, fill, error }, 'Failed to process fill');
+    }
+  };
+
+  await setupWsOrderGateways({
+    wsUrl: cfg.ws_url,
+    routeFill,
+    routeOrder: routeOrderUpdate,
+    routeAccount: async (accountId, update) => {
+      try {
+        const symbols = extractSymbolsFromAccount(update);
+        if (symbols.size === 0) {
+          await refreshRisk(primarySymbol);
+        } else {
+          for (const symbol of symbols) {
+            await refreshRisk(symbol);
+          }
+        }
+        await maybeTriggerKillSwitch('account');
+      } catch (error) {
+        logger.error({ accountId, update, error }, 'Failed to process account update');
+      }
+    }
+  });
+
+  router.attachCancelHandlers(
+    async orderId => {
+      try {
+        const snapshot = globalCoordinator.peek(orderId);
+        const accountId = snapshot?.accountId ?? makerAccountId;
+        const adapter = adapterRegistry.get(accountId);
+        const symbol = snapshot?.symbol ?? defaultGridSymbol;
+        if (snapshot?.clientOrderId) {
+          await adapter.cancelByClientId(snapshot.clientOrderId, symbol);
+          globalCoordinator.release(orderId);
+        } else {
+          await adapter.cancel(orderId, symbol);
+          globalCoordinator.release(orderId);
+        }
+      } catch (error) {
+        logger.error({ orderId, error: normalizeError(error) }, 'Failed to cancel order');
+      }
+    },
+    async clientId => {
+      try {
+        const snapshot = globalCoordinator.peekByClientId(clientId);
+        const accountId = snapshot?.accountId ?? makerAccountId;
+        const adapter = adapterRegistry.get(accountId);
+        const symbol = snapshot?.symbol ?? defaultGridSymbol;
+        await adapter.cancelByClientId(clientId, symbol);
+        globalCoordinator.releaseByClientId(clientId);
+      } catch (error) {
+        logger.error({ clientId, error: normalizeError(error) }, 'Failed to cancel by clientId');
+      }
+    }
+  );
+
+  const hedgeEngine = new HedgeEngine(
+    cfg.hedge || { kp: 0.6, ki: 0.05, Qmax: 0.4, minIntervalMs: 200 },
+    async (order: Order) => {
+      if (killSwitchActive) {
+        throw new Error('kill-switch active');
+      }
+      try {
+        const result = await dispatchOrder({ ...order, accountId: order.accountId ?? hedgerAccountId });
+        riskEngine.recordHedgeSuccess();
+        return result;
+      } catch (error) {
+        riskEngine.recordHedgeFailure();
+        await maybeTriggerKillSwitch('hedge_failure');
+        throw error;
+      }
+    },
+    () => shadow.mid(primarySymbol)
+  );
+
+  // 根据策略模式启动不同策略
+  if (strategyMode === 'grid' || strategyMode === 'both') {
+    logger.info({ gridConfig: cfg.grid }, 'Starting Grid strategy');
+
+    const gridConfig = {
+      symbol: cfg.grid.symbol || primarySymbol,
+      gridStepBps: cfg.grid.grid_step_bps,
+      gridRangeBps: cfg.grid.grid_range_bps,
+      baseClipUsd: cfg.grid.base_clip_usd,
+      maxLayers: cfg.grid.max_layers,
+      hedgeThresholdBase: cfg.grid.hedge_threshold_base,
+      accountId: cfg.grid.account_id || makerAccountId,
+      tickSize: cfg.grid.tick_size,
+      lotSize: cfg.grid.lot_size
+    };
+
+    const adaptiveConfig: AdaptiveGridConfig | undefined = cfg.grid.adaptive?.enabled
+      ? {
+          enabled: true,
+          volatilityWindowMinutes: cfg.grid.adaptive.volatility_window_minutes ?? 30,
+          minVolatilityBps: cfg.grid.adaptive.min_volatility_bps ?? 20,
+          maxVolatilityBps: cfg.grid.adaptive.max_volatility_bps ?? 200,
+          minGridStepBps: cfg.grid.adaptive.min_grid_step_bps ?? 10,
+          maxGridStepBps: cfg.grid.adaptive.max_grid_step_bps ?? 100,
+          recenterEnabled: cfg.grid.adaptive.recenter_enabled ?? true,
+          recenterThresholdBps: cfg.grid.adaptive.recenter_threshold_bps ?? 150,
+          recenterCooldownMs: cfg.grid.adaptive.recenter_cooldown_ms ?? 300_000,
+          minStepChangeRatio: cfg.grid.adaptive.min_step_change_ratio ?? 0.2,
+          minSamples: cfg.grid.adaptive.min_samples,
+          maxCadenceMs: cfg.grid.adaptive.max_cadence_ms,
+          hedgePendingTimeoutMs: cfg.grid.adaptive.hedge_pending_timeout_ms,
+          postOnlyCushionBps: cfg.grid.adaptive.post_only_cushion_bps ?? 5
+        }
+      : undefined;
+
+    const cancelAllOrders = async (symbol: string) => {
+      try {
+        const adapter = adapterRegistry.get(gridConfig.accountId);
+        await adapter.cancelAll(symbol);
+      } catch (error) {
+        logger.error({ symbol, error: normalizeError(error) }, 'Grid cancel_all failed');
+        throw error;
+      }
+    };
+
+    const releaseOrder = (orderId: string, clientOrderId?: string) => {
+      if (clientOrderId) {
+        globalCoordinator.releaseByClientId(clientOrderId);
+      }
+      if (orderId) {
+        globalCoordinator.release(orderId);
+      }
+    };
+
+    const gridMaker = new GridMaker(
+      gridConfig,
+      router,
+      hedgeEngine,
+      shadow,
+      logger,
+      adaptiveConfig,
+      cancelAllOrders,
+      releaseOrder
+    );
+    gridMakerInstance = gridMaker;
+
+    registerFillHandler(gridConfig.accountId, async fill => {
+      if (fill.symbol !== gridConfig.symbol) return;
+      await gridMaker.onFill(fill);
+    });
+
+    registerFillHandler(hedgerAccountId, async fill => {
+      if (fill.symbol !== gridConfig.symbol) return;
+      await gridMaker.onHedgeFill(fill);
+    });
+
+    // 初始化网格
+    await gridMaker.initialize();
+
+    // 模拟 Fill 事件监听(实际需要从 adapter 或 WebSocket 获取)
+    // adapter.onFill((fill: Fill) => gridMaker.onFill(fill));
+
+    // 定期输出状态
+    gridStatusTimer = setInterval(() => {
+      if (killSwitchActive) return;
+      const status = gridMaker.getStatus();
+      logger.info({ status }, 'Grid status');
+    }, 30000); // 每 30 秒
+
+    gridTickTimer = setInterval(async () => {
+      if (killSwitchActive) return;
+      try {
+        await gridMaker.onTick();
+      } catch (error) {
+        logger.error({ error }, 'GridMaker onTick failed');
+      }
+    }, cfg.grid.adaptive?.tick_interval_ms ?? 60_000);
+  }
+
+  if (strategyMode === 'scalper' || strategyMode === 'both') {
+    logger.info('Starting MarketMaker + Scalper strategies');
+
+    const mm = new MarketMaker(
+      {
+        symbol: primarySymbol,
+        tickSz: marketMakerCfg.tick_sz ?? 0.5,
+        clipSz: marketMakerCfg.clip_sz ?? 0.001,
+        spreadBps: marketMakerCfg.spread_bps ?? 1.6
+      },
+      router,
+      ()=>shadow.snapshot(primarySymbol),
+      logger
+    );
+    marketMakerInstance = mm;
+    registerFillHandler(makerAccountId, async fill => {
+      if (fill.symbol !== primarySymbol) return;
+      await mm.onFill(fill);
+    });
+
+    const scalp = new MicroScalper(
+      {
+        symbol: primarySymbol,
+        clipSz: scalperCfg.clip_sz ?? 0.001,
+        triggerSpreadBps: scalperCfg.trigger?.spread_bps ?? 1.8,
+        tpBps: scalperCfg.tp_bps ?? 3,
+        slBps: scalperCfg.sl_bps ?? 6,
+        cooldownMs: scalperCfg.trigger?.min_cooldown_ms ?? 250
+      },
+      router,
+      ()=>shadow.snapshot(primarySymbol),
+      logger
+    );
+    scalperInstance = scalp;
+    registerFillHandler(makerAccountId, async fill => {
+      if (fill.symbol !== primarySymbol) return;
+      await scalp.onFill(fill);
+    });
+
+    const mmInterval = marketMakerCfg.reprice_ms ?? 300;
+    const scalperInterval = scalperCfg.on_book_interval_ms ?? 150;
+    marketMakerTimer = setInterval(async () => {
+      if (killSwitchActive) return;
+      try {
+        await mm.onTick();
+      } catch (error) {
+        logger.error({ error }, 'MarketMaker onTick failed');
+      }
+    }, mmInterval);
+    scalperTimer = setInterval(async () => {
+      if (killSwitchActive) return;
+      try {
+        await scalp.onBook();
+      } catch (error) {
+        logger.error({ error }, 'MicroScalper onBook failed');
+      }
+    }, scalperInterval);
+  }
+}
+
+main().catch(e=>{ logger.error(e); process.exit(1); });
+
+function createRunnerLogger() {
+  const logLevel = (process.env.LOG_LEVEL ?? "info") as pino.Level;
+  const streams: pino.StreamEntry[] = [
+    { level: logLevel, stream: process.stdout }
+  ];
+
+  let filePath: string | undefined;
+
+  try {
+    const fileLevel = (process.env.LOG_FILE_LEVEL ?? logLevel) as pino.Level;
+    const customPath = process.env.LOG_FILE;
+    const logDir = process.env.LOG_DIR ?? "logs";
+    const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
+    const resolvedPath = resolve(customPath ?? join(logDir, `runner-${timestamp}.log`));
+    mkdirSync(dirname(resolvedPath), { recursive: true });
+    const destination = pino.destination({ dest: resolvedPath, mkdir: true, sync: false });
+    streams.push({ level: fileLevel, stream: destination });
+    filePath = resolvedPath;
+  } catch (error) {
+    console.error("Failed to enable file logging", error);
+  }
+
+  const instance =
+    streams.length > 1
+      ? pino({ level: logLevel }, pino.multistream(streams))
+      : pino({ level: logLevel });
+
+  if (filePath) {
+    instance.info({ logFile: filePath }, "File logging enabled");
+  }
+
+  return instance;
+}
+
+async function setupWsOrderGateways(options: {
+  wsUrl?: string;
+  routeFill: (accountId: string, fill: Fill) => Promise<void>;
+  routeOrder: (accountId: string, update: any) => void;
+  routeAccount: (accountId: string, update: any) => Promise<void>;
+}): Promise<void> {
+  const { wsUrl, routeFill, routeOrder, routeAccount } = options;
+  if (!wsUrl) {
+    logger.warn('No ws_url configured; skipping WebSocket order gateway setup');
+    return;
+  }
+
+  const tasks = wsGatewayConfigs
+    .filter(entry => entry.address && entry.privateKey)
+    .map(async entry => {
+      try {
+        const address = entry.address!;
+        const privateKey = entry.privateKey!;
+        const wsClient = new PacificaWebSocket({
+          url: wsUrl,
+          apiKey: address,
+          secret: privateKey
+        });
+        const gateway = new PacificaWsOrderGateway(wsClient, {
+          apiKey: address,
+          secret: privateKey
+        });
+        wsClient.on('message', message => {
+          handlePrivateMessage(address, message, routeFill, routeOrder, routeAccount);
+        });
+        await gateway.connect();
+        adapterRegistry.get(entry.id).attachWsGateway(gateway);
+        wsClient.subscribeAuthenticated(`fills.${address}`);
+        wsClient.subscribeAuthenticated(`orders.${address}`);
+        logger.info({ accountId: entry.id }, 'WS order gateway connected');
+      } catch (error) {
+        logger.error({ accountId: entry.id, error }, 'Failed to initialize WS order gateway');
+        throw error;
+      }
+    });
+
+  await Promise.all(tasks);
+}
+
+function handlePrivateMessage(
+  address: string,
+  raw: unknown,
+  routeFill: (accountId: string, fill: Fill) => Promise<void>,
+  routeOrder: (accountId: string, update: any) => void,
+  routeAccount: (accountId: string, update: any) => Promise<void>
+): void {
+  const accountId = addressToAccountId.get(address);
+  if (!accountId) return;
+
+  let text: string;
+  if (typeof raw === "string") {
+    text = raw;
+  } else if (raw instanceof Buffer) {
+    text = raw.toString("utf8");
+  } else if (typeof (raw as any)?.toString === "function") {
+    text = (raw as any).toString();
+  } else {
+    return;
+  }
+
+  let parsed: any;
+  try {
+    parsed = JSON.parse(text);
+  } catch {
+    return;
+  }
+
+  const channel = parsed?.channel;
+  if (typeof channel !== "string") return;
+
+  if (channel.startsWith("fills.") && channel.endsWith(address)) {
+    const payload = parsed.data;
+    const records = Array.isArray(payload) ? payload : [payload];
+    for (const record of records) {
+      const fill = mapFillPayload(record);
+      if (!fill) continue;
+      routeFill(accountId, fill).catch(error => {
+        logger.error({ accountId, error }, 'Failed to route fill');
+      });
+    }
+    return;
+  }
+
+  if (channel.startsWith("orders.") && channel.endsWith(address)) {
+    const payload = parsed.data;
+    const records = Array.isArray(payload) ? payload : [payload];
+    for (const record of records) {
+      const update = record ?? {};
+      routeOrder(accountId, update);
+    }
+    return;
+  }
+
+  if (channel.startsWith("account.") && channel.endsWith(address)) {
+    const payload = parsed.data ?? parsed;
+    routeAccount(accountId, payload).catch(error => {
+      logger.error({ accountId, error }, 'Failed to route account update');
+    });
+    return;
+  }
+}
+
+function mapFillPayload(data: any): Fill | undefined {
+  if (!data) return undefined;
+  const orderId = data.order_id ?? data.orderId;
+  const symbol = data.symbol;
+  const rawSide = (data.side ?? data.direction ?? "").toString().toLowerCase();
+  const side = rawSide === "bid" || rawSide === "buy" ? "buy" : rawSide === "ask" || rawSide === "sell" ? "sell" : undefined;
+  const price = Number(data.price ?? data.px);
+  const sizeRaw = Number(data.size ?? data.amount);
+  if (!orderId || !symbol || !side || !Number.isFinite(price) || !Number.isFinite(sizeRaw)) {
+    return undefined;
+  }
+  const size = Math.abs(sizeRaw);
+  const fee = data.fee !== undefined ? Number(data.fee) : 0;
+  const liquidityRaw = (data.liquidity ?? '').toString().toLowerCase();
+  const liquidity = liquidityRaw === 'taker' ? 'taker' : 'maker';
+  const tradeId = String(data.trade_id ?? data.tradeId ?? `${orderId}-${Date.now()}`);
+  const tsValue = data.ts !== undefined ? Number(data.ts) : Date.now();
+  const ts = Number.isFinite(tsValue) ? tsValue : Date.now();
+  return {
+    orderId: String(orderId),
+    tradeId,
+    symbol: String(symbol),
+    side,
+    px: price,
+    sz: size,
+    fee,
+    liquidity: liquidity as "maker" | "taker",
+    ts
+  };
+}
+
+function mapKillSwitch(raw: any): KillSwitchConfig | undefined {
+  if (!raw) return undefined;
+  const drawdownPct = Number(raw.drawdown_pct ?? raw.drawdownPct ?? 0);
+  const triggers = Array.isArray(raw.triggers)
+    ? raw.triggers
+        .map((t: any) => ({
+          type: t.type,
+          threshold: Number(t.threshold)
+        }))
+        .filter((trigger: { type?: string; threshold: number }): trigger is { type: string; threshold: number } => {
+          return typeof trigger.type === "string" && !Number.isNaN(trigger.threshold);
+        })
+    : undefined;
+  return {
+    drawdownPct: Math.abs(drawdownPct),
+    triggers
+  };
+}
+
+function extractSymbolsFromAccount(update: any): Set<string> {
+  const symbols = new Set<string>();
+  if (!update) return symbols;
+
+  const collect = (value: any) => {
+    if (!value) return;
+    const items = Array.isArray(value) ? value : [value];
+    for (const item of items) {
+      const symbol = item?.symbol ?? item?.asset ?? item?.pair;
+      if (typeof symbol === "string" && symbol.trim().length > 0) {
+        symbols.add(symbol.trim());
+      }
+    }
+  };
+
+  if (Array.isArray(update)) {
+    for (const item of update) {
+      const nested = extractSymbolsFromAccount(item);
+      nested.forEach(s => symbols.add(s));
+    }
+    return symbols;
+  }
+
+  collect(update.positions ?? update.position);
+  collect(update.balances ?? update.balance);
+  collect(update.holdings);
+  if (update.symbol) {
+    collect(update);
+  }
+
+  return symbols;
+}
+
+function normalizeError(error: unknown): { message?: string; name?: string; status?: number; code?: string } | undefined {
+  if (!error) return undefined;
+  if (error instanceof Error) {
+    const normalized: { message: string; name: string; status?: number; code?: string } = {
+      message: error.message,
+      name: error.name
+    };
+    const anyErr = error as any;
+    if (typeof anyErr.status === "number") {
+      normalized.status = anyErr.status;
+    }
+    if (anyErr.code !== undefined) {
+      normalized.code = String(anyErr.code);
+    }
+    return normalized;
+  }
+  if (typeof error === "object") {
+    return error as any;
+  }
+  return { message: String(error) };
+}

+ 118 - 0
config/config.example.yaml

@@ -0,0 +1,118 @@
+env: mainnet
+api_base: ${PACIFICA_API_BASE}
+symbols: [BTC, ETH, SOL]
+
+accounts:
+  maker:
+    address: ${PACIFICA_MAKER_ADDRESS}
+    private_key: ${PACIFICA_MAKER_PRIVATE_KEY}
+    subaccount: maker-01
+    role: maker
+  hedger:
+    address: ${PACIFICA_HEDGER_ADDRESS}
+    private_key: ${PACIFICA_HEDGER_PRIVATE_KEY}
+    subaccount: hedger-01
+    role: hedger
+
+# 策略选择:grid(网格), scalper(剥头皮), both(混合)
+strategy_mode: grid  # 推荐先用 grid 验证对冲架构
+
+# ========================================
+# 网格策略配置(Grid Trading Strategy)
+# ========================================
+grid:
+  enabled: true
+
+  # 单标的配置(M1.5 MVP)
+  symbol: BTC
+  grid_step_bps: 100   # 网格间距 1%(基于波动率调整:BTC 0.8-1.2%, ETH 1.0-1.5%, SOL 1.5-2.5%)
+  grid_range_bps: 400  # 网格范围 4%(基于极端波动:BTC 3-5%, ETH 4-6%, SOL 6-10%)
+  base_clip_usd: 500   # 单层订单大小(USD)
+  max_layers: 4        # 单边最大层数
+  hedge_threshold_base: 0.3  # 累积 0.3 BTC 触发对冲(批量对冲模式)
+  tick_size: 1         # 价格步长
+  lot_size: 0.00001    # 最小数量步长
+
+  # 多标的配置(M2.5 增强版,注释掉则使用上面的单标的配置)
+  # symbols:
+  #   - symbol: BTC
+  #     grid_step_bps: 100
+  #     grid_range_bps: 400
+  #     base_clip_usd: 500
+  #     max_layers: 4
+  #   - symbol: ETH
+  #     grid_step_bps: 120
+  #     grid_range_bps: 500
+  #     base_clip_usd: 400
+  #     max_layers: 4
+
+  # 自适应参数(可选)
+  adaptive:
+    enabled: true
+    volatility_window_minutes: 30
+    min_volatility_bps: 20
+    max_volatility_bps: 200
+    min_grid_step_bps: 10
+    max_grid_step_bps: 100
+    recenter_enabled: true
+    recenter_threshold_bps: 150
+    recenter_cooldown_ms: 300000
+    min_step_change_ratio: 0.2
+    tick_interval_ms: 60000
+    hedge_pending_timeout_ms: 30000
+
+  # 趋势检测与暂停(M2.5)
+  trend_filter:
+    enabled: false  # MVP 阶段建议关闭
+    lookback_periods: 12  # 12 * 5min = 1 hour
+    trend_threshold_bps: 50  # 1 小时涨跌 > 0.5% 暂停网格
+
+  # 低波动监控(M2.5)
+  volatility_monitor:
+    enabled: false
+    min_daily_range_bps: 80  # 日内波动 < 0.8% 时告警
+    action: notify  # notify | reduce_step | switch_strategy
+
+# ========================================
+# 被动做市 + 剥头皮策略配置(原有策略)
+# ========================================
+mm:
+  layers: 2
+  base_clip_usd: 1000
+  spread_bps: 1.6
+  reprice_ms: 300
+
+scalper:
+  trigger:
+    spread_bps: 1.8
+    min_cooldown_ms: 250
+  tp_bps: 3
+  sl_bps: 6
+
+risk:
+  max_notional_abs: 100000
+  max_base_abs: 0.8
+  max_order_sz: 0.2
+  kill_switch:
+    drawdown_pct: 0.5
+    triggers:
+      - type: delta_abs
+        threshold: 1.6
+      - type: hedge_failure_count
+        threshold: 3
+      - type: data_gap_sec
+        threshold: 3
+      - type: pnl_drawdown
+        threshold: 1.0
+
+hedge:
+  kp: 0.6
+  ki: 0.05
+  qmax: 0.4
+  min_interval_ms: 200
+
+execution:
+  max_slippage_bps: 5
+  min_order_interval_ms: 250
+funding:
+  poll_interval_ms: 60000

+ 106 - 0
config/config.yaml

@@ -0,0 +1,106 @@
+env: mainnet
+api_base: https://api.pacifica.fi/api/v1
+ws_url: wss://ws.pacifica.fi/ws
+symbols: [BTC, ETH, SOL]
+
+# 策略选择:grid(网格), scalper(剥头皮), both(混合)
+strategy_mode: grid  # 推荐先用 grid 验证对冲架构
+accounts:
+  maker:
+    address: '3v2fE8y6uPVu5pmNCpmygpGNgdP3kGL3SMoVa86uvLLu'
+    private_key: '5r698iSYYz9NgX19igrAAiRSWzhtFmn98oMbb1KbX1J4JAVtmsy9PS27r37Qofqs7rEcYqdvZF9LVuBva6WtdXmw'
+    subaccount: maker-01
+    role: maker
+  hedger:
+    address: 'GkFi4YUFVTYKVqzsT98QiiUVbwuTiXWe8XsbRZRYafv3'
+    private_key: '5RSm4vGQt26ZwHLj1b3gb9TWEU9UEmThxBQ2evubgWxJfXVC5LsNk2x3gnRFZGXq5at3qH82EJq6VgDKPBQvPjud'
+    subaccount: hedger-01
+    role: hedger
+# ========================================
+# 网格策略配置(Grid Trading Strategy)
+# ========================================
+grid:
+  enabled: true
+
+  # 单标的配置(M1.5 MVP)
+  symbol: BTC
+  grid_step_bps: 30   # 网格间距 1%(基于波动率调整:BTC 0.8-1.2%, ETH 1.0-1.5%, SOL 1.5-2.5%)
+  grid_range_bps: 300  # 网格范围 4%(基于极端波动:BTC 3-5%, ETH 4-6%, SOL 6-10%)
+  base_clip_usd: 100   # 单层订单大小(USD)
+  max_layers: 10        # 单边最大层数
+  hedge_threshold_base: 0.12  # 累积 0.3 BTC 触发对冲(批量对冲模式)
+  tick_size: 1         # 价格步长
+  lot_size: 0.00001    # 最小数量步长
+
+  # 多标的配置(M2.5 增强版,注释掉则使用上面的单标的配置)
+  # symbols:
+  #   - symbol: BTC
+  #     grid_step_bps: 100
+  #     grid_range_bps: 400
+  #     base_clip_usd: 500
+  #     max_layers: 4
+  #   - symbol: ETH
+  #     grid_step_bps: 120
+  #     grid_range_bps: 500
+  #     base_clip_usd: 400
+  #     max_layers: 4
+
+  # 自适应参数(M1.5+)
+  adaptive:
+    enabled: true
+    volatility_window_minutes: 20   # 波动率计算窗口
+    min_volatility_bps: 20          # 最低波动率
+    max_volatility_bps: 200         # 最高波动率
+    min_grid_step_bps: 10           # 网格间距下限(配置值,会被盘口价差覆盖)
+    max_grid_step_bps: 100          # 网格间距上限
+    recenter_enabled: true          # 偏离阈值后自动重置
+    recenter_threshold_bps: 150     # 偏离阈值
+    recenter_cooldown_ms: 300000    # 重置冷却时间
+    min_step_change_ratio: 0.2      # 调整间距的最小相对变化
+    tick_interval_ms: 60000         # 自适应检查间隔
+    hedge_pending_timeout_ms: 30000 # 对冲挂单超过阈值仍未成交则告警
+    post_only_cushion_bps: 5        # PostOnly 保护缓冲(防止价格穿过盘口)
+
+  # 趋势检测与暂停(M2.5)
+  trend_filter:
+    enabled: false  # MVP 阶段建议关闭
+    lookback_periods: 12  # 12 * 5min = 1 hour
+    trend_threshold_bps: 50  # 1 小时涨跌 > 0.5% 暂停网格
+
+  # 低波动监控(M2.5)
+  volatility_monitor:
+    enabled: false
+    min_daily_range_bps: 80  # 日内波动 < 0.8% 时告警
+    action: notify  # notify | reduce_step | switch_strategy
+
+# ========================================
+# 被动做市 + 剥头皮策略配置(原有策略)
+# ========================================
+mm:
+  layers: 2
+  base_clip_usd: 1000
+  spread_bps: 1.6
+  reprice_ms: 300
+
+scalper:
+  trigger:
+    spread_bps: 1.8
+    min_cooldown_ms: 250
+  tp_bps: 3
+  sl_bps: 6
+
+risk:
+  max_notional_abs: 100000
+  max_base_abs: 0.8
+  max_order_sz: 1
+  kill_switch_dd_pct: -0.5
+
+hedge:
+  kp: 0.6
+  ki: 0.05
+  qmax: 0.4
+  min_interval_ms: 200
+
+execution:
+  max_slippage_bps: 150
+  min_order_interval_ms: 100

+ 43 - 0
config/grid.example.yaml

@@ -0,0 +1,43 @@
+# Grid strategy starter configuration
+# Set strategy_mode=grid in config.yaml and include these values (adjust per account size).
+
+env: testnet
+api_base: https://api.pacifica.fi/api/v1
+ws_url: wss://ws.pacifica.fi/ws
+
+strategy_mode: grid
+
+grid:
+  enabled: true
+
+  # Single-symbol MVP settings
+  symbol: BTC
+  grid_step_bps: 100          # 1.0% spacing between layers
+  grid_range_bps: 400         # +/-4.0% coverage
+  base_clip_usd: 500          # each layer notional in USD
+  max_layers: 4               # 4 levels per side
+  hedge_threshold_base: 0.3   # accumulate 0.3 BTC before hedging
+
+  adaptive_recenter:
+    enabled: false
+    recenter_threshold_bps: 200
+
+  trend_filter:
+    enabled: false
+    lookback_periods: 12
+    trend_threshold_bps: 50
+
+  volatility_monitor:
+    enabled: false
+    min_daily_range_bps: 80
+    action: notify
+
+risk:
+  max_notional_abs: 100000
+  max_base_abs: 0.8
+
+hedge:
+  kp: 0.6
+  ki: 0.05
+  qmax: 0.4
+  min_interval_ms: 200

+ 153 - 0
docs/API_CONNECTOR_SPEC.md

@@ -0,0 +1,153 @@
+# Pacifica 连接器接口规范
+
+> 覆盖 REST / WebSocket 端点、鉴权、错误处理以及调度策略,为 `packages/connectors/pacifica` 的实现提供统一参考。
+
+---
+
+## 1. REST API
+
+| 功能 | Method | Path | 关键参数 | 返回值 (JSON) | 说明 |
+|------|--------|------|----------|----------------|------|
+| 查询合约配置 | `GET` | `/products/{symbol}` | `symbol` | `tickSize`, `lotSize`, `minOrderSize`, `maxLeverage` | 启动时拉取一次,落盘缓存 |
+| 查询余额 | `GET` | `/account/balances` | `subaccount` | `[{ asset, available, total, equity }]` | 按账户区分,聚合至 RiskEngine |
+| 查询持仓 | `GET` | `/account/positions` | `account`, `subaccount` | `[{ symbol, base, quote, entryPrice, pnl }]` | 每次下单或定时刷新 |
+| 新建订单 | `POST` | `/orders/create` | `symbol`, `side (bid/ask)`, `price`, `amount`, `tif`, `postOnly`, `clientOrderId`, `account`, `subaccount?` | `{ orderId }` | `clientOrderId` 由 Router 生成,需幂等 |
+| 批量撤单 | `POST` | `/orders/cancel` | `orderIds[]` 或 `clientOrderIds[]` | `{ cancelled: string[] }` | 对 Kill-switch、重置网格使用 |
+| 订单列表 | `GET` | `/orders` | `symbol`, `status`, `limit` | `[{ orderId, status, filledSize, avgPrice }]` | 同步本地影子簿 |
+| 成交列表 | `GET` | `/fills` | `symbol`, `since` | `[{ orderId, tradeId, price, size, fee, liquidity }]` | 对账或调试 |
+| 资金费率 | `GET` | `/funding/{symbol}` | `symbol` | `{ rate, markPrice, timestamp }` | HedgeEngine / FundingMonitor 使用 |
+
+### 1.1 请求与签名
+
+```text
+Header:
+  X-Pacific-Key: <account_address>
+  X-Pacific-Timestamp: <epoch_ms>
+  X-Pacific-Signature: <ed25519_signature_base64>
+
+Signature base string = `${timestamp}:${method}:${path}:${body || ''}`
+signature = sign_ed25519(private_key, base_string_bytes)
+```
+
+- `<account_address>` 为 Pacifica 账户地址(原文档中称 `api_key`),与 `config.accounts.*.address` 一致。
+- `timestamp` 精度毫秒,容忍 ±5 秒;偏差超出将返回 `401`。
+- `body` 需按原始 JSON 字符串序列化(不去空格);GET、DELETE 无 body 也需保留空字符串。
+- 提供单元测试覆盖:空 body、数组体、浮点数序列化。
+
+### 1.2 限频与退避
+
+| 端点 | 限速 | 退避策略 |
+|------|------|----------|
+| 下单/撤单 | 60 req/min | 429 → 100ms 起步指数退避 (×2),最多 5 次 |
+| 查询类 | 120 req/min | 429 → 200ms 固定延迟,最多 3 次 |
+| 资金费率 | 10 req/min | 429 → 1s 固定延迟,放弃超过 3 次 |
+
+- 所有调用统一写入 metrics:`pacifica_rest_requests_total{endpoint, status}` 与 `pacifica_rest_latency_ms`。
+- 超过最大重试次数时抛出自定义错误 `PacificaRateLimitError`,由上层降级策略处理。
+
+### 1.3 错误码映射
+
+| HTTP 状态 | Pacifica 错误码 | 含义 | 处理建议 |
+|-----------|-----------------|------|----------|
+| 400 | `INVALID_SIZE` | 订单尺寸不符合 tick/lot | 记录告警,阻止重试 |
+| 400 | `POST_ONLY_WOULD_CROSS` | Post-only 订单会吃单 | Router 调整价差或放弃 |
+| 401 | `UNAUTHENTICATED` | 签名/时间戳错误 | 立即 Halt 账户,通知运维 |
+| 404 | `ORDER_NOT_FOUND` | 撤单目标不存在 | 视为成功,更新本地缓存 |
+| 409 | `DUPLICATE_CLIENT_ID` | clientId 重复 | Router 重新生成 ID 重试一次 |
+| 429 | `RATE_LIMITED` | 超限 | 按退避策略重试 |
+| 500 | `INTERNAL_ERROR` | 服务异常 | 快速重试 3 次,仍失败上报 |
+
+---
+
+## 2. WebSocket API
+
+### 2.1 连接与鉴权
+
+```json
+{
+  "op": "login",
+  "args": {
+    "key": "<account_address>",
+    "timestamp": "<epoch_ms>",
+    "signature": "<ed25519_signature_base64>"
+  }
+}
+```
+
+- 连接 URL:`wss://ws.pacifica.fi/ws`
+- 心跳:客户端每 30 秒发送 `{"op":"ping"}`,收到 `{"op":"pong"}`;10 秒内无响应视为断线。
+- 断线重连需回放增量:`lastSeq` 由订阅回调附带,重连时通过 `{"op":"subscribe","channel":"orders","lastSeq":12345}`。
+
+### 2.2 订阅频道
+
+| 频道 | 事件结构 | 说明 |
+|------|----------|------|
+| `book.{symbol}` | `{ seq, bids:[[px,sz],...], asks:[], ts }` | 影子订单簿、延迟监控 |
+| `trades.{symbol}` | `{ seq, side, price, size, tradeId, ts }` | 剥头皮信号、回测 |
+| `orders.{subaccount}` | `{ seq, orderId, clientId, status, filled, remaining, avgPrice, ts }` | 同步本地状态 |
+| `fills.{subaccount}` | `{ seq, orderId, tradeId, price, size, fee, liquidity, ts }` | GridMaker / Scalper onFill |
+| `account.{subaccount}` | `{ seq, balances:[...], positions:[...] }` | 风控快照 |
+
+### 2.3 消息处理规范
+
+1. 依据 `seq` 单调递增校验;出现跳号立即触发 full snapshot。
+2. 所有事件写入 `events.log`(用于回放),并发送到内存事件总线。
+3. WS 错误回执:
+   ```json
+   { "op":"error", "code":"RATE_LIMITED", "message":"..." }
+   ```
+   - `RATE_LIMITED` → 立刻退订 5 秒,重订。
+   - `AUTH_FAILED` → 触发 Kill-switch。
+
+### 2.4 重连策略
+
+```mermaid
+flowchart TD
+    A[连接断开] --> B{重试次数 < 5?}
+    B -- 是 --> C[等待 backoff (0.5s × 2^n)]
+    C --> D[重连 → login → resubscribe]
+    D -->|成功| E[恢复]
+    D -->|失败| B
+    B -- 否 --> F[触发 HALT + 告警]
+```
+
+---
+
+## 3. 调用封装约定
+
+- 所有 REST 请求均通过 `PacificaHttpClient`,统一附加签名、限频控制、日志。
+- WS 由 `PacificaWebSocket` 封装,提供:
+  - `subscribe(channel, handler)`
+  - `unsubscribe(channel)`
+  - `onReconnect(cb)`:用于重新分发共享 socket。
+- Adapter 暴露 Promise API,错误以自定义错误类区分:
+  - `PacificaValidationError`
+  - `PacificaRateLimitError`
+  - `PacificaAuthError`
+  - `PacificaUnavailableError`
+
+---
+
+## 4. 监控指标
+
+| 指标 | 标签 | 含义 |
+|------|------|------|
+| `pacifica_rest_requests_total` | `endpoint`, `status` | REST 请求计数 |
+| `pacifica_rest_latency_ms_bucket` | `endpoint` | REST 延迟直方图 |
+| `pacifica_ws_events_total` | `channel`, `type` | WS 事件数量 |
+| `pacifica_ws_reconnects_total` |  | 重连次数 |
+| `pacifica_ws_gaps_total` | `channel` | 序列号断层次数 |
+
+报警建议:
+- 5 分钟内 `pacifica_ws_reconnects_total` 增长 > 3:触发网络告警。
+- `pacifica_rest_requests_total{status="429"}` 连续 10 次:提示策略降频。
+
+---
+
+## 5. 集成清单
+
+- [ ] `signing.ts` 单元测试覆盖空 body、有序列化差异、错误私钥。
+- [ ] REST/WS 客户端具备重试、限流、断线恢复。
+- [ ] 统一错误码映射并接入降级策略。
+- [ ] Prometheus 指标注册并在 `Telemetry` 包内导出。
+- [ ] 文档随 API 变化更新版本号。

+ 48 - 0
docs/API_SPEC_Pacifica_Signing.md

@@ -0,0 +1,48 @@
+# Pacifica API 对接规范(仓库内说明)
+> **重要**:以下为仓库集成的对接说明与占位模板。**以官方文档为准**:
+> https://docs.pacifica.fi/api-documentation/api
+
+## 1. 环境
+- 主网 REST: `https://api.pacifica.fi/api/v1`
+- 测试网 REST: `https://test-api.pacifica.fi/api/v1`
+- WebSocket:按官方文档使用(若提供)。
+
+## 2. 鉴权与签名(Ed25519)
+- POST 请求需签名;GET/行情通常无需签名(以官方为准)。
+- 请求必须包含:`timestamp`、`type`(operation type)、`signature`、`X-API-KEY` 等(字段名以官方为准)。
+- 签名输入:**确定性 JSON 序列化** 的请求体;算法:**Ed25519**;编码:Base64/Hex 以官方为准。
+- 本仓库的占位代码:`packages/connectors/pacifica/src/signing.ts`。**你必须**按照官方规范替换 `signRequest` 的实现。
+
+### 签名步骤(模板)
+1. 生成 `timestamp`(毫秒);
+2. 构造 `payload`(字段顺序按官方示例);
+3. 对序列化字节做哈希(如需)→ Ed25519 私钥签名;
+4. 在 Header 中放入 `X-API-KEY`、`X-TS`、`X-SIGNATURE`(或官方要求的键名);
+5. Body 放 `payload` JSON。
+
+## 3. 主要端点(示例清单)
+- 市场规格:`GET /api/v1/info`
+- 订单簿:`GET /api/v1/book?symbol=XXX&agg_level=1`
+- 下单:`POST /api/v1/orders/create`(字段:symbol/side/price/size/tif/postOnly/clientId/...)
+- 撤单:`POST /api/v1/orders/cancel`(字段:orderId/...)
+- 批量撤:`POST /api/v1/orders/cancel_all`
+- 持仓:`GET /api/v1/account/positions`
+- 资金费率/最近成交/K线:参考 `Markets` 章节
+
+> **注意**:以上字段名/路径仅作**仓库内对接导航**,请以官方页面实时为准。
+
+## 4. 错误与限频
+- HTTP 4xx/5xx 需解析并上报;
+- 遵守 `API Config Keys / Rate limits`;
+- 对 429/限频,使用指数退避 + 配额窗口重试。
+
+## 5. STP(自成交预防)
+- 若平台原生支持,仍建议在本地做影子簿检查:
+  - 下单前校验对手价是否为自家挂单;
+  - 必要时先撤旧单再挂新单。
+
+## 6. 本仓库落地位置
+- `packages/connectors/pacifica/src/adapter.ts`:REST 封装;
+- `packages/connectors/pacifica/src/signing.ts`:签名占位;
+- `docs/PRD_Pacifica_DeltaNeutral_Scalping.md`:策略与系统需求;
+- `docs/API_SPEC_Pacifica_Signing.md`:本文。

+ 268 - 0
docs/ARCHITECTURE_DESIGN.md

@@ -0,0 +1,268 @@
+# Pacifica Delta-Neutral 策略架构设计
+
+## 1. 设计目标
+- 在 **Delta≈0** 约束下,通过被动做市与微剥头皮捕捉点差、Maker 返佣与短期均值回归收益。
+- 全链路符合交易场所及监管要求:自成交预防、可审计日志、风控优先。
+- 模块化 TypeScript 架构,可在实盘/回测之间复用同一套策略逻辑,支持多 venue 扩展与持续运维。
+
+### 1.1 文档遵循
+- 架构实施需与以下详细规范保持一致,任何偏离必须先更新对应文档并获评审认可:
+  - `docs/API_CONNECTOR_SPEC.md`:连接层端点、签名与错误处理
+  - `docs/MODULE_INTERFACES.md`:模块接口、依赖图与错误类型
+  - `docs/SEQUENCE_FLOW.md`:行情、策略、执行、对冲、降级的时序
+  - `docs/CONFIG_REFERENCE.md`:配置字段、默认值与热更新策略
+  - `docs/TESTING_PLAN.md`:测试矩阵、验收指标
+  - `docs/OPERATIONS_PLAYBOOK.md`:运维巡检、降级和恢复流程
+- 代码实现、配置模板、测试计划及运维脚本发生变更时,需同步更新以上文档并在 PR 中引用。
+
+## 2. 总体架构
+```
+┌───────────────────────────────────────────────────────────────┐
+│                         Apps / Runners                        │
+│  - live-runner  - canary-runner  - replay-runner (backtest)   │
+└────────────┬─────────────────────────────┬────────────────────┘
+             │                             │
+      ┌──────▼────────┐            ┌───────▼───────────────────┐
+      │ Strategy Bus  │            │ Risk & Compliance Engine │
+      │ (MM, Scalper, │            │ (limits, STP, kill-switch│
+      │ ExecutionPolicy)│          │ audit, policy gating)    │
+      └─────┬──────────┘            └─────────┬────────────────┘
+            │                                   │
+     ┌──────▼─────────┐                  ┌──────▼───────────┐
+     │ Order Router   │◄────Health──────►│ Telemetry & Logs │
+     │ (child orders, │                  │ (Prom, Pino,     │
+     │ slippage guard)│                  │  audit trail)    │
+     └─────┬──────────┘                  └──────┬───────────┘
+           │                                   │
+   ┌───────▼──────────┐            ┌───────────▼──────────────┐
+   │ Exchange Adapter │◄──────────►│ Market Data Pipeline     │
+   │ (Pacifica, +N)   │ (WS/REST)  │ (shadow book, derived
+data)│
+   └────────┬─────────┘            └───────────┬──────────────┘
+            │                                  │
+      ┌─────▼──────────┐              ┌────────▼───────────┐
+      │ Persistence    │              │ Backtest/Simulator │
+      │ (PnL, fills,   │              │ (event replay, fee│
+      │ configs, audit)│              │ model)             │
+      └────────────────┘              └────────────────────┘
+```
+
+**架构优化说明**(2025-10 更新):
+- 新增 **Symbol Registry & Allocator**:管理多标的生命周期,动态分配全局风险预算
+- 新增 **Global Order Coordinator**:跨账户 STP 检查,防止关联账户间的经济自成交
+- 新增 **Strategy Coordinator**:协调 MM 和 Scalper 信号,避免方向冲突和重复对冲成本
+- Exchange Adapter 支持多账户(Account A/B)管理,实现跨账户对冲
+- Market Data Pipeline 增强延迟追踪,支持回测延迟注入防止前瞻偏差
+
+## 3. 模块分解
+### 3.1 App Runner 层
+- `apps/runner`: 负责 DI 容器、配置加载与热更新、生命周期管理(启动、平滑关闭)。
+- 提供 `live-runner`、`canary-runner`、`replay-runner` 三种应用入口,统一注入策略与引擎。
+
+### 3.2 配置与密钥管理
+- `.env`:API base、key、Ed25519 私钥、子账户等敏感信息。
+- `config/config.yaml`:策略参数(symbols、mm、scalper、risk、hedge、telemetry),通过 `zod` 校验与热更新。
+- 配置更新事件写入审计表,支持回滚与差异比对。
+
+### 3.3 Symbol Registry & Allocator(`packages/registry`,新增)
+- 动态管理多标的(BTC/ETH/SOL 等)的启动、暂停、卸载生命周期
+- 全局风险预算分配:将总名义上限、回撤阈值按标的流动性与相关性分配
+- 标的间相关性监控:检测多标的同时持仓时的隐性风险暴露
+- 自动标的筛选器:基于流动性、spread、funding rate 相关性评分,动态启用/禁用标的
+- 配置热更新时,协调各标的策略实例的参数同步
+
+### 3.4 Global Order Coordinator(`packages/execution/globalCoordinator.ts`,新增)
+- **跨账户订单注册表**:维护所有账户/venue 的挂单镜像(clientId + account + symbol + side + px)
+- **跨账户 STP 检查**:
+  - 在任一账户下单前,检查对手价是否来自关联账户的挂单
+  - 若是对冲单(由 HedgeEngine 标记),允许跨账户匹配
+  - 若是策略单(MM/Scalper),拒绝经济自成交
+- **订单冲突检测**:检测同一标的上多个策略的订单是否方向相反或价格重叠
+- **降级策略触发**:当检测到异常(如单账户延迟过高、对冲失败率>阈值),触发该账户的降级模式
+
+### 3.5 Strategy Coordinator(`packages/strategies/coordinator.ts`,新增)
+- **信号聚合与冲突解决**:
+  - MM 和 Scalper 提交"意向订单"(intent)而非直接下单
+  - 检测同一标的上的方向冲突(如 MM sell + Scalper buy)
+  - 冲突解决策略:优先级(MM > Scalper)、合并同向订单、取消相反订单
+- **净仓位变化输出**:将多个策略的意向订单合并为"净仓位变化",避免对冲引擎频繁触发
+- **策略隔离模式**:当某策略表现劣化(EV < 0 持续 N 分钟),自动暂停该策略
+- **全局冷却管理**:跨策略的订单频率限制,避免触发 venue 限频
+
+### 3.6 Exchange Adapter(`packages/connectors/pacifica`)
+- `adapter.ts`:封装 REST 请求(下单、撤单、批量撤、持仓、资金费率、账户信息)。
+- `signing.ts`:Ed25519 签名实现(确定性 JSON 序列化、时间戳、header 注入)。
+- 限频器(token bucket)与重试策略,统一错误码映射。
+- **多账户支持**(优化):每个 adapter 实例绑定一个账户(address + subaccount),通过 `adapterRegistry` 管理 Account A/B。
+- **对冲延迟追踪**(优化):记录每笔对冲单从发起到成交的延迟,暴露 P50/P95/P99 指标。
+
+### 3.7 市场数据管线(`packages/utils/shadowBook.ts`, `packages/utils/marketDataAdapter.ts`)
+- 订阅 REST/WS 行情,合并增量,维护影子订单簿与本地时间戳;市场数据适配器负责定期刷新快照并推送事件。
+- 提供派生特征:`mid`、`spread_bps`、`order_book_imbalance(k)`、短时波动率 `rv`、队列估算 `queue_alpha`。
+- 对数据断流进行健康检查,异常时触发风险引擎 HALT。
+
+### 3.8 Execution / Order Routing(`packages/execution/orderRouter.ts`)
+- 对外接受策略订单(limit/IOC/FOK/postOnly/OCO 子单),路由至指定 adapter。
+- 包含滑点守卫(对比影子簿 top of book)、`clientId` 去重、STP 检查(不得与本地挂单交叉)。
+- 提供批量撤单、定时补单、状态回调接口。
+
+### 3.9 Risk & Compliance(`packages/risk`)
+- `RiskEngine`: 名义/库存/单笔上限,订单前置校验;实时记录 realized PnL,触发回撤熔断。
+- **Kill-switch(增强)**:
+  - **跨账户聚合模式**:计算 Account A + Account B 的总权益与总 PnL,避免单账户误杀
+  - **多维度熔断触发器**:
+    1. PnL 回撤超阈值(-0.5%)
+    2. Delta 绝对值失控(>2x max_base_abs)
+    3. 连续对冲失败次数 >3
+    4. 行情数据断流 >3 秒
+  - **时间窗口回撤**:基于滑动窗口(如 1 小时)计算回撤,而非从启动时刻开始
+- 审计模块:记录信号、决策、风控审批、下单、成交、对冲全过程(trace id)。
+
+### 3.10 策略层(`packages/strategies`)
+- `MarketMaker`: 按 mid±δ 多层挂单;基于波动 regime 调整 spread/clip/reprice 周期。
+- `MicroScalper`: spread 扩张 + 成交流不平衡触发;以 `ExecutionPolicy` 决定被动/主动模式、tp/sl 目标。
+- **`GridMaker`(新增,备选策略)**:
+  - 在价格区间内均匀分布买卖订单网格,捕捉震荡市往返价差
+  - 买单成交 @ price_buy → 在 price_buy * (1 + grid_step) 挂卖单
+  - 卖单成交 @ price_sell → 在 price_sell * (1 - grid_step) 挂买单
+  - 批量对冲:Delta 累积到阈值(如 0.3 BTC)触发对冲引擎
+  - 自适应调整:价格偏离网格中心 >2% 时,撤旧单重新布网格
+  - 趋势检测:1 小时涨跌 >0.5% 时自动暂停,避免单边持仓
+  - **优势**:实现简单(200 行)、对冲频率低(2-5 次/小时 vs 剥头皮 10-20 次)、参数稳定
+  - **适用场景**:震荡市、高波动低趋势、DEX 高延迟环境
+  - **策略切换**:通过 `config.yaml` 中的 `strategy_mode: grid | scalper | both` 选择
+- 通过 `StrategyBus` 协调执行节奏,避免信号竞态;支持启停、参数热更。
+- **策略协调器(混合模式)**:当 `strategy_mode=both` 时,GridMaker 和 Scalper 通过优先级避免冲突(网格优先,剥头皮仅在极端 spread 触发)。
+
+### 3.11 对冲与仓位管理(`packages/portfolio`, `packages/hedge`)
+- `PositionManager`: 聚合多 venue/子账户仓位、计算净 Delta、资金费暴露。
+- `HedgeEngine`: PI 控制器计算对冲量,强制最小间隔,调用对冲 venue 的 IOC/紧限价单;记录 hedge 成本。
+- **对冲延迟预算模型(新增)**:
+  - 目标:对冲延迟 P50 < 500ms, P99 < 2s
+  - 超时处理:若对冲未在 3 秒内完成,触发强制市价平仓
+  - 对冲失败重试:最多重试 2 次,每次增加滑点容忍度 +5bps
+- **资金费率套利风险监控(新增)**:
+  - 监控双账户所在 venue 的 funding rate 相关性
+  - 若相关性 < 0.8 或同向支付,告警并建议减仓
+- 支持资金费率偏置(当期 funding 不利时主动减仓)。
+
+### 3.12 TriggerEngine(`packages/execution/triggerEngine.ts`)
+- 管理 OCO 止盈止损、时间止损、离散触发器;确保与 RiskEngine/OrderRouter 共用校验路径。
+
+### 3.13 Telemetry & Logging(`packages/telemetry`)
+- `prom-client` 暴露指标:`maker_ratio`, `avg_edge_bps`, `real_slip_bps`, `delta_abs`, `hedge_cost_bps`, `latency_p99`, `cancel_rate`, `pnl_intraday`, `stp_hits` 等。
+- **对冲效率指标(新增)**:
+  - `hedge_success_rate`: 对冲订单成交率(目标 >98%)
+  - `hedge_latency_p50/p95/p99`: 从信号到对冲完成的延迟分位数
+  - `hedge_slippage_bps`: 对冲实际成交价 vs 预期价的偏差
+  - `cross_venue_basis_bps`: 双账户所在 venue 的价差,用于发现套利机会或数据异常
+- `pino` 结构化日志 + `pino-pretty` 本地调试;审计日志写入 append-only 存储(文件或数据库)。
+- 与报警系统联动(Grafana/Loki/Slack Webhook)。
+
+### 3.14 Persistence(可插拔)
+- 初期:本地文件或 SQLite 记录 fills、PnL、配置历史、审计日志。
+- 扩展:接入云数据库(PostgreSQL/ClickHouse)用于长周期分析与回测数据管理。
+
+### 3.15 Backtest / Simulator (`packages/backtest`)
+- 事件重放:对实时 `MarketDataEvent` 与 `OrderEvent` 进行回放,驱动同一套策略模块。
+- **延迟注入模块(新增)**:
+  - 记录实盘"链上事件时间 vs 策略接收时间"的延迟分布
+  - 回测时按真实延迟分布注入随机延迟,防止前瞻偏差
+  - 验证:回测 Sharpe 应略低于实盘(若高于则存在数据泄漏)
+- 手续费/滑点模型、资金费率模型注入;输出报表与 KPI。
+- 支持参数搜索、策略回测对比、敏感性分析。
+
+## 4. 数据与控制流(增强版)
+1. **行情路径**:
+   - Multi-venue Adapter 拉取/订阅 → 各 ShadowBook 实例更新 → Derived metrics (OBI/RV/spread)
+   - → Symbol Registry 聚合 → Strategy Bus 消费 → 策略生成意向订单
+
+2. **订单路径(协调版)**:
+   - 策略(MM/Scalper)发出 intent → **Strategy Coordinator 冲突检测与合并**
+   - → **Global Order Coordinator 跨账户 STP 检查**
+   - → RiskEngine 审批(限额/kill-switch)
+   - → OrderRouter 发单(滑点守卫)→ Adapter 执行
+   - → Fill/PnL 回流 RiskEngine/Telemetry + PositionManager
+
+3. **对冲路径(增强版)**:
+   - Fill 触发 → PositionManager 更新净仓(跨账户聚合)
+   - → HedgeEngine 计算对冲量(PI 控制器 + 延迟预算检查)
+   - → 对冲 venue IOC 单(带重试机制)
+   - → 结果回写(成功/失败/延迟)→ Telemetry 记录对冲成本与延迟
+   - → 若失败次数 >3,触发 Global Order Coordinator 降级策略
+
+4. **监控路径**:
+   - 各模块推送指标/日志(含对冲效率指标)→ Telemetry 聚合
+   - → Dashboard/报警系统处理 → 触发降级或 kill-switch
+
+5. **配置热更(金丝雀流程)**:
+   - 配置变更请求 → zod 校验 + 回测烟囱测试
+   - → 金丝雀(单标的试运行 10 分钟)→ KPI 验证
+   - → 若达标则全量发布,否则自动回滚
+   - → 全程审计日志记录(who/when/what/result)
+
+6. **降级路径(新增)**:
+   - 异常检测(对冲失败/延迟过高/数据断流)→ Global Order Coordinator 触发
+   - → 根据降级矩阵执行(撤单/只平不开/暂停策略/HALT)
+   - → 告警推送 + 审计日志
+
+## 5. 合规与风控设计
+- **STP(增强)**:
+  - 单账户内 STP:Router 前置自查,避免与本地挂单交叉
+  - 跨账户经济 STP:Global Order Coordinator 检查对手价是否来自关联账户
+  - 对冲例外:对冲单允许跨账户匹配,但需明确标记
+- **风险限额**:订单前置、仓位持续监控、跨账户聚合 PnL 回撤 Kill-switch、外部命令可触发 HALT。
+- **审计**:所有决策节点生成唯一 trace id,写入 append-only 日志,含参数版本、行情快照摘要。
+- **操作规范**:禁止主动制造虚假成交量,策略仅在合规 venue 与账户运行。
+
+## 5.5 降级策略矩阵(新增)
+
+| 故障场景                  | 检测条件                              | 降级动作                                    | 恢复条件                |
+|--------------------------|--------------------------------------|-------------------------------------------|------------------------|
+| 主账户行情断流            | >5s 无行情更新                        | 撤主账户所有挂单,保留对冲账户               | 行情恢复且延迟 <500ms   |
+| 对冲账户 API 故障          | 连续 3 次对冲失败                     | 主账户切换为"只平不开"模式                   | 对冲账户恢复正常 5 分钟 |
+| 双账户都断流              | 双重断流 >3s                          | 全部撤单 + HALT + 告警                      | 手动恢复                |
+| 对冲延迟过高              | hedge_latency_p95 > 2s 持续 1 分钟   | 暂停新信号,仅处理存量仓位                   | 延迟降至 <1s            |
+| Gas fee 暴涨(链上 DEX)  | 对冲成本 > edge_bps * 2               | 暂停新信号,延长对冲间隔至 1 分钟            | Gas 降至正常水平        |
+| Delta 失控                | abs(delta) > 2x max_base_abs          | 强制市价对冲,暂停所有策略 1 分钟            | Delta 归零              |
+| Funding rate 异常         | 双 venue funding rate 相关性 <0.5     | 减仓至 50%,告警通知人工审查                | 人工确认后恢复          |
+| 策略表现劣化              | 某策略 EV <0 持续 10 分钟             | 自动暂停该策略,保留其他策略运行             | 参数调整后手动启用       |
+| 跨账户 STP 命中率过高     | STP 拒绝率 >5%                        | 暂停 Scalper,仅保留 MM                     | STP 率 <1% 持续 5 分钟  |
+
+**降级策略实现要点**:
+- 所有降级动作触发时,立即写入审计日志并推送告警(Slack/PagerDuty)
+- 降级状态持久化到配置文件,重启后保留降级状态
+- 提供手动干预接口:`/api/override-degradation`(需认证)
+
+## 6. 部署与运维
+- Node 22 + pnpm;容器化(Docker)部署到 Kubernetes/自管主机。
+- 热更:配置文件挂载 + SIGHUP 触发重载;策略参数金丝雀发布。
+- 健康检查:Adapter 连通性、行情延迟、PnL drawdown、STP 命中频次。
+- Playbook:冷启动、波动骤升、数据断流、对冲失败、资金费率极端等场景演练。
+
+## 7. 技术栈与依赖
+- 语言/构建:TypeScript 5、pnpm、ts-node/tsx。
+- 网络:`undici`, `ws`;加密:`@noble/ed25519`。
+- 校验:`zod`;配置:`dotenv`, `cosmiconfig`。
+- 日志:`pino`, `pino-pretty`;指标:`prom-client`。
+- 测试:`vitest`/`jest` + 自定义事件回放框架。
+
+## 8. 运行时序示例
+1. **MarketMaker 定时循环**
+   - `StrategyBus` 触发 `MarketMaker.onTimer` → ShadowBook 提供 mid/spread → 生成双边限价单 → RiskEngine 校验 → OrderRouter(postOnly) 下单 → Telemetry 记录。
+2. **Scalper 行情触发**
+   - `ShadowBook` 发布新行情 → `MicroScalper.onBook` 检测 spread 扩张 → 发起 taker/被动组合 → TriggerEngine 设置 OCO → OrderRouter 执行。
+3. **对冲**
+   - Fill 事件回写 → PositionManager 更新净仓 → HedgeEngine PI 计算 → RiskEngine 审批 → 对冲 Adapter IOC → Telemetry 记录成本。
+4. **Kill-Switch**
+   - RiskEngine 监测 PnL drawdown/数据断流 → 触发 HALT → Router 撤单、策略暂停、报警推送。
+
+## 9. 测试与质量保障
+- 单元测试:签名、影子簿、风控、策略决策函数。
+- 集成测试:模拟行情流 + 订单回执,验证完整链路。
+- 回测:在 CI 中跑缩短数据集作为烟囱测试;PR 需附回测 KPI。
+- 覆盖率与代码质量:ESLint、Prettier、TypeDoc 文档生成。
+
+## 10. 迭代路线
+- 与 `docs/IMPLEMENTATION_PLAN.md` 里程碑保持同步,每阶段结束更新本架构文档状态。
+- 记录关键设计变更(如新 venue、风险策略调整)并在 PR 中说明影响。

+ 269 - 0
docs/CODE_DELIVERY_PLAN.md

@@ -0,0 +1,269 @@
+# Pacifica Delta-Neutral 策略代码交付计划
+
+## 0. 启动准备(第 0 周)
+- **签名链路**:实现 `packages/connectors/pacifica/src/signing.ts`,依据官方规范补全 Ed25519 签名流程,并撰写单元测试验证时间戳、Payload 序列化、Header 注入。
+- **CI 基线**:配置 `pnpm lint`、`pnpm test`,在 PR 流程中强制执行;准备 `.env` 与 `config.yaml` 模板及 `zod` 校验器确保配置完整性。
+- **文档基线**:在启动开发前确认下列规范与当前需求一致,所有代码改动必须遵守或同步更新这些文档:
+  - `docs/API_CONNECTOR_SPEC.md`
+  - `docs/MODULE_INTERFACES.md`
+  - `docs/SEQUENCE_FLOW.md`
+  - `docs/CONFIG_REFERENCE.md`
+  - `docs/TESTING_PLAN.md`
+  - `docs/OPERATIONS_PLAYBOOK.md`
+  - 如需偏离,先更新对应文档并在 PR 描述中说明变更理由与影响范围。
+
+## 0.5 策略选择决策点
+
+在 M1 完成后,团队需要决定优先实现哪个策略:
+
+| 选项 | 时间 | 风险 | 收益潜力 | 建议场景 |
+|------|------|------|---------|---------|
+| **M1.5 网格 MVP** | 2-3 天 | 低 | 中(1-2%/月) | **优先推荐**:快速验证对冲架构 |
+| **M2 剥头皮** | 2-3 周 | 高 | 高(3-5%/月) | 若有充足历史数据支持 EV>0 |
+| **两者并行** | 同时 | 中 | 最高 | 团队 ≥2 人 |
+
+**推荐路径**:M1 → M1.5(网格 MVP)→ 验证 2-4 小时 → 若成功再决定 M2.5(增强网格)或 M2(剥头皮)
+
+---
+
+## 1. M1 核心骨架 + 多账户基础设施(第 1 周)
+- `domain/types.ts`:定义订单、成交、行情、仓位等核心类型,供全局复用。
+- `packages/connectors/pacifica/adapter.ts`:封装下单、撤单、批量撤、持仓、资金费率接口,加入限频器与错误码映射。
+  - **多账户支持(新增)**:每个 adapter 实例绑定一个账户(address + subaccount),通过 adapterRegistry 管理 maker/hedger 账户。
+- 所有连接层实现需对照 `docs/API_CONNECTOR_SPEC.md` 的端点、签名、限频及错误映射;若接口差异无法避免,必须同步更新文档。
+- `packages/registry/` **(新增模块)**:
+  - `SymbolRegistry.ts`:多标的生命周期管理(启动/暂停/卸载)
+  - `RiskAllocator.ts`:全局风险预算分配器,按标的流动性与相关性分配限额
+  - `SymbolScorer.ts`:标的自动评分与筛选(流动性、funding rate 相关性)
+- `packages/execution/globalCoordinator.ts` **(新增)**:
+  - 跨账户订单注册表(clientId + account + symbol + side + px)
+  - 跨账户 STP 检查基础框架
+  - 降级策略触发器接口
+- `packages/utils/shadowBook.ts`:实现影子订单簿合并、Mid/Spread/OBI/RV 等派生特征,并记录行情延迟与断流状态。
+- `packages/execution/orderRouter.ts`:提供限价/IOC/FOK、滑点守卫、PostOnly、`clientId` 去重及 STP 前置检查,输出基础执行指标。
+- `packages/risk/RiskEngine.ts`:实现名义/库存/单笔限额校验、实时 PnL 汇总及 **增强 Kill-switch**:
+  - 跨账户聚合 PnL 与权益
+  - 多维度熔断触发器(PnL/Delta/对冲失败/数据断流)
+  - 1 小时滑动窗口回撤计算
+
+## 1.5. M1.5 网格策略 MVP(可选,+2-3 天)
+
+**目标**:用最小代码量快速验证对冲架构是否可行
+
+**前置条件**:M1 完成
+
+**交付清单**:
+
+- `packages/strategies/gridMaker.ts` **(新建,约 200 行)**:
+  - `GridMaker` 类:核心网格逻辑
+  - `initialize()`: 在当前 mid 周围布置双边网格
+  - `onFill()`: 成交后挂对手单,更新 Delta,检查对冲阈值
+  - `reset()`: 撤销所有旧订单,重新布网格
+  - 批量对冲:Delta 累积到阈值(如 0.3 BTC)触发 HedgeEngine
+  - 实现逻辑需与 `docs/SEQUENCE_FLOW.md` 中的网格流程保持一致,并使用 `docs/CONFIG_REFERENCE.md` 定义的参数字段。
+
+- `packages/domain/src/types.ts` **(扩展)**:
+  - 新增 `GridLevel` 接口定义
+
+- `apps/runner/src/index.ts` **(修改)**:
+  - 根据 `config.strategy_mode` 选择加载 GridMaker 或 Scalper
+  - 监听 Fill 事件,回调 `gridMaker.onFill()`
+
+- `config/config.yaml` **(扩展)**:
+  ```yaml
+  strategy_mode: grid  # grid | scalper | both
+
+  grid:
+    enabled: true
+    symbol: BTC
+    grid_step_bps: 100   # 1%
+    grid_range_bps: 400  # 4%
+    base_clip_usd: 500
+    max_layers: 4
+    hedge_threshold_base: 0.3
+  ```
+
+- `packages/strategies/__tests__/gridMaker.test.ts` **(新建)**:
+  - 测试网格初始化
+  - 测试 Fill 后挂对手单
+  - 测试 Delta 累积与对冲触发
+  - 用例覆盖与断言参照 `docs/TESTING_PLAN.md` 的单元测试要求。
+
+**验收标准**:
+- ✅ 网格初始化成功,双边各 N 层订单
+- ✅ 买单成交后,自动挂卖单在更高价
+- ✅ Delta 累积到阈值,触发对冲
+- ✅ 对冲完成后,Delta 归零
+- ✅ |Delta| 始终 < max_base_abs
+
+**测试计划**:
+- Day 1: 实现 GridMaker 核心逻辑 + Runner 集成
+- Day 2: 单元测试 + 本地 mock 测试
+- Day 3: 测试网小额测试 1-2 小时,观察:成交率、对冲延迟、Delta 控制
+
+**成功判断**:
+- 若测试网运行正常(Delta 控制有效、对冲成功率 >95%、月收益率 >0.5%)
+  → 继续 M2.5(增强网格)或 M2(剥头皮)
+- 若失败
+  → 问题大概率在对冲架构,修复后再回到 M1.5
+
+## 2. M2 策略闭环 + 增强对冲(第 2 周)
+- `packages/strategies/coordinator.ts` **(新增)**:
+  - 策略协调器:MM 和 Scalper 提交 intent,检测方向冲突
+  - 冲突解决策略(优先级/合并/取消)
+  - 净仓位变化输出,避免频繁触发对冲
+  - 接口签名、异常类型与优先级规则需符合 `docs/MODULE_INTERFACES.md` 与 `docs/SEQUENCE_FLOW.md` 中的描述。
+- `packages/strategies/MarketMaker.ts`:基于 mid±δ 的多层挂单逻辑,支持波动分层和定时重挂。
+- `packages/strategies/MicroScalper.ts`:实现 Spread 扩张 + 成交流不平衡触发的微剥头皮策略,含多执行模式与冷却控制。
+- `packages/portfolio/PositionManager.ts` & `packages/hedge/HedgeEngine.ts`:聚合多 venue 仓位,使用 PI 控制器触发跨账户对冲并记录成本。
+  - **对冲延迟追踪(新增)**:记录 P50/P95/P99 延迟,暴露 Prometheus 指标
+  - **对冲重试机制(新增)**:失败自动重试最多 2 次,每次增加滑点容忍度 +5bps
+  - **超时强制平仓(新增)**:若 3 秒内未完成,触发市价平仓
+  - PI 参数、重试与超时阈值应与 `docs/CONFIG_REFERENCE.md`、`docs/TESTING_PLAN.md` 和 `docs/OPERATIONS_PLAYBOOK.md` 对齐。
+- `packages/hedge/fundingRateMonitor.ts` **(新增)**:
+  - 抓取双 venue funding rate 历史
+  - 计算 30 天滚动相关性
+  - 检测同向支付频率,告警与减仓建议
+- `packages/execution/degradationPolicy.ts` **(新增)**:
+  - 降级策略状态机实现(数据断流/对冲失败/Delta 失控等核心场景)
+  - 降级动作执行器(撤单/只平不开/HALT)
+  - 降级状态持久化
+- `packages/execution/TriggerEngine.ts`:管理 OCO 止盈止损、时间止损,与 RiskEngine/Router 共享校验链路。
+- `apps/runner`:构建 `live-runner`、`canary-runner`,整合配置加载、策略注入、生命周期管理。
+  - 启动、热更新、降级与恢复流程必须遵循 `docs/SEQUENCE_FLOW.md` 与 `docs/OPERATIONS_PLAYBOOK.md` 的时序和 SOP。
+
+## 2.5. M2.5 增强版网格(可选,+1 周)
+
+**目标**:在 M1.5 基础上,增加生产级功能,使网格策略可长期稳定运行
+
+**前置条件**:M1.5 完成且验证成功
+
+**交付清单**:
+
+- `packages/strategies/gridMaker.ts` **(扩展,+150 行)**:
+  - **自适应网格中心调整**:
+    - `onTimer()`: 定期检查价格偏离网格中心的程度
+    - 若偏离 >2%,撤旧单并以新 mid 为中心重新布网格
+  - **暂停/恢复机制**:
+    - `pause()`: 撤销所有挂单,停止策略
+    - `resume()`: 重新初始化网格
+  - **多标的支持**:
+    - 每个标的独立管理 grids、currentDelta
+
+- `packages/strategies/trendFilter.ts` **(新建,100 行)**:
+  - `TrendFilter` 类:趋势检测器
+  - `isTrending()`: 检测 1 小时涨跌是否 >0.5%
+  - 维护价格历史队列(lookback_periods = 12 * 5min)
+
+- `packages/strategies/volatilityMonitor.ts` **(新建,80 行)**:
+  - `VolatilityMonitor` 类:低波动监控
+  - `checkDailyRange()`: 计算 24h 价格范围
+  - 若 <0.8%,触发告警或自动缩小 grid_step
+
+- `packages/backtest/gridBacktest.ts` **(新建,300 行)**:
+  - `GridBacktest` 类:网格策略回测框架
+  - `run()`: 事件重放,驱动 GridMaker 逻辑
+  - 输出:总收益、Sharpe、最大回撤、往返次数、对冲成本
+
+- `apps/runner/src/index.ts` **(扩展)**:
+  - 支持多标的并行运行
+  - 集成 TrendFilter 和 VolatilityMonitor
+  - 定时触发 `gridMaker.onTimer()`(检查重置)
+
+- `config/config.yaml` **(扩展)**:
+  ```yaml
+  grid:
+    symbols:  # 支持多标的
+      - symbol: BTC
+        grid_step_bps: 100
+        grid_range_bps: 400
+      - symbol: ETH
+        grid_step_bps: 120
+        grid_range_bps: 500
+
+    adaptive_recenter:
+      enabled: true
+      recenter_threshold_bps: 200
+
+    trend_filter:
+      enabled: true
+      lookback_periods: 12
+      trend_threshold_bps: 50
+
+    volatility_monitor:
+      enabled: true
+      min_daily_range_bps: 80
+      action: notify  # notify | reduce_step | switch_strategy
+  ```
+
+- `config/grid_backtest.example.yaml` **(新建)**:
+  - 回测专用配置模板
+
+- `docs/GRID_BACKTEST_GUIDE.md` **(新建)**:
+  - 回测使用指南
+  - 参数优化建议
+
+**验收标准**:
+- ✅ 网格自动重置触发准确率 >90%
+- ✅ 趋势检测准确率 >70%
+- ✅ 低波动告警及时性 <30 分钟延迟
+- ✅ 多标的(3 个)并行运行无冲突,CPU <30%, 内存 <500MB
+- ✅ 回测月收益率 >0.8%(震荡市数据)
+- ✅ Sharpe >2.5, 最大回撤 <-2%
+
+**测试计划**:
+- Day 1-2: 自适应网格中心 + 趋势过滤器
+- Day 3-4: 低波动监控 + 多标的支持
+- Day 5: 回测框架核心逻辑
+- Day 6: 回测报告与参数优化
+- Day 7: 集成测试 + 文档更新
+
+## 3. M3 回测与参数化(第 3 周)
+- `packages/backtest/Replay.ts`:搭建事件重放框架,复用生产策略接口驱动回测。
+- `packages/backtest/latencyInjector.ts` **(新增)**:
+  - 记录实盘"链上时间 vs 策略接收时间"的延迟分布
+  - 回测时按真实延迟分布注入随机延迟
+  - 验证:回测 Sharpe 应略低于实盘(前瞻偏差检测)
+- 整合费用/资金费率/滑点模型,输出 Sharpe、EV、分桶等报表;撰写回测 CLI。
+- 实现参数搜索器(Grid/Random),保存配置快照,更新审计日志与结果摘要。
+- **配置热更新金丝雀流程(新增)**:
+  - `packages/config/canaryDeployer.ts`:zod 校验 → 回测烟囱测试 → 单标的试运行
+  - KPI 自动验证(EV/delta/cancel_rate),达标则全量发布,否则自动回滚
+  - 审计日志记录(who/when/what/result)
+
+## 4. M4 合规加固与多 venue 扩展(第 4 周)
+- 新增第二 venue/子账户 Adapter,实现跨 venue STP 与一致性维护。
+- **完善 Global Order Coordinator(增强)**:
+  - 跨账户经济 STP 全流程集成(检测关联账户挂单交叉)
+  - 订单冲突检测与自动化解(MM vs Scalper 方向冲突)
+  - 降级策略触发器全面集成
+- `packages/utils/liquidityMonitor.ts` **(新增)**:
+  - top10_depth_usd 实时追踪
+  - clip 动态调整(depth 下降 >30% 时自动降低 clip)
+  - 实际滑点 vs 预期滑点监控与告警(差异 >2bps)
+- **标的筛选器增强**:
+  - `packages/registry/fundingRateScorer.ts`:评估双 venue funding rate 相关性
+  - 自动拒绝相关性 <0.8 或同向支付频率 >20% 的 venue 对
+  - 定期重新评估已上线标的,自动下线不合格标的
+- 审计日志落地(append-only),全链路 trace id;补充断线恢复(快照+增量)流程。
+- 自适应参数策略:根据实时 RV/OBI 调整 spread、层数、clip;完善金丝雀机制。
+
+## 5. M5 运维与持续改进(持续迭代)
+- **完整降级策略矩阵(新增)**:
+  - 实现 ARCHITECTURE_DESIGN.md 5.5 节中的全部 9 种故障场景
+  - 每种场景的检测器、降级动作执行器、恢复条件检查器
+  - 降级状态持久化与审计日志
+- **对冲效率监控面板(新增)**:
+  - Grafana dashboard:hedge_success_rate, hedge_latency_p50/p95/p99, hedge_slippage_bps, cross_venue_basis
+  - 告警规则:hedge_success_rate < 95%, hedge_latency_p99 > 2s, hedge_slippage_bps > 0.5
+- **手动干预 API(新增)**:
+  - `apps/runner/src/api/degradationOverride.ts`:`POST /api/override-degradation`(需 API key 认证)
+  - 允许运维人员强制退出降级模式或触发特定降级动作
+- Telemetry:完善 Prometheus 指标、Grafana 仪表盘、报警策略(delta_abs、hedge_cost、latency_p99、**funding_correlation**、**hedge_success_rate** 等)。
+- SRE Playbook:脚本化常见操作(全部撤单、HALT、Kill-switch、自检、**funding rate 异常处理**),定期演练。
+- 回测回归:建立周期性回测与 KPI 复盘机制,记录参数变更与风险审查结果。
+
+## 协作与质量要求
+- 每个阶段完成后提交代码、更新相关文档(`docs/`)、附带自测结果或回测报表。
+- PR 必须通过 CI,并附上与当前阶段相关的 KPI 截图/日志;重要参数调整需记录在审计日志中。
+- 与 `docs/IMPLEMENTATION_PLAN.md`、`docs/ARCHITECTURE_DESIGN.md` 保持一致性,重大架构变更需同步更新文档。

+ 29 - 0
docs/CONFIG_GUIDE.md

@@ -0,0 +1,29 @@
+# 配置与运行指南(仓库内)
+
+## 1. 环境变量
+复制 `.env.example` 为 `.env` 并填写:
+- PACIFICA_API_BASE / PACIFICA_TEST_API_BASE
+- PACIFICA_MAKER_ADDRESS / PACIFICA_HEDGER_ADDRESS(或统一使用 PACIFICA_ACCOUNT_ADDRESS)
+- PACIFICA_MAKER_PRIVATE_KEY / PACIFICA_HEDGER_PRIVATE_KEY(或 PACIFICA_ACCOUNT_PRIVATE_KEY)
+- PACIFICA_SUBACCOUNT(可选,未提供则使用 `config.yaml` 中的 `subaccount`)
+
+## 2. 策略配置
+复制 `config/config.example.yaml` 为 `config/config.yaml`,调整:
+- `symbols`:推荐 BTC/ETH/SOL 起步;
+- `mm` & `scalper`:点差、层数、clip、冷却、tp/sl;
+- `risk`:名义/库存/回撤阈值;
+- `hedge`:kp/ki/qmax/最小间隔。
+
+## 3. 启动
+```bash
+pnpm i
+cp .env.example .env
+cp config/config.example.yaml config/config.yaml
+pnpm dev
+```
+
+## 4. 下一步
+- 按官方文档完成签名实现;
+- 对齐下单/撤单请求体字段;
+- 接入(可选)WebSocket 行情以替代轮询;
+- 实现 OCO/触发单与回测模块。

+ 309 - 0
docs/CONFIG_REFERENCE.md

@@ -0,0 +1,309 @@
+# 配置参考与运行手册
+
+> 说明 `config/config.yaml` 及关联 `.env` 字段的含义、取值范围、默认值,以及热更新与运行流程。
+
+---
+
+## 1. 总览
+
+配置分为以下章节:
+
+1. `env` & 账户
+2. 策略模式与参数 (`strategy_mode`, `grid`, `scalper`)
+3. 风控 (`risk`)
+4. 对冲 (`hedge`)
+5. 监控与降级 (`telemetry`, `adaptive_mode`, `liquidity`, `funding`)
+
+所有配置经过 `zod` 校验:缺失/类型不匹配会阻止启动。运行期可通过配置热更新,但需要遵循金丝雀流程(见下文)。
+
+---
+
+## 2. 基础配置
+
+| 字段 | 类型 / 默认值 | 描述 | 热更新 |
+|------|----------------|------|--------|
+| `env` | `mainnet` \| `testnet`(默认 `testnet`) | 影响 API base/WS endpoint | 需重启 |
+| `api_base` | URL (`https://api.pacifica.fi/api/v1`) | REST 接口根路径 | 否 |
+| `ws_url` | URL (`wss://ws.pacifica.fi/ws`) | 行情 WebSocket | 否 |
+
+`.env` 需提供:
+```
+PACIFICA_MAKER_ADDRESS=<address>
+PACIFICA_MAKER_PRIVATE_KEY=<base58/base64 ed25519>
+PACIFICA_HEDGER_ADDRESS=<address>
+PACIFICA_HEDGER_PRIVATE_KEY=<private key>
+```
+
+---
+
+## 3. 账户配置
+
+```yaml
+accounts:
+  maker:
+    address: ${PACIFICA_MAKER_ADDRESS}
+    private_key: ${PACIFICA_MAKER_PRIVATE_KEY}
+    subaccount: maker-01
+    role: maker
+  hedger:
+    address: ${PACIFICA_HEDGER_ADDRESS}
+    private_key: ${PACIFICA_HEDGER_PRIVATE_KEY}
+    subaccount: hedger-01
+    role: hedger
+```
+
+- `role` 标识账户用途(如 maker/hedger),Runner 会根据该字段分配策略与对冲。若未填写,则使用账户 ID。
+- `address` 是 Pacifica 账户地址(API key),用于 REST header `X-Pacific-Key` 以及下单 payload 中的 `account` 字段。
+- `private_key` 采用 base58 / base64 表示的 Ed25519 私钥。
+- 如需兼容旧配置,可继续使用 `api_key` / `secret` 字段,Runner 会自动回退处理,但推荐尽快迁移到新命名。
+- 热更新:❌(变更后必须重启以刷新签名上下文)。
+
+---
+
+## 4. 策略模式
+
+```yaml
+strategy_mode: grid  # grid | scalper | both
+```
+
+| 模式 | 描述 | 热更新行为 |
+|------|------|------------|
+| `grid` | 仅运行网格策略 | 支持,下一周期生效 |
+| `scalper` | 仅运行剥头皮 | 支持 |
+| `both` | 同时运行,Coordinator 协调 | 支持,需确保两策略配置完整 |
+
+---
+
+## 5. 网格策略 (`grid`)
+
+```yaml
+grid:
+  enabled: true
+  symbol: BTC
+  grid_step_bps: 30
+  grid_range_bps: 300
+  base_clip_usd: 80
+  max_layers: 10
+  hedge_threshold_base: 0.12
+  tick_size: 1
+  lot_size: 0.00001
+  adaptive:
+    enabled: true
+    volatility_window_minutes: 30
+    min_volatility_bps: 20
+    max_volatility_bps: 200
+    min_grid_step_bps: 10
+    max_grid_step_bps: 100
+    recenter_enabled: true
+    recenter_threshold_bps: 150
+    recenter_cooldown_ms: 300000
+    min_step_change_ratio: 0.2
+    tick_interval_ms: 60000
+```
+
+| 字段 | 取值 / 默认 | 说明 | 热更新 |
+|------|--------------|------|--------|
+| 字段 | 取值 / 默认 | 说明 | 热更新 |
+|------|--------------|------|--------|
+| `grid_step_bps` | `10`–`300` (默认 `30`) | 初始单层间距 (bps),自适应启用时作为参考值 | 支持,立即重挂 |
+| `grid_range_bps` | `200`–`800` | 覆盖范围 | 更新后触发 `reset()` |
+| `base_clip_usd` | >0 | 单层挂单名义 | 支持 |
+| `max_layers` | `2`–`16` | 每侧层数上限 | 支持 |
+| `tick_size` | `>0` (默认 `1`) | 价格步长;下单时按此对报价做整形 | 支持 |
+| `lot_size` | `>0` (默认 `0.00001`) | 数量步长;下单时向下取整到该步长 | 支持 |
+| `adaptive.enabled` | bool | 启用波动率自适应与中心重置 | 支持 |
+| `adaptive.min/max_volatility_bps` | `>0` | 映射区间:短期波动率 → 网格间距 | 支持 |
+| `adaptive.min/max_grid_step_bps` | `>0` | 网格间距上下限 | 支持 |
+| `adaptive.min_step_change_ratio` | `0.1`–`0.5` | 触发重布的最小相对变化 | 支持 |
+| `adaptive.recenter_threshold_bps` | `50`–`400` | 中心偏离阈值 (bps) | 支持 |
+| `adaptive.recenter_cooldown_ms` | ≥60_000 | 单次重心重置后的冷却时间 | 支持 |
+| `adaptive.tick_interval_ms` | ≥10_000 | 自适应检查间隔 | 支持 |
+| `adaptive.hedge_pending_timeout_ms` | 10_000–120_000 | 对冲挂单超时阈值 | 支持 |
+
+> 🎯 **推荐起始配置**:参考 `config/grid.example.yaml`,适用于测试网或小额实盘。根据账户规模调整 `base_clip_usd` 与 `hedge_threshold_base`,并保持 `max_base_abs ≥ hedge_threshold_base × 1.5` 以预留缓冲。
+
+---
+
+## 6. 剥头皮策略 (`scalper`)
+
+```yaml
+scalper:
+  enabled: false
+  trigger:
+    spread_bps: 180
+    obi_threshold: 0.6
+    trade_imbalance: 0.55
+    min_cooldown_ms: 300
+  execution:
+    passive_clip_usd: 400
+    taker_clip_usd: 200
+    tp_bps: 30
+    sl_bps: 60
+```
+
+| 字段 | 说明 | 默认 | 范围 | 热更新 |
+|------|------|------|------|--------|
+| `spread_bps` | 触发最小价差 | 180 | 120–400 | 支持 |
+| `obi_threshold` | 订单簿不平衡阈值 | 0.6 | 0.3–0.8 | 支持 |
+| `trade_imbalance` | 成交流偏置 | 0.55 | 0.4–0.8 | 支持 |
+| `min_cooldown_ms` | 触发冷却 | 300 | ≥100 | 支持 |
+| `tp_bps` | 止盈 | 30 | 10–80 | 支持 |
+| `sl_bps` | 止损 | 60 | 20–120 | 支持 |
+
+热更新后,Scalper 在下一次触发时使用新参数。
+
+---
+
+## 7. 风控 (`risk`)
+
+```yaml
+risk:
+  max_notional_abs: 100000
+  max_base_abs: 0.8
+  max_order_sz: 0.2
+  kill_switch:
+    drawdown_pct: 0.5     # 累计回撤 50bps 触发停机
+    triggers:
+      - type: delta_abs
+        threshold: 1.6    # |Delta| > 1.6 BTC
+      - type: hedge_failure_count
+        threshold: 3      # 连续对冲失败 3 次
+      - type: data_gap_sec
+        threshold: 3      # 行情断流 3 秒
+      - type: pnl_drawdown
+        threshold: 1.0    # 单独的 PnL 触发(可选)
+```
+
+- `max_notional_abs`:策略总名义,若超出 RiskEngine 拒单。
+- `max_base_abs`:库存上限;对网格策略尤为重要。
+- `max_order_sz`:单笔订单上限;适配拆单逻辑。
+- `kill_switch` 支持多触发器;可扩展 `latency_p99`, `funding_correlation`, `stp_rate` 等指标。
+- 热更新:允许调整阈值;若需要更改 kill-switch 类型,应经评审后重启。
+
+---
+
+## 8. 对冲 (`hedge`)
+
+```yaml
+hedge:
+  kp: 0.6
+  ki: 0.05
+  qmax: 0.4
+  min_interval_ms: 200
+  latency_budget:
+    target_p50_ms: 500
+    target_p99_ms: 2000
+    max_exposure_sec: 3
+    retry_max: 2
+    retry_slippage_increment_bps: 5
+```
+
+| 字段 | 说明 | 备注 |
+|------|------|------|
+| `kp`, `ki` | PI 控制参数 | 需结合回测调节 |
+| `qmax` | 单次对冲最大数量 | 通常 ≤ `max_base_abs` |
+| `min_interval_ms` | 对冲节流 | 防止秒内提交多笔 |
+| `max_exposure_sec` | 超时强平阈值 | 超过时发起市价单 |
+| `retry_max` | IOC 重试次数 | 失败记录指标 |
+
+热更新:支持参数变化,但若增大 `qmax` 超过风控则需同步调整 Risk 配置。
+
+---
+
+## 9. 执行 (`execution`)
+
+```yaml
+execution:
+  max_slippage_bps: 5
+  min_order_interval_ms: 250
+
+market_data:
+  poll_interval_ms: 1000
+```
+
+| 字段 | 说明 | 备注 |
+|------|------|------|
+| `max_slippage_bps` | Router 允许的最大滑点阈值 | 超过后拒单,单位 bps |
+| `min_order_interval_ms` | 同账户连续下单的最小间隔 | 防止触发限频或竞态 |
+
+热更新:支持阈值调整,变更后下一次下单立即生效。
+
+---
+
+## 9. 监控与自适应
+
+```yaml
+telemetry:
+  push_interval_ms: 5000
+  enabled_metrics:
+    - maker_ratio
+    - hedge_latency_p99
+
+adaptive_mode:
+  enabled: true
+  rv_threshold_high: 0.5
+  actions_on_high_vol:
+    - disable: scalper
+    - grid.max_layers: 2
+
+liquidity:
+  min_top10_depth_usd: 50000
+  max_clip_ratio: 0.05
+  slippage_alert_bps: 2
+
+funding:
+  min_correlation: 0.8
+  max_same_sign_ratio: 0.2
+  alert_net_cost_bps_per_8h: 1
+```
+
+- `adaptive_mode.actions_on_high_vol` 支持指令:`disable:<strategy>`, `grid.max_layers:<value>`, `hedge.qmax:<value>`等。
+- `liquidity` 被监控模块用于触发降级或调参。
+- `funding` 指标用于 FundingRateMonitor 报警。
+
+---
+
+## 10. 热更新流程
+
+1. 修改 `config.yaml` → `pnpm config:validate`(调用 zod 校验)。
+2. 运行 `pnpm backtest --smoke` 验证核心指标。
+3. `curl POST /api/config/reload` 触发 Runner 加载新配置:
+   - Runner 校验新旧差异,写入审计日志。
+   - 若 `strategy_mode` 变化 → Graceful 停止旧策略,启动新策略。
+4. 观测金丝雀 10 分钟:`delta_abs`, `hedge_latency`, `hedge_success_rate`。
+5. 达标后推广至生产;失败则 `POST /api/config/rollback`。
+
+---
+
+## 11. Runbook(常用操作)
+
+### 11.1 启动
+```bash
+pnpm install
+cp config/config.example.yaml config/config.yaml
+pnpm run build
+pnpm run live -- --config config/config.yaml
+```
+
+### 11.2 停止
+```bash
+curl -X POST http://localhost:4000/api/halt -d '{"reason":"maintenance"}'
+```
+
+### 11.3 查看当前配置
+```bash
+curl http://localhost:4000/api/config/current
+```
+
+### 11.4 金丝雀流程
+```bash
+pnpm run canary -- --config config/new.yaml --symbols BTC
+# 观察 metrics: maker_ratio, delta_abs, hedge_latency_p95
+```
+
+---
+
+## 12. 版本管理
+
+- 配置文档更新需同步 bump `configSchemaVersion`。
+- Runner 在启动日志中打印当前 schema 版本与配置摘要。

+ 701 - 0
docs/GRID_IMPLEMENTATION_PLAN.md

@@ -0,0 +1,701 @@
+# 微网格策略实施计划
+
+**版本**: v1.0.0
+**日期**: 2025-10
+**目标**: 在现有架构基础上快速实现网格策略,作为剥头皮策略的替代/补充方案
+
+---
+
+## 总体策略
+
+### 实施路径
+
+```
+M1.5 (MVP)        M2.5 (增强版)      M3.5 (混合策略)
+   ↓                   ↓                   ↓
+2-3 天            +1 周               +1-2 周
+固定网格          自适应网格           网格+剥头皮
+验证对冲          生产就绪             最大化收益
+```
+
+### 与现有里程碑的关系
+
+```
+M1 (核心骨架) → M1.5 (网格 MVP) → M2 (剥头皮) 或 M2.5 (增强网格)
+                    ↓
+                验证对冲架构
+                    ↓
+            若成功 → 继续优化网格
+            若失败 → 修复对冲再做剥头皮
+```
+
+---
+
+## M1.5 – 网格策略 MVP(2-3 天)
+
+### 目标
+用最小代码量验证:
+1. 双账户对冲架构是否可行
+2. 资金费率是否真的对冲
+3. 对冲延迟是否可接受
+4. 网格策略是否有正 EV
+
+### 范围
+
+#### 包含功能
+- ✅ 固定网格(不支持动态调整)
+- ✅ 单一标的(BTC)
+- ✅ 批量对冲(累积阈值触发)
+- ✅ 基础风控(max_base_abs 熔断)
+- ✅ 复用现有模块(OrderRouter, HedgeEngine, RiskEngine)
+
+#### 不包含功能
+- ❌ 趋势检测
+- ❌ 动态网格中心调整
+- ❌ 多标的支持
+- ❌ 复杂低波动处理
+- ❌ 自适应参数
+
+### 交付清单
+
+#### 1. 核心模块
+
+**`packages/strategies/gridMaker.ts`**(新建,约 200 行)
+```typescript
+export interface GridConfig {
+  symbol: string;
+  gridStepBps: number;     // 网格间距(bps)
+  gridRangeBps: number;    // 网格范围(bps)
+  baseClipUsd: number;     // 单层订单大小(USD)
+  maxLayers: number;       // 最大层数
+  hedgeThresholdBase: number; // 对冲阈值
+}
+
+export interface GridLevel {
+  index: number;           // 网格索引(正=卖单,负=买单)
+  side: Side;
+  px: number;
+  orderId?: string;
+  filled: boolean;
+}
+
+export class GridMaker {
+  private grids: Map<number, GridLevel> = new Map();
+  private currentDelta: number = 0;
+
+  constructor(
+    private config: GridConfig,
+    private router: OrderRouter,
+    private hedgeEngine: HedgeEngine,
+    private shadowBook: ShadowBook
+  ) {}
+
+  // 初始化网格
+  async initialize(): Promise<void>;
+
+  // Fill 回调
+  async onFill(fill: Fill): Promise<void>;
+
+  // 重置网格(清除旧订单,重新布置)
+  async reset(): Promise<void>;
+
+  // 私有方法
+  private async placeGridOrder(index: number, side: Side, px: number): Promise<string>;
+  private findGridLevel(orderId: string): GridLevel | undefined;
+  private updateDelta(fill: Fill): void;
+}
+```
+
+**功能点**:
+- `initialize()`: 在当前 mid 周围布置双边网格
+- `onFill()`: 成交后挂对手单,更新 Delta,检查对冲阈值
+- `reset()`: 撤销所有旧订单,重新布网格
+
+#### 2. 配置扩展
+
+**`config/config.yaml`** 补充:
+```yaml
+# 策略选择
+strategy_mode: grid  # grid | scalper | both
+
+# 网格策略配置
+grid:
+  enabled: true
+  symbol: BTC
+  grid_step_bps: 100   # 1%
+  grid_range_bps: 400  # 4%
+  base_clip_usd: 500
+  max_layers: 4
+  hedge_threshold_base: 0.3  # 累积 0.3 BTC 触发对冲
+```
+
+#### 3. Runner 集成
+
+**`apps/runner/src/index.ts`** 修改:
+```typescript
+const cfg = parse(readFileSync("config/config.yaml","utf8"));
+
+if (cfg.strategy_mode === 'grid' || cfg.strategy_mode === 'both') {
+  const gridMaker = new GridMaker(
+    cfg.grid,
+    router,
+    hedgeEngine,
+    shadow
+  );
+
+  await gridMaker.initialize();
+
+  // 监听 Fill 事件
+  adapter.onFill((fill) => gridMaker.onFill(fill));
+}
+
+if (cfg.strategy_mode === 'scalper' || cfg.strategy_mode === 'both') {
+  // 原有 Scalper 逻辑
+}
+```
+
+#### 4. 类型定义补充
+
+**`packages/domain/src/types.ts`** 补充:
+```typescript
+export interface GridLevel {
+  index: number;
+  side: Side;
+  px: number;
+  orderId?: string;
+  filled: boolean;
+  timestamp: number;
+}
+```
+
+#### 5. 测试用例
+
+**`packages/strategies/__tests__/gridMaker.test.ts`**(新建)
+```typescript
+describe('GridMaker', () => {
+  it('should initialize grid orders correctly');
+  it('should place opposite order on fill');
+  it('should trigger hedge when delta exceeds threshold');
+  it('should not exceed max_base_abs');
+});
+```
+
+### 验收标准
+
+#### 功能验收
+- ✅ 网格初始化成功,双边各 N 层订单
+- ✅ 买单成交后,自动挂卖单在更高价
+- ✅ 卖单成交后,自动挂买单在更低价
+- ✅ Delta 累积到阈值,触发对冲
+- ✅ 对冲完成后,Delta 归零
+
+#### 性能验收
+- ✅ 初始化耗时 < 5s
+- ✅ Fill 处理延迟 < 500ms
+- ✅ 对冲触发延迟 < 1s
+
+#### 风控验收
+- ✅ |Delta| 始终 < max_base_abs
+- ✅ 单笔订单 sz < max_order_sz
+- ✅ Kill-switch 触发时,网格自动暂停
+
+### 时间表
+
+```yaml
+Day 1:
+  - 实现 GridMaker 核心逻辑(initialize + onFill)
+  - 配置扩展
+  - Runner 集成
+
+Day 2:
+  - 测试用例
+  - 本地模拟测试(mock adapter)
+  - 修复 bug
+
+Day 3:
+  - 测试网小额测试(1-2 小时)
+  - 观察指标:成交率、对冲延迟、Delta 控制
+  - 文档更新
+```
+
+---
+
+## M2.5 – 增强版网格(+1 周)
+
+### 目标
+在 MVP 基础上,增加生产级功能,使其可长期稳定运行。
+
+### 新增功能
+
+#### 1. 自适应网格中心调整
+
+**`packages/strategies/gridMaker.ts`** 扩展:
+```typescript
+class GridMaker {
+  private gridCenter: number;  // 当前网格中心
+
+  async onTimer() {
+    const mid = this.shadowBook.mid();
+    const deviation = Math.abs(mid - this.gridCenter) / this.gridCenter;
+
+    if (deviation > this.config.recenterThresholdBps / 1e4) {
+      logger.info(`Grid recenter triggered: deviation=${deviation}`);
+      await this.reset();  // 撤旧单,以新 mid 为中心重新布网格
+      this.gridCenter = mid;
+    }
+  }
+}
+```
+
+**配置**:
+```yaml
+grid:
+  adaptive_recenter:
+    enabled: true
+    recenter_threshold_bps: 200  # 偏离 2% 重置
+```
+
+#### 2. 趋势检测与暂停
+
+**`packages/strategies/trendFilter.ts`**(新建)
+```typescript
+export class TrendFilter {
+  private priceHistory: Array<{ts: number, price: number}> = [];
+
+  isTrending(): boolean {
+    if (this.priceHistory.length < this.config.lookbackPeriods) {
+      return false;
+    }
+
+    const oldest = this.priceHistory[0].price;
+    const latest = this.priceHistory[this.priceHistory.length - 1].price;
+    const change = Math.abs(latest - oldest) / oldest;
+
+    return change > this.config.trendThresholdBps / 1e4;
+  }
+}
+```
+
+**集成到 GridMaker**:
+```typescript
+async onTimer() {
+  if (this.trendFilter.isTrending()) {
+    logger.warn('Trend detected, pausing grid');
+    await this.pause();  // 撤销所有挂单
+    return;
+  }
+
+  // 正常逻辑...
+}
+```
+
+**配置**:
+```yaml
+grid:
+  trend_filter:
+    enabled: true
+    lookback_periods: 12  # 12 * 5min = 1h
+    trend_threshold_bps: 50  # 1h 涨跌 > 0.5%
+```
+
+#### 3. 低波动处理
+
+**`packages/strategies/volatilityMonitor.ts`**(新建)
+```typescript
+export class VolatilityMonitor {
+  async checkDailyRange(): Promise<void> {
+    const range = this.calc24hRange();
+
+    if (range < this.config.minDailyRangeBps / 1e4) {
+      logger.warn(`Low volatility detected: ${range}`);
+
+      switch (this.config.action) {
+        case 'notify':
+          await this.sendAlert('Low volatility');
+          break;
+        case 'reduce_step':
+          this.gridMaker.updateGridStep(0.5);  // 缩小到 0.5%
+          break;
+        case 'switch_strategy':
+          await this.switchToMM();
+          break;
+      }
+    }
+  }
+}
+```
+
+**配置**:
+```yaml
+grid:
+  volatility_monitor:
+    enabled: true
+    min_daily_range_bps: 80
+    action: notify  # notify | reduce_step | switch_strategy
+```
+
+#### 4. 多标的支持
+
+**Runner 修改**:
+```typescript
+const grids = new Map<string, GridMaker>();
+
+for (const gridCfg of cfg.grid.symbols) {
+  const gridMaker = new GridMaker(gridCfg, ...);
+  await gridMaker.initialize();
+  grids.set(gridCfg.symbol, gridMaker);
+}
+```
+
+**配置**:
+```yaml
+grid:
+  symbols:
+    - symbol: BTC
+      grid_step_bps: 100
+      grid_range_bps: 400
+    - symbol: ETH
+      grid_step_bps: 120
+      grid_range_bps: 500
+```
+
+#### 5. 完整回测框架
+
+**`packages/backtest/gridBacktest.ts`**(新建)
+```typescript
+export class GridBacktest {
+  async run(params: {
+    symbol: string;
+    startDate: Date;
+    endDate: Date;
+    gridConfig: GridConfig;
+  }): Promise<BacktestResult> {
+    // 事件重放
+    // 模拟 Fill
+    // 计算 PnL
+    // 输出报告
+  }
+}
+
+export interface BacktestResult {
+  totalReturn: number;
+  sharpeRatio: number;
+  maxDrawdown: number;
+  roundTrips: number;
+  avgProfitPerRound: number;
+  hedgeCost: number;
+  fundingCost: number;
+}
+```
+
+**CLI**:
+```bash
+pnpm backtest:grid --symbol BTC --start 2024-08-01 --end 2024-10-31 --config config/grid_bt.yaml
+```
+
+### 交付清单
+
+```yaml
+新增模块:
+  - packages/strategies/trendFilter.ts (100 行)
+  - packages/strategies/volatilityMonitor.ts (80 行)
+  - packages/backtest/gridBacktest.ts (300 行)
+
+扩展模块:
+  - packages/strategies/gridMaker.ts (+150 行)
+    - adaptive recenter
+    - pause/resume
+    - 多标的支持
+
+配置:
+  - config/config.yaml (新增 adaptive/trend/volatility 配置)
+  - config/grid_backtest.example.yaml (新建)
+
+测试:
+  - packages/strategies/__tests__/trendFilter.test.ts
+  - packages/strategies/__tests__/volatilityMonitor.test.ts
+  - packages/backtest/__tests__/gridBacktest.test.ts
+
+文档:
+  - 更新 GRID_STRATEGY_DESIGN.md (v2.0 特性说明)
+  - 新增 GRID_BACKTEST_GUIDE.md (回测使用指南)
+```
+
+### 验收标准
+
+```yaml
+功能:
+  - ✅ 网格自动重置触发准确率 > 90%
+  - ✅ 趋势检测准确率 > 70%
+  - ✅ 低波动告警及时性 < 30 分钟延迟
+  - ✅ 多标的并行运行无冲突
+
+性能:
+  - ✅ 3 个标的同时运行,CPU < 30%
+  - ✅ 内存占用 < 500MB
+
+回测:
+  - ✅ 回测月收益率 > 0.8%(震荡市数据)
+  - ✅ Sharpe > 2.5
+  - ✅ 最大回撤 < -2%
+```
+
+### 时间表
+
+```yaml
+Week 1:
+  Day 1-2: 自适应网格中心 + 趋势过滤器
+  Day 3-4: 低波动监控 + 多标的支持
+  Day 5: 回测框架核心逻辑
+  Day 6: 回测报告与参数优化
+  Day 7: 集成测试 + 文档更新
+```
+
+---
+
+## M3.5 – 混合策略(可选,+1-2 周)
+
+### 目标
+将网格与剥头皮策略结合,最大化收益。
+
+### 架构设计
+
+```
+┌─────────────────────────────────────────┐
+│       Strategy Coordinator              │
+│  (优先级管理 + 冲突检测)                 │
+└──────┬─────────────────────┬────────────┘
+       │                     │
+┌──────▼────────┐    ┌───────▼──────────┐
+│  GridMaker    │    │  MicroScalper    │
+│  (底层做市)   │    │  (极端信号叠加)  │
+└───────────────┘    └──────────────────┘
+       │                     │
+       └──────────┬──────────┘
+                  │
+          ┌───────▼────────┐
+          │  Order Router  │
+          └────────────────┘
+```
+
+### 策略协调规则
+
+```typescript
+class StrategyCoordinator {
+  async submitIntent(intent: OrderIntent, source: 'grid' | 'scalper'): Promise<boolean> {
+    // 1. 优先级检查
+    if (source === 'scalper' && this.hasGridOrderAt(intent.px)) {
+      logger.warn('Scalper intent conflicts with grid, rejecting');
+      return false;
+    }
+
+    // 2. 方向冲突检查
+    const existingIntent = this.getIntentAt(intent.symbol, intent.px);
+    if (existingIntent && existingIntent.side !== intent.side) {
+      logger.warn('Direction conflict, canceling lower priority');
+      await this.cancel(existingIntent);
+    }
+
+    // 3. 通过检查,执行
+    await this.router.sendLimitChild(intent.toOrder());
+    return true;
+  }
+}
+```
+
+### 配置示例
+
+```yaml
+strategy_mode: both
+
+grid:
+  enabled: true
+  symbol: BTC
+  grid_step_bps: 100
+  grid_range_bps: 400
+  priority: 1  # 高优先级
+
+scalper:
+  enabled: true
+  trigger:
+    spread_bps: 3.0  # 仅极端 spread 触发(避免与网格频繁冲突)
+    min_cooldown_ms: 500
+  tp_bps: 5
+  sl_bps: 10
+  max_clip_ratio: 0.2  # 仓位小于网格
+  priority: 2  # 低优先级
+
+strategy_coordinator:
+  conflict_resolution: cancel_lower  # 冲突时取消低优先级
+```
+
+### 交付清单
+
+```yaml
+新增:
+  - packages/strategies/coordinator.ts (200 行)
+    - 优先级管理
+    - 冲突检测与解决
+    - 意向订单队列
+
+扩展:
+  - packages/strategies/gridMaker.ts (+50 行)
+    - 改为提交 intent 而非直接下单
+  - packages/strategies/microScalper.ts (+50 行)
+    - 改为提交 intent
+
+配置:
+  - config/config.yaml (新增 strategy_coordinator 配置)
+
+测试:
+  - packages/strategies/__tests__/coordinator.test.ts
+    - 冲突检测准确性
+    - 优先级执行顺序
+```
+
+### 验收标准
+
+```yaml
+- ✅ 网格与剥头皮无自成交事件
+- ✅ 冲突检测准确率 100%
+- ✅ 混合策略月收益率 > 单一策略 20%
+- ✅ Delta 控制依然有效(P95 < max_base_abs * 0.5)
+```
+
+---
+
+## 风险评估与缓解
+
+### 风险 1:网格表现不如预期(EV < 0)
+
+**缓解**:
+- M1.5 快速验证(2-3 天),失败成本低
+- 若失败,问题大概率在对冲架构而非策略
+- 修复对冲后,任何策略都能跑
+
+### 风险 2:趋势市导致单边持仓
+
+**缓解**:
+- M1.5 有 max_base_abs 熔断
+- M2.5 增加趋势检测自动暂停
+- Kill-switch 作为最后保护
+
+### 风险 3:资金费率双重支付
+
+**缓解**:
+- 标的筛选器(M4)拒绝 funding correlation < 0.8
+- 实时监控 funding_cost_net_bps
+- 超过阈值自动减仓
+
+### 风险 4:开发资源分散
+
+**缓解**:
+- M1.5 仅 2-3 天,不影响主线(剥头皮)
+- 可并行开发:一人网格,一人剥头皮
+- 若网格验证失败,立即回归剥头皮
+
+---
+
+## KPI 与监控
+
+### 实时监控指标
+
+```yaml
+网格特定指标:
+  - grid_active_orders: 当前活跃网格订单数
+  - grid_filled_orders_per_hour: 每小时成交次数
+  - grid_recenter_count: 网格重置次数
+  - grid_trend_pause_count: 趋势暂停次数
+  - grid_round_trip_count: 往返成交次数
+  - grid_avg_profit_per_round: 单次往返平均利润
+
+复用现有指标:
+  - delta_abs
+  - hedge_success_rate
+  - hedge_latency_p99
+  - funding_cost_net_bps
+  - pnl_intraday
+```
+
+### 告警规则
+
+```yaml
+- grid_filled_orders_per_hour < 1 (持续 2 小时)
+  → 告警:低成交率,检查波动率
+
+- grid_recenter_count > 5 (单日)
+  → 告警:频繁重置,可能趋势市
+
+- grid_avg_profit_per_round < 0 (持续 1 小时)
+  → 告警:负收益,检查手续费或对冲成本
+
+- delta_abs > max_base_abs * 0.8
+  → 告警:库存接近上限
+```
+
+---
+
+## 与现有文档的整合
+
+### ARCHITECTURE_DESIGN.md 补充
+
+在 3.10 策略层补充:
+```markdown
+### GridMaker(可选策略)
+- 在价格区间内均匀分布买卖订单,捕捉震荡市价差
+- 买单成交 → 挂卖单;卖单成交 → 挂买单
+- 批量对冲:Delta 累积到阈值触发
+- 自适应调整:价格偏离时重置网格中心
+- 趋势检测:单边行情时自动暂停
+```
+
+### PRD 补充
+
+在第 4 节策略设计后补充:
+```markdown
+### 4.5 网格策略(备选方案)
+- 适用场景:震荡市、高波动低趋势
+- 收益模型:grid_step * 往返次数 - 手续费 - 对冲成本
+- 优势:实现简单、对冲压力小、参数稳定
+- 劣势:趋势市表现差、理论收益上限低
+```
+
+---
+
+## 总结
+
+### 推荐路径
+
+```
+1. 完成 M1(核心骨架)
+   ↓
+2. 用 2-3 天实现 M1.5(网格 MVP)
+   ↓
+3. 测试网验证 2-4 小时
+   ↓
+4. 若成功 → M2.5(增强网格)或 M2(剥头皮)
+   若失败 → 修复对冲架构,再回到 M1.5
+```
+
+### 成功标准
+
+**M1.5 成功**:
+- 网格能稳定运行 24 小时
+- Delta 控制有效
+- 对冲成功率 > 95%
+- 月收益率 > 0.5%(即使低于预期,只要为正即算成功)
+
+**M2.5 成功**:
+- 月收益率 > 0.8%
+- Sharpe > 2.5
+- 可长期无人值守运行
+
+### 下一步行动
+
+1. **立即开始 M1.5**(若 M1 已完成)
+2. **2-3 天后决策**:继续网格 or 切换剥头皮
+3. **保持灵活**:两个策略不互斥,最终可能都实现
+
+---
+
+**文档完成**。代码实现见 CODE_DELIVERY_PLAN.md M1.5 & M2.5 章节。

+ 557 - 0
docs/GRID_STRATEGY_DESIGN.md

@@ -0,0 +1,557 @@
+# 微网格策略设计文档(Grid Trading Strategy)
+
+**版本**: v1.0.0
+**日期**: 2025-10
+**适用范围**: Pacifica DEX 永续合约(BTC/ETH/SOL)
+
+---
+
+## 1. 设计目标
+
+### 1.1 核心理念
+在 **Delta≈0** 约束下,通过在价格区间内均匀分布买卖订单,**捕捉震荡市场的往返价差**,同时保持库存中性。
+
+### 1.2 优势
+- ✅ **实现简单**:无需复杂信号(OBI/RV/trade flow),只需价格网格
+- ✅ **逻辑清晰**:买单成交 → 挂卖单;卖单成交 → 挂买单
+- ✅ **对冲压力小**:成交频率低,可批量对冲
+- ✅ **参数稳定**:grid_step 基于波动率,无需频繁调参
+- ✅ **回测简单**:不依赖微观市场结构
+
+### 1.3 适用场景
+- ✅ **震荡市**:BTC 在 48k-52k 反复震荡
+- ✅ **高波动低趋势**:日内波动 >1%,但无明显方向
+- ✅ **资金费率对冲有效**:双 venue funding correlation > 0.8
+
+### 1.4 不适用场景
+- ❌ **单边趋势**:BTC 从 50k 直线涨到 60k(网格全部卖飞)
+- ❌ **低波动**:日内波动 <0.3%(网格不触发成交)
+
+---
+
+## 2. 策略原理
+
+### 2.1 基本逻辑
+
+```
+初始状态:在当前价格 P₀ 周围布置 N 层网格
+
+买单网格:P₀ * (1 - grid_step), P₀ * (1 - 2*grid_step), ..., P₀ * (1 - grid_range)
+卖单网格:P₀ * (1 + grid_step), P₀ * (1 + 2*grid_step), ..., P₀ * (1 + grid_range)
+
+运行规则:
+1. 任一买单成交 @ price_buy
+   → 在 price_buy * (1 + grid_step) 挂卖单(获利 grid_step)
+   → 更新 Delta(+base_sz)
+
+2. 任一卖单成交 @ price_sell
+   → 在 price_sell * (1 - grid_step) 挂买单(获利 grid_step)
+   → 更新 Delta(-base_sz)
+
+3. Delta 累积到阈值
+   → 触发对冲引擎,跨账户对冲
+```
+
+### 2.2 数学模型
+
+**单笔收益**:
+```
+profit_per_round = base_sz * mid_price * grid_step - fees - hedge_cost
+```
+
+**费用模型**:
+```
+fees = base_sz * mid_price * (maker_fee_buy + maker_fee_sell)
+     ≈ base_sz * mid_price * (-0.02% * 2)  # maker rebate
+     = -base_sz * mid_price * 0.04%
+
+hedge_cost = base_sz * mid_price * (hedge_slippage + taker_fee)
+           ≈ base_sz * mid_price * (0.3bps + 5bps)
+           = base_sz * mid_price * 0.53%
+```
+
+**盈亏平衡点**:
+```
+grid_step > (fees + hedge_cost) / base_sz / mid_price
+grid_step > 0.53% - 0.04% = 0.49%
+
+实际建议:grid_step ≥ 0.8%(含安全边际)
+```
+
+**日收益估算**:
+```
+假设:
+- grid_step = 1%
+- 日均往返次数 = 5(震荡市)
+- base_sz = 0.01 BTC, mid_price = 50000
+
+daily_profit = 5 * (0.01 * 50000 * 1% - 0.01 * 50000 * 0.49%)
+             = 5 * (5 - 2.45)
+             = 12.75 USD/day
+
+年化收益率(假设 100k 本金):
+12.75 * 365 / 100000 = 4.65%
+```
+
+### 2.3 网格参数设计
+
+#### grid_step(网格间距)
+```yaml
+计算方法:基于历史波动率
+- 获取最近 7 天的日内波动率(high - low)/ mid
+- 取 P50 波动率作为 grid_step 基准
+- 例如:BTC P50 日内波动 = 2.5% → grid_step = 1.0%(约 40% 的日内波动)
+
+推荐值:
+  BTC: 0.8% - 1.2%
+  ETH: 1.0% - 1.5%
+  SOL: 1.5% - 2.5%
+```
+
+#### grid_range(网格范围)
+```yaml
+计算方法:基于极端波动
+- 获取最近 30 天的日内最大波动
+- grid_range = max_daily_range * 0.7(覆盖 70% 极端情况)
+
+推荐值:
+  BTC: 3% - 5%
+  ETH: 4% - 6%
+  SOL: 6% - 10%
+```
+
+#### layers(网格层数)
+```yaml
+计算方法:
+- layers = grid_range / grid_step
+- 例如:grid_range = 4%, grid_step = 1% → layers = 4
+
+单边层数限制:
+  min: 3 层(太少无法覆盖波动)
+  max: 10 层(太多资金分散)
+```
+
+#### base_sz(单层订单大小)
+```yaml
+计算方法:
+- 总名义 = max_notional_abs
+- 单侧总层数 = layers
+- base_sz = (总名义 / mid_price) / layers / 2(买卖各一半)
+
+例如:
+  max_notional_abs = 100000 USD
+  mid_price = 50000
+  layers = 5
+  base_sz = (100000 / 50000) / 5 / 2 = 0.2 BTC
+```
+
+### 2.4 自适应控制(Adaptive Grid)
+
+为适应不同波动环境,Runner 调度 `gridMaker.onTick()` 每分钟采样一次 mid 价并执行:
+
+1. **波动率→网格间距映射**:使用 `VolatilityEstimator` 计算最近窗口的小时波动率(bps),按线性映射更新 `grid_step_bps`,变化超过阈值(默认 20%)时自动 `reset()`。
+2. **中心偏移监控**:若当前 mid 偏离网格中心超过 `recenter_threshold_bps` 且已过冷却期,重置中心并重新布网格。
+3. **对冲挂单超时**:`grid_pending_hedges` 超过 `hedge_pending_timeout_ms` 会产生 warning,避免对冲单长期悬挂。
+4. **指标输出**:Prometheus 暴露 `grid_step_bps`, `grid_volatility_hourly_bps`, `grid_pending_hedges`, `grid_current_delta`(按 symbol)。Grafana 可绘制波动-间距响应曲线。
+
+运行日志示例:
+```
+INFO GridMaker adjusting grid step { hourlyVolBps: 420, currentGridStepBps: 30, targetGridStepBps: 80 }
+WARN GridMaker pending hedge expired { orderId: "hedge-...", ageMs: 32000 }
+```
+
+---
+
+## 3. 风险管理
+
+### 3.1 趋势行情风险
+
+**问题**:单边上涨时,买单全部成交,卖单挂空 → 累积多头仓位
+
+**解决方案**:
+
+#### 方案 A:库存上限熔断
+```yaml
+risk:
+  max_base_abs: 0.8 BTC  # 单边仓位上限
+
+触发条件:abs(delta) > max_base_abs
+动作:
+  1. 暂停同方向新订单
+  2. 仅允许反方向订单(平仓方向)
+  3. 触发对冲引擎强制对冲
+```
+
+#### 方案 B:动态网格中心调整
+```yaml
+adaptive_grid:
+  enabled: true
+  recenter_threshold_bps: 200  # 价格偏离初始中心 2%
+
+触发条件:abs(current_price - grid_center) > grid_center * 2%
+动作:
+  1. 撤销所有旧网格订单
+  2. 以 current_price 为新中心重新布网格
+  3. 记录调整事件到审计日志
+```
+
+#### 方案 C:趋势检测暂停
+```yaml
+trend_filter:
+  enabled: true
+  lookback_periods: 12  # 12 * 5min = 1 hour
+  trend_threshold: 0.5%  # 1 小时涨跌 > 0.5%
+
+触发条件:abs(price_now - price_1h_ago) / price_1h_ago > 0.5%
+动作:
+  1. 暂停网格策略
+  2. 仅保留对冲功能
+  3. 趋势结束后(震荡恢复)自动重启
+```
+
+### 3.2 低波动风险
+
+**问题**:价格在窄幅震荡,网格不触发成交 → 无收益且占用资金
+
+**解决方案**:
+
+```yaml
+volatility_monitor:
+  min_daily_range_bps: 80  # 日内波动 < 0.8% 时告警
+  action:
+    - 通知人工审查
+    - 可选:自动缩小 grid_step 至 0.3%
+    - 可选:切换到被动做市策略
+```
+
+### 3.3 对冲失败风险
+
+**问题**:网格成交后,对冲账户 API 故障 → 单边敞口暴露
+
+**解决方案**(复用现有降级策略):
+
+```yaml
+参考 ARCHITECTURE_DESIGN.md 5.5 节降级矩阵:
+- 连续 3 次对冲失败 → 主账户切换为"只平不开"模式
+- 仅允许平仓方向的网格订单成交
+```
+
+---
+
+## 4. 与剥头皮策略对比
+
+| 维度 | 微网格 | 剥头皮 | 优势方 |
+|------|--------|--------|--------|
+| **实现复杂度** | 200-300 行 | 800-1000 行 | **网格** |
+| **参数敏感度** | 低(grid_step 基于波动率) | 高(spread/tp/sl 需大量校准) | **网格** |
+| **对冲频率** | 低(2-5 次/小时) | 高(10-20 次/小时) | **网格** |
+| **成交依赖** | 价格往返震荡 | 短期价格均值回归 | 网格(更普遍) |
+| **DEX 适应性** | 高(对延迟不敏感) | 低(依赖低延迟) | **网格** |
+| **趋势市表现** | 差(单边持仓) | 中性(有止损) | 剥头皮 |
+| **震荡市表现** | 优(持续收割) | 中(信号频繁) | **网格** |
+| **开发周期** | 2-3 天 | 2-3 周 | **网格** |
+| **理论收益上限** | 低(1-2%/月) | 高(3-5%/月) | 剥头皮 |
+
+**结论**:网格作为**最小可验证策略**,适合优先实施以验证对冲架构;若表现良好,可长期运行或叠加剥头皮。
+
+---
+
+## 5. 盈利模型与预期
+
+### 5.1 理论收益拆解
+
+```
+月收益 = 往返次数 * 单次收益 - 固定成本
+
+往返次数:
+- 震荡市(波动 2-3%/天):30-50 次/月
+- 趋势市(波动 1-2%/天):10-20 次/月
+
+单次收益(grid_step = 1%, base_sz = 0.1 BTC, mid = 50k):
+= 0.1 * 50000 * 1% - 0.1 * 50000 * 0.49%
+= 50 - 24.5
+= 25.5 USD
+
+月收益(震荡市):
+= 40 * 25.5
+= 1020 USD
+
+月收益率(本金 100k):
+= 1020 / 100000
+= 1.02%
+```
+
+### 5.2 风险调整收益(Sharpe Ratio)
+
+```yaml
+假设:
+  月收益: 1.02%
+  月波动率: 0.8%(网格有库存上限保护)
+
+月 Sharpe = 1.02% / 0.8% = 1.28
+年化 Sharpe = 1.28 * sqrt(12) = 4.43
+
+对比:
+  剥头皮理论 Sharpe: 2-3(高频高风险)
+  网格 Sharpe: 3-5(低频低风险)
+```
+
+### 5.3 最坏情况分析
+
+**场景 1:连续趋势市(30 天单边上涨 15%)**
+```
+- 网格买单全部成交,卖单挂空
+- 累积多头仓位 = max_base_abs = 0.8 BTC
+- 对冲成本 = 0.8 * 50000 * 0.53% = 212 USD
+- 趋势结束后网格恢复正常
+- 最大损失 ≈ 200-300 USD(0.2-0.3%)
+```
+
+**场景 2:低波动横盘(30 天波动 <0.5%/天)**
+```
+- 网格几乎无成交
+- 损失 = 0(仅机会成本)
+- 可手动切换策略或缩小 grid_step
+```
+
+**场景 3:资金费率双重支付**
+```
+- 双 venue funding 同向支付 0.01%/8h
+- 月成本 = 持仓名义 * 0.01% * 90 次
+- 最大持仓名义 = max_base_abs * mid = 0.8 * 50000 = 40000
+- 月成本 = 40000 * 0.01% * 90 = 360 USD(0.36%)
+- 解决:标的筛选器拒绝 funding correlation < 0.8 的 venue 对
+```
+
+---
+
+## 6. 配置示例
+
+### 6.1 保守配置(低风险)
+
+```yaml
+strategy_mode: grid  # 或 scalper, both
+
+grid:
+  enabled: true
+  symbols:
+    - symbol: BTC
+      grid_step_bps: 100  # 1%
+      grid_range_bps: 400  # 4%
+      base_clip_usd: 500  # 单层 500 USD
+      max_layers: 4
+
+  # 趋势防护
+  adaptive_recenter:
+    enabled: true
+    recenter_threshold_bps: 200  # 偏离 2% 重置网格
+
+  trend_filter:
+    enabled: true
+    lookback_periods: 12
+    trend_threshold_bps: 50  # 1h 涨跌 > 0.5% 暂停
+
+  # 对冲策略
+  hedge_mode: batch  # batch | immediate
+  hedge_threshold_base: 0.3  # 累积 0.3 BTC 触发对冲
+
+  # 低波动处理
+  volatility_monitor:
+    min_daily_range_bps: 80
+    action: notify  # notify | reduce_step | switch_strategy
+```
+
+### 6.2 激进配置(高收益)
+
+```yaml
+grid:
+  enabled: true
+  symbols:
+    - symbol: BTC
+      grid_step_bps: 60   # 0.6%(更密集)
+      grid_range_bps: 500  # 5%(更宽)
+      base_clip_usd: 1000
+      max_layers: 8
+
+  adaptive_recenter:
+    enabled: true
+    recenter_threshold_bps: 300
+
+  trend_filter:
+    enabled: false  # 不过滤趋势(承担单边风险)
+
+  hedge_mode: immediate  # 每笔成交立即对冲
+
+  volatility_monitor:
+    min_daily_range_bps: 50  # 更低阈值
+    action: reduce_step  # 自动缩小 grid_step
+```
+
+### 6.3 混合配置(网格 + 剥头皮)
+
+```yaml
+strategy_mode: both
+
+grid:
+  enabled: true
+  # ...保守配置
+
+scalper:
+  enabled: true
+  trigger:
+    spread_bps: 3.0  # 仅极端 spread 触发(避免与网格冲突)
+    min_cooldown_ms: 500
+  tp_bps: 5
+  sl_bps: 10
+  max_clip_ratio: 0.2  # 仓位小于网格
+
+# 策略协调
+strategy_coordinator:
+  priority: [grid, scalper]  # 网格优先
+  conflict_resolution: cancel_lower  # 冲突时取消低优先级
+```
+
+---
+
+## 7. 回测目标与验证指标
+
+### 7.1 回测数据要求
+
+```yaml
+data:
+  timeframe: 最近 3 个月
+  granularity: 1 分钟 OHLCV
+  venues: 双 venue 同时回测(验证对冲)
+
+scenarios:
+  - 震荡市(2024-08-01 至 2024-08-31)
+  - 趋势市(2024-09-01 至 2024-09-30)
+  - 低波动(2024-10-01 至 2024-10-15)
+```
+
+### 7.2 成功标准
+
+```yaml
+盈利指标:
+  - 月收益率 > 0.8%(震荡市)
+  - 月收益率 > 0.3%(趋势市)
+  - Sharpe Ratio > 2.5
+  - 最大回撤 < -2%
+
+风控指标:
+  - |Delta| P95 < 0.5 * max_base_abs
+  - 对冲成功率 > 98%
+  - 对冲延迟 P99 < 2s
+  - 资金费率净成本 < 收益的 30%
+
+操作指标:
+  - 网格订单成交率 > 60%(震荡市)
+  - 趋势暂停触发准确率 > 70%
+  - 自动重置网格次数 < 5 次/月
+```
+
+---
+
+## 8. 下一步行动
+
+### 8.1 最小可验证产品(MVP)
+
+**目标**:2-3 天内完成,验证对冲架构
+
+```yaml
+范围:
+  - 固定网格(不支持自动调整)
+  - 单一标的(BTC)
+  - 批量对冲(累积阈值触发)
+  - 复用现有 OrderRouter + HedgeEngine
+
+不包括:
+  - 趋势检测
+  - 动态网格调整
+  - 多标的支持
+  - 复杂风控
+```
+
+### 8.2 增强版(v2.0)
+
+**目标**:1 周内完成,生产级功能
+
+```yaml
+新增:
+  - 自适应网格中心调整
+  - 趋势检测与暂停
+  - 低波动自动缩小 grid_step
+  - 多标的并行运行
+  - 完整回测报告
+```
+
+### 8.3 与剥头皮混合(v3.0)
+
+**目标**:视 v2.0 表现决定
+
+```yaml
+策略:
+  - 网格作为底层持续做市
+  - 剥头皮叠加在极端 spread 时触发
+  - Strategy Coordinator 协调两者冲突
+```
+
+---
+
+## 9. 附录:代码示例
+
+### 9.1 核心网格逻辑(伪代码)
+
+```typescript
+class GridMaker {
+  private grids: Map<number, GridLevel> = new Map();
+  private filledGrids: Set<number> = new Set();
+
+  async initialize() {
+    const mid = this.shadowBook.mid();
+    const { grid_step, grid_range, layers } = this.config;
+
+    // 初始化网格
+    for (let i = 1; i <= layers; i++) {
+      // 买单网格
+      const buyPx = mid * (1 - i * grid_step);
+      await this.placeGridOrder(i, 'buy', buyPx);
+
+      // 卖单网格
+      const sellPx = mid * (1 + i * grid_step);
+      await this.placeGridOrder(-i, 'sell', sellPx);
+    }
+  }
+
+  async onFill(fill: Fill) {
+    const gridLevel = this.findGridLevel(fill.orderId);
+    if (!gridLevel) return;
+
+    // 标记该层已成交
+    this.filledGrids.add(gridLevel.index);
+
+    // 挂对手单
+    const oppositeSide = fill.side === 'buy' ? 'sell' : 'buy';
+    const oppositePx = fill.side === 'buy'
+      ? fill.px * (1 + this.config.grid_step)
+      : fill.px * (1 - this.config.grid_step);
+
+    await this.placeGridOrder(
+      gridLevel.index,
+      oppositeSide,
+      oppositePx
+    );
+
+    // 更新 Delta 并检查对冲阈值
+    this.updateDelta(fill);
+    const hedge = await this.hedgeEngine.maybeHedge(this.symbol, this.currentDelta);
+    if (Math.abs(hedge.hedged) > 0 && hedge.orderId) {
+      this.pendingHedges.set(hedge.orderId, hedge.hedged);
+    }
+  }
+}
+```
+
+---
+
+**文档完成**。详细的实施计划见 `GRID_IMPLEMENTATION_PLAN.md`。

+ 82 - 0
docs/IMPLEMENTATION_PLAN.md

@@ -0,0 +1,82 @@
+# Pacifica Delta-Neutral Scalping Implementation Plan
+
+## Overview
+This document consolidates the product requirements, technical architecture, and execution roadmap for the Pacifica delta-neutral market-making plus micro-scalping system. It targets a TypeScript/Node 22 stack and covers connector integration, risk controls, strategy logic, observability, and operational practices under a strict compliance posture.
+
+> **Documentation contract**  
+> 本计划与以下支持性文档配套使用:`docs/API_CONNECTOR_SPEC.md`, `docs/MODULE_INTERFACES.md`, `docs/SEQUENCE_FLOW.md`, `docs/CONFIG_REFERENCE.md`, `docs/TESTING_PLAN.md`, `docs/OPERATIONS_PLAYBOOK.md`。所有实现、配置或流程改动若偏离这些文档,需先修改文档再执行开发任务。
+
+## Objectives & Constraints
+- Maintain net delta ≈ 0 while capturing spread and maker rebates across BTC/ETH/SOL perpetual markets.
+- Enforce self-trade prevention, audit logging, and regulatory safeguards across all order flows.
+- Prioritise risk: enforce notional/inventory/order caps, intraday drawdown kill-switch, latency and data-gap protection.
+- Deliver production-grade observability (Prometheus metrics, alerting, traceable decisions) and hot-reloadable configuration with audit trails.
+
+## Functional Scope
+- **Exchange connectivity**: Pacifica REST (and optional WS) adapter, Ed25519 signing, rate-limit handling, structured error surfacing.
+- **Market data**: shadow order book with mid/spread/OBI/short-term RV derivations; latency tracking.
+- **Execution**: order router with limit/IOC modes, slippage guard, post-only support, deterministic `clientId`, and STP cross-checks.
+- **Risk & compliance**: order pre-checks, inventory/notional/order-size constraints, realised PnL accumulation, kill-switch triggers, append-only audit logs.
+- **Strategy layer**: passive market making (multi-layer reprice loop) and micro-scalping (spread expansion + flow imbalance triggers).
+- **Hedging**: PI-controlled cross-venue/account hedger with throttle and funding-rate bias adjustments.
+- **Trigger/OCO**: take-profit/stop-loss legs, timeout exits, shared STP/risk pipeline.
+- **Telemetry**: Prometheus metrics (maker_ratio, delta_abs, latency_p99, pnl_intraday, hedge_cost_bps, ev_estimate, cancel_rate, stp_hits) and alert definitions.
+- **Backtesting**: event replay (books+trades), fee/funding models, EV evaluation, parameter search, sharpe and bucketed reports.
+- **Configuration**: `.env` secrets, `config.yaml` strategy parameters (symbols, mm, scalper, risk, hedge), zod validation, hot reload with audit entries.
+
+## Non-Functional Targets (KPI / SLO)
+- EV_p50 ≥ 1.5 bps; EV_p10 ≥ 0.
+- |delta| P95 ≤ 0.5 × `max_base_abs`; taker_ratio ≤ 35%; hedge_cost_bps ≤ 0.4 × edge_bps.
+- **对冲效率(新增)**:hedge_success_rate > 98%; hedge_latency_p50 < 500ms, p99 < 2s; hedge_slippage_bps < 0.5.
+- **资金费率(新增)**:funding_rate_correlation > 0.8; funding_cost_net_bps < 1 per 8h; funding_same_sign_ratio < 20%.
+- Latency_p99 within venue SLA; zero self-trade incidents; compliant audit log coverage.
+
+## Milestones & Deliverables
+1. **M1 – Core Skeleton & Multi-Account Infrastructure (≈1–2 weeks)**
+   - Type/domain definitions, Pacifica adapter with signing placeholder replaced by official implementation.
+   - **Multi-account adapter registry**:支持 Account A (maker) / Account B (hedger) 独立配置与管理。
+   - Shadow book pipeline, latency metrics, baseline Prometheus exporter.
+   - **Symbol Registry & Allocator (新增)**:多标的生命周期管理与全局风险预算分配。
+   - **Global Order Coordinator (新增)**:跨账户订单注册表与 STP 检查基础框架。
+   - OrderRouter + SlippageGuard with post-only/STP checks.
+   - RiskEngine v1 covering inventory/notional/order caps and **enhanced kill-switch**(跨账户聚合、多维度触发器)。
+2. **M2 – Strategy Loop & Enhanced Hedging (≈1 week)**
+   - **Strategy Coordinator (新增)**:信号聚合与冲突检测,MM 和 Scalper 提交 intent 而非直接下单。
+   - MarketMaker v1 (mid±δ layers, periodic reprice) and MicroScalper v1 (spread + flow triggers, IOC exit fallback).
+   - PositionManager & HedgeEngine (PI-controlled cross-account hedging, throttle, funding bias options).
+   - **对冲延迟预算与重试机制(新增)**:P50/P99 追踪,超时强制市价,失败自动重试(最多 2 次)。
+   - **资金费率监控(新增)**:双 venue funding rate 抓取、相关性计算、同向支付检测。
+   - TriggerEngine for OCO tp/sl and timeout exits; funding rate ingestion.
+   - **降级策略状态机(新增)**:实现降级矩阵中的核心场景(数据断流、对冲失败、Delta 失控)。
+3. **M3 – Backtest & Parameterisation (≈1 week)**
+   - Event replay harness reusing production interfaces; fee/funding/slippage modelling.
+   - **延迟注入模块(新增)**:记录实盘延迟分布,回测时注入真实延迟,防止前瞻偏差。
+   - EV/Sharpe/bucket reporting, parameter sweeps (grid/random) with config snapshots.
+   - **配置热更新金丝雀流程(新增)**:zod 校验 → 回测烟囱测试 → 单标的试运行 10 分钟 → KPI 验证 → 全量发布或自动回滚。
+   - Audit trail for parameter edits (who/when/what/result).
+4. **M4 – Compliance Hardening & Multi-Venue (≈1–2 weeks)**
+   - Second venue or sub-account integration; cross-venue STP; consistent snapshot+incremental recovery.
+   - **完善 Global Order Coordinator**:跨账户经济 STP、冲突检测、降级策略触发全流程集成。
+   - Append-only audit log store with trace IDs across signal→order→fill→hedge.
+   - **流动性冲击成本监控(新增)**:top10_depth 追踪、clip 动态调整、实际滑点 vs 预期滑点告警。
+   - **资金费率筛选器(新增)**:标的上线前评估双 venue funding rate 相关性,自动拒绝不合格 venue 对。
+   - Adaptive parameter policy reacting to RV/OBI regimes; expanded symbol onboarding automation.
+5. **M5 – Operations & Resilience (continuous)**
+   - **完整降级策略矩阵实现**:覆盖所有 9 种故障场景(见 ARCHITECTURE_DESIGN.md 5.5 节)。
+   - SRE playbooks (volatility shock, data gap, signing failure, hedge miss, funding rate anomaly) and regular drills.
+   - **对冲效率指标监控面板**:hedge_success_rate, hedge_latency_p50/p95/p99, hedge_slippage_bps, cross_venue_basis 实时展示与报警。
+   - Risk parameter auto-rollback and alert-driven mitigation; blue/green deploy + canary configs.
+   - **手动干预接口**:`/api/override-degradation` (需认证),允许运维人员强制退出降级模式。
+   - Periodic KPI reviews (EV, hedge_cost, delta_abs, hedge efficiency, funding cost) feeding strategy tuning roadmap.
+
+## Work Cadence & Governance
+- Each milestone concludes with code, self-tests (simulation/backtest), and documentation updates.
+- `apps/runner` exposes CLI for live, dry-run, and replay modes; CI runs lint/test/backtest smoke.
+- PRs must attach KPI snapshots or logs; doc updates tracked under `docs/` with revision history.
+- Maintain change log of strategy parameters and incidents; schedule monthly compliance review.
+
+## Immediate Next Steps
+1. Finalise Pacifica signing specification and implement `signRequest` in `packages/connectors/pacifica/src/signing.ts`.
+2. Wire Runner / MarketData to pass API credentials for private WebSocket channels, then run live smoke tests against Pacifica orders/fills/account feeds以验证签名负载。
+3. Stand up the shadow book + metrics pipeline, ensuring data feeds for MarketMaker/Scalper prototypes。
+4. Define baseline Prometheus dashboard and risk parameter defaults to support M1 testing。

+ 421 - 0
docs/MODULE_INTERFACES.md

@@ -0,0 +1,421 @@
+# 核心模块接口说明
+
+> 定义各 package 暴露的主要类、方法签名、数据模型及依赖关系,统一编码约束。
+
+---
+
+## 1. 公共类型
+
+- 统一 Typescript 接口位于 `packages/domain/src/types.ts`,需至少包含:
+  ```ts
+  export type Side = 'buy' | 'sell';
+  export type TimeInForce = 'GTC' | 'IOC' | 'FOK';
+
+  export interface OrderIntent {
+    symbol: string;
+    side: Side;
+    price: number;
+    size: number;
+    tif: TimeInForce;
+    postOnly?: boolean;
+    clientId: string;
+    strategyTag: 'grid' | 'scalper' | 'hedge';
+  }
+
+  export interface FillEvent {
+    orderId: string;
+    clientId: string;
+    symbol: string;
+    side: Side;
+    price: number;
+    size: number;
+    fee: number;
+    liquidity: 'maker' | 'taker';
+    timestamp: number;
+  }
+  ```
+- 所有模块引用公共类型,不允许重复定义字段。
+
+---
+
+## 2. MarketData / ShadowBook
+
+| 模块 | 文件 | 责任 |
+|------|------|------|
+| `ShadowBook` | `packages/utils/src/shadowBook.ts` | 聚合多标的订单簿,提供 mid/spread/OBI 等派生指标 |
+| `MarketDataAdapter` | `packages/utils/src/marketDataAdapter.ts` | 拉取或订阅行情并同步至 ShadowBook |
+
+核心接口:
+
+```ts
+class ShadowBook {
+  constructor(options?: { maxDepth?: number; stalenessMs?: number });
+  updateFromSnapshot(symbol: string, snapshot: OrderBook, seq?: number): void;
+  applyIncrement(symbol: string, delta: OrderBookDelta): void;
+  snapshot(symbol: string): OrderBook | undefined;
+  mid(symbol: string): number | undefined;
+  spreadBps(symbol: string): number | undefined;
+  computeObi(symbol: string, depth?: number): number | undefined;
+  topDepthUsd(symbol: string, depth?: number): number | undefined;
+  detectDataGap(symbol: string, now?: number): boolean;
+  reset(): void;
+}
+```
+
+`ShadowBook` 维护每个标的的最新订单簿,与策略/风控共享数据;若底层行情支持增量更新,可通过 `applyIncrement` 合并。
+
+```ts
+class MarketDataAdapter extends EventEmitter {
+  constructor(options: { symbols: string[]; shadowBook: ShadowBook; fetchSnapshot: (symbol: string) => Promise<OrderBook>; pollIntervalMs?: number });
+  start(): Promise<void>;
+  stop(): void;
+  ingestSnapshot(symbol: string, snapshot: OrderBook, seq?: number): void;
+  ingestDelta(symbol: string, delta: OrderBookDelta): void;
+}
+```
+
+- 默认实现基于轮询 REST 快照,可扩展为 WebSocket 增量模式。
+
+---
+
+## 3. Registry 层
+
+### 3.1 SymbolRegistry
+
+```ts
+type SymbolStatus = 'inactive' | 'active' | 'paused' | 'disabled';
+
+interface SymbolConfig {
+  symbol: string;
+  maxNotional: number;
+  maxBase: number;
+  enabled?: boolean;
+  tags?: string[];
+  metadata?: Record<string, unknown>;
+}
+
+class SymbolRegistry extends EventEmitter {
+  constructor(initialConfigs?: SymbolConfig[]);
+  register(config: SymbolConfig): SymbolRuntimeState;
+  updateConfig(symbol: string, patch: Partial<SymbolConfig>): SymbolRuntimeState;
+  activate(symbol: string): SymbolRuntimeState;
+  pause(symbol: string, reason: string): SymbolRuntimeState;
+  disable(symbol: string, reason?: string): SymbolRuntimeState;
+  remove(symbol: string): void;
+  list(status?: SymbolStatus): SymbolRuntimeState[];
+  listActive(): SymbolRuntimeState[];
+  setAllocation(symbol: string, allocation: RiskAllocation | undefined): SymbolRuntimeState;
+  setScore(symbol: string, score: number | undefined): SymbolRuntimeState;
+  on(event: 'registered' | 'updated' | 'statusChanged' | 'removed', handler: (...args)=>void): this;
+}
+```
+
+- 保存符号配置、当前状态、最近分配的风险预算与评分。
+- 所有状态更新通过事件触发,便于 Runner / RiskAllocator 监听。
+
+### 3.2 RiskAllocator
+
+```ts
+interface RiskBudget { totalNotional: number; totalBase: number; }
+
+class RiskAllocator {
+  constructor(budget: RiskBudget, options?: { minNotionalPerSymbol?: number; minBasePerSymbol?: number });
+  allocate(states: SymbolRuntimeState[]): Map<string, RiskAllocation>;
+}
+```
+
+- 根据 `SymbolRuntimeState.score` 按比例分配风险预算,自动裁剪至每个符号的配置上限。
+- 若配置了 `minNotionalPerSymbol` / `minBasePerSymbol`,会确保分配不低于该最小值。
+
+### 3.3 SymbolScorer
+
+```ts
+interface SymbolMetrics {
+  spreadBps: number;
+  topDepthUsd: number;
+  volumePerMin: number;
+  fundingCorrelation: number;
+  dataLatencyMs?: number;
+}
+
+class SymbolScorer {
+  constructor(options?: ScorerOptions);
+  score(metrics: SymbolMetrics): number;      // 0~1,越高代表越适合启用
+  shouldEnable(metrics: SymbolMetrics): boolean;
+}
+```
+
+- 默认权重均分在 spread / depth / volume / funding 之间,并对过期行情加惩罚。
+- `shouldEnable` 同时检查 spread、深度、资金费率等硬阈值。
+
+---
+
+## 4. Strategy 层
+
+### 3.1 StrategyCoordinator
+
+```ts
+interface StrategyCoordinatorOptions {
+  priorities: ('grid' | 'scalper')[];
+}
+
+class StrategyCoordinator {
+  constructor(options: StrategyCoordinatorOptions);
+  registerProducer(tag: 'grid' | 'scalper', producer: AsyncGenerator<OrderIntent[]>): void;
+  async nextIntentBatch(): Promise<OrderIntent[]>; // 按优先级+冲突处理返回
+  resolveConflicts(intents: OrderIntent[]): OrderIntent[];
+}
+```
+
+- 冲突规则:同一 symbol、对向意向 → 保留优先级高的策略;同向合并尺寸。
+- `nextIntentBatch` 需支持 `Promise.race` 超时,避免阻塞。
+
+### 3.2 GridMaker
+
+当前网格策略直接依赖 `OrderRouter` 与 `HedgeEngine`:
+
+```ts
+class GridMaker {
+  constructor(
+    cfg: GridConfig,
+    router: OrderRouter,
+    hedgeEngine: HedgeEngine,
+    shadowBook: ShadowBook
+  );
+  initialize(): Promise<void>;   // 读取 mid,按配置挂出买卖网格
+  onFill(fill: Fill): Promise<void>; // 补挂对手单、更新 delta、触发 hedge
+  onHedgeFill(fill: Fill): Promise<void>; // Hedger 成交后,根据实际结果校准 delta
+  onTick(): Promise<void>;       // 定时调用,驱动自适应逻辑
+  reset(): Promise<void>;        // 撤销未成交挂单并重新初始化
+  getStatus(): GridStatus;       // 提供监控数据
+}
+```
+
+- `GridConfig` 字段详见 `config/grid.example.yaml` 与 `docs/CONFIG_REFERENCE.md`。
+- 未来若接入 `StrategyCoordinator`,需新增返回 `OrderIntent[]` 的变体并在此文档说明。
+
+### 3.3 MicroScalper
+
+```ts
+class MicroScalper {
+  constructor(cfg: ScalperConfig, shadow: ShadowBook, signalBus: EventEmitter);
+  onBook(book: OrderBookEvent): void;
+  onTrade(trade: TradeEvent): void;
+  drainIntents(): OrderIntent[];
+}
+```
+
+- 内部维护冷却时间;`drainIntents` 每个周期输出并清空累计意向。
+
+---
+
+## 5. Execution 层
+
+### 4.1 GlobalOrderCoordinator
+
+```ts
+interface GlobalOrderSnapshot {
+  orderId: string;
+  accountId: string;
+  symbol: string;
+  side: Side;
+  price: number;
+}
+
+class GlobalOrderCoordinator extends EventEmitter {
+  constructor(options?: { stpToleranceBps?: number });
+  validate(intent: { accountId: string; symbol: string; side: Side; price: number }): void;
+  register(snapshot: GlobalOrderSnapshot): void;
+  release(orderId: string): void;
+}
+```
+
+- 维护 `(symbol, price)` 到挂单列表的索引,阻止关联账户在相同价位对向挂单触发 STP。
+- `stpToleranceBps` 用于允许极小价差以内的并行挂单(默认 0 表示严格禁止)。
+
+### 5.2 OrderRouter
+
+```ts
+interface OrderRouterConfig {
+  maxBps: number;
+  minIntervalMs?: number;
+  forbidPostOnlyCross?: boolean;
+  clientIdCacheSize?: number;
+}
+
+class OrderRouter {
+  constructor(
+    sendLimit: (order: Order) => Promise<{ id: string }>,
+    getBook: (symbol: string) => OrderBook | undefined,
+    config: OrderRouterConfig
+  );
+  sendLimit(order: Order): Promise<string>;
+  sendLimitChild(order: Order): Promise<string>; // 兼容旧接口
+  sendIOC(order: Order): Promise<string>;
+}
+```
+
+- 下单前执行滑点守卫、post-only 交叉检查、clientId 去重以及最小时间间隔节流。
+- `getBook` 通常由 `ShadowBook.snapshot(symbol)` 实现,确保 router 获得最新 top-of-book。
+
+---
+
+## 6. Risk & Hedge
+
+### 6.1 RiskEngine
+
+```ts
+interface RiskLimits {
+  maxBaseAbs: number;
+  maxNotionalAbs: number;
+  maxOrderSz: number;
+}
+
+interface KillSwitchConfig {
+  drawdownPct: number;
+  triggers?: Array<{ type: 'pnl_drawdown' | 'delta_abs' | 'hedge_failure_count' | 'data_gap_sec'; threshold: number }>;
+}
+
+class RiskEngine {
+  constructor(limits: RiskLimits, killSwitch?: KillSwitchConfig);
+  preCheck(order: Order, position: PositionSnapshot, midPrice: number): void;
+  reportFill(pnlDelta: number): void;
+  updateEquity(equity: number): void;
+  updateDeltaAbs(deltaAbs: number): void;
+  recordHedgeFailure(): void;
+  recordHedgeSuccess(): void;
+  setDataGap(seconds: number): void;
+  shouldHalt(): boolean;
+  getStatus(): RiskStatus;
+}
+```
+
+- `preCheck` 在下单前验证名义、库存、单笔限额。
+- `updateEquity`/`updateDeltaAbs`/`recordHedgeFailure` 等接口由上层定期调用,Kill-switch 根据 drawdown 与触发器自动判定停机。
+
+### 5.2 HedgeEngine
+
+```ts
+interface HedgeCfg { kp: number; ki: number; Qmax: number; minIntervalMs: number; }
+interface HedgeResult { hedged: number; orderId?: string; clientId?: string; }
+
+class HedgeEngine {
+  constructor(cfg: HedgeCfg, place: (order: Order) => Promise<{ id: string }>, getMid: () => number | undefined);
+  compute(delta: number): number;        // PI 控制器输出,截断至 ±Qmax
+  maybeHedge(symbol: string, delta: number): Promise<HedgeResult>; // 按节流阈值提交 IOC 对冲,返回下单信息
+}
+```
+
+- 当前版本未与风险引擎集成;若需要队列、重试等特性,请扩展代码并更新文档。
+
+-### 6.2 FundingRateMonitor
+
+```ts
+interface FundingRate { rate: number; timestamp: number; venue?: string; }
+
+class FundingRateMonitor extends EventEmitter {
+  constructor(options: { fetchPrimary: () => Promise<FundingRate>; fetchHedge: () => Promise<FundingRate>; pollIntervalMs?: number });
+  start(): Promise<void>;
+  stop(): void;
+}
+```
+
+- 定期抓取主账户与对冲账户的资金费率,输出是否同向支付等指标。后续可计算相关性、驱动风险降级。
+
+---
+
+## 7. Telemetry
+
+```ts
+class Telemetry {
+  recordCounter(name: string, labels: Record<string,string>, value?: number): void;
+  observeHistogram(name: string, labels: Record<string,string>, value: number): void;
+  setGauge(name: string, labels: Record<string,string>, value: number): void;
+}
+```
+
+- 指标定义集中于 `packages/telemetry/metrics.ts`,模块通过依赖注入使用。
+- 不允许直接调用 `prom-client`,避免重复注册。
+
+---
+
+## 8. 依赖与初始化
+
+最小可运行流程:
+
+1. 读取 `config.yaml`(或 `config/grid.example.yaml`)并实例化 `GridConfig`、`HedgeCfg`、风险参数。
+2. 初始化发送下单的回调(正式版应调用 `Pacifica` adapter)。
+3. 创建单例 `ShadowBook`,在收到行情快照时调用 `shadowBook.set(book)`.
+4. 实例化 `OrderRouter`、`HedgeEngine`、`GridMaker`,并在首个 mid 可用时调用 `gridMaker.initialize()`.
+5. 将成交事件传递给 `gridMaker.onFill(fill)`,由其内部处理补单与对冲。
+
+> 引入其他策略或协调器后,请扩展本节并保证文档与实现保持一致。
+
+---
+
+## 8. 错误与日志规范
+
+- 每个模块定义自有错误类型,位于 `packages/errors`.
+- 捕获异常需写入结构化日志:
+  ```ts
+  logger.error({ err, intent, module: 'OrderRouter' }, 'order rejected');
+  ```
+- 日志字段:
+  - `traceId`: 贯穿意向 → 下单 → 成交 → 对冲 → 降级
+  - `accountId`, `strategyTag`, `symbol`
+  - `deltaBefore`, `deltaAfter` (RiskEngine / HedgeEngine)
+
+---
+
+## 9. 文档维护
+
+- 本文件版本号与代码同步更新(`Version: 1.0.0` 初始)。
+- 新增模块需补充章节,PR 必须更新该文档并通过审查。
+### 5.3 AdapterRegistry(`packages/connectors/pacifica/src/adapterRegistry.ts`)
+
+```ts
+interface AdapterRegistryEntry {
+  id: string;
+  role?: string;
+  adapter: PacificaAdapter;
+}
+
+class AdapterRegistry {
+  register(id: string, config: PacificaConfig, role?: string): AdapterRegistryEntry;
+  attach(id: string, adapter: PacificaAdapter, role?: string): AdapterRegistryEntry;
+  get(id: string): PacificaAdapter;
+  findByRole(role: string): PacificaAdapter | undefined;
+  collectPositions(symbol: string): Promise<PositionSnapshot[]>;
+}
+```
+
+- 同时管理 maker / hedger 等账户 adapter,便于在执行层动态选择账户。`collectPositions` 供 PositionManager 聚合风险使用。
+
+### 5.4 PacificaWebSocket(`packages/connectors/pacifica/src/wsClient.ts`)
+
+```ts
+interface PacificaWebSocketConfig {
+  url: string;
+  apiKey?: string;      // account address
+  secret?: string;      // private key
+  subaccount?: string;
+  reconnectIntervalMs?: number;
+  maxReconnectIntervalMs?: number;
+  heartbeatIntervalMs?: number;
+}
+
+class PacificaWebSocket extends EventEmitter {
+  constructor(config: PacificaWebSocketConfig);
+  connect(): void;
+  disconnect(): void;
+  subscribe(channel: string, params?: Record<string, unknown>): void;
+  subscribeAuthenticated(channel: string, params?: Record<string, unknown>, authOverride?: SigningConfig): void;
+  sendRaw(payload: unknown): void;
+}
+```
+
+- 负责维护 WebSocket 连接、登录校验(如提供账户 address / private key)、心跳与重连,断线后自动恢复订阅。事件:`open`、`close`、`error`、`message`、`reconnected`。
+- `SigningConfig` 定义复用 `packages/connectors/pacifica/src/signing.ts`。
+- `subscribeAuthenticated` 可显式传入凭证(多账号时很有用),否则默认使用 constructor 配置;`orders.*` / `fills.*` / `account.*` 会自动套用签名。
+
+---

+ 205 - 0
docs/OPERATIONS_PLAYBOOK.md

@@ -0,0 +1,205 @@
+# 运维与降级操作手册
+
+> 提供日常巡检、故障响应、降级策略及人工干预流程,确保系统在异常环境下可控可退。
+
+---
+
+## 1. 角色与职责
+
+| 角色 | 主要任务 |
+|------|----------|
+| 值班工程师 (On-call) | 监控报警、执行降级/恢复、记录 incident |
+| 策略负责人 | 审核参数、决策策略切换或停机 |
+| SRE / DevOps | 维护基础设施、日志、告警渠道 |
+
+所有关键操作需在 Slack #trading-ops 频道同步,并在 incident log 记录。
+
+---
+
+## 2. 日常巡检清单
+
+| 时间频率 | 检查项 | 通过标准 | 指标/命令 |
+|----------|--------|----------|-----------|
+| 每 15 分钟 | WS 延迟 / 重连 | `pacifica_ws_reconnects_total` 无增长 | `curl /metrics | grep ws_reconnect` |
+| 每小时 | Delta 风险 | `delta_abs` < `max_base_abs * 0.5` | Grafana 面板 |
+| 每小时 | Hedge 成功率 | `hedge_success_rate > 0.95`,`latency_p99 < 2s` | Grafana |
+| 每 4 小时 | 资金费率相关性 | `funding_rate_correlation > 0.8` | Funding 面板 |
+| 每日 | PnL 汇总 | Intraday PnL 不超过 -0.5% | `pnpm reports:daily` |
+| 每小时 | 网格自适应状态 | `grid_step_bps` 梯度平滑、`grid_pending_hedges=0` | `/metrics -> grid_*` |
+
+异常时立即记录:时间、指标、上下文。
+
+---
+
+## 3. 常见故障场景与操作
+
+### 3.1 对冲连续失败
+
+- **触发条件**:`hedge_failure_count >= 3` 或报警 `hedge_success_rate < 0.9`
+- **操作步骤**:
+  1. `POST /api/override-degradation` `{"mode":"REDUCE_ONLY","reason":"hedge_fail"}`;
+  2. 检查对冲账户余额、API 状态;
+  3. 手动对冲库存(`/api/manual-hedge?symbol=BTC&size=0.3`);
+  4. 恢复前执行回归测试(见第 5 节)。
+
+### 3.2 行情数据断流
+
+- **触发条件**:`pacifica_ws_gaps_total` 提升或 `data_gap_sec > 3`
+- **操作**:
+  1. `OrderRouter.cancelAll(symbol)`(API: `POST /api/cancel-all`);
+  2. `POST /api/halt` 暂停策略;
+  3. 联系交易所确认网络状态;
+  4. 数据恢复后执行金丝雀再上线。
+
+### 3.3 Delta 失控
+
+- **触发条件**:`delta_abs > max_base_abs` 告警
+- **操作**:
+  1. `POST /api/override-degradation` `{"mode":"REDUCE_ONLY","reason":"delta_high"}`;
+  2. 触发 `HedgeEngine.enqueueDelta`,必要时人工平仓;
+  3. 调整 `grid.max_layers`、`grid_step_bps` 或暂时关闭策略。
+
+### 3.4 资金费率同向支付
+
+- **触发条件**:`funding_same_sign_ratio > 0.2`
+- **操作**:
+  1. 减少持仓:`grid.base_clip_usd` 下调 30%,或禁用网格;
+  2. 与策略负责人讨论是否关闭某个 venue;
+  3. 记录 incident,跟踪资金费率走势。
+
+### 3.5 API 认证失败 / 签名错误
+
+- **症状**:REST 返回 `401` / WS `AUTH_FAILED`
+- **操作**:
+  1. 立即 HALT (`POST /api/halt`);
+  2. 检查 `.env` 是否过期、时间同步 (`ntp`);
+  3. 更新密钥后重启 Runner;
+  4. 验证签名测试通过 (`pnpm test --filter contract`。
+
+---
+
+## 4. 降级与恢复流程
+
+### 4.1 自动降级矩阵回顾
+
+| 场景 | Action | 触发方式 |
+|------|--------|----------|
+| 对冲失败 | `REDUCE_ONLY` | 风控自动 |
+| 数据断流 | `HALT` | 降级策略 |
+| STP 率过高 | `DISABLE_SCALPER` | 降级策略 |
+| 高波动 | `ADAPTIVE` (`grid.max_layers=1`) | Adaptive Mode |
+
+### 4.2 人工强制降级
+
+```bash
+curl -X POST http://localhost:4000/api/override-degradation \
+  -H 'Content-Type: application/json' \
+  -d '{"mode":"HALT","reason":"manual_intervention"}'
+```
+
+- 支持模式:`HALT`, `REDUCE_ONLY`, `NORMAL`.
+- 请求会记录 audit log(操作者、原因、时间)。
+
+### 4.3 恢复流程
+
+1. 完成根因确认;
+2. 确认指标恢复正常(延迟、Delta、WS 状态);
+3. `POST /api/override-degradation` `{"mode":"NORMAL"}`;
+4. 在 canary 模式跑 10 分钟;
+5. 恢复生产,发布 incident 总结。
+
+> 📣 **Kill-switch 通知**:Runner 会在触发时向 `KILL_SWITCH_WEBHOOK`(若配置)发送 JSON 负载,包含 `status`、`gridStatus`、`source` 等字段,用于集成 Slack / PagerDuty。默认日志位于 `logs/runner-*.log`。
+
+---
+
+## 5. 手动对冲与下单
+
+- 手动对冲 API:`POST /api/manual-hedge`
+  ```json
+  { "symbol": "BTC", "size": 0.25, "side": "sell", "reason": "manual_adjustment" }
+  ```
+  - 使用 hedger 账户 IOC 下单,记录 `manual=true`。
+- 临时挂单:`POST /api/manual-order` → 仅在 reduce-only 模式下允许。
+
+所有手动操作需记录:
+- 操作人
+- 触发指标
+- 结果与后续动作
+
+---
+
+## 6. 日志与排障
+
+| 日志文件 | 内容 | 查看命令 |
+|----------|------|----------|
+| `logs/runner-*.log` | 主流程、策略、Risk/Kill-switch、Grid 自适应事件 | `tail -f logs/runner-*.log` |
+| `logs/execution.log` | 下单、撤单、错误码 | `less +F logs/execution.log` |
+| `logs/metrics.log` | 指标采集快照 | `grep hedge_latency logs/metrics.log` |
+| `logs/events.log` | WS 事件 (JSONL) | `jq '.' logs/events.log` |
+
+排障流程:
+1. 获取 traceId (`grep traceId=<id> logs/app.log`)
+2. 追踪相关日志 (策略 → 下单 → 成交 → 对冲)
+3. 如需回放,在 `data/replay/incidents/` 保存事件序列。
+
+---
+
+## 7. Incident 记录模板
+
+```
+Incident ID: YYYYMMDD-HHMM-<short>
+Severity: SEV2/SEV3
+Start Time:
+Detection Method: (Alert / Manual)
+Primary Symptoms: (Delta spike / Hedge fail / WS gap)
+Actions Taken:
+  - 00:01 Set mode=REDUCE_ONLY
+  - 00:03 Manual hedge 0.4 BTC
+Resolution Time:
+Root Cause:
+Follow-up Tasks:
+```
+
+- 记录存放在 `docs/incidents/`,并在每周复盘会议回顾。
+
+---
+
+## 8. Goldens / 回归测试
+
+在恢复前运行:
+
+```bash
+pnpm test --filter integration
+pnpm backtest --config backtest/regression.yaml --from yesterday --to today
+```
+
+- 确保策略行为与基线一致,避免携带隐患。
+
+---
+
+## 9. 指标报警建议
+
+| 指标 | 阈值 | 动作 |
+|------|------|------|
+| `hedge_success_rate < 0.9` (5 分钟) | Slack SEV2 | 触发 reduce-only |
+| `delta_abs > max_base_abs` (1 分钟) | Slack SEV2 | 手动对冲 |
+| `pacifica_ws_reconnects_total >= 3` (5 分钟) | Slack SEV3 | 检查网络/交易所 |
+| `funding_rate_correlation < 0.8` (1 小时) | 邮件 | 评估撤标的 |
+
+---
+
+## 10. 版本控制与回滚
+
+- 所有策略、配置版本在 Git tag 中保留:`strategy-vX.Y.Z`.
+- 回滚步骤:
+  1. `git checkout strategy-vX.Y.Z`
+  2. `pnpm install && pnpm run build`
+  3. `pnpm run live -- --config config/config.yaml --rollback`
+  4. 验证指标正常后发布公告。
+
+---
+
+## 11. 手册维护
+
+- 更新流程:任何新报警、降级策略或 API 变更需 PR 更新本文件。
+- 每季度复审一次,结合最新 incident 调整 SOP。

+ 189 - 0
docs/PRD_Pacifica_DeltaNeutral_Scalping.md

@@ -0,0 +1,189 @@
+# Pacifica Delta-Neutral + Dual-Sided Scalping
+**版本**: v0.1.0  
+**类型**: 需求文档(PRD) + 技术方案(Tech Spec)  
+**适用范围**: Pacifica DEX(主网/测试网)
+
+## 1. 背景与目标
+- 在 **Delta≈0** 约束下,使用“被动做市 + 双向微剥头皮”获取点差与成交周转,严格避免自成交/违规行为。
+- 以主流合约(BTC/ETH/SOL)起步,自动评分筛选是否扩展更多标的。
+
+## 2. 关键原则
+- **合规**:启用 STP(自成交预防),对冲腿在另一账户/venue 完成;保留审计日志。
+- **风险优先**:名义/库存/单笔上限、回撤熔断、延迟与数据断流保护。
+- **可观测性**:Prometheus 指标与报警;全链路可追踪。
+
+## 3. 指标/KPI(验收)
+- EV_p50 ≥ 1.5 bps;EV_p10 ≥ 0
+- |Delta| P95 ≤ max_base_abs 的 50%
+- taker_ratio ≤ 35%;hedge_cost_bps ≤ edge_bps 的 40%
+- 当日回撤不突破 kill_switch_dd_pct;latency_p99 达标
+
+## 4. 策略设计
+### 4.1 信号
+- Spread 扩张:spread_bps = (ask1 - bid1)/mid * 1e4
+- Order Book Imbalance (OBI):k 档不平衡
+- Trade Imbalance:T=0.5–2s 窗口的买卖量差
+- 短时 RV:200–500ms 波动率,自适应 tp/sl/δ
+
+### 4.2 交易模式
+- **被动优先**:mid±δ 两侧 postOnly;一侧成交 → 对冲腿 IOC/紧限价;OCO(tp/sl) 管理退出。
+- **微吃单**:spread_bps 放大 + 成交流向偏置时,小额吃入→在中点内侧被动挂出;超时 IOC 退出。
+
+### 4.3 Delta 控制
+- PI 控制:hedge_qty = clamp(kp*Δ + ki*∑Δ, ±Qmax),含最小间隔防抖;资金费率不利时增加偏压。
+- **对冲延迟风险预算(新增)**:
+  - 目标延迟:P50 < 500ms, P99 < 2s(从成交信号到对冲完成)
+  - 延迟窗口风险:在高波动期,10 秒单边敞口可能造成 10-50 bps 滑点损失
+  - 超时处理:若对冲未在 3 秒内完成,触发强制市价平仓
+  - 对冲失败重试:最多 2 次,每次增加滑点容忍度 +5bps
+  - 预对冲机制(高级):在 MM 挂单前,预先在对冲账户挂反向单,成交时立即对冲
+- **资金费率套利风险(新增)**:
+  - **关键风险**:双账户持有相反方向仓位时,若两 venue 资金费率同向,将双重支付而非互相抵消
+  - 监控指标:
+    - `funding_rate_correlation`:两 venue 资金费率的 30 天滚动相关性(目标 > 0.8)
+    - `funding_cost_net_bps`:双账户净资金费支付/收入(目标每 8h < 1bps)
+  - 风控动作:
+    - 若相关性 < 0.8,告警并建议减仓至 50%
+    - 若出现同向支付(如双方都支付 +0.01%),立即减仓至 30% 并人工审查
+  - 标的筛选要求:仅选择资金费率相关性 > 0.8 的 venue 对进行双账户对冲
+
+### 4.4 期望价值
+EV ≈ edge_bps - taker_fee_bps*taker_ratio - real_slip_bps - cancel_cost_bps,需 EV_p50 > 1–2 bps 且 EV_p10 ≥ 0。
+
+## 5. 标的筛选(自动评分任务)
+- 从 `/info` 读取 tick/lot/min_order;
+- 从 `/book` 计算 spread_bps、top10_depth_usd、queue_turnover;
+- 从成交流估每分钟笔数;
+- **流动性冲击成本评估(新增)**:
+  - 要求:`base_clip_usd ≤ top10_depth_usd * 5%`(单笔订单不超过前 10 档深度的 5%)
+  - 动态调整:若 depth 下降 >30%,自动降低 clip 至新的 5% 阈值
+  - 监控指标:实际成交滑点 vs 预期滑点,若差异 >2bps 持续 5 分钟,暂停该标的
+- **资金费率相关性评估(新增)**:
+  - 从双 venue 抓取最近 30 天的 funding rate 历史
+  - 计算 Pearson 相关系数,要求 > 0.8
+  - 检查同向支付频率:若 >20% 的时间双方都支付正费率或都收取负费率,拒绝该 venue 对
+- 依据费率/滑点回测 EV,满足阈值放行。
+
+## 6. 系统架构(与仓库结构对齐)
+- `packages/connectors/pacifica`:REST 客户端 + 签名;
+- `packages/utils/shadowBook.ts`:影子订单簿;
+- `packages/execution/orderRouter.ts`:滑点守卫、STP 检查、下单/撤单;
+- `packages/strategies`:MarketMaker + MicroScalper;
+- `packages/portfolio` & `packages/hedge`:持仓聚合与跨账户对冲;
+- `packages/risk`:限额、回撤熔断;
+- `packages/telemetry`:Prom 指标;
+- `apps/runner`:参数加载、定时任务、主循环。
+
+## 7. 风控与合规
+- 硬限:max_notional_abs、max_base_abs、max_order_sz、kill_switch_dd_pct
+- **Kill-Switch(跨账户聚合,增强)**:
+  - **聚合模式**:计算 Account A + Account B 的总权益与总 PnL,避免单账户误杀
+  - **多维度熔断触发器**:
+    1. 跨账户聚合 PnL 回撤 > -0.5%
+    2. Delta 绝对值 > 2x max_base_abs(失控)
+    3. 连续对冲失败 >3 次
+    4. 行情数据断流 >3 秒
+  - **时间窗口回撤**:基于 1 小时滑动窗口计算,而非从启动时刻
+- 订单前置:tick/lot 对齐、滑点守卫、对手价 STP、本地影子簿自家挂单识别
+- **跨账户 STP(新增)**:Global Order Coordinator 检查对手价是否来自关联账户,防止经济自成交
+- 审计:信号→决策→下单→成交→对冲→OCO 全链路日志(append-only + trace id)
+
+## 8. 可观测性
+- **核心指标**:maker_ratio、avg_edge_bps、real_slip_bps、hedge_cost_bps、delta_abs、latency_p99、cancel_rate、pnl_intraday、ev_estimate
+- **对冲效率指标(新增)**:
+  - `hedge_success_rate`:对冲订单成交率(目标 >98%)
+  - `hedge_latency_p50/p95/p99`:对冲延迟分位数(目标 P50<500ms, P99<2s)
+  - `hedge_slippage_bps`:对冲实际成交价 vs 预期价偏差(目标 <0.5bps)
+  - `hedge_retry_rate`:对冲重试率(目标 <5%)
+  - `cross_venue_basis_bps`:双 venue 价差监控(发现套利或数据异常)
+- **资金费率指标(新增)**:
+  - `funding_rate_correlation`:双 venue 资金费率 30 天相关性
+  - `funding_cost_net_bps`:双账户净资金费成本(每 8h)
+  - `funding_same_sign_ratio`:同向支付频率(目标 <20%)
+- **报警**:delta_abs>阈值、pnl_dd>阈值、latency_p99↑、data_gap、hedge_error、STP_hit、**funding_correlation<0.8**、**hedge_success_rate<95%**
+
+## 9. 里程碑(M1–M4)
+- **M1**:接入 `/info` `/book` `/orders/*` `/account/*`,签名链路跑通;影子簿/路由/指标基础。
+- **M2**:策略闭环(MM + Scalper)、对冲引擎、OCO、资金费抓取与面板。
+- **M3**:事件重放与费用/资金费模型、参数搜索、策略报告。
+- **M4**:稳态化(限频、重连、审计)、自适应参数(随 RV/OBI)、多标的扩容。
+
+## 10. 配置(示例)
+```yaml
+env: mainnet
+api_base: https://api.pacifica.fi/api/v1
+symbols: [BTC, ETH, SOL]
+
+# 双账户配置
+accounts:
+  maker:
+    address: ${MAKER_ADDRESS}
+    private_key: ${MAKER_PRIVATE_KEY}
+    subaccount: maker-01
+  hedger:
+    address: ${HEDGER_ADDRESS}
+    private_key: ${HEDGER_PRIVATE_KEY}
+    subaccount: hedger-01  # 或不同 venue
+
+mm: { layers: 2, base_clip_usd: 1000, spread_bps: 1.6, reprice_ms: 300 }
+
+scalper:
+  trigger: { spread_bps: 1.8, min_cooldown_ms: 250 }
+  tp_bps: 3
+  sl_bps: 6
+
+risk:
+  max_notional_abs: 100000
+  max_base_abs: 0.8
+  kill_switch:
+    mode: aggregated  # 跨账户聚合
+    drawdown_pct: -0.5
+    lookback_window_sec: 3600  # 1 小时滑动窗口
+    triggers:
+      - { type: pnl_drawdown, threshold: -0.5 }
+      - { type: delta_abs, threshold: 1.6 }  # 2x max_base_abs
+      - { type: hedge_failure_count, threshold: 3 }
+      - { type: data_gap_sec, threshold: 3 }
+
+hedge:
+  kp: 0.6
+  ki: 0.05
+  qmax: 0.4
+  min_interval_ms: 200
+  # 对冲延迟预算
+  latency_budget:
+    target_p50_ms: 500
+    target_p99_ms: 2000
+    max_exposure_sec: 3  # 超过则强制市价
+    retry_max: 2
+    retry_slippage_increment_bps: 5
+
+# 自适应降级(新增)
+adaptive_mode:
+  enabled: true
+  rv_threshold_high: 0.5  # realized vol >0.5%/min
+  actions_on_high_vol:
+    - disable: scalper
+    - mm.layers: 1
+    - mm.spread_bps: 3.0
+    - mm.clip_multiplier: 0.5
+
+# 流动性监控(新增)
+liquidity:
+  min_top10_depth_usd: 50000  # 前 10 档最小深度
+  max_clip_ratio: 0.05  # clip ≤ depth * 5%
+  slippage_alert_bps: 2  # 实际滑点超过预期 2bps 告警
+
+# 资金费率监控(新增)
+funding:
+  min_correlation: 0.8  # 双 venue 最低相关性
+  max_same_sign_ratio: 0.2  # 最大同向支付频率
+  alert_net_cost_bps_per_8h: 1  # 净成本告警阈值
+```
+
+## 11. 上线演练(Playbook)
+- 金丝雀:低名义跑 30–60 分钟,观察指标与报警;
+- 高波动:自动降层/降 clip,放宽 tp/sl;
+- 数据断流:撤单→HALT;
+- 对冲失败:冻结新增信号,仅处理存量;
+- 资金费极端:降低持仓时长/名义,或只做有利方向的被动单。

+ 198 - 0
docs/SEQUENCE_FLOW.md

@@ -0,0 +1,198 @@
+# 核心流程时序图
+
+> 描述行情处理、策略执行、下单、对冲和降级的关键步骤,确保团队在实现与调试时对事件顺序达成共识。
+
+---
+
+## 1. 行情 → 网格执行
+
+```mermaid
+sequenceDiagram
+    participant WS as Pacifica WS
+    participant MD as MarketDataAdapter
+    participant SB as ShadowBook
+    participant GM as GridMaker
+    participant SC as StrategyCoordinator
+    participant GOC as GlobalOrderCoordinator
+    participant RE as RiskEngine
+    participant OR as OrderRouter
+
+    WS->>MD: book.BTC update (seq, bids, asks)
+    MD->>SB: applyIncrement()
+    Note right of SB: 校验 seq<br/>更新 mid, spread
+    loop tradingInterval
+        GM->>SB: getMid()
+        SB-->>GM: mid price
+        GM->>GM: 计算网格差额 / Delta
+        GM->>SC: emit intents[]
+        SC->>SC: 按优先级去冲突
+        SC->>GOC: intents[]
+        GOC->>GOC: STP / 自成交检查
+        GOC->>RE: intent
+        RE->>RE: preCheck()
+        RE-->>GOC: OK
+        GOC->>OR: sendLimit()
+        OR->>OR: 滑点守卫 / 节流
+        OR->>Pacifica: POST /orders
+        OR-->>GOC: orderId
+    end
+```
+
+- `GridMaker` 在 `onTimer` 内调用一次,返回的意向批量处理。
+- 若 `RiskEngine.preCheck` 抛错,`StrategyCoordinator` 记录失败并等待下一周期。
+
+> **当前实现差异**:代码中尚未启用 `StrategyCoordinator` / `GlobalOrderCoordinator`,`GridMaker` 会直接调用 `OrderRouter.sendLimitChild()`。待协调器上线后,请将实现与上述流程对齐并更新此注释。
+
+---
+
+## 2. 成交 → 对冲流程
+
+```mermaid
+sequenceDiagram
+    participant WS as Pacifica WS
+    participant MD as MarketDataAdapter
+    participant SB as ShadowBook
+    participant GM as GridMaker
+    participant HE as HedgeEngine
+    participant RE as RiskEngine
+    participant OR as OrderRouter
+
+    WS->>MD: fills.maker fillEvent
+    MD->>SB: update position snapshot
+    MD->>GM: onFill(fill)
+    GM->>GM: 更新 Delta / 重挂对手单
+    GM-->>HE: enqueueDelta(symbol, delta)
+    HE->>HE: PI 控制计算需要对冲量
+    HE->>RE: preCheck(intent: hedge)
+    RE-->>HE: OK
+    HE->>OR: sendIOC()
+    OR->>Pacifica: POST /orders (IOC)
+    Pacifca-->>OR: fill | partial | reject
+    alt success
+        OR-->>HE: orderId
+        HE->>RE: reportFill()
+    else failure
+        OR-->>HE: error
+        HE->>HE: 重试 <= retryMax
+        HE->>Telemetry: hedge_failure_count++
+    end
+```
+
+- 对冲在独立工作线程执行,遵守 `minIntervalMs` 与 `maxExposureSec`。
+- 超时未完成 → `HE` 触发强制市价平仓并记录 incident。
+
+---
+
+## 3. 剥头皮执行路径
+
+```mermaid
+sequenceDiagram
+    participant WS as Pacifica WS
+    participant MD as MarketDataAdapter
+    participant SB as ShadowBook
+    participant MS as MicroScalper
+    participant SC as StrategyCoordinator
+    participant GOC as GlobalOrderCoordinator
+    participant RE as RiskEngine
+    participant OR as OrderRouter
+
+    WS->>MD: trade stream
+    MD->>SB: update trade stats
+    SB-->>MS: onTrade()
+    MD->>MS: onBook() (spread, OBI)
+    MS->>MS: 判断触发条件 / 冷却
+    MS-->>SC: intents (passive + taker legs)
+    SC->>SC: 合并 grid & scalper
+    Note right of SC: 剥头皮优先级低于网格<br/>冲突时保留网格
+    SC->>GOC: intents
+    GOC->>RE: preCheck
+    RE-->>GOC: OK
+    GOC->>OR: sendLimit / sendIOC
+```
+
+- Scalper 意向可能包含组合单:先下被动单,再设 OCO;`StrategyCoordinator` 需按顺序传递。
+- 若 `strategy_mode='both'`,GridMaker 生成的挂单优先。
+
+---
+
+## 4. Kill-switch / 降级流程
+
+```mermaid
+sequenceDiagram
+    participant RE as RiskEngine
+    participant DP as DegradationPolicy
+    participant OR as OrderRouter
+    participant STR as StrategyCoordinator
+    participant RUN as Runner
+
+    Note over RE: 每 1s 检查指标<br/>PnL / Delta / HedgeFailure / DataGap
+    RE->>RE: checkKillSwitch()
+    alt 无异常
+        RE-->>DP: { action: 'NONE' }
+    else Kill-switch
+        RE-->>DP: { action: 'HALT', reason }
+        DP->>OR: cancelAll()
+        DP->>STR: pauseStrategies()
+        DP->>Telemetry: emit incident
+        DP->>RUN: notify operator
+    else 降级
+        RE-->>DP: { action: 'REDUCE_ONLY' }
+        DP->>STR: enterReduceOnly()
+    end
+```
+
+- 降级状态需持久化(文件或内存镜像),重启时恢复。
+- 运维可通过 API 覆盖状态(参见 Operations Playbook)。
+
+---
+
+## 5. 配置热更新金丝雀
+
+```mermaid
+sequenceDiagram
+    participant OPS as Operator
+    participant CFG as ConfigService
+    participant TEST as Backtest Harness
+    participant CAN as Canary Runner
+    participant PROD as Live Runner
+
+    OPS->>CFG: 提交新 config.yaml
+    CFG->>CFG: zod 校验
+    CFG-->>OPS: 校验通过/失败
+    OPS->>TEST: 触发 backtest --smoke
+    TEST-->>OPS: 结果 (EV, Sharpe, delta_abs)
+    alt 结果合格
+        OPS->>CAN: reload config (single symbol)
+        CAN->>Telemetry: 推送 KPI (10 分钟)
+        CAN-->>OPS: KPI 达标?
+        alt 达标
+            OPS->>PROD: reload config 全量
+            PROD->>Telemetry: 监控 30 分钟
+        else 回滚
+            OPS->>CAN: rollback config
+        end
+    else 回滚
+        CFG->>CFG: restore previous version
+    end
+```
+
+- 全流程写审计日志:提交人、时间、版本差异、结果。
+
+---
+
+## 6. 日志 trace 链
+
+```
+TRACE_ID 生命周期:
+  StrategyIntentCreated → IntentValidated → OrderSubmitted → OrderAcknowledged →
+  FillReceived → HedgeTriggered → HedgeCompleted → ExposureSnapshotUpdated → TelemetryFlushed
+```
+
+- 所有模块必须将 `traceId` 传递下去,便于事故回放。
+
+---
+
+## 7. 开发注意事项
+
+- 新增流程需同时更新本文件与相关模块文档。
+- 时序图采用 Mermaid 语法,PR 中通过预览确认可视化正常。

+ 174 - 0
docs/TESTING_PLAN.md

@@ -0,0 +1,174 @@
+# 测试与可观测性计划
+
+> 定义单元、集成、回测、性能测试以及指标验证,确保策略在上线前经过系统性验证。
+
+---
+
+## 1. 测试矩阵概览
+
+| 测试类型 | 范围 | 触发方式 | 通过标准 |
+|----------|------|----------|----------|
+| 单元测试 | 单模块逻辑 (GridMaker, RiskEngine, OrderRouter 等) | `pnpm test --filter unit` | 核心断言覆盖率 ≥ 80% |
+| 合约测试 | REST/WS 适配器与签名 | `pnpm test --filter contract` | 对官方 sandbox 通过 |
+| 私有 WS 冒烟 | Pacifica 私有频道 (orders/fills/account) | `pnpm test --filter contract -- --suite pacifica-ws` + 手动连接 | 成功订阅并收到增量,签名校验通过 |
+| 集成测试 | 策略→执行→风控链闭环(Mock adapter) | `pnpm test --filter integration` | 无错误,Delta 控制达标 |
+| 回测 / 仿真 | 实际行情数据回放 | `pnpm backtest --config backtest.yaml` | 指标满足门槛(下表) |
+| 性能 / 压力 | OrderRouter、HedgeEngine 并发 | `pnpm test --filter perf` | CPU<60%,p99 延迟 < 20ms |
+| 可观测性 | Prom 指标、日志字段存在 | `pnpm test --filter telemetry` | 指标注册完整 |
+
+---
+
+## 2. 单元测试
+
+| 模块 | 文件 | 关键用例 | Mock 依赖 |
+|------|------|----------|-----------|
+| `GridMaker` | `packages/strategies/__tests__/gridMaker.test.ts` | 初始化 N 层、成交后补对手单、Delta 超阈触发对冲 | `ShadowBookMock`, `HedgeEngineStub` |
+| `RiskEngine` | `packages/risk/__tests__/riskEngine.test.ts` | 超限拒单、Kill-switch 触发、熔断状态持久化 | 自定义 Fake Telemetry |
+| `OrderRouter` | `packages/execution/__tests__/orderRouter.test.ts` | 滑点守卫、PostOnly 拒绝、clientId 去重 | `ExchangeAdapterMock` |
+| `HedgeEngine` | `packages/hedge/__tests__/hedgeEngine.test.ts` | PI 控制器稳定性、重试逻辑、延迟预算 | Stub Router |
+| `StrategyCoordinator` | `packages/strategies/__tests__/coordinator.test.ts` | 多策略合并、冲突解决、超时 | Faked producers |
+| `PacificaSigning` | `packages/connectors/pacifica/__tests__/signing.test.ts` | body 序列化一致、时间戳漂移、错误密钥 | N/A |
+
+- 覆盖边界条件:零深度、订单被拒、负仓位等。
+- 所有单测运行时禁止真实网络(使用 nock 或本地 stub)。
+
+---
+
+## 3. 集成测试
+
+### 3.1 Grid 执行链
+
+```ts
+describe('Grid strategy integration', () => {
+  it('should place ladder orders, receive fills, and hedge within threshold', async () => {
+    // Arrange: Fake adapter returns deterministic fills
+    // Assert: risk.currentExposure().abs <= max_base_abs
+  });
+});
+```
+
+要求:
+- 使用 `MockPacificaAdapter`,模拟下单、成交、延迟。
+- 验证:订单落地 → Fill → Hedge → Exposure 回到 0 ±0.05。
+
+### 3.2 Scalper + Coordinator
+
+- 场景:Scalper 触发后网格已有挂单;应优先保留网格,Scalper fallback。
+- 验证:冲突解决日志中包含 `resolution: "drop_scalper"`。
+
+### 3.3 Kill-switch
+
+- 模拟 Delta 失控:连续填充多头订单,确认 Kill-switch 进入 `REDUCE_ONLY` 并阻止新开仓。
+
+运行命令:
+```bash
+pnpm test --filter integration
+```
+
+### 3.4 Pacifica 私有 WebSocket 冒烟
+
+- 条件:本地或测试网 API key/secret/subaccount 已配置。
+- 步骤:
+  1. 运行 `pnpm test --filter contract -- --suite pacifica-ws`,确保签名拼接、订阅 payload 通过本地断言。
+  2. 启动 Runner,调用 `PacificaWebSocket.subscribe('orders.{sub}')` 等频道,确认 10 秒内收到订单/成交/account 更新。
+  3. 人为断开网络或重启 WS,确认自动重连后重新带上 auth,增量继续。
+- 期望:官方日志中无鉴权错误;本地记录 `pacifica_ws_auth_fail_total=0`。
+
+---
+
+## 4. 回测与仿真
+
+### 4.1 数据准备
+
+- 行情数据:`data/replay/{symbol}_{date}.jsonl`,包含 book/trade/fill 事件。
+- 转换脚本:`pnpm scripts:fetch --symbol BTC --date 2025-09-01`.
+
+### 4.2 回测命令
+
+```bash
+pnpm backtest --config backtest/grid.yaml --from 2025-09-01 --to 2025-09-07
+```
+
+输出:
+- `reports/grid/{timestamp}/summary.json`
+  ```json
+  {
+    "ev_bps_p50": 1.9,
+    "ev_bps_p10": 0.4,
+    "delta_abs_p95": 0.32,
+    "hedge_cost_bps": 0.6,
+    "max_drawdown_pct": -0.45
+  }
+  ```
+- 图表:`equity_curve.png`, `delta_histogram.png`.
+
+通过标准:
+- `ev_bps_p50 ≥ 1.5`
+- `delta_abs_p95 ≤ 0.5`
+- `max_drawdown_pct ≥ -1.0`
+- `hedge_success_rate ≥ 0.95`
+
+---
+
+## 5. 性能测试
+
+目标:在高频行情下,核心组件 CPU 使用率 < 60%,事件延迟满足 SLO。
+
+### 5.1 OrderRouter 压测
+
+```bash
+pnpm test --filter perf -- --suite order-router
+```
+
+- 模拟 1000 QPS 下单请求,统计:
+  - `sendLimit` 平均 9ms,p99 < 20ms
+  - 重试率 < 1%
+
+### 5.2 HedgeEngine 延迟
+
+- 生成 100 次连续 Delta 超阈事件,确保:
+  - 队列无 backlog
+  - 对冲完成 P50 < 400ms, P99 < 1800ms
+
+---
+
+## 6. 可观测性验证
+
+| 指标 | 验证方法 | 期望 |
+|------|----------|------|
+| `maker_ratio` | 回测 / 真实运行 10 分钟 | 输出至 Prom,值在 0–1 |
+| `hedge_latency_ms_bucket` | 触发对冲 | 直方图 buckets 填充 |
+| `delta_abs` | Grid 运行 | Gauge 反映实时 Delta |
+| `pacifica_ws_reconnects_total` | 人为断网 | 指标增加,告警触发 |
+| `funding_rate_correlation` | FundingMonitor | 计算并输出最新值 |
+
+自动化检查:
+```bash
+pnpm test --filter telemetry
+```
+- 测试读取 `/metrics` 输出,确认关键指标存在并有标签。
+
+---
+
+## 7. 发布前检查清单
+
+- [ ] 单元测试通过 (`pnpm test --filter unit`)
+- [ ] 集成测试通过 (`pnpm test --filter integration`)
+- [ ] 回测近 7 天表现达标 (`reports/grid/.../summary.json`)
+- [ ] 性能测试通过 (`pnpm test --filter perf`)
+- [ ] 指标面板更新,报警规则生效(见 `docs/OPERATIONS_PLAYBOOK.md`)
+- [ ] 配置通过金丝雀流程验证
+
+---
+
+## 8. 持续验证
+
+- 上线后每日回顾:`ev_bps`, `delta_abs`, `hedge_latency`, `funding_same_sign_ratio`。
+- 每周回测最新数据,比较策略参数与实盘表现,必要时调整。
+
+---
+
+## 9. 文档维护
+
+- 新增模块需更新测试覆盖项。
+- 若引入新指标,必须在本文件补充验证步骤。

+ 505 - 0
docs/合规_dex_perp_做市_剥头皮_执行架构(type_script).md

@@ -0,0 +1,505 @@
+> 重要声明:你在上一条中提出“双账户对敲 + 剥头皮”。**对敲/刷量**通常违反多数交易场所的服务条款及所在司法辖区法律/监管要求,属于**市场操纵**范畴。我无法提供任何实现或指导。但我可以提供一份**合规的做市 + 剥头皮(单账户/多 venue、中性、反自成交)**的执行架构与落地方案,帮助你在合规框架下实现高频与中性化目标。下文全部以**合规**为前提:包含自成交预防(STP)、风控守则、合规审计日志等模块。
+
+---
+
+# 目标与约束
+- **目标**:在不承担方向性风险的前提下,通过被动做市与微剥头皮提高成交与绩效。
+- **约束**:
+  - 不与自身账户成交(Self-Trade Prevention,STP)。
+  - 遵守各协议和司法辖区的市场行为规则;保留审计日志。
+  - 所有策略均以**净敞口≈0**为约束(库存中性),以风控优先。
+
+---
+
+# 系统总览
+```
+┌───────────────────────────────────────────────────────────────┐
+│                           App Runner                          │
+│         (DI container, lifecycle, config hot-reload)          │
+└───────────────┬───────────────────────────┬────────────────────┘
+                │                           │
+        ┌───────▼────────┐            ┌─────▼──────────────────────┐
+        │  Strategy Bus  │            │  Risk & Compliance Engine │
+        │ (MM, Scalper)  │            │  (limits, STP, kill-switch)│
+        └───┬─────────────┘            └───────────┬────────────────┘
+            │                                       │
+     ┌──────▼─────────┐                      ┌──────▼──────────────┐
+     │ Order Router   │◄────Health/Latency──►│  Telemetry & Alerts │
+     │ (batch, child  │                      └─────────────────────┘
+     │  orders)       │
+     └───┬────────────┘
+         │
+  ┌──────▼───────────────┐      ┌──────────────────────────────┐
+  │ Exchange Connectors  │◄────►│  Market Data (books,trades)  │
+  │ (Venue SDK/REST/WS)  │      │  + Derived Feeds (greeks,VRP)│
+  └─────────┬────────────┘      └───────────┬───────────────────┘
+            │                                 │
+     ┌──────▼───────────┐              ┌──────▼───────────┐
+     │ Persistence/DB   │              │ Backtest/Sim     │
+     │ (PnL, fills,     │              │ (event-replay)   │
+     │  snapshots, logs)│              └──────────────────┘
+     └──────────────────┘
+```
+
+---
+
+# 模块设计(TypeScript)
+
+## 1. 类型与通用接口
+```ts
+// domain/types.ts
+export type Side = "buy" | "sell";
+export type TimeInForce = "GTC" | "IOC" | "FOK";
+
+export interface Order {
+  id?: string;
+  clientId: string; // unique per venue to support STP
+  symbol: string;   // e.g., BTC-PERP
+  side: Side;
+  px: number;       // limit price
+  sz: number;       // base size
+  tif: TimeInForce;
+  postOnly?: boolean; // passive MM
+}
+
+export interface Fill {
+  orderId: string;
+  tradeId: string;
+  px: number;
+  sz: number;
+  fee: number;
+  liquidity: "maker" | "taker";
+  ts: number;
+}
+
+export interface PositionSnapshot {
+  symbol: string;
+  base: number; // signed
+  quote: number;
+  entryPx?: number;
+  ts: number;
+}
+
+export interface BookLevel { px: number; sz: number; }
+export interface OrderBook { bids: BookLevel[]; asks: BookLevel[]; ts: number; }
+```
+
+## 2. 交易所连接器(抽象 + 具体实现)
+```ts
+// connectors/ExchangeAdapter.ts
+export interface ExchangeAdapter {
+  name(): string;
+  // order
+  place(o: Order): Promise<{ id: string }>;
+  cancel(id: string): Promise<void>;
+  amend(id: string, patch: Partial<Order>): Promise<void>;
+  // data
+  streamOrderBook(symbol: string, onBook: (b: OrderBook) => void): () => void;
+  streamTrades(symbol: string, onTrade: (t: Fill) => void): () => void;
+  // account
+  getPosition(symbol: string): Promise<PositionSnapshot>;
+  getFunding(symbol: string): Promise<{ rate: number; ts: number }>;
+  // venue features
+  supportsSTP(): boolean; // self-trade prevention flag
+}
+```
+> 说明:具体 DEX(如 Drift、Hyperliquid、Aperture 等)各有 SDK,与接口做适配。若无 STP,需在本地实现**自成交预防**:
+- 通过 `clientId` + 本地订单簿镜像,避免对冲到自身挂单;
+- Router 层做“**cross check**”:下买单前,检查卖一是否为本账户挂单,反之同理。
+
+## 3. 订单路由与子单引擎
+```ts
+// execution/OrderRouter.ts
+import { Order, OrderBook } from "../domain/types";
+
+export interface SlippageGuardCfg { maxBps: number; }
+
+export class OrderRouter {
+  constructor(
+    private ex: ExchangeAdapter,
+    private slip: SlippageGuardCfg,
+    private getLocalTop: () => OrderBook | undefined,
+  ) {}
+
+  async sendLimitChild(o: Order): Promise<string> {
+    const top = this.getLocalTop?.();
+    if (top) {
+      // basic slippage guard
+      const best = o.side === "buy" ? top.asks[0]?.px : top.bids[0]?.px;
+      if (best) {
+        const bps = Math.abs((o.px - best) / best) * 1e4;
+        if (bps > this.slip.maxBps) throw new Error(`slippage > ${this.slip.maxBps} bps`);
+      }
+    }
+    const { id } = await this.ex.place(o);
+    return id;
+  }
+}
+```
+
+## 4. 风控与合规
+```ts
+// risk/RiskEngine.ts
+import { PositionSnapshot, Order } from "../domain/types";
+
+export interface RiskLimits {
+  maxBaseAbs: number;      // 单品种库存上限
+  maxNotionalAbs: number;  // 名义敞口上限
+  maxOrderSz: number;      // 单笔下单上限
+  killSwitchDrawdown: number; // 当日回撤阈值
+}
+
+export class RiskEngine {
+  private realizedPnL = 0;
+
+  constructor(private limits: RiskLimits) {}
+
+  checkOrder(o: Order, pos: PositionSnapshot) {
+    if (o.sz > this.limits.maxOrderSz) throw new Error("order size too large");
+    const nextBase = pos.base + (o.side === "buy" ? o.sz : -o.sz);
+    if (Math.abs(nextBase) > this.limits.maxBaseAbs) throw new Error("inventory limit");
+  }
+
+  reportFill(pnlDelta: number) {
+    this.realizedPnL += pnlDelta;
+  }
+
+  shouldHalt(): boolean { return this.realizedPnL < -Math.abs(this.limits.killSwitchDrawdown); }
+}
+```
+
+## 5. 策略层(做市 + 微剥头皮)
+```ts
+// strategies/MarketMaker.ts
+import { OrderBook, Order } from "../domain/types";
+import { OrderRouter } from "../execution/OrderRouter";
+
+export interface MMConfig {
+  symbol: string;
+  tickSz: number;
+  clipSz: number;     // 单笔下单手数
+  spreadBps: number;  // 基础点差
+  repriceMs: number;  // 重新挂单周期
+}
+
+export class MarketMaker {
+  private bidId?: string; private askId?: string;
+  constructor(private cfg: MMConfig, private router: OrderRouter, private getBook: () => OrderBook | undefined) {}
+
+  onTimer = async () => {
+    const book = this.getBook(); if (!book?.bids[0] || !book?.asks[0]) return;
+    const mid = (book.bids[0].px + book.asks[0].px) / 2;
+    const pxBid = Math.floor(mid * (1 - this.cfg.spreadBps/1e4) / this.cfg.tickSz) * this.cfg.tickSz;
+    const pxAsk = Math.ceil (mid * (1 + this.cfg.spreadBps/1e4) / this.cfg.tickSz) * this.cfg.tickSz;
+    const baseOrder = ({symbol: this.cfg.symbol, tif: "GTC" as const, postOnly: true, clientId: `mm-${Date.now()}`});
+    this.bidId = await this.router.sendLimitChild({...baseOrder, side: "buy",  px: pxBid, sz: this.cfg.clipSz});
+    this.askId = await this.router.sendLimitChild({...baseOrder, side: "sell", px: pxAsk, sz: this.cfg.clipSz});
+  }
+}
+```
+
+```ts
+// strategies/MicroScalper.ts
+import { OrderBook, Order } from "../domain/types";
+import { OrderRouter } from "../execution/OrderRouter";
+
+export interface ScalperCfg {
+  symbol: string; clipSz: number; takeProfitBps: number; stopBps: number; cooldownMs: number;
+}
+
+export class MicroScalper {
+  private lastTs = 0;
+  constructor(private cfg: ScalperCfg, private router: OrderRouter, private getBook: () => OrderBook | undefined) {}
+
+  onBook(book: OrderBook) {
+    const now = Date.now();
+    if (now - this.lastTs < this.cfg.cooldownMs) return;
+    if (!book.bids[0] || !book.asks[0]) return;
+
+    const spread = (book.asks[0].px - book.bids[0].px) / ((book.asks[0].px + book.bids[0].px)/2) * 1e4;
+    // 微剥头皮触发:当点差暂时性放大
+    if (spread > 1.2) { // 示例阈值
+      this.lastTs = now;
+      const buyPx  = book.bids[0].px;
+      const sellPx = book.asks[0].px;
+      // 方案:在买一吃一点,目标在中点或买一+tp反手平
+      const tpPx = buyPx * (1 + this.cfg.takeProfitBps/1e4);
+      const slPx = buyPx * (1 - this.cfg.stopBps/1e4);
+      // 这里仅给出下单示例;实际需用OCO/触发单封装
+      this.router.sendLimitChild({symbol: this.cfg.symbol, side: "buy", px: buyPx, sz: this.cfg.clipSz, tif: "IOC", clientId: `scalp-${now}`});
+      // 平仓逻辑交给 PositionManager/TriggerEngine(略)
+    }
+  }
+}
+```
+
+## 6. 触发/风控一体化(OCO、止盈止损)
+```ts
+// execution/TriggerEngine.ts
+export interface OCO { takeProfitPx: number; stopPx: number; qty: number; }
+// 将策略信号转化为触发单,挂在 Router 前端,确保 STP 与风控校验一致。
+```
+
+## 7. 数据与回测
+```ts
+// backtest/Replay.ts
+// 以原始 trades+books 事件重放;策略接口复用生产代码,实现"同一份逻辑"在回测与实盘之间切换。
+```
+
+## 8. 监控与可观测性
+- **指标**:成交通(maker/taker)、库存、名义敞口、滑点、cancel/replace 比例、延迟 P50/P99、资金费率影响。
+- **报警**:延迟>阈值、库存突破、PnL 回撤>阈值、数据断流、挂单失配(shadow book vs venue book)。
+
+---
+
+# 自成交预防(STP)与合规模块
+- **订单级**:clientId + 本地影子簿比对,避免与自身挂单交叉;
+- **会话级**:多 venue / 多账户时,强制使用不同 `account/owner`;
+- **审计日志**:将所有信号、下单、撤单、成交、风控决策写入不可变存储(append-only);
+- **合规守则**:
+  1) 禁止任何设计以“制造虚假成交量”为目的;
+  2) 保留策略参数与改动记录,支持外部审计;
+  3) 采用被动做市 + 合理剥头皮,不进行虚假挂单(spoofing)或层叠(layering)。
+
+---
+
+# 部署与运维
+- **语言/环境**:Node 22+,TypeScript 5+;
+- **时序**:以 `event-queue` 驱动,严格单线程撮合上下文(防竞态),IO 异步。
+- **容错**:断线重连、序列号校验、重放增量事件;
+- **配置热更**:YAML/JSON + schema 校验(zod),参数更改写入审计表。
+
+---
+
+# 任务分解(Milestones)
+**M1 — 骨架与单 venue 通路(~1–2周)**
+- [ ] types 与 ExchangeAdapter 抽象
+- [ ] Drift/某 DEX 适配(下单、撤单、WS 行情)
+- [ ] 影子订单簿与基础延迟计
+- [ ] OrderRouter + SlippageGuard
+- [ ] RiskEngine v1(库存/名义限额,kill-switch)
+
+**M2 — 策略最小闭环(~1周)**
+- [ ] MarketMaker v1(被动两侧挂)
+- [ ] MicroScalper v1(点差触发)
+- [ ] TriggerEngine(OCO/止盈止损)
+- [ ] Telemetry 仪表盘(Grafana/Prom)
+
+**M3 — 回测 & 复用(~1周)**
+- [ ] 事件回放框架,复用策略接口
+- [ ] 参数搜索(grid/random)
+- [ ] 手续费/资金费率建模与 PnL 分解
+
+**M4 — 多 venue 扩展与合规加固(~1–2周)**
+- [ ] 第二家 DEX 适配,跨 venue STP
+- [ ] 审计日志与只增存储
+- [ ] 断线/重放/重启一致性(快照+增量)
+
+**M5 — 生产化与SRE(~持续)**
+- [ ] 蓝绿发布、参数金丝雀
+- [ ] 风险参数自动化回滚
+- [ ] 异常演练:行情暴涨暴跌、数据缺口
+
+---
+
+# PnL 与费用建模(简化)
+```
+RealizedPnL = Σ(±qty * (exitPx - entryPx)) - Σ(fees) + Σ(fundingPnL)
+fees = makerFee * makerQty + takerFee * takerQty
+fundingPnL ≈ positionNotional * fundingRate * holdingTime
+```
+优化方向:
+- 提高 maker 成交占比(返佣)
+- 减少 taker 比例与滑点
+- 在资金费率极端时段降低库存
+
+---
+
+# 开源/依赖建议
+- **math/校验**:`decimal.js`, `zod`
+- **网络/WS**:`undici`, `ws`
+- **日志**:`pino`, `pino-pretty`
+- **观测**:`prom-client`
+- **配置**:`dotenv`, `cosmiconfig`
+
+---
+
+# 后续可选增强
+- 价格预言机加权(CEX 指南价)防止 DEX 局部失真
+- 盘口微结构特征:order-book imbalance、queue position 估计
+- 自适应点差:根据 realized volatility/queue alpha 动态调参
+- 执行智能体:在风控约束下,对参数进行 bandit 优化
+
+> 以上架构与代码骨架可以直接落到 monorepo 中,按 Milestone 拉分支推进。若你提供目标 DEX/SDK,我可以把 `ExchangeAdapter` 的样例实现补齐。
+
+
+
+---
+
+# Delta 中性 + 双向剥头皮(合规模块化方案)
+以下是**不涉及对敲**的合规方案:通过**跨 venue / 子账户**持仓对冲实现 Delta≈0,同时在盘口两侧进行**微剥头皮**与**被动成交**,以获取点差与返佣收益。
+
+## 0) 运行前置(假设)
+- 标的:BTC-PERP / ETH-PERP
+- 交易场所:DEX-A、DEX-B(或一个 venue 的两个独立子账户,启用/自建 STP)
+- 费率:maker -2 bps 返佣,taker 5–7 bps
+- 风险参数:最大名义 50k–250k,kill-switch 当日 -0.5% 账户净值
+
+## 1) 策略总览
+- **目标**:保持净 Delta≈0(跨账户或跨 venue 对冲),在盘口**点差扩张**与**订单流失衡**时进行**方向无关**的短线剥利。
+- **收益来源**:
+  1) maker 成交点差(spread capture)
+  2) taker/maker 组合的微利(短期均值回归或瞬时动量)
+  3) 资金费率对冲后残差(可选)
+- **关键约束**:
+  - STP:避免任何自成交;
+  - “先持仓后对冲”,对冲在另一 venue 或子账户完成;
+  - 始终有**库存上/下限**与**名义上限**。
+
+## 2) 信号与执行逻辑
+### 2.1 盘口与订单流特征
+- **Spread 扩张信号**:
+  - `spread_bps = (ask1 - bid1) / mid * 1e4`
+  - 触发条件:`spread_bps > θ_spread`(如 1.2–2.5 bps,随波动调整)
+- **Order Book Imbalance**:
+  - `obi = Σ(bids[1:k].sz) / (Σ(bids[1:k].sz) + Σ(asks[1:k].sz))`
+  - 极端值(<0.35 或 >0.65)提示瞬时偏移
+- **Trade Imbalance(t-rolling)**:
+  - 最近 `T=0.5–2s` 内的买入/卖出成交量差
+- **Short-term RV(Realized Volatility)**:
+  - 以 200–500ms bar 估计;RV 高→扩大目标点差与止损
+
+### 2.2 入场/出场(方向无关)
+- **做法 A(被动优先)**:
+  1) 在 `mid±δ` 两侧各挂 1–N 层被动单(postOnly);
+  2) 当一侧成交后,**触发对冲**:另一 venue 以市价/紧限价对冲,使 `Δ≈0`;
+  3) 同时为已成交腿设置 `OCO`(tp/sl),tp 优先走 maker(排队),sl 用 taker。
+- **做法 B(微动量吃单)**:
+  1) 当 `spread_bps↑` 且 `trade_imbalance` 指向一侧,薄量吃单进入;
+  2) 立即在同一 venue 以 `mid` 附近被动挂出平仓;
+  3) 若未成交触发 `IOC` 快速退出。
+
+### 2.3 预期价值(EV)校验
+```
+EV_per_trade ≈ edge_bps - taker_bps * taker_ratio - slip_bps - cancel_cost_bps
+要求:EV_per_trade > safety_bps
+```
+- `edge_bps` 来自 spread capture 或短期均值回归预期;
+- `safety_bps` 通常取 1–2 bps 以上。
+
+## 3) Delta 中性与库存控制
+- **控制律(PI/PID 简化)**:
+```
+Δ_target = 0
+error_t = pos_base - Δ_target
+hedge_qty = clamp(kp * error_t + ki * Σerror, [-Qmax, +Qmax])
+```
+- **执行**:在对冲 venue/账户用 `IOC`/紧限价完成;
+- **节流**:`min_hedge_interval_ms`,避免抖动;
+- **资金费率偏置**(可选):若当期 funding 对我方不利,附加微偏置减少该方向库存。
+
+## 4) 参数按波动分层(示例)
+| regime | spread 触发(bps) | maker 层数 | clipSz | tp/sl(bps) | 冷却(ms) |
+|---|---|---|---|---|---|
+| 低波动 | 1.2 | 2–3 | 0.5–1x | 2 / 4 | 300–600 |
+| 中波动 | 1.8 | 2 | 0.3–0.7x | 3 / 6 | 200–400 |
+| 高波动 | 2.5 | 1–2 | 0.2–0.5x | 5 / 10 | 120–250 |
+> `x` 为基础手数:`base_clip = max( min( equity * α / mid, maxOrderSz ), tickSz )`
+
+## 5) TypeScript 关键模块(新增/细化)
+```ts
+// portfolio/PositionManager.ts
+export class PositionManager {
+  constructor(private exA: ExchangeAdapter, private exB: ExchangeAdapter) {}
+  async snapshot(symbol: string) { /* 聚合 A/B 持仓,返回总 Delta */ }
+}
+
+// hedge/HedgeEngine.ts
+export class HedgeEngine {
+  constructor(private ex: ExchangeAdapter, private cfg: { kp:number; ki:number; Qmax:number; minInterval:number }) {}
+  private integ = 0; private last = 0;
+  compute(pos: number) { this.integ += pos; const raw = this.cfg.kp*pos + this.cfg.ki*this.integ; return Math.max(Math.min(raw, this.cfg.Qmax), -this.cfg.Qmax); }
+  async maybeHedge(symbol: string, pos: number) {
+    const now = Date.now(); if (now - this.last < this.cfg.minInterval) return;
+    const q = this.compute(pos); if (Math.abs(q) < 1e-8) return;
+    // 市价/紧限价对冲
+    await this.ex.place({ symbol, side: q>0?"sell":"buy", px: await this.bestPx(symbol, q), sz: Math.abs(q), tif: "IOC", clientId:`hedge-${now}`});
+    this.last = now;
+  }
+  private async bestPx(symbol: string, qty:number){ /* 引用本地簿与滑点守卫 */ return 0; }
+}
+
+// policy/ExecutionPolicy.ts
+export interface ExecutionPolicy { choose(obi:number, spreadBps:number, rv:number): {mode:"passive"|"taker"; layers:number; tp:number; sl:number}; }
+```
+
+## 6) 运行时状态机
+```
+IDLE → (信号触发) → ENTER
+ENTER → (下单成功) → MANAGE
+MANAGE → (对冲完成 & OCO 挂好) → HOLD
+HOLD → (tp/sl 任一触发) → FLAT
+任一状态 → (kill-switch/断流) → HALT
+```
+
+## 7) 回测与仿真
+- 重放 `book+trades`(10–50ms 级粒度);
+- 费用/滑点模型:maker/taker 分别估算;
+- 资金费率:按周期离散计入;
+- 报表:Sharpe(秒级)、胜率、buckets(按 spread/obi/rv 分层),库存时间、对冲成本。
+
+## 8) 观测与风控指标(上线必备)
+- 指标:`maker_ratio`, `avg_edge_bps`, `real_slip_bps`, `hedge_cost_bps`, `delta_abs`, `latency_p99`, `cancel_rate`;
+- 告警:`delta_abs>阈值`、`PnL_drawdown>阈值`、`数据断流`、`STP 命中`、`对冲失败`;
+- 审计:信号 → 决策 → 下单 → 成交 全链路追踪(trace id)。
+
+## 9) 上线清单(Tasks)
+1. **接口层**:完成 2 个目标 DEX 的 `ExchangeAdapter`(下单/撤单/WS)
+2. **影子订单簿**:合并增量深度,提供 mid/top/obi/spread/rv
+3. **OrderRouter**:限价/IOC + 滑点守卫 + STP 检查
+4. **Signals**:spread/obi/trade_imbalance/rv 实现与校准
+5. **Strategy**:被动层 + taker 模式切换的 `ExecutionPolicy`
+6. **PositionManager & HedgeEngine**:跨 venue Delta 汇总与对冲
+7. **TriggerEngine**:OCO(tp/sl)与超时退出
+8. **Risk/Compliance**:名义/库存/回撤限额、kill-switch、审计日志
+9. **Backtest**:事件重放、费用/滑点/资金费率模型
+10. **SRE**:Prom+Grafana 仪表盘、报警、重连/重放、参数热更
+
+## 10) 配置样例(YAML)
+```yaml
+symbol: BTC-PERP
+venues:
+  - name: dexA
+    key: ${A_KEY}
+  - name: dexB
+    key: ${B_KEY}
+mm:
+  layers: 2
+  spread_bps: 1.6
+  clip_usd: 1200
+scalper:
+  trigger:
+    spread_bps: 1.8
+    min_cooldown_ms: 250
+  tp_bps: 3
+  sl_bps: 6
+risk:
+  max_notional: 100000
+  max_base_abs: 0.8
+  kill_dd: -0.5%
+hedge:
+  kp: 0.6
+  ki: 0.05
+  qmax: 0.4
+  min_interval_ms: 200
+```
+
+## 11) 上线演练(Playbook)
+- **冷启动**:先在 `notional=小` 下 30–60 分钟金丝雀;
+- **波动骤升**:切换高波动参数或自动降层/降 clip;
+- **数据断流**:立即撤单→HALT;
+- **对冲失败**:限制新增信号,仅处理存量仓位→安全退出;
+- **资金费率极端**:降低持仓时长/名义,或只开对资金费率有利的一侧被动单。
+
+---
+

+ 37 - 0
package.json

@@ -0,0 +1,37 @@
+{
+  "name": "pacifica-delta-neutral",
+  "private": true,
+  "version": "0.1.0",
+  "packageManager": "pnpm@9.0.0",
+  "scripts": {
+    "build": "pnpm -r build",
+    "dev": "tsx apps/runner/src/index.ts",
+    "lint": "eslint --ext .ts packages apps tests",
+    "typecheck": "tsc --noEmit -p tsconfig.base.json",
+    "test": "vitest --run",
+    "start": "node dist/index.js"
+  },
+  "devDependencies": {
+    "@typescript-eslint/eslint-plugin": "^7.17.0",
+    "@typescript-eslint/parser": "^7.17.0",
+    "@types/node": "^20.11.30",
+    "@types/ws": "^8.5.10",
+    "typescript": "^5.6.2",
+    "tsx": "^4.16.2",
+    "zod": "^3.23.8",
+    "eslint": "^8.57.0",
+    "pino-pretty": "^11.0.0",
+    "vitest": "^2.0.5"
+  },
+  "dependencies": {
+    "bs58": "^6.0.0",
+    "undici": "^6.19.8",
+    "ws": "^8.18.0",
+    "pino": "^9.0.0",
+    "decimal.js": "^10.4.3",
+    "dotenv": "^16.4.5",
+    "prom-client": "^15.1.2",
+    "tweetnacl": "^1.0.3",
+    "yaml": "^2.6.0"
+  }
+}

+ 531 - 0
packages/connectors/pacifica/src/adapter.ts

@@ -0,0 +1,531 @@
+import { request } from "undici";
+import { URL } from "node:url";
+
+import type { Order, OrderBook, PositionSnapshot } from "../../../domain/src/types";
+import { signRequest, signStructuredPayload, SignatureHeader } from "./signing";
+import { PacificaApiError, buildPacificaError, PacificaRateLimitError } from "./errors";
+import { RateLimiter } from "./rateLimiter";
+import { observeRestRequest } from "./metrics";
+import { extractEndpoint, logDebug } from "./helper";
+import { PacificaWsOrderGateway } from "./wsOrderGateway";
+
+type HttpMethod = "GET" | "POST" | "DELETE";
+
+export interface PacificaConfig {
+  baseUrl: string;
+  apiKey?: string;
+  secret?: string;
+  subaccount?: string;
+  userAgent?: string;
+  signatureExpiryWindowMs?: number;
+}
+
+interface PlaceOrderResponse {
+  orderId?: string;
+  id?: string;
+  clientOrderId?: string;
+}
+
+interface CancelResponse {
+  cancelled?: string[];
+  success?: boolean;
+}
+
+interface PositionsResponse {
+  positions?: Array<{
+    symbol: string;
+    base: string | number;
+    quote: string | number;
+    entryPrice?: string | number;
+    pnl?: string | number;
+  }>;
+}
+
+interface BookLevel {
+  p?: string | number;
+  a?: string | number;
+  n?: number;
+}
+
+interface BookResponse {
+  success?: boolean;
+  data?: {
+    s?: string;
+    l?: [BookLevel[], BookLevel[]];
+    t?: number;
+  };
+}
+
+const DEFAULT_SIGNATURE_EXPIRY_WINDOW_MS = 5_000;
+
+export class PacificaAdapter {
+  private readonly base: URL;
+  private readonly limiter: RateLimiter;
+  private wsGateway?: PacificaWsOrderGateway;
+  private accountId?: string;
+
+  constructor(private readonly cfg: PacificaConfig) {
+    if (!cfg.baseUrl) {
+      throw new Error("Pacifica baseUrl is required");
+    }
+    const baseUrl = cfg.baseUrl.endsWith("/")
+      ? cfg.baseUrl
+      : `${cfg.baseUrl}/`;
+    this.base = new URL(baseUrl);
+    this.limiter = new RateLimiter({
+      capacity: 60,
+      refillAmount: 60,
+      refillIntervalMs: 60_000
+    });
+  }
+
+  name() {
+    return "Pacifica";
+  }
+
+  attachWsGateway(gateway: PacificaWsOrderGateway) {
+    this.wsGateway = gateway;
+  }
+
+  setAccountId(id: string): void {
+    this.accountId = id;
+  }
+
+  async getProduct(symbol: string) {
+    return this.rest<any>("GET", `/products/${encodeURIComponent(symbol)}`);
+  }
+
+  async getOrderBook(symbol: string): Promise<OrderBook> {
+    const json = await this.rest<BookResponse>("GET", "/book", {
+      query: { symbol }
+    });
+    const payload = json.data ?? {};
+    const levels = payload.l ?? [];
+    const [bidLevels = [], askLevels = []] = levels;
+
+    const mapLevels = (
+      entries: Array<{ p?: string | number; a?: string | number }>
+    ) =>
+      entries
+        .map(entry => {
+          const px = entry.p !== undefined ? Number(entry.p) : undefined;
+          const sz = entry.a !== undefined ? Number(entry.a) : undefined;
+          if (px === undefined || Number.isNaN(px) || sz === undefined || Number.isNaN(sz)) {
+            return undefined;
+          }
+          return { px, sz };
+        })
+        .filter((entry): entry is { px: number; sz: number } => entry !== undefined);
+
+    const bids = mapLevels(bidLevels);
+    const asks = mapLevels(askLevels);
+
+    return {
+      bids,
+      asks,
+      ts: payload.t ?? Date.now()
+    };
+  }
+
+  async listPositions(): Promise<PositionSnapshot[]> {
+    if (!this.cfg.apiKey) {
+      logDebug("Pacifica account address missing, skipping positions fetch");
+      return [];
+    }
+
+    let json: PositionsResponse | undefined;
+    try {
+      const query: Record<string, unknown> = {};
+      if (this.cfg.apiKey) {
+        query.account = this.cfg.apiKey;
+      }
+      json = await this.rest<PositionsResponse>("GET", "/account/positions", {
+        query
+      });
+    } catch (error) {
+      if (error instanceof PacificaApiError && error.status === 404) {
+        logDebug("Pacifica positions endpoint returned 404, treating as empty", {
+          account: this.cfg.apiKey
+        });
+        return [];
+      }
+      throw error;
+    }
+
+    const now = Date.now();
+    return (json.positions || []).map(pos => ({
+      symbol: pos.symbol,
+      base: Number(pos.base ?? 0),
+      quote: Number(pos.quote ?? 0),
+      entryPx: pos.entryPrice !== undefined ? Number(pos.entryPrice) : undefined,
+      ts: now,
+      accountId: this.accountId
+    }));
+  }
+
+  async getPosition(symbol: string): Promise<PositionSnapshot> {
+    const positions = await this.listPositions();
+    return (
+      positions.find(p => p.symbol === symbol) ?? {
+        symbol,
+        base: 0,
+        quote: 0,
+        ts: Date.now(),
+        accountId: this.accountId
+      }
+    );
+  }
+
+  async place(order: Order): Promise<{ id: string }> {
+    const timeInForce = (order.tif ?? "GTC").toUpperCase();
+
+    if (!this.cfg.apiKey) {
+      throw new Error("Pacifica account address (apiKey) is required to place orders");
+    }
+
+    if (this.wsGateway) {
+      const result = await this.wsGateway.createOrder({
+        symbol: order.symbol,
+        side: order.side === "buy" ? "bid" : "ask",
+        price: formatNumeric(order.px),
+        amount: formatNumeric(order.sz),
+        tif: timeInForce as "GTC" | "IOC" | "FOK",
+        reduceOnly: false,
+        clientOrderId: order.clientId
+      });
+      return { id: result.orderId };
+    }
+
+    const expiryWindow = this.cfg.signatureExpiryWindowMs ?? DEFAULT_SIGNATURE_EXPIRY_WINDOW_MS;
+
+    const signatureHeader: SignatureHeader = {
+      timestamp: Date.now(),
+      expiry_window: expiryWindow,
+      type: "create_order"
+    };
+
+    const signaturePayload: Record<string, unknown> = {
+      symbol: order.symbol,
+      side: order.side === "buy" ? "bid" : "ask",
+      price: formatNumeric(order.px),
+      amount: formatNumeric(order.sz),
+      tif: timeInForce,
+      reduce_only: false,
+      client_order_id: order.clientId
+    };
+
+    const { signature, message } = signStructuredPayload(this.cfg, signatureHeader, signaturePayload);
+    logDebug("Pacifica order signature", { signaturePayload, signatureMessage: message, signature });
+
+    const json = await this.rest<PlaceOrderResponse>("POST", "/orders/create", {
+      body: {
+        account: this.cfg.apiKey,
+        signature,
+        timestamp: signatureHeader.timestamp,
+        expiry_window: signatureHeader.expiry_window,
+        ...signaturePayload
+      }
+    });
+
+    const id =
+      json.orderId ?? json.id ?? json.clientOrderId ?? order.clientId;
+
+    return { id };
+  }
+
+  async cancel(orderId: string, symbol?: string): Promise<void> {
+    if (!this.cfg.apiKey) {
+      throw new Error("Pacifica account address (apiKey) is required to cancel orders");
+    }
+
+    if (this.wsGateway) {
+      try {
+        await this.wsGateway.cancelOrder({ orderId });
+        return;
+      } catch (error) {
+        logDebug("Pacifica WS cancel failed, falling back to REST", {
+          orderId,
+          error: formatError(error)
+        });
+      }
+    }
+
+    const expiryWindow = this.cfg.signatureExpiryWindowMs ?? DEFAULT_SIGNATURE_EXPIRY_WINDOW_MS;
+
+    const signatureHeader: SignatureHeader = {
+      timestamp: Date.now(),
+      expiry_window: expiryWindow,
+      type: "cancel_order"
+    };
+
+    const signaturePayload: Record<string, unknown> = {
+      order_id: orderId
+    };
+    if (symbol) {
+      signaturePayload.symbol = symbol;
+    }
+
+    const { signature, message } = signStructuredPayload(this.cfg, signatureHeader, signaturePayload);
+    logDebug("Pacifica cancel signature", { signaturePayload, signatureMessage: message, signature });
+
+    const json = await this.rest<CancelResponse>("POST", "/orders/cancel", {
+      body: {
+        account: this.cfg.apiKey,
+        signature,
+        timestamp: signatureHeader.timestamp,
+        expiry_window: signatureHeader.expiry_window,
+        ...signaturePayload
+      }
+    });
+
+    if (json.cancelled && !json.cancelled.includes(orderId)) {
+      throw new PacificaApiError("Order cancel not acknowledged", {
+        status: 200,
+        payload: json
+      });
+    }
+  }
+
+  async cancelByClientId(clientOrderId: string, symbol?: string): Promise<void> {
+    if (!this.cfg.apiKey) {
+      throw new Error("Pacifica account address (apiKey) is required to cancel orders by client id");
+    }
+
+    if (this.wsGateway) {
+      try {
+        await this.wsGateway.cancelOrder({ clientOrderId });
+        return;
+      } catch (error) {
+        logDebug("Pacifica WS cancel by client id failed, falling back to REST", {
+          clientOrderId,
+          error: formatError(error)
+        });
+      }
+    }
+
+    const expiryWindow = this.cfg.signatureExpiryWindowMs ?? DEFAULT_SIGNATURE_EXPIRY_WINDOW_MS;
+
+    const signatureHeader: SignatureHeader = {
+      timestamp: Date.now(),
+      expiry_window: expiryWindow,
+      type: "cancel_order"
+    };
+
+    const signaturePayload: Record<string, unknown> = {
+      client_order_id: clientOrderId
+    };
+    if (symbol) {
+      signaturePayload.symbol = symbol;
+    }
+
+    const { signature, message } = signStructuredPayload(this.cfg, signatureHeader, signaturePayload);
+    logDebug("Pacifica cancel signature", { signaturePayload, signatureMessage: message, signature });
+
+    await this.rest<CancelResponse>("POST", "/orders/cancel", {
+      body: {
+        account: this.cfg.apiKey,
+        signature,
+        timestamp: signatureHeader.timestamp,
+        expiry_window: signatureHeader.expiry_window,
+        ...signaturePayload
+      }
+    });
+  }
+
+  async cancelAll(symbol?: string): Promise<void> {
+    if (!this.cfg.apiKey) {
+      throw new Error("Pacifica account address (apiKey) is required to cancel orders");
+    }
+
+    if (this.wsGateway) {
+      try {
+        await this.wsGateway.cancelAll({ allSymbols: symbol ? false : true, symbol });
+        return;
+      } catch (error) {
+        logDebug("Pacifica WS cancel_all failed, falling back to REST", {
+          symbol,
+          error: formatError(error)
+        });
+      }
+    }
+
+    const body: Record<string, unknown> = {
+      account: this.cfg.apiKey
+    };
+    if (symbol) {
+      body.symbol = symbol;
+    }
+    await this.rest("POST", "/orders/cancel/all", { body });
+  }
+
+  supportsSTP(): boolean {
+    return true;
+  }
+
+  private async rest<T>(
+    method: HttpMethod,
+    path: string,
+    options?: { body?: unknown; query?: Record<string, unknown> }
+  ): Promise<T> {
+    await this.limiter.acquire();
+    const normalizedPath = this.buildPath(path, options?.query);
+    const relativePath = normalizedPath.startsWith("/")
+      ? normalizedPath.slice(1)
+      : normalizedPath;
+    const url = new URL(relativePath, this.base).toString();
+
+    const { headers: signatureHeaders, body: bodyString } = signRequest(
+      this.cfg,
+      method,
+      normalizedPath,
+      options?.body
+    );
+
+    const headers: Record<string, string> = {
+      Accept: "application/json",
+      ...signatureHeaders
+    };
+
+    if (options?.body !== undefined) {
+      headers["Content-Type"] = "application/json";
+    }
+    if (this.cfg.userAgent) {
+      headers["User-Agent"] = this.cfg.userAgent;
+    }
+
+    const start = Date.now();
+    let attempt = 0;
+    let res;
+    let completed = false;
+
+    logDebug("Pacifica REST request", {
+      method,
+      endpoint: extractEndpoint(normalizedPath),
+      query: options?.query ? JSON.stringify(options.query) : undefined,
+      hasBody: options?.body !== undefined,
+      url
+    });
+
+    while (!completed) {
+      attempt += 1;
+      res = await request(url, {
+        method,
+        headers,
+        body: bodyString.length ? bodyString : undefined
+      });
+      if (res.statusCode === 429 || res.statusCode >= 500) {
+        if (attempt >= 5) {
+          completed = true;
+        } else {
+          const retryAfterDelay = parseRetryAfter(res.headers["retry-after"]);
+          await sleep(retryAfterDelay ?? attempt * 100);
+          continue;
+        }
+      }
+      completed = true;
+    }
+
+    logDebug("Pacifica REST response", {
+      method,
+      endpoint: extractEndpoint(normalizedPath),
+      status: res?.statusCode,
+      attempt
+    });
+
+    if (!res) {
+      throw new PacificaApiError('Pacifica request did not return a response', {
+        status: 0,
+        code: 'no_response'
+      });
+    }
+
+    const text = await res.body.text();
+    let json: any;
+    if (text) {
+      try {
+        json = JSON.parse(text);
+      } catch {
+        json = undefined;
+      }
+    }
+
+    if (res.statusCode >= 400) {
+      const retryAfter = parseRetryAfter(res.headers["retry-after"]);
+      if (res.statusCode === 429) {
+        throw new PacificaRateLimitError(text || "rate limited", {
+          status: res.statusCode,
+          code: json?.code,
+          retryAfterMs: retryAfter,
+          payload: json ?? text
+        });
+      }
+      throw buildPacificaError(res.statusCode, text, json, retryAfter);
+    }
+
+    observeRestRequest({ endpoint: normalizedPath, method, status: res.statusCode }, Date.now() - start);
+
+    return (json as T) ?? (undefined as T);
+  }
+
+  private buildPath(
+    path: string,
+    query?: Record<string, unknown>
+  ): string {
+    const base = new URL(path, "http://placeholder.local");
+    if (query) {
+      for (const [key, value] of Object.entries(query)) {
+        if (value === undefined || value === null) continue;
+        base.searchParams.set(key, String(value));
+      }
+    }
+    const search = base.searchParams.toString();
+    return base.pathname + (search ? `?${search}` : "");
+  }
+}
+
+function parseRetryAfter(value: string | string[] | undefined): number | undefined {
+  if (!value) return undefined;
+  const raw = Array.isArray(value) ? value[0] : value;
+  if (!raw) return undefined;
+
+  const seconds = Number(raw);
+  if (!Number.isNaN(seconds)) {
+    return seconds * 1000;
+  }
+
+  const date = Date.parse(raw);
+  if (Number.isNaN(date)) return undefined;
+  const diff = date - Date.now();
+  return diff > 0 ? diff : undefined;
+}
+
+function sleep(ms: number): Promise<void> {
+  return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+function formatNumeric(value: number): string {
+  if (!Number.isFinite(value)) {
+    throw new Error(`Pacifica numeric field must be finite, received ${value}`);
+  }
+  return value.toString();
+}
+
+function formatError(error: unknown): Record<string, unknown> {
+  if (!error) {
+    return {};
+  }
+  if (error instanceof Error) {
+    const anyErr = error as any;
+    return {
+      name: error.name,
+      message: error.message,
+      code: anyErr?.code,
+      status: anyErr?.status,
+      stack: error.stack
+    };
+  }
+  if (typeof error === "object") {
+    return error as Record<string, unknown>;
+  }
+  return { message: String(error) };
+}

+ 72 - 0
packages/connectors/pacifica/src/adapterRegistry.ts

@@ -0,0 +1,72 @@
+import type { PacificaConfig } from "./adapter";
+import { PacificaAdapter } from "./adapter";
+import type { PositionSnapshot } from "../../../domain/src/types";
+
+export interface RegisteredAdapter {
+  id: string;
+  role?: "maker" | "hedger" | string;
+  adapter: PacificaAdapter;
+}
+
+export class AdapterRegistry {
+  private readonly adapters = new Map<string, RegisteredAdapter>();
+
+  constructor(initial: Array<{ id: string; role?: string; config: PacificaConfig }> = []) {
+    initial.forEach(entry => this.register(entry.id, entry.config, entry.role));
+  }
+
+  register(id: string, config: PacificaConfig, role?: string): RegisteredAdapter {
+    const adapter = new PacificaAdapter(config);
+    adapter.setAccountId(id);
+    const entry: RegisteredAdapter = { id, role, adapter };
+    this.adapters.set(id, entry);
+    return entry;
+  }
+
+  attach(id: string, adapter: PacificaAdapter, role?: string): RegisteredAdapter {
+    if (typeof adapter.setAccountId === "function") {
+      adapter.setAccountId(id);
+    }
+    const entry: RegisteredAdapter = { id, role, adapter };
+    this.adapters.set(id, entry);
+    return entry;
+  }
+
+  get(id: string): PacificaAdapter {
+    const entry = this.adapters.get(id);
+    if (!entry) {
+      throw new Error(`Adapter ${id} is not registered`);
+    }
+    return entry.adapter;
+  }
+
+  findByRole(role: string): PacificaAdapter | undefined {
+    for (const entry of this.adapters.values()) {
+      if (entry.role === role) return entry.adapter;
+    }
+    return undefined;
+  }
+
+  findEntryByRole(role: string): RegisteredAdapter | undefined {
+    for (const entry of this.adapters.values()) {
+      if (entry.role === role) return entry;
+    }
+    return undefined;
+  }
+
+  list(): RegisteredAdapter[] {
+    return Array.from(this.adapters.values());
+  }
+
+  async collectPositions(
+    symbol: string
+  ): Promise<Array<PositionSnapshot & { accountId: string }>> {
+    const snapshots: Array<PositionSnapshot & { accountId: string }> = [];
+    for (const entry of this.adapters.values()) {
+      const snapshot = await entry.adapter.getPosition(symbol);
+      const accountId = snapshot.accountId ?? entry.id;
+      snapshots.push({ ...snapshot, accountId });
+    }
+    return snapshots;
+  }
+}

+ 93 - 0
packages/connectors/pacifica/src/errors.ts

@@ -0,0 +1,93 @@
+export interface PacificaErrorOptions {
+  status: number;
+  code?: string;
+  retryAfterMs?: number;
+  payload?: unknown;
+}
+
+export class PacificaApiError extends Error {
+  readonly status: number;
+  readonly code?: string;
+  readonly retryAfterMs?: number;
+  readonly payload?: unknown;
+
+  constructor(message: string, options: PacificaErrorOptions) {
+    super(message);
+    this.name = "PacificaApiError";
+    this.status = options.status;
+    this.code = options.code;
+    this.retryAfterMs = options.retryAfterMs;
+    this.payload = options.payload;
+  }
+}
+
+export class PacificaAuthError extends PacificaApiError {
+  constructor(message: string, options: PacificaErrorOptions) {
+    super(message, { ...options, status: options.status || 401 });
+    this.name = "PacificaAuthError";
+  }
+}
+
+export class PacificaValidationError extends PacificaApiError {
+  constructor(message: string, options: PacificaErrorOptions) {
+    super(message, options);
+    this.name = "PacificaValidationError";
+  }
+}
+
+export class PacificaRateLimitError extends PacificaApiError {
+  constructor(message: string, options: PacificaErrorOptions) {
+    super(message, options);
+    this.name = "PacificaRateLimitError";
+  }
+}
+
+export class PacificaUnavailableError extends PacificaApiError {
+  constructor(message: string, options: PacificaErrorOptions) {
+    super(message, options);
+    this.name = "PacificaUnavailableError";
+  }
+}
+
+export function buildPacificaError(
+  status: number,
+  rawText: string,
+  json?: any,
+  retryAfter?: number
+): PacificaApiError {
+  const code =
+    json?.code ??
+    json?.errorCode ??
+    json?.error_code ??
+    json?.error;
+
+  const rawMessage =
+    json?.message ??
+    json?.errorMessage ??
+    json?.error ??
+    rawText;
+
+  const message = rawMessage || `Pacifica API error (status ${status})`;
+
+  const options: PacificaErrorOptions = {
+    status,
+    code,
+    retryAfterMs: retryAfter,
+    payload: json ?? rawText
+  };
+
+  if (status === 401 || status === 403 || code === "UNAUTHENTICATED") {
+    return new PacificaAuthError(message, options);
+  }
+  if (status === 429 || code === "RATE_LIMITED") {
+    return new PacificaRateLimitError(message, options);
+  }
+  if (status === 400 || code === "INVALID_SIZE" || code === "POST_ONLY_WOULD_CROSS") {
+    return new PacificaValidationError(message, options);
+  }
+  if (status >= 500) {
+    return new PacificaUnavailableError(message, options);
+  }
+
+  return new PacificaApiError(message, options);
+}

+ 9 - 0
packages/connectors/pacifica/src/helper.ts

@@ -0,0 +1,9 @@
+export function extractEndpoint(path: string): string {
+  return path.split('?')[0] ?? path;
+}
+
+export function logDebug(...args: unknown[]): void {
+  if (process.env.DEBUG_PACIFICA === '1') {
+    console.debug('[Pacifica]', ...args);
+  }
+}

+ 29 - 0
packages/connectors/pacifica/src/metrics.ts

@@ -0,0 +1,29 @@
+import { Counter, Histogram } from 'prom-client';
+
+const restRequestsTotal = new Counter({
+  name: 'pacifica_rest_requests_total',
+  help: 'Pacifica REST requests',
+  labelNames: ['endpoint', 'method', 'status']
+});
+
+const restLatencyMs = new Histogram({
+  name: 'pacifica_rest_latency_ms',
+  help: 'Pacifica REST latency in milliseconds',
+  labelNames: ['endpoint', 'method'],
+  buckets: [50, 100, 200, 500, 1000, 2000, 5000]
+});
+
+export interface RestMetricLabels {
+  endpoint: string;
+  method: string;
+  status: number | string;
+}
+
+export function observeRestRequest(labels: RestMetricLabels, durationMs: number): void {
+  restRequestsTotal.inc({
+    endpoint: labels.endpoint,
+    method: labels.method,
+    status: String(labels.status)
+  });
+  restLatencyMs.observe({ endpoint: labels.endpoint, method: labels.method }, durationMs);
+}

+ 50 - 0
packages/connectors/pacifica/src/rateLimiter.ts

@@ -0,0 +1,50 @@
+export interface RateLimiterConfig {
+  capacity: number;
+  refillAmount: number;
+  refillIntervalMs: number;
+}
+
+export class RateLimiter {
+  private tokens: number;
+  private readonly capacity: number;
+  private readonly refillAmount: number;
+  private readonly refillIntervalMs: number;
+  private lastRefill: number;
+
+  constructor(config: RateLimiterConfig) {
+    this.capacity = Math.max(1, config.capacity);
+    this.refillAmount = Math.max(1, config.refillAmount);
+    this.refillIntervalMs = Math.max(1, config.refillIntervalMs);
+    this.tokens = this.capacity;
+    this.lastRefill = Date.now();
+  }
+
+  async acquire(): Promise<void> {
+    let acquired = false;
+    while (!acquired) {
+      this.refillTokens();
+      if (this.tokens > 0) {
+        this.tokens -= 1;
+        acquired = true;
+        break;
+      }
+      const delay = Math.max(this.refillIntervalMs - (Date.now() - this.lastRefill), 0);
+      await sleep(delay || 1);
+    }
+  }
+
+  private refillTokens(): void {
+    const now = Date.now();
+    if (now - this.lastRefill < this.refillIntervalMs) return;
+
+    const cycles = Math.floor((now - this.lastRefill) / this.refillIntervalMs);
+    if (cycles <= 0) return;
+
+    this.tokens = Math.min(this.capacity, this.tokens + cycles * this.refillAmount);
+    this.lastRefill = now;
+  }
+}
+
+function sleep(ms: number): Promise<void> {
+  return new Promise(resolve => setTimeout(resolve, ms));
+}

+ 158 - 0
packages/connectors/pacifica/src/signing.ts

@@ -0,0 +1,158 @@
+import nacl from "tweetnacl";
+import bs58 from "bs58";
+
+export interface SignedRequest {
+  headers: Record<string, string>;
+  body: string;
+  timestamp: string;
+}
+
+export interface SigningConfig {
+  apiKey?: string;
+  secret?: string;
+  subaccount?: string;
+}
+
+export interface SignatureHeader {
+  timestamp: number;
+  expiry_window: number;
+  type: string;
+}
+
+const encoder = new TextEncoder();
+
+/**
+ * Sign request according to Pacifica Ed25519 spec.
+ *
+ * Signature base string: `${timestamp}:${method}:${path}:${body || ''}`
+ * - `timestamp`: milliseconds since epoch
+ * - `method`: upper-case HTTP verb
+ * - `path`: request path including query string (e.g. `/orders?symbol=BTC`)
+ * - `body`: JSON string if present, otherwise empty string
+ */
+export function signRequest(
+  cfg: SigningConfig,
+  method: string,
+  path: string,
+  body: unknown
+): SignedRequest {
+  const bodyString =
+    body === undefined || body === null
+      ? ""
+      : typeof body === "string"
+      ? body
+      : JSON.stringify(body);
+
+  // Public endpoints may omit keys.
+  if (!cfg.apiKey || !cfg.secret) {
+    return {
+      headers: {},
+      body: bodyString,
+      timestamp: Date.now().toString()
+    };
+  }
+
+  const timestamp = Date.now().toString();
+  const baseString = `${timestamp}:${method.toUpperCase()}:${path}:${bodyString}`;
+
+  const secretKey = deriveSecretKey(cfg.secret);
+  const keyPair = nacl.sign.keyPair.fromSecretKey(secretKey);
+  const derivedAccount = bs58.encode(keyPair.publicKey);
+
+  if (cfg.apiKey && cfg.apiKey !== derivedAccount) {
+    throw new Error(
+      `Pacifica apiKey (${cfg.apiKey}) does not match private key (derived ${derivedAccount})`
+    );
+  }
+  const signatureBytes = nacl.sign.detached(encoder.encode(baseString), secretKey);
+  const signature = Buffer.from(signatureBytes).toString("base64");
+
+  const headers: Record<string, string> = {
+    "X-Pacific-Key": cfg.apiKey,
+    "X-Pacific-Timestamp": timestamp,
+    "X-Pacific-Signature": signature
+  };
+
+  if (cfg.subaccount) {
+    headers["X-Pacific-Subaccount"] = cfg.subaccount;
+  }
+
+  return {
+    headers,
+    body: bodyString,
+    timestamp
+  };
+}
+
+export function signStructuredPayload(
+  cfg: SigningConfig,
+  header: SignatureHeader,
+  payload: Record<string, unknown>
+): { message: string; signature: string } {
+  if (!cfg.secret) {
+    throw new Error("Pacifica secret is required for message signing");
+  }
+
+  const secretKey = deriveSecretKey(cfg.secret);
+  const canonical = canonicalize({ ...header, data: payload });
+  const message = JSON.stringify(canonical);
+
+  const signatureBytes = nacl.sign.detached(encoder.encode(message), secretKey);
+  const signature = bs58.encode(signatureBytes);
+
+  return { message, signature };
+}
+
+function deriveSecretKey(secret: string): Uint8Array {
+  const trimmed = secret.trim();
+  let decoded: Uint8Array | undefined;
+
+  // Try base58 first.
+  try {
+    decoded = bs58.decode(trimmed);
+  } catch {
+    // Ignore and try base64.
+  }
+
+  if (!decoded || decoded.length === 0) {
+    const buf = Buffer.from(trimmed, "base64");
+    if (buf.length === 0) {
+      throw new Error("Pacifica secret is not valid base58/base64 Ed25519 key");
+    }
+    decoded = new Uint8Array(buf);
+  }
+
+  if (decoded.length === nacl.sign.secretKeyLength) {
+    return decoded;
+  }
+
+  if (decoded.length === nacl.sign.seedLength) {
+    return nacl.sign.keyPair.fromSeed(decoded).secretKey;
+  }
+
+  throw new Error(
+    `Pacifica secret has invalid length ${decoded.length}, expected ${nacl.sign.seedLength} or ${nacl.sign.secretKeyLength}`
+  );
+}
+
+function canonicalize(value: unknown): unknown {
+  if (Array.isArray(value)) {
+    return value.map(item => canonicalize(item));
+  }
+
+  if (value && typeof value === "object") {
+    const entries = Object.entries(value as Record<string, unknown>).sort(([a], [b]) => {
+      if (a < b) return -1;
+      if (a > b) return 1;
+      return 0;
+    });
+
+    const sorted: Record<string, unknown> = {};
+    for (const [key, val] of entries) {
+      sorted[key] = canonicalize(val);
+    }
+    return sorted;
+  }
+
+  return value;
+}

+ 307 - 0
packages/connectors/pacifica/src/wsClient.ts

@@ -0,0 +1,307 @@
+import { EventEmitter } from 'node:events';
+import WebSocket from 'ws';
+import type { SigningConfig } from './signing';
+import { signRequest } from './signing';
+import { randomUUID } from 'node:crypto';
+
+export interface PacificaWebSocketConfig {
+  url: string;
+  apiKey?: string;
+  secret?: string;
+  subaccount?: string;
+  reconnectIntervalMs?: number;
+  maxReconnectIntervalMs?: number;
+  heartbeatIntervalMs?: number;
+}
+
+type Subscription = {
+  channel: string;
+  params?: Record<string, unknown>;
+  auth?: SigningConfig;
+};
+
+type SubscribePayload = {
+  method: 'subscribe';
+  params: Record<string, unknown>;
+};
+
+const PRIVATE_CHANNEL_PREFIXES = ['orders.', 'fills.', 'account.'] as const;
+const DEFAULT_AUTH_METHOD = 'SUBSCRIBE';
+const DEFAULT_AUTH_PATH = '/ws';
+
+export class PacificaWebSocket extends EventEmitter {
+  private socket?: WebSocket;
+  private reconnectTimer?: NodeJS.Timeout;
+  private heartbeatTimer?: NodeJS.Timeout;
+  private manuallyClosed = false;
+  private reconnectAttempts = 0;
+  private readonly subscriptions: Subscription[] = [];
+  private readonly pendingRequests = new Map<
+    string,
+    {
+      resolve: (value: any) => void;
+      reject: (error: Error) => void;
+      timer: NodeJS.Timeout;
+    }
+  >();
+
+  constructor(private readonly config: PacificaWebSocketConfig) {
+    super();
+  }
+
+  connect(): void {
+    this.manuallyClosed = false;
+    this.openSocket();
+  }
+
+  disconnect(): void {
+    this.manuallyClosed = true;
+    this.clearHeartbeat();
+    if (this.reconnectTimer) {
+      clearTimeout(this.reconnectTimer);
+      this.reconnectTimer = undefined;
+    }
+    this.socket?.close();
+    this.socket = undefined;
+  }
+
+  subscribe(
+    channel: string,
+    params?: Record<string, unknown>,
+    authOverride?: SigningConfig
+  ): void {
+    const auth = this.resolveAuth(channel, authOverride);
+    if (!auth && this.requiresAuthentication(channel)) {
+      throw new Error(`Missing credentials for private Pacifica channel ${channel}`);
+    }
+    const payload: Subscription = { channel, params, auth };
+    this.subscriptions.push(payload);
+    this.flushSubscriptions();
+  }
+
+  subscribeAuthenticated(
+    channel: string,
+    params?: Record<string, unknown>,
+    authOverride?: SigningConfig
+  ): void {
+    this.subscribe(channel, params, authOverride ?? this.configCredentials());
+  }
+
+  sendRaw(data: unknown): void {
+    if (this.socket?.readyState === WebSocket.OPEN) {
+      this.socket.send(JSON.stringify(data));
+    }
+  }
+
+  private openSocket(): void {
+    this.socket = new WebSocket(this.config.url);
+    this.socket.on('open', () => {
+      this.reconnectAttempts = 0;
+      this.emit('open');
+      this.flushSubscriptions(true);
+      this.startHeartbeat();
+    });
+
+    this.socket.on('message', message => {
+      this.handleMessage(message);
+    });
+
+    this.socket.on('error', error => {
+      this.emit('error', error);
+    });
+
+    this.socket.on('close', () => {
+      this.emit('close');
+      this.clearHeartbeat();
+      this.rejectPendingRequests(new Error('Pacifica WebSocket closed'));
+      if (!this.manuallyClosed) {
+        this.scheduleReconnect();
+      }
+    });
+  }
+
+  private flushSubscriptions(force = false): void {
+    if (!force && this.socket?.readyState !== WebSocket.OPEN) return;
+    for (const sub of this.subscriptions) {
+      const baseParams: Record<string, unknown> = sub.params
+        ? { ...sub.params, channel: sub.channel }
+        : { channel: sub.channel };
+      const payload: SubscribePayload = { method: 'subscribe', params: baseParams };
+      if (sub.auth) {
+        const authParams = this.buildAuthParams(sub.auth, payload);
+        const paramsWithAuth = {
+          ...baseParams,
+          auth: authParams
+        };
+        this.sendRaw({ method: 'subscribe', params: paramsWithAuth });
+      } else {
+        this.sendRaw(payload);
+      }
+    }
+  }
+
+  private startHeartbeat(): void {
+    this.clearHeartbeat();
+    const interval = this.config.heartbeatIntervalMs ?? 30_000;
+    this.heartbeatTimer = setInterval(() => {
+      this.sendRaw({ method: 'ping' });
+    }, interval);
+  }
+
+  private clearHeartbeat(): void {
+    if (this.heartbeatTimer) {
+      clearInterval(this.heartbeatTimer);
+      this.heartbeatTimer = undefined;
+    }
+  }
+
+  private scheduleReconnect(): void {
+    if (this.reconnectTimer) return;
+    this.reconnectAttempts += 1;
+    const base = this.config.reconnectIntervalMs ?? 1_000;
+    const max = this.config.maxReconnectIntervalMs ?? 30_000;
+    const delay = Math.min(base * 2 ** (this.reconnectAttempts - 1), max);
+    this.reconnectTimer = setTimeout(() => {
+      this.reconnectTimer = undefined;
+      this.emit('reconnected', { attempt: this.reconnectAttempts });
+      this.openSocket();
+    }, delay);
+  }
+
+  private requiresAuthentication(channel: string): boolean {
+    return PRIVATE_CHANNEL_PREFIXES.some(prefix => channel.startsWith(prefix));
+  }
+
+  private configCredentials(): SigningConfig {
+    return {
+      apiKey: this.config.apiKey,
+      secret: this.config.secret,
+      subaccount: this.config.subaccount
+    };
+  }
+
+  private resolveAuth(channel: string, override?: SigningConfig): SigningConfig | undefined {
+    const combined: SigningConfig = {
+      apiKey: override?.apiKey ?? this.config.apiKey,
+      secret: override?.secret ?? this.config.secret,
+      subaccount: override?.subaccount ?? this.config.subaccount
+    };
+    const hasCredentials = Boolean(combined.apiKey && combined.secret);
+    if (!hasCredentials) {
+      return undefined;
+    }
+    if (override) {
+      return combined;
+    }
+    return this.requiresAuthentication(channel) ? combined : undefined;
+  }
+
+  private buildAuthParams(signing: SigningConfig, payload: SubscribePayload): Record<string, unknown> {
+    if (!signing.apiKey || !signing.secret) {
+      throw new Error('Pacifica WebSocket auth requires both apiKey and secret');
+    }
+
+    const { headers, timestamp } = signRequest(
+      signing,
+      DEFAULT_AUTH_METHOD,
+      DEFAULT_AUTH_PATH,
+      payload
+    );
+
+    const signature = headers['X-Pacific-Signature'];
+    if (!signature) {
+      throw new Error('Pacifica signing result missing X-Pacific-Signature header');
+    }
+
+    const auth: Record<string, unknown> = {
+      key: signing.apiKey,
+      timestamp,
+      signature
+    };
+
+    const subaccount = signing.subaccount;
+    if (subaccount) {
+      auth.subaccount = subaccount;
+    }
+
+    return auth;
+  }
+
+  async waitForOpen(): Promise<void> {
+    if (this.socket?.readyState === WebSocket.OPEN) return;
+    if (this.socket?.readyState === WebSocket.CONNECTING) {
+      await new Promise<void>(resolve => this.once('open', resolve));
+      return;
+    }
+    throw new Error('Pacifica WebSocket is not connected');
+  }
+
+  async sendRpc(action: string, params: Record<string, unknown>, timeoutMs = 10_000): Promise<any> {
+    await this.waitForOpen();
+    const id = randomUUID();
+    const message = {
+      id,
+      params: {
+        [action]: params
+      }
+    };
+
+    return new Promise((resolve, reject) => {
+      const timer = setTimeout(() => {
+        this.pendingRequests.delete(id);
+        reject(new Error(`Pacifica WebSocket request ${action} timed out`));
+      }, timeoutMs);
+
+      this.pendingRequests.set(id, { resolve, reject, timer });
+      this.sendRaw(message);
+    });
+  }
+
+  private handleMessage(message: WebSocket.RawData): void {
+    this.emit('message', message);
+    let parsed: any;
+    try {
+      const text = typeof message === 'string' ? message : message.toString();
+      parsed = JSON.parse(text);
+    } catch {
+      return;
+    }
+
+    if (!parsed || typeof parsed !== 'object' || !parsed.id) {
+      return;
+    }
+
+    const pending = this.pendingRequests.get(parsed.id as string);
+    if (!pending) return;
+    this.pendingRequests.delete(parsed.id as string);
+    clearTimeout(pending.timer);
+
+    if (parsed.error) {
+      const errMsg = typeof parsed.error === 'string'
+        ? parsed.error
+        : parsed.error.message || JSON.stringify(parsed.error);
+      pending.reject(new Error(errMsg));
+      return;
+    }
+
+    if ('result' in parsed) {
+      pending.resolve(parsed.result);
+      return;
+    }
+
+    if ('data' in parsed) {
+      pending.resolve(parsed.data);
+      return;
+    }
+
+    pending.resolve(parsed);
+  }
+
+  private rejectPendingRequests(error: Error): void {
+    for (const [, entry] of this.pendingRequests) {
+      clearTimeout(entry.timer);
+      entry.reject(error);
+    }
+    this.pendingRequests.clear();
+  }
+}

+ 163 - 0
packages/connectors/pacifica/src/wsOrderGateway.ts

@@ -0,0 +1,163 @@
+import { randomUUID } from "node:crypto";
+import type { SigningConfig, SignatureHeader } from "./signing";
+import { signStructuredPayload } from "./signing";
+import { PacificaWebSocket } from "./wsClient";
+
+const DEFAULT_EXPIRY_WINDOW_MS = 5_000;
+const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
+
+export interface CreateOrderRequest {
+  symbol: string;
+  side: "bid" | "ask";
+  price: string | number;
+  amount: string | number;
+  tif: "GTC" | "IOC" | "FOK";
+  reduceOnly?: boolean;
+  clientOrderId: string;
+}
+
+export interface CancelOrderRequest {
+  orderId?: string;
+  clientOrderId?: string;
+}
+
+export interface CancelAllRequest {
+  allSymbols?: boolean;
+  excludeReduceOnly?: boolean;
+  symbol?: string;
+}
+
+export interface CreateOrderResult {
+  orderId: string;
+}
+
+export class PacificaWsOrderGateway {
+  constructor(
+    private readonly ws: PacificaWebSocket,
+    private readonly signing: SigningConfig,
+    private readonly expiryWindowMs: number = DEFAULT_EXPIRY_WINDOW_MS,
+    private readonly requestTimeoutMs: number = DEFAULT_REQUEST_TIMEOUT_MS
+  ) {
+    if (!signing.apiKey || !signing.secret) {
+      throw new Error("Pacifica WS gateway requires apiKey and secret");
+    }
+  }
+
+  async connect(): Promise<void> {
+    this.ws.connect();
+    await this.ws.waitForOpen();
+  }
+
+  async createOrder(request: CreateOrderRequest): Promise<CreateOrderResult> {
+    const header = this.buildHeader("create_order");
+    const payload = {
+      symbol: request.symbol,
+      side: request.side,
+      price: this.formatNumeric(request.price),
+      amount: this.formatNumeric(request.amount),
+      tif: request.tif,
+      reduce_only: Boolean(request.reduceOnly),
+      client_order_id: request.clientOrderId
+    };
+
+    const body = this.buildSignedBody(header, payload);
+    const response = await this.ws.sendRpc("create_order", body, this.requestTimeoutMs);
+    const orderId = extractOrderId(response);
+
+    if (!orderId) {
+      throw new Error(`Pacifica WS create_order response missing order id: ${JSON.stringify(response)}`);
+    }
+
+    return { orderId };
+  }
+
+  async cancelOrder(request: CancelOrderRequest): Promise<void> {
+    const header = this.buildHeader("cancel_order");
+
+    if (!request.orderId && !request.clientOrderId) {
+      throw new Error("Pacifica cancel request requires orderId or clientOrderId");
+    }
+
+    const payload: Record<string, unknown> = {};
+    if (request.orderId) {
+      payload.order_id = request.orderId;
+    }
+    if (request.clientOrderId) {
+      payload.client_order_id = request.clientOrderId;
+    }
+
+    const body = this.buildSignedBody(header, payload);
+    await this.ws.sendRpc("cancel_order", body, this.requestTimeoutMs);
+  }
+
+  async cancelAll(request: CancelAllRequest = {}): Promise<void> {
+    const header = this.buildHeader("cancel_all_orders");
+    const payload: Record<string, unknown> = {
+      all_symbols: request.allSymbols ?? true,
+      exclude_reduce_only: request.excludeReduceOnly ?? false
+    };
+    if (request.symbol) {
+      payload.symbol = request.symbol;
+    }
+    const body = this.buildSignedBody(header, payload);
+    await this.ws.sendRpc("cancel_all_orders", body, this.requestTimeoutMs);
+  }
+
+  private buildHeader(type: string): SignatureHeader {
+    return {
+      timestamp: Date.now(),
+      expiry_window: this.expiryWindowMs,
+      type
+    };
+  }
+
+  private buildSignedBody(header: SignatureHeader, payload: Record<string, unknown>) {
+    const { signature } = signStructuredPayload(this.signing, header, payload);
+    return {
+      account: this.signing.apiKey,
+      signature,
+      timestamp: header.timestamp,
+      expiry_window: header.expiry_window,
+      ...payload
+    };
+  }
+
+  private formatNumeric(value: string | number): string {
+    if (typeof value === "string") return value;
+    if (!Number.isFinite(value)) {
+      throw new Error(`Pacifica WS numeric field must be finite, received ${value}`);
+    }
+    return value.toString();
+  }
+}
+
+function extractOrderId(raw: any): string | undefined {
+  if (!raw) return undefined;
+  const candidates: Array<unknown> = [
+    raw.order_id,
+    raw.orderId,
+    raw.id,
+    raw.i,
+    raw.result?.order_id,
+    raw.result?.orderId,
+    raw.result?.id,
+    raw.result?.i,
+    raw.data?.order_id,
+    raw.data?.orderId,
+    raw.data?.id,
+    raw.data?.i,
+    raw.I,
+    raw.result?.I,
+    raw.data?.I
+  ];
+  for (const candidate of candidates) {
+    if (candidate === undefined || candidate === null) continue;
+    if (typeof candidate === "string" && candidate.trim().length > 0) {
+      return candidate;
+    }
+    if (typeof candidate === "number" && Number.isFinite(candidate)) {
+      return candidate.toString();
+    }
+  }
+  return undefined;
+}

+ 76 - 0
packages/domain/src/types.ts

@@ -0,0 +1,76 @@
+export type Side = "buy" | "sell";
+export type TIF = "GTC" | "IOC" | "FOK";
+
+export interface Order {
+  clientId: string;
+  symbol: string;
+  side: Side;
+  px: number;
+  sz: number;
+  tif: TIF;
+  postOnly?: boolean;
+  accountId?: string;
+}
+
+export interface Fill {
+  orderId: string;
+  tradeId: string;
+  symbol: string;
+  side: Side;
+  px: number;
+  sz: number;
+  fee: number;
+  liquidity: "maker" | "taker";
+  ts: number;
+}
+
+export interface PositionSnapshot {
+  symbol: string;
+  base: number;
+  quote: number;
+  entryPx?: number;
+  ts: number;
+  accountId?: string;
+}
+
+export interface BookLevel { px: number; sz: number; }
+export interface OrderBook { bids: BookLevel[]; asks: BookLevel[]; ts: number; }
+
+export interface RiskLimits {
+  maxBaseAbs: number;
+  maxNotionalAbs: number;
+  maxOrderSz: number;
+  killSwitchDrawdownPct: number;
+}
+
+export interface OrderBookUpdate {
+  price: number;
+  size: number;
+}
+
+export interface OrderBookDelta {
+  bids?: OrderBookUpdate[];
+  asks?: OrderBookUpdate[];
+  seq?: number;
+  ts?: number;
+}
+
+// Grid Trading Types
+export interface GridLevel {
+  index: number;            // 网格索引(正=卖单,负=买单)
+  side: Side;
+  px: number;
+  sz: number;
+  orderId?: string;
+  filled: boolean;
+  timestamp: number;
+}
+
+export interface GridConfig {
+  symbol: string;
+  gridStepBps: number;      // 网格间距(bps)
+  gridRangeBps: number;     // 网格范围(bps)
+  baseClipUsd: number;      // 单层订单大小(USD)
+  maxLayers: number;        // 最大层数
+  hedgeThresholdBase: number; // 对冲阈值(base asset)
+}

+ 157 - 0
packages/execution/src/globalOrderCoordinator.ts

@@ -0,0 +1,157 @@
+import { EventEmitter } from "node:events";
+
+import type { Side } from "../../domain/src/types";
+
+export interface GlobalOrderSnapshot {
+  orderId: string;
+  clientOrderId?: string;
+  accountId: string;
+  symbol: string;
+  side: Side;
+  price: number;
+  timestamp: number;
+}
+
+export interface ValidationIntent {
+  accountId: string;
+  symbol: string;
+  side: Side;
+  price: number;
+}
+
+export interface GlobalCoordinatorOptions {
+  stpToleranceBps?: number;
+}
+
+type SymbolMap = Map<string, Map<number, GlobalOrderSnapshot[]>>;
+
+export class GlobalOrderCoordinator extends EventEmitter {
+  private readonly stpToleranceBps: number;
+  private readonly orderIndex = new Map<string, GlobalOrderSnapshot>();
+  private readonly books: SymbolMap = new Map();
+  private readonly clientIndex = new Map<string, string>();
+
+  constructor(options: GlobalCoordinatorOptions = {}) {
+    super();
+    this.stpToleranceBps = options.stpToleranceBps ?? 0;
+  }
+
+  validate(intent: ValidationIntent): void {
+    const conflicts = this.findConflicts(intent);
+    if (conflicts.length > 0) {
+      this.emit("conflict", { intent, conflicts });
+      throw new Error("STP violation: conflicting order detected");
+    }
+  }
+
+  register(snapshot: GlobalOrderSnapshot): void {
+    this.orderIndex.set(snapshot.orderId, snapshot);
+    if (snapshot.clientOrderId) {
+      this.clientIndex.set(snapshot.clientOrderId, snapshot.orderId);
+    }
+    const book = getOrCreateMap(
+      this.books,
+      snapshot.symbol,
+      () => new Map<number, GlobalOrderSnapshot[]>()
+    );
+    const level = getOrCreateArray(book, snapshot.price);
+    level.push(snapshot);
+    this.emit("registered", snapshot);
+  }
+
+  release(orderId: string): void {
+    const snapshot = this.orderIndex.get(orderId);
+    if (!snapshot) return;
+    this.orderIndex.delete(orderId);
+    if (snapshot.clientOrderId) {
+      this.clientIndex.delete(snapshot.clientOrderId);
+    }
+    const book = this.books.get(snapshot.symbol);
+    if (!book) return;
+    const level = book.get(snapshot.price);
+    if (!level) return;
+    const idx = level.findIndex(item => item.orderId === orderId);
+    if (idx >= 0) {
+      level.splice(idx, 1);
+      if (level.length === 0) {
+        book.delete(snapshot.price);
+      }
+    }
+    this.emit("released", snapshot);
+  }
+
+  releaseByClientId(clientOrderId: string): void {
+    const orderId = this.clientIndex.get(clientOrderId);
+    if (!orderId) return;
+    this.release(orderId);
+  }
+
+  peekByClientId(clientOrderId: string): GlobalOrderSnapshot | undefined {
+    const orderId = this.clientIndex.get(clientOrderId);
+    if (!orderId) return undefined;
+    return this.orderIndex.get(orderId);
+  }
+
+  peek(orderId: string): GlobalOrderSnapshot | undefined {
+    return this.orderIndex.get(orderId);
+  }
+
+  list(): GlobalOrderSnapshot[] {
+    return Array.from(this.orderIndex.values());
+  }
+
+  private findConflicts(intent: ValidationIntent): GlobalOrderSnapshot[] {
+    const book = this.books.get(intent.symbol);
+    if (!book) return [];
+    const conflictSide: Side = intent.side === "buy" ? "sell" : "buy";
+    const priceLevels = Array.from(book.keys());
+    const tolerance = this.stpToleranceBps;
+    const results: GlobalOrderSnapshot[] = [];
+
+    for (const price of priceLevels) {
+      const entries = book.get(price);
+      if (!entries) continue;
+      for (const entry of entries) {
+        if (entry.accountId === intent.accountId) continue;
+        if (entry.side !== conflictSide) continue;
+        if (isCross(entry.price, intent.price, intent.side, tolerance)) {
+          results.push(entry);
+        }
+      }
+    }
+    return results;
+  }
+}
+
+function getOrCreateMap<K, V>(map: Map<K, V>, key: K, factory: () => V): V {
+  const existing = map.get(key);
+  if (existing) return existing;
+  const value = factory();
+  map.set(key, value);
+  return value;
+}
+
+function getOrCreateArray<K, T>(map: Map<K, T[]>, key: K): T[] {
+  const existing = map.get(key);
+  if (existing) return existing;
+  const arr: T[] = [];
+  map.set(key, arr);
+  return arr;
+}
+
+function isCross(levelPrice: number, intentPrice: number, intentSide: Side, toleranceBps: number): boolean {
+  if (intentSide === "buy") {
+    if (intentPrice < levelPrice) return false;
+    return compareWithTolerance(intentPrice, levelPrice, toleranceBps) >= 0;
+  }
+  if (intentPrice > levelPrice) return false;
+  return compareWithTolerance(levelPrice, intentPrice, toleranceBps) >= 0;
+}
+
+function compareWithTolerance(value: number, reference: number, toleranceBps: number): number {
+  const diff = value - reference;
+  if (toleranceBps <= 0) return diff;
+  const allowed = (toleranceBps / 10_000) * reference;
+  if (Math.abs(diff) <= allowed) return 0;
+  return diff;
+}

+ 164 - 0
packages/execution/src/orderRouter.ts

@@ -0,0 +1,164 @@
+import { randomUUID } from "node:crypto";
+import type { Order, OrderBook } from "../../domain/src/types";
+
+export interface OrderRouterConfig {
+  maxBps: number;
+  minIntervalMs?: number;
+  forbidPostOnlyCross?: boolean;
+  clientIdCacheSize?: number;
+}
+
+export interface DispatchResult {
+  id: string;
+}
+
+export type DispatchFn = (order: Order) => Promise<DispatchResult>;
+export type CancelFn = (id: string) => Promise<void>;
+export type CancelClientFn = (clientId: string) => Promise<void>;
+
+export class OrderRouter {
+  private readonly minIntervalMs: number;
+  private readonly forbidPostOnlyCross: boolean;
+  private readonly maxBps: number;
+  private readonly clientIdCacheSize: number;
+  private lastSendAt = 0;
+  private readonly recentClientIds: string[] = [];
+  private readonly clientIdSet = new Set<string>();
+  private readonly clientIdAlias = new Map<string, string>();
+  private cancelFn?: CancelFn;
+  private cancelClientFn?: CancelClientFn;
+
+  constructor(
+    private readonly sendLimitFn: DispatchFn,
+    private readonly getBook: (symbol: string) => OrderBook | undefined,
+    config: OrderRouterConfig
+  ) {
+    this.maxBps = config.maxBps;
+    this.minIntervalMs = config.minIntervalMs ?? 0;
+    this.forbidPostOnlyCross = config.forbidPostOnlyCross ?? true;
+    this.clientIdCacheSize = config.clientIdCacheSize ?? 512;
+  }
+
+  async sendLimit(order: Order): Promise<string> {
+    order.clientId = this.normalizeClientId(order.clientId);
+    await this.ensureThrottle();
+    this.ensureUniqueClientId(order.clientId);
+    this.applySlipGuard(order);
+    this.ensurePostOnly(order);
+
+    const { id } = await this.sendLimitFn(order);
+    return id;
+  }
+
+  async sendIOC(order: Order): Promise<string> {
+    order.clientId = this.normalizeClientId(order.clientId);
+    await this.ensureThrottle();
+    this.ensureUniqueClientId(order.clientId);
+    const { id } = await this.sendLimitFn(order);
+    return id;
+  }
+
+  async sendLimitChild(order: Order): Promise<string> {
+    return this.sendLimit(order);
+  }
+
+  attachCancelHandlers(cancelFn?: CancelFn, cancelClientFn?: CancelClientFn): void {
+    this.cancelFn = cancelFn;
+    this.cancelClientFn = cancelClientFn;
+  }
+
+  async cancel(orderId: string): Promise<void> {
+    if (!this.cancelFn) {
+      throw new Error("OrderRouter cancel handler not configured");
+    }
+    await this.cancelFn(orderId);
+    this.clientIdSet.delete(orderId);
+    this.cleanupAlias(orderId);
+  }
+
+  async cancelByClientId(clientId: string): Promise<void> {
+    const resolved = this.resolveClientId(clientId);
+    const handler = this.cancelClientFn ?? this.cancelFn;
+    if (!handler) {
+      throw new Error("OrderRouter cancel handler not configured");
+    }
+    await handler(resolved);
+    this.clientIdSet.delete(resolved);
+    this.cleanupAlias(resolved);
+  }
+
+  private applySlipGuard(order: Order) {
+    if (order.postOnly) return;
+    const book = this.getBook(order.symbol);
+    if (!book?.bids?.length || !book?.asks?.length) return;
+    const reference = order.side === "buy" ? book.asks[0].px : book.bids[0].px;
+    if (!reference) return;
+    const diff = Math.abs((order.px - reference) / reference) * 10_000;
+    if (diff > this.maxBps) {
+      throw new Error(`slippage>${this.maxBps}bps`);
+    }
+  }
+
+  private ensurePostOnly(order: Order) {
+    if (!this.forbidPostOnlyCross) return;
+    if (!order.postOnly) return;
+    const book = this.getBook(order.symbol);
+    if (!book?.bids?.length || !book?.asks?.length) return;
+    const bestBid = book.bids[0].px;
+    const bestAsk = book.asks[0].px;
+    if (order.side === "buy" && order.px >= bestAsk) {
+      throw new Error("post-only order would cross best ask");
+    }
+    if (order.side === "sell" && order.px <= bestBid) {
+      throw new Error("post-only order would cross best bid");
+    }
+  }
+
+  private async ensureThrottle() {
+    if (this.minIntervalMs <= 0) return;
+    const now = Date.now();
+    const elapsed = now - this.lastSendAt;
+    if (elapsed < this.minIntervalMs) {
+      await new Promise(resolve => setTimeout(resolve, this.minIntervalMs - elapsed));
+    }
+    this.lastSendAt = Date.now();
+  }
+
+  private ensureUniqueClientId(clientId: string) {
+    if (this.clientIdSet.has(clientId)) {
+      throw new Error(`duplicate clientId ${clientId}`);
+    }
+    this.clientIdSet.add(clientId);
+    this.recentClientIds.push(clientId);
+    if (this.recentClientIds.length > this.clientIdCacheSize) {
+      const removed = this.recentClientIds.shift();
+      if (removed) {
+        this.clientIdSet.delete(removed);
+      }
+    }
+  }
+
+  private normalizeClientId(clientId: string): string {
+    const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
+    if (uuidPattern.test(clientId)) {
+      return clientId;
+    }
+    const existing = this.clientIdAlias.get(clientId);
+    if (existing) return existing;
+    const generated = randomUUID();
+    this.clientIdAlias.set(clientId, generated);
+    return generated;
+  }
+
+  private resolveClientId(clientId: string): string {
+    return this.clientIdAlias.get(clientId) ?? clientId;
+  }
+
+  private cleanupAlias(resolved: string): void {
+    for (const [original, alias] of this.clientIdAlias.entries()) {
+      if (alias === resolved) {
+        this.clientIdAlias.delete(original);
+      }
+    }
+  }
+}

+ 64 - 0
packages/hedge/src/fundingRateMonitor.ts

@@ -0,0 +1,64 @@
+import { EventEmitter } from 'node:events';
+
+export interface FundingRate { rate: number; timestamp: number; venue?: string; }
+
+export interface FundingRateMonitorOptions {
+  fetchPrimary: () => Promise<FundingRate>;
+  fetchHedge: () => Promise<FundingRate>;
+  pollIntervalMs?: number;
+}
+
+export interface FundingMetrics {
+  primaryRate: FundingRate;
+  hedgeRate: FundingRate;
+  correlation?: number;
+  sameSign?: boolean;
+}
+
+export class FundingRateMonitor extends EventEmitter {
+  private readonly fetchPrimary: () => Promise<FundingRate>;
+  private readonly fetchHedge: () => Promise<FundingRate>;
+  private readonly pollIntervalMs: number;
+  private timer?: NodeJS.Timeout;
+  private running = false;
+
+  constructor(options: FundingRateMonitorOptions) {
+    super();
+    this.fetchPrimary = options.fetchPrimary;
+    this.fetchHedge = options.fetchHedge;
+    this.pollIntervalMs = options.pollIntervalMs ?? 60_000;
+  }
+
+  async start(): Promise<void> {
+    if (this.running) return;
+    this.running = true;
+    await this.poll();
+    this.timer = setInterval(() => {
+      this.poll().catch(error => this.emit('error', error));
+    }, this.pollIntervalMs);
+  }
+
+  stop(): void {
+    if (!this.running) return;
+    if (this.timer) clearInterval(this.timer);
+    this.timer = undefined;
+    this.running = false;
+  }
+
+  private async poll(): Promise<void> {
+    try {
+      const [primaryRate, hedgeRate] = await Promise.all([
+        this.fetchPrimary(),
+        this.fetchHedge()
+      ]);
+      const metrics: FundingMetrics = {
+        primaryRate,
+        hedgeRate,
+        sameSign: Math.sign(primaryRate.rate) === Math.sign(hedgeRate.rate)
+      };
+      this.emit('metrics', metrics);
+    } catch (error) {
+      this.emit('error', error);
+    }
+  }
+}

+ 25 - 0
packages/hedge/src/hedgeEngine.ts

@@ -0,0 +1,25 @@
+import type { Order } from "../../domain/src/types";
+
+export interface HedgeCfg { kp:number; ki:number; Qmax:number; minIntervalMs:number; }
+
+export interface HedgeResult {
+  hedged: number;
+  orderId?: string;
+  clientId?: string;
+}
+
+export class HedgeEngine {
+  private integ=0; private last=0;
+  constructor(private cfg:HedgeCfg, private place:(o:Order)=>Promise<{id:string}>, private getMid:()=>number|undefined){}
+  compute(pos:number){ this.integ += pos; const raw = this.cfg.kp*pos + this.cfg.ki*this.integ; return Math.max(Math.min(raw, this.cfg.Qmax), -this.cfg.Qmax); }
+  async maybeHedge(symbol:string, delta:number): Promise<HedgeResult> {
+    const now=Date.now(); if (now-this.last < this.cfg.minIntervalMs) return { hedged: 0 };
+    const q=this.compute(delta); if (Math.abs(q)<1e-8) return { hedged: 0 };
+    const mid = this.getMid() ?? 0;
+    const side = q>0? "sell":"buy";
+    const clientId = `hedge-${now}`;
+    const { id } = await this.place({ clientId, symbol, side: side as any, px: mid, sz: Math.abs(q), tif:"IOC" });
+    this.last = now;
+    return { hedged: q, orderId: id, clientId };
+  }
+}

+ 36 - 0
packages/portfolio/src/positionManager.ts

@@ -0,0 +1,36 @@
+import type { PositionSnapshot } from "../../domain/src/types";
+
+export interface AccountPosition extends PositionSnapshot {
+  accountId: string;
+  fundingRate?: number;
+  fundingCorrelation?: number;
+}
+
+export interface AggregatedPosition {
+  symbol: string;
+  base: number;
+  quote: number;
+  ts: number;
+  accounts: AccountPosition[];
+}
+
+export class PositionManager {
+  async snapshot(symbol: string, snapshots: Array<PositionSnapshot & { accountId?: string }>): Promise<AggregatedPosition> {
+    const accounts: AccountPosition[] = snapshots.map((snap, idx) => ({
+      accountId: snap.accountId ?? `account-${idx}`,
+      symbol: snap.symbol,
+      base: snap.base,
+      quote: snap.quote,
+      entryPx: snap.entryPx,
+      ts: snap.ts,
+      fundingRate: (snap as any).fundingRate,
+      fundingCorrelation: (snap as any).fundingCorrelation
+    }));
+
+    const base = accounts.reduce((sum, snap) => sum + snap.base, 0);
+    const quote = accounts.reduce((sum, snap) => sum + snap.quote, 0);
+    const ts = Date.now();
+
+    return { symbol, base, quote, ts, accounts };
+  }
+}

+ 4 - 0
packages/registry/src/index.ts

@@ -0,0 +1,4 @@
+export * from "./types";
+export * from "./symbolRegistry";
+export * from "./riskAllocator";
+export * from "./symbolScorer";

+ 50 - 0
packages/registry/src/riskAllocator.ts

@@ -0,0 +1,50 @@
+import type { RiskAllocation, RiskBudget, SymbolRuntimeState } from "./types";
+
+export interface AllocationOptions {
+  minNotionalPerSymbol?: number;
+  minBasePerSymbol?: number;
+}
+
+export class RiskAllocator {
+  constructor(private readonly budget: RiskBudget, private readonly options: AllocationOptions = {}) {}
+
+  allocate(states: SymbolRuntimeState[]): Map<string, RiskAllocation> {
+    if (!states.length) {
+      return new Map();
+    }
+    const activeStates = states.filter(s => s.status === "active" && (s.score ?? 0) > 0);
+    if (!activeStates.length) {
+      return new Map();
+    }
+
+    const scoreSum = activeStates.reduce((sum, state) => sum + (state.score ?? 0), 0);
+    if (scoreSum <= 0) {
+      return new Map();
+    }
+
+    const allocations = new Map<string, RiskAllocation>();
+    for (const state of activeStates) {
+      const score = state.score ?? 0;
+      const ratio = score / scoreSum;
+
+      let notionalLimit = ratio * this.budget.totalNotional;
+      let baseLimit = ratio * this.budget.totalBase;
+
+      if (this.options.minNotionalPerSymbol) {
+        notionalLimit = Math.max(notionalLimit, this.options.minNotionalPerSymbol);
+      }
+      if (this.options.minBasePerSymbol) {
+        baseLimit = Math.max(baseLimit, this.options.minBasePerSymbol);
+      }
+
+      notionalLimit = Math.min(notionalLimit, state.config.maxNotional);
+      baseLimit = Math.min(baseLimit, state.config.maxBase);
+
+      allocations.set(state.config.symbol, {
+        notionalLimit,
+        baseLimit
+      });
+    }
+    return allocations;
+  }
+}

+ 131 - 0
packages/registry/src/symbolRegistry.ts

@@ -0,0 +1,131 @@
+import { EventEmitter } from "node:events";
+
+import type {
+  RiskAllocation,
+  SymbolConfig,
+  SymbolRuntimeState,
+  SymbolStatus
+} from "./types";
+
+export interface RegistryEvents {
+  registered: SymbolRuntimeState;
+  updated: SymbolRuntimeState;
+  statusChanged: SymbolRuntimeState;
+  removed: { symbol: string };
+}
+
+type EventKey = keyof RegistryEvents;
+
+export class SymbolRegistry extends EventEmitter {
+  private readonly symbols = new Map<string, SymbolRuntimeState>();
+
+  constructor(initialConfigs: SymbolConfig[] = []) {
+    super();
+    initialConfigs.forEach(cfg => this.register(cfg));
+  }
+
+  register(config: SymbolConfig): SymbolRuntimeState {
+    const prev = this.symbols.get(config.symbol);
+    const state: SymbolRuntimeState = {
+      config: { ...config },
+      status: config.enabled ?? true ? "inactive" : "disabled",
+      updatedAt: Date.now()
+    };
+    this.symbols.set(config.symbol, state);
+    this.emit("registered", state);
+    if (prev) {
+      this.emit("updated", state);
+    }
+    return state;
+  }
+
+  updateConfig(symbol: string, patch: Partial<SymbolConfig>): SymbolRuntimeState {
+    const state = this.require(symbol);
+    state.config = { ...state.config, ...patch, symbol };
+    state.updatedAt = Date.now();
+    this.emit("updated", state);
+    return state;
+  }
+
+  setStatus(symbol: string, status: SymbolStatus, reason?: string): SymbolRuntimeState {
+    const state = this.require(symbol);
+    if (state.status === status && state.reason === reason) {
+      return state;
+    }
+    state.status = status;
+    state.reason = reason;
+    state.updatedAt = Date.now();
+    this.emit("statusChanged", state);
+    return state;
+  }
+
+  activate(symbol: string): SymbolRuntimeState {
+    return this.setStatus(symbol, "active");
+  }
+
+  pause(symbol: string, reason: string): SymbolRuntimeState {
+    return this.setStatus(symbol, "paused", reason);
+  }
+
+  disable(symbol: string, reason?: string): SymbolRuntimeState {
+    return this.setStatus(symbol, "disabled", reason);
+  }
+
+  remove(symbol: string): void {
+    if (!this.symbols.has(symbol)) {
+      return;
+    }
+    this.symbols.delete(symbol);
+    this.emit("removed", { symbol });
+  }
+
+  list(status?: SymbolStatus): SymbolRuntimeState[] {
+    const states = Array.from(this.symbols.values());
+    if (!status) return states;
+    return states.filter(s => s.status === status);
+  }
+
+  listActive(): SymbolRuntimeState[] {
+    return this.list("active");
+  }
+
+  get(symbol: string): SymbolRuntimeState | undefined {
+    return this.symbols.get(symbol);
+  }
+
+  setAllocation(symbol: string, allocation: RiskAllocation | undefined): SymbolRuntimeState {
+    const state = this.require(symbol);
+    state.allocation = allocation;
+    state.updatedAt = Date.now();
+    this.emit("updated", state);
+    return state;
+  }
+
+  setScore(symbol: string, score: number | undefined): SymbolRuntimeState {
+    const state = this.require(symbol);
+    state.score = score;
+    state.updatedAt = Date.now();
+    this.emit("updated", state);
+    return state;
+  }
+
+  on<Event extends EventKey>(event: Event, listener: (payload: RegistryEvents[Event]) => void): this {
+    return super.on(event, listener);
+  }
+
+  once<Event extends EventKey>(event: Event, listener: (payload: RegistryEvents[Event]) => void): this {
+    return super.once(event, listener);
+  }
+
+  emit<Event extends EventKey>(event: Event, payload: RegistryEvents[Event]): boolean {
+    return super.emit(event, payload);
+  }
+
+  private require(symbol: string): SymbolRuntimeState {
+    const state = this.symbols.get(symbol);
+    if (!state) {
+      throw new Error(`Symbol ${symbol} is not registered`);
+    }
+    return state;
+  }
+}

+ 97 - 0
packages/registry/src/symbolScorer.ts

@@ -0,0 +1,97 @@
+import type { SymbolMetrics } from "./types";
+
+export interface ScorerOptions {
+  spreadWeight?: number;
+  depthWeight?: number;
+  volumeWeight?: number;
+  fundingWeight?: number;
+  maxSpreadBps?: number;
+  minDepthUsd?: number;
+  minFundingCorrelation?: number;
+  freshnessThresholdMs?: number;
+  enableThreshold?: number;
+}
+
+const DEFAULTS: Required<ScorerOptions> = {
+  spreadWeight: 0.25,
+  depthWeight: 0.25,
+  volumeWeight: 0.25,
+  fundingWeight: 0.25,
+  maxSpreadBps: 200,
+  minDepthUsd: 50_000,
+  minFundingCorrelation: 0.75,
+  freshnessThresholdMs: 5_000,
+  enableThreshold: 0.55
+};
+
+export class SymbolScorer {
+  private readonly opts: Required<ScorerOptions>;
+
+  constructor(options: ScorerOptions = {}) {
+    this.opts = { ...DEFAULTS, ...options };
+  }
+
+  score(metrics: SymbolMetrics): number {
+    const spreadScore = this.normalizeSpread(metrics.spreadBps);
+    const depthScore = this.normalizeDepth(metrics.topDepthUsd);
+    const volumeScore = this.normalizeVolume(metrics.volumePerMin);
+    const fundingScore = this.normalizeFunding(metrics.fundingCorrelation);
+    const freshnessPenalty = this.computeFreshnessPenalty(metrics.dataLatencyMs);
+
+    const weighted =
+      spreadScore * this.opts.spreadWeight +
+      depthScore * this.opts.depthWeight +
+      volumeScore * this.opts.volumeWeight +
+      fundingScore * this.opts.fundingWeight -
+      freshnessPenalty;
+
+    return clamp(weighted, 0, 1);
+  }
+
+  shouldEnable(metrics: SymbolMetrics): boolean {
+    if (metrics.spreadBps > this.opts.maxSpreadBps) return false;
+    if (metrics.topDepthUsd < this.opts.minDepthUsd) return false;
+    if (metrics.fundingCorrelation < this.opts.minFundingCorrelation) return false;
+    if (
+      metrics.dataLatencyMs !== undefined &&
+      metrics.dataLatencyMs > this.opts.freshnessThresholdMs * 3
+    ) {
+      return false;
+    }
+    return this.score(metrics) >= this.opts.enableThreshold;
+  }
+
+  private normalizeSpread(spreadBps: number): number {
+    if (spreadBps <= 0) return 1;
+    if (spreadBps >= this.opts.maxSpreadBps) return 0;
+    return 1 - spreadBps / this.opts.maxSpreadBps;
+  }
+
+  private normalizeDepth(depthUsd: number): number {
+    if (depthUsd <= 0) return 0;
+    const normalized = Math.log10(depthUsd / this.opts.minDepthUsd);
+    return clamp(normalized / 2, 0, 1);
+  }
+
+  private normalizeVolume(volumePerMin: number): number {
+    if (volumePerMin <= 0) return 0;
+    const normalized = Math.log10(volumePerMin + 1) / 4;
+    return clamp(normalized, 0, 1);
+  }
+
+  private normalizeFunding(correlation: number): number {
+    return clamp((correlation + 1) / 2, 0, 1);
+  }
+
+  private computeFreshnessPenalty(latencyMs?: number): number {
+    if (latencyMs === undefined) return 0;
+    if (latencyMs <= this.opts.freshnessThresholdMs) return 0;
+    const exceeded = latencyMs - this.opts.freshnessThresholdMs;
+    const penalty = exceeded / (this.opts.freshnessThresholdMs * 10);
+    return clamp(penalty, 0, 0.3);
+  }
+}
+
+function clamp(value: number, min: number, max: number): number {
+  return Math.min(Math.max(value, min), max);
+}

+ 37 - 0
packages/registry/src/types.ts

@@ -0,0 +1,37 @@
+export type SymbolStatus = "inactive" | "active" | "paused" | "disabled";
+
+export interface SymbolConfig {
+  symbol: string;
+  maxNotional: number;
+  maxBase: number;
+  enabled?: boolean;
+  tags?: string[];
+  metadata?: Record<string, unknown>;
+}
+
+export interface SymbolRuntimeState {
+  config: SymbolConfig;
+  status: SymbolStatus;
+  updatedAt: number;
+  reason?: string;
+  allocation?: RiskAllocation;
+  score?: number;
+}
+
+export interface RiskAllocation {
+  notionalLimit: number;
+  baseLimit: number;
+}
+
+export interface RiskBudget {
+  totalNotional: number;
+  totalBase: number;
+}
+
+export interface SymbolMetrics {
+  spreadBps: number;
+  topDepthUsd: number;
+  volumePerMin: number;
+  fundingCorrelation: number;
+  dataLatencyMs?: number;
+}

+ 169 - 0
packages/risk/src/riskEngine.ts

@@ -0,0 +1,169 @@
+import type { Order, PositionSnapshot } from "../../domain/src/types";
+
+export interface RiskLimits {
+  maxBaseAbs: number;
+  maxNotionalAbs: number;
+  maxOrderSz: number;
+}
+
+export type KillSwitchTriggerType =
+  | "pnl_drawdown"
+  | "delta_abs"
+  | "hedge_failure_count"
+  | "data_gap_sec";
+
+export interface KillSwitchTrigger {
+  type: KillSwitchTriggerType;
+  threshold: number;
+}
+
+export interface KillSwitchConfig {
+  drawdownPct: number;
+  triggers?: KillSwitchTrigger[];
+}
+
+export interface RiskStatus {
+  realizedPnL: number;
+  peakEquity: number;
+  currentEquity: number;
+  deltaAbs: number;
+  hedgeFailures: number;
+  dataGapSeconds: number;
+  halted: boolean;
+  haltReason?: string;
+}
+
+export class RiskEngine {
+  private realizedPnL = 0;
+  private currentEquity = 0;
+  private peakEquity = 0;
+  private deltaAbs = 0;
+  private hedgeFailures = 0;
+  private dataGapSeconds = 0;
+  private halted = false;
+  private haltReason?: string;
+
+  constructor(
+    private readonly limits: RiskLimits,
+    private readonly killSwitch?: KillSwitchConfig
+  ) {}
+
+  preCheck(order: Order, position: PositionSnapshot, midPrice: number): void {
+    if (this.halted) {
+      throw new Error(`kill-switch active: ${this.haltReason ?? "unknown"}`);
+    }
+    if (order.sz > this.limits.maxOrderSz) {
+      throw new Error("order size exceeds limit");
+    }
+    const nextBase = position.base + (order.side === "buy" ? order.sz : -order.sz);
+    if (Math.abs(nextBase) > this.limits.maxBaseAbs) {
+      throw new Error("inventory limit exceeded");
+    }
+    const nextNotional = Math.abs(nextBase * midPrice);
+    if (nextNotional > this.limits.maxNotionalAbs) {
+      throw new Error("notional limit exceeded");
+    }
+  }
+
+  reportFill(pnlDelta: number): void {
+    this.realizedPnL += pnlDelta;
+  }
+
+  updateEquity(equity: number): void {
+    this.currentEquity = equity;
+    if (equity > this.peakEquity) {
+      this.peakEquity = equity;
+    }
+    this.evaluateKillSwitch();
+  }
+
+  updateDeltaAbs(deltaAbs: number): void {
+    this.deltaAbs = Math.abs(deltaAbs);
+    this.evaluateKillSwitch();
+  }
+
+  recordHedgeFailure(): void {
+    this.hedgeFailures += 1;
+    this.evaluateKillSwitch();
+  }
+
+  recordHedgeSuccess(): void {
+    if (this.hedgeFailures > 0) {
+      this.hedgeFailures -= 1;
+    }
+  }
+
+  setDataGap(seconds: number): void {
+    this.dataGapSeconds = seconds;
+    this.evaluateKillSwitch();
+  }
+
+  shouldHalt(): boolean {
+    return this.halted;
+  }
+
+  getStatus(): RiskStatus {
+    return {
+      realizedPnL: this.realizedPnL,
+      peakEquity: this.peakEquity,
+      currentEquity: this.currentEquity,
+      deltaAbs: this.deltaAbs,
+      hedgeFailures: this.hedgeFailures,
+      dataGapSeconds: this.dataGapSeconds,
+      halted: this.halted,
+      haltReason: this.haltReason
+    };
+  }
+
+  resetKillSwitch(): void {
+    this.halted = false;
+    this.haltReason = undefined;
+  }
+
+  private evaluateKillSwitch(): void {
+    if (!this.killSwitch || this.halted) return;
+
+    if (this.killSwitch.drawdownPct > 0 && this.peakEquity > 0) {
+      const drawdown = (this.currentEquity - this.peakEquity) / this.peakEquity;
+      if (drawdown <= -Math.abs(this.killSwitch.drawdownPct)) {
+        this.triggerHalt("drawdown threshold");
+      }
+    }
+
+    if (!this.killSwitch.triggers) return;
+
+    for (const trigger of this.killSwitch.triggers) {
+      if (this.halted) break;
+      switch (trigger.type) {
+        case "delta_abs":
+          if (this.deltaAbs > trigger.threshold) {
+            this.triggerHalt("delta_abs limit");
+          }
+          break;
+        case "hedge_failure_count":
+          if (this.hedgeFailures >= trigger.threshold) {
+            this.triggerHalt("hedge failures");
+          }
+          break;
+        case "data_gap_sec":
+          if (this.dataGapSeconds >= trigger.threshold) {
+            this.triggerHalt("data gap");
+          }
+          break;
+        case "pnl_drawdown":
+          if (this.peakEquity > 0) {
+            const drop = (this.currentEquity - this.peakEquity) / this.peakEquity;
+            if (drop <= -Math.abs(trigger.threshold)) {
+              this.triggerHalt("pnl drawdown");
+            }
+          }
+          break;
+      }
+    }
+  }
+
+  private triggerHalt(reason: string): void {
+    this.halted = true;
+    this.haltReason = reason;
+  }
+}

+ 372 - 0
packages/strategies/__tests__/gridMaker.test.ts

@@ -0,0 +1,372 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import pino from 'pino';
+import { GridMaker, type AdaptiveGridConfig } from '../src/gridMaker';
+import type { Order, Fill, OrderBook } from '../../domain/src/types';
+import type { OrderRouter } from '../../execution/src/orderRouter';
+import type { HedgeEngine } from '../../hedge/src/hedgeEngine';
+import type { ShadowBook } from '../../utils/src/shadowBook';
+import pino from 'pino';
+
+describe('GridMaker', () => {
+  let gridMaker: GridMaker;
+  let mockRouter: OrderRouter;
+  let mockHedgeEngine: HedgeEngine;
+  let mockShadowBook: ShadowBook;
+
+  const testConfig = {
+    symbol: 'BTC',
+    gridStepBps: 100,  // 1%
+    gridRangeBps: 400,  // 4%
+    baseClipUsd: 500,
+    maxLayers: 4,
+    hedgeThresholdBase: 0.3,
+    accountId: 'maker'
+  };
+
+  const mockMidPrice = 50000;
+
+  beforeEach(() => {
+    // Mock OrderRouter
+    mockRouter = {
+      sendLimitChild: vi.fn(async (order: Order) => {
+        return `order-${order.clientId}`;
+      })
+    } as any;
+
+    // Mock HedgeEngine
+    mockHedgeEngine = {
+      maybeHedge: vi.fn(async () => ({ hedged: 0, clientId: undefined, orderId: undefined }))
+    } as any;
+
+    // Mock ShadowBook
+    mockShadowBook = {
+      mid: vi.fn(() => mockMidPrice),
+      snapshot: vi.fn(() => ({
+        bids: [{ px: 49900, sz: 1 }],
+        asks: [{ px: 50100, sz: 1 }],
+        ts: Date.now()
+      } as OrderBook))
+    } as any;
+
+    const mockLogger = pino({ level: 'silent' });
+
+    gridMaker = new GridMaker(
+      testConfig,
+      mockRouter,
+      mockHedgeEngine,
+      mockShadowBook,
+      mockLogger,
+      undefined,
+      async () => {},
+      () => {}
+    );
+  });
+
+  describe('initialize', () => {
+    it('should initialize grid orders correctly', async () => {
+      await gridMaker.initialize();
+
+      // 应该创建 4 层买单 + 4 层卖单 = 8 个订单
+      expect(mockRouter.sendLimitChild).toHaveBeenCalledTimes(8);
+
+      const status = gridMaker.getStatus();
+      expect(status.isInitialized).toBe(true);
+      expect(status.totalGrids).toBe(8);
+      expect(status.activeOrders).toBe(8);
+    });
+
+    it('should place buy orders below mid price', async () => {
+      await gridMaker.initialize();
+
+      const calls = (mockRouter.sendLimitChild as any).mock.calls;
+      const buyOrders = calls.filter((call: any) => call[0].side === 'buy');
+
+      // 所有买单价格应该低于 mid
+      buyOrders.forEach((call: any) => {
+        const order = call[0] as Order;
+        expect(order.px).toBeLessThan(mockMidPrice);
+        expect(order.postOnly).toBe(true);
+        expect(order.tif).toBe('GTC');
+      });
+    });
+
+    it('should place sell orders above mid price', async () => {
+      await gridMaker.initialize();
+
+      const calls = (mockRouter.sendLimitChild as any).mock.calls;
+      const sellOrders = calls.filter((call: any) => call[0].side === 'sell');
+
+      // 所有卖单价格应该高于 mid
+      sellOrders.forEach((call: any) => {
+        const order = call[0] as Order;
+        expect(order.px).toBeGreaterThan(mockMidPrice);
+        expect(order.postOnly).toBe(true);
+      });
+    });
+
+    it('should throw error if no mid price available', async () => {
+      (mockShadowBook.mid as any).mockReturnValue(undefined);
+
+      await expect(gridMaker.initialize()).rejects.toThrow(
+        'Cannot initialize grid: no mid price available'
+      );
+    });
+  });
+
+  describe('onFill', () => {
+    beforeEach(async () => {
+      await gridMaker.initialize();
+      vi.clearAllMocks();
+    });
+
+    it('should place opposite order when buy order filled', async () => {
+      const buyFill: Fill = {
+        orderId: 'order-grid-BTC--1-' + Date.now(),
+        tradeId: 'trade-1',
+        symbol: 'BTC',
+        side: 'buy',
+        px: 49500,  // 买单在 -1% 成交
+        sz: 0.01,
+        fee: 0.00001,
+        liquidity: 'maker',
+        ts: Date.now()
+      };
+
+      await gridMaker.onFill(buyFill);
+
+      // 应该挂一个卖单在更高价
+      expect(mockRouter.sendLimitChild).toHaveBeenCalledTimes(1);
+
+      const call = (mockRouter.sendLimitChild as any).mock.calls[0];
+      const oppositeOrder = call[0] as Order;
+
+      expect(oppositeOrder.side).toBe('sell');
+      expect(oppositeOrder.px).toBeGreaterThan(buyFill.px);
+      // 应该在 buyFill.px * (1 + 1%) 附近
+      expect(oppositeOrder.px).toBeCloseTo(buyFill.px * 1.01, 0);
+      expect(oppositeOrder.accountId).toBe('maker');
+    });
+
+    it('should place opposite order when sell order filled', async () => {
+      const sellFill: Fill = {
+        orderId: 'order-grid-BTC-1-' + Date.now(),
+        tradeId: 'trade-2',
+        symbol: 'BTC',
+        side: 'sell',
+        px: 50500,  // 卖单在 +1% 成交
+        sz: 0.01,
+        fee: 0.00001,
+        liquidity: 'maker',
+        ts: Date.now()
+      };
+
+      await gridMaker.onFill(sellFill);
+
+      // 应该挂一个买单在更低价
+      expect(mockRouter.sendLimitChild).toHaveBeenCalledTimes(1);
+
+      const call = (mockRouter.sendLimitChild as any).mock.calls[0];
+      const oppositeOrder = call[0] as Order;
+
+      expect(oppositeOrder.side).toBe('buy');
+      expect(oppositeOrder.px).toBeLessThan(sellFill.px);
+      // 应该在 sellFill.px * (1 - 1%) 附近
+      expect(oppositeOrder.px).toBeCloseTo(sellFill.px * 0.99, 0);
+      expect(oppositeOrder.accountId).toBe('maker');
+    });
+
+    it('should update delta correctly', async () => {
+      const buyFill: Fill = {
+        orderId: 'order-grid-BTC--1-' + Date.now(),
+        tradeId: 'trade-1',
+        symbol: 'BTC',
+        side: 'buy',
+        px: 49500,
+        sz: 0.1,  // 买入 0.1 BTC
+        fee: 0.0001,
+        liquidity: 'maker',
+        ts: Date.now()
+      };
+
+      await gridMaker.onFill(buyFill);
+
+      const status = gridMaker.getStatus();
+      expect(status.currentDelta).toBe(0.1);  // Delta 应该增加 0.1
+    });
+
+    it('should trigger hedge when threshold reached', async () => {
+      // 连续成交 3 笔买单,累积 Delta = 0.3(达到阈值)
+      (mockHedgeEngine.maybeHedge as any).mockResolvedValueOnce({
+        hedged: 0.3,
+        orderId: 'hedge-order-1',
+        clientId: 'hedge-1'
+      });
+
+      for (let i = 0; i < 3; i++) {
+        const buyFill: Fill = {
+          orderId: `order-grid-BTC--${i + 1}-` + (Date.now() + i),
+          tradeId: `trade-${i}`,
+          symbol: 'BTC',
+          side: 'buy',
+          px: 49500 - i * 100,
+          sz: 0.1,
+          fee: 0.0001,
+          liquidity: 'maker',
+          ts: Date.now() + i
+        };
+
+        await gridMaker.onFill(buyFill);
+      }
+
+      const status = gridMaker.getStatus();
+      expect(status.currentDelta).toBe(0.3);
+      expect(status.pendingHedges).toBe(1);
+
+      // 应该触发对冲
+      expect(mockHedgeEngine.maybeHedge).toHaveBeenCalled();
+      expect(mockHedgeEngine.maybeHedge).toHaveBeenCalledWith('BTC', 0.3);
+    });
+
+    it('should ignore fill from non-grid orders', async () => {
+      const externalFill: Fill = {
+        orderId: 'external-order-123',  // 非网格订单
+        tradeId: 'trade-ext',
+        symbol: 'BTC',
+        side: 'buy',
+        px: 50000,
+        sz: 0.5,
+        fee: 0.0005,
+        liquidity: 'taker',
+        ts: Date.now()
+      };
+
+      await gridMaker.onFill(externalFill);
+
+      // 不应该挂对手单
+      expect(mockRouter.sendLimitChild).not.toHaveBeenCalled();
+
+      // Delta 不应该更新
+      const status = gridMaker.getStatus();
+      expect(status.currentDelta).toBe(0);
+    });
+  });
+
+  describe('getStatus', () => {
+    it('should return correct status before initialization', () => {
+      const status = gridMaker.getStatus();
+
+      expect(status.isInitialized).toBe(false);
+      expect(status.totalGrids).toBe(0);
+      expect(status.activeOrders).toBe(0);
+      expect(status.filledGrids).toBe(0);
+      expect(status.pendingHedges).toBe(0);
+    });
+
+    it('should return correct status after initialization', async () => {
+      await gridMaker.initialize();
+
+      const status = gridMaker.getStatus();
+
+      expect(status.isInitialized).toBe(true);
+      expect(status.gridCenter).toBe(mockMidPrice);
+      expect(status.totalGrids).toBe(8);
+      expect(status.activeOrders).toBe(8);
+      expect(status.filledGrids).toBe(0);
+      expect(status.pendingHedges).toBe(0);
+    });
+
+    it('should correct delta when hedge fill arrives', async () => {
+      await gridMaker.initialize();
+      (mockHedgeEngine.maybeHedge as any).mockResolvedValue({
+        hedged: 0.3,
+        orderId: 'hedge-order-1',
+        clientId: 'hedge-1'
+      });
+
+      // 累积 delta 触发对冲
+      for (let i = 0; i < 3; i++) {
+        const fill: Fill = {
+          orderId: `order-grid-BTC-${i}-` + (Date.now() + i),
+          tradeId: `trade-${i}`,
+          symbol: 'BTC',
+          side: 'buy',
+          px: 49500,
+          sz: 0.1,
+          fee: 0,
+          liquidity: 'maker',
+          ts: Date.now() + i
+        };
+        await gridMaker.onFill(fill);
+      }
+
+      const before = gridMaker.getStatus();
+      expect(before.currentDelta).toBe(0.3);
+      expect(before.pendingHedges).toBe(1);
+
+      const hedgeFill: Fill = {
+        orderId: 'hedge-order-1',
+        tradeId: 'hedge-trade-1',
+        symbol: 'BTC',
+        side: 'sell',
+        px: 49600,
+        sz: 0.3,
+        fee: 0,
+        liquidity: 'taker',
+        ts: Date.now() + 10
+      };
+
+      await gridMaker.onHedgeFill(hedgeFill);
+
+      const after = gridMaker.getStatus();
+      expect(after.currentDelta).toBeCloseTo(0.0, 5);
+      expect(after.pendingHedges).toBe(0);
+    });
+  });
+
+  describe('adaptive grid behaviour', () => {
+    it('adjusts grid step and triggers reset when volatility jumps', async () => {
+      const adaptiveConfig: AdaptiveGridConfig = {
+        enabled: true,
+        volatilityWindowMinutes: 1,
+        minVolatilityBps: 20,
+        maxVolatilityBps: 400,
+        minGridStepBps: 20,
+        maxGridStepBps: 200,
+        recenterEnabled: true,
+        recenterThresholdBps: 150,
+        recenterCooldownMs: 60_000,
+        minStepChangeRatio: 0.1,
+        minSamples: 2
+      };
+
+      let currentMid = mockMidPrice;
+      (mockShadowBook.mid as any).mockImplementation(() => currentMid);
+
+      const mockLogger = pino({ level: 'silent' });
+
+      const adaptiveMaker = new GridMaker(
+        { ...testConfig },
+        mockRouter,
+        mockHedgeEngine,
+        mockShadowBook,
+        mockLogger,
+        adaptiveConfig,
+        async () => {},
+        () => {}
+      );
+
+      await adaptiveMaker.initialize();
+      const resetSpy = vi.spyOn(adaptiveMaker as any, 'reset').mockResolvedValue(undefined);
+
+      await adaptiveMaker.onTick(); // baseline
+
+      currentMid = mockMidPrice * 1.12; // ~12% move
+      await adaptiveMaker.onTick();
+
+      const status = adaptiveMaker.getStatus();
+      expect(status.adaptive?.currentGridStepBps).toBeGreaterThan(testConfig.gridStepBps);
+      expect(resetSpy).toHaveBeenCalled();
+      resetSpy.mockRestore();
+    });
+  });
+});

+ 766 - 0
packages/strategies/src/gridMaker.ts

@@ -0,0 +1,766 @@
+import type { Order, Fill, Side } from '../../domain/src/types';
+import type { OrderRouter } from '../../execution/src/orderRouter';
+import type { HedgeEngine } from '../../hedge/src/hedgeEngine';
+import type { ShadowBook } from '../../utils/src/shadowBook';
+import { VolatilityEstimator } from '../../utils/src/volatilityEstimator';
+import pino, { type Logger } from 'pino';
+import { observeGridMetrics } from '../../telemetry/src/gridMetrics';
+const PLACE_RETRY_ATTEMPTS = 3;
+const PLACE_RETRY_BASE_DELAY_MS = 200;
+const HEDGE_PENDING_TIMEOUT_MS = 30_000;
+
+export interface GridConfig {
+  symbol: string;
+  gridStepBps: number;      // 网格间距(bps)
+  gridRangeBps: number;     // 网格范围(bps)
+  baseClipUsd: number;      // 单层订单大小(USD)
+  maxLayers: number;        // 最大层数
+  hedgeThresholdBase: number; // 对冲阈值(base asset)
+  accountId?: string;
+  tickSize?: number;
+  lotSize?: number;
+}
+
+export interface GridLevel {
+  index: number;            // 网格索引(正=卖单,负=买单)
+  side: Side;
+  px: number;
+  sz: number;
+  orderId?: string;
+  clientId?: string;
+  filled: boolean;
+  timestamp: number;
+}
+
+export class GridMaker {
+  private grids: Map<number, GridLevel> = new Map();
+  private currentDelta: number = 0;
+  private gridCenter: number = 0;
+  private isInitialized: boolean = false;
+  private readonly accountId: string;
+  private readonly tickSize: number;
+  private readonly lotSize: number;
+  private readonly pendingHedges = new Map<string, { qty: number; ts: number }>();
+  private readonly adaptiveConfig?: AdaptiveGridConfig;
+  private readonly volatilityEstimator?: VolatilityEstimator;
+  private readonly cancelAllOrders: (symbol: string) => Promise<void>;
+  private readonly releaseOrder: (orderId: string, clientOrderId?: string) => void;
+  private readonly logger: Logger;
+  private currentGridStepBps: number;
+  private lastRecenterTs = 0;
+  private resetting = false;
+  private consecutivePlaceFailures = 0;
+  private needsReinit = false;
+
+  constructor(
+    private config: GridConfig,
+    private router: OrderRouter,
+    private hedgeEngine: HedgeEngine,
+    private shadowBook: ShadowBook,
+    logger?: Logger,
+    adaptiveConfig?: AdaptiveGridConfig,
+    cancelAllOrders?: (symbol: string) => Promise<void>,
+    releaseOrder?: (orderId: string, clientOrderId?: string) => void
+  ) {
+    this.accountId = config.accountId ?? "maker";
+    this.tickSize = config.tickSize ?? 1;
+    this.lotSize = config.lotSize ?? 0.00001;
+    this.currentGridStepBps = config.gridStepBps;
+    this.cancelAllOrders = cancelAllOrders ?? (async () => {});
+    this.releaseOrder = releaseOrder ?? (() => {});
+    const baseLogger = logger ?? pino({ name: 'GridMaker' });
+    this.logger = baseLogger.child({ component: 'GridMaker' });
+    if (adaptiveConfig?.enabled) {
+      this.adaptiveConfig = adaptiveConfig;
+      this.volatilityEstimator = new VolatilityEstimator({
+        windowMinutes: adaptiveConfig.volatilityWindowMinutes,
+        minSamples: adaptiveConfig.minSamples,
+        maxCadenceMs: adaptiveConfig.maxCadenceMs
+      });
+      this.logger.info({ adaptiveConfig }, 'Adaptive grid enabled');
+    }
+  }
+
+  /**
+   * 初始化网格:在当前 mid 周围布置双边网格
+   */
+  async initialize(): Promise<void> {
+    const mid = this.shadowBook.mid(this.config.symbol);
+    if (!mid) {
+      throw new Error('Cannot initialize grid: no mid price available');
+    }
+
+    this.gridCenter = mid;
+    if (this.volatilityEstimator) {
+      this.volatilityEstimator.update(mid);
+    }
+    this.logger.info({ mid, config: this.config, currentGridStepBps: this.currentGridStepBps }, 'Initializing grid');
+
+    const { gridRangeBps, maxLayers, baseClipUsd } = this.config;
+    const stepRatio = this.currentGridStepBps / 1e4;
+    const rangeRatio = gridRangeBps / 1e4;
+
+    // 计算实际层数(不超过 maxLayers)
+    const calculatedLayers = Math.floor(rangeRatio / stepRatio);
+    const actualLayers = Math.min(calculatedLayers, maxLayers);
+
+    // 计算单层订单大小(base asset)
+    const baseSz = baseClipUsd / mid;
+    const normalizedBaseSz = this.normalizeSize(baseSz);
+
+    if (normalizedBaseSz <= 0) {
+      throw new Error(`Grid base size ${baseSz} rounded to zero with lot size ${this.lotSize}`);
+    }
+
+    this.logger.info({ actualLayers, baseSz: normalizedBaseSz, gridStepBps: this.currentGridStepBps }, 'Grid parameters calculated');
+
+    // 重置连续失败计数
+    this.consecutivePlaceFailures = 0;
+
+    // 布置网格
+    const expectedOrders = actualLayers * 2;
+    for (let i = 1; i <= actualLayers; i++) {
+      if (this.needsReinit) break;
+      // 买单网格(负索引)
+      const buyPx = mid * (1 - i * stepRatio);
+      try {
+        await this.placeGridOrder(-i, 'buy', buyPx, normalizedBaseSz);
+      } catch (error) {
+        this.logger.warn({ index: -i, side: 'buy', px: buyPx, error: normalizeError(error) }, 'Skipping failed grid level');
+      }
+
+      // 卖单网格(正索引)
+      const sellPx = mid * (1 + i * stepRatio);
+      try {
+        await this.placeGridOrder(i, 'sell', sellPx, normalizedBaseSz);
+      } catch (error) {
+        this.logger.warn({ index: i, side: 'sell', px: sellPx, error: normalizeError(error) }, 'Skipping failed grid level');
+      }
+
+      // 自动回退机制:记录过多失败,留给 onTick 处理
+      if (this.consecutivePlaceFailures >= 5) {
+        const oldStep = this.currentGridStepBps;
+        this.currentGridStepBps = Math.min(oldStep * 1.5, this.adaptiveConfig?.maxGridStepBps ?? 100);
+        this.logger.warn({
+          consecutivePlaceFailures: this.consecutivePlaceFailures,
+          oldGridStepBps: oldStep,
+          newGridStepBps: this.currentGridStepBps,
+          action: 'auto_fallback_schedule'
+        }, 'Too many consecutive place failures, increasing grid step; will re-initialize on next tick');
+        this.needsReinit = true;
+        this.isInitialized = false;
+        break;
+      }
+    }
+
+    if (this.needsReinit) {
+      this.logger.info({ action: 'schedule_reinit', currentGridStepBps: this.currentGridStepBps }, 'Reinitialization scheduled due to placement failures');
+      return;
+    }
+
+    this.needsReinit = false;
+    this.isInitialized = true;
+    this.logger.info({
+      totalGrids: this.grids.size,
+      expectedLayers: expectedOrders,
+      successRate: expectedOrders > 0 ? ((this.grids.size / expectedOrders) * 100).toFixed(1) + '%' : 'N/A'
+    }, 'Grid initialization completed');
+  }
+
+  /**
+   * Fill 回调:成交后挂对手单,更新 Delta,检查对冲阈值
+   */
+  async onFill(fill: Fill): Promise<void> {
+    if (!this.isInitialized) {
+      this.logger.warn('Grid not initialized, ignoring fill');
+      return;
+    }
+
+    const gridLevel = this.findGridLevel(fill.orderId);
+    if (!gridLevel) {
+      this.logger.debug({ orderId: fill.orderId }, 'Fill not from grid order, ignoring');
+      return;
+    }
+
+    this.logger.info({
+      gridIndex: gridLevel.index,
+      side: fill.side,
+      px: fill.px,
+      sz: fill.sz,
+      fee: fill.fee
+    }, 'Grid order filled');
+
+    // 标记该层已成交
+    gridLevel.filled = true;
+    gridLevel.timestamp = Date.now();
+
+    // 更新 Delta
+    this.updateDelta(fill);
+
+    // 挂对手单
+    const oppositeSide: Side = fill.side === 'buy' ? 'sell' : 'buy';
+    const stepRatio = this.currentGridStepBps / 1e4;
+    const oppositePx = fill.side === 'buy'
+      ? fill.px * (1 + stepRatio)
+      : fill.px * (1 - stepRatio);
+
+    try {
+      const oppositeOrderId = await this.placeGridOrder(
+        gridLevel.index,
+        oppositeSide,
+        oppositePx,
+        fill.sz
+      );
+
+      this.logger.info({
+        gridIndex: gridLevel.index,
+        oppositeSide,
+        oppositePx,
+        orderId: oppositeOrderId
+      }, 'Placed opposite grid order');
+    } catch (error) {
+      this.logger.error({ error, gridIndex: gridLevel.index }, 'Failed to place opposite order');
+    }
+
+    // 检查是否需要对冲
+    await this.maybeHedge();
+  }
+
+  /**
+   * Hedger 账户成交回调:用实际成交修正 Delta
+   */
+  async onHedgeFill(fill: Fill): Promise<void> {
+    if (!this.isInitialized) {
+      this.logger.warn('Grid not initialized, ignoring hedge fill');
+      return;
+    }
+    if (fill.symbol !== this.config.symbol) {
+      return;
+    }
+
+    const tracked = this.pendingHedges.get(fill.orderId);
+    if (tracked) {
+      this.pendingHedges.delete(fill.orderId);
+    }
+
+    const correction = fill.side === 'sell' ? -fill.sz : fill.sz;
+    const beforeDelta = this.currentDelta;
+    this.currentDelta += correction;
+
+    this.logger.info({
+      orderId: fill.orderId,
+      fillSide: fill.side,
+      fillSz: fill.sz,
+      correction,
+      beforeDelta,
+      afterDelta: this.currentDelta,
+      wasTracked: Boolean(tracked)
+    }, 'Hedge fill received, delta adjusted');
+  }
+
+  /**
+   * 重置网格:撤销所有旧订单,重新布置
+   */
+  async reset(): Promise<void> {
+    if (this.resetting) return;
+    this.resetting = true;
+    this.needsReinit = false;
+    this.logger.info({ gridSize: this.grids.size, pendingOrders: Array.from(this.grids.values()).filter(g => g.orderId && !g.filled).length }, 'Resetting grid');
+
+    // 尝试批量撤单
+    let cancelAllSuccess = false;
+    try {
+      this.logger.info({ action: 'cancelAll_start', symbol: this.config.symbol }, 'Starting cancelAll');
+      await this.cancelAllOrders(this.config.symbol);
+      this.logger.info({ action: 'cancelAll_complete' }, 'cancelAll completed successfully');
+      cancelAllSuccess = true;
+
+      // 等待 500ms 让交易所处理撤单
+      await sleep(500);
+
+      // 直接清理本地状态
+      for (const level of this.grids.values()) {
+        if (level.orderId) {
+          this.releaseOrder(level.orderId, level.clientId);
+        }
+      }
+    } catch (error) {
+      this.logger.error({ error: normalizeError(error), action: 'cancelAll_failed' }, 'cancelAll failed, will try individual cancels');
+    }
+
+    // 如果 cancelAll 失败,才逐个撤单
+    if (!cancelAllSuccess) {
+      const pendingLevels = Array.from(this.grids.values()).filter(level => level.orderId && !level.filled);
+      this.logger.info({ pendingCount: pendingLevels.length }, 'Attempting individual cancels');
+
+      for (const level of pendingLevels) {
+        await this.cancelGridLevel(level).catch(err =>
+          this.logger.warn({ orderId: level.orderId, clientId: level.clientId, error: normalizeError(err) }, 'Individual cancel failed')
+        );
+      }
+    }
+
+    // 清空网格
+    this.grids.clear();
+    this.isInitialized = false;
+    this.pendingHedges.clear();
+
+    // 重新初始化
+    try {
+      this.logger.info({ action: 'initialize_start' }, 'Re-initializing grid after reset');
+      await this.initialize();
+      this.logger.info({ action: 'reset_complete', gridSize: this.grids.size }, 'Grid reset and re-initialized successfully');
+    } catch (error) {
+      this.logger.error({ error: normalizeError(error), action: 'initialize_failed', gridSize: this.grids.size }, 'Failed to re-initialize grid after reset');
+      // 不抛出异常,允许下次 onTick 重试
+    } finally {
+      this.resetting = false;
+    }
+  }
+
+  /**
+   * 获取当前状态(用于监控)
+   */
+  getStatus() {
+    const adaptiveStatus = this.volatilityEstimator
+      ? {
+          currentGridStepBps: this.currentGridStepBps,
+          volatility: this.volatilityEstimator.getStatus()
+        }
+      : undefined;
+
+    const expectedLayers = this.currentGridStepBps > 0
+      ? Math.min(this.config.maxLayers, Math.floor(this.config.gridRangeBps / this.currentGridStepBps))
+      : 0;
+    const expectedOrders = expectedLayers * 2;
+    const effectiveOrderRatio = expectedOrders > 0 ? this.grids.size / expectedOrders : undefined;
+
+    const status = {
+      isInitialized: this.isInitialized,
+      gridCenter: this.gridCenter,
+      currentDelta: this.currentDelta,
+      totalGrids: this.grids.size,
+      filledGrids: Array.from(this.grids.values()).filter(g => g.filled).length,
+      activeOrders: Array.from(this.grids.values()).filter(g => g.orderId && !g.filled).length,
+      pendingHedges: this.pendingHedges.size,
+      expectedOrders,
+      effectiveOrderRatio,
+      adaptive: adaptiveStatus
+    };
+    observeGridMetrics(this.config.symbol, {
+      stepBps: this.currentGridStepBps,
+      pendingHedges: this.pendingHedges.size,
+      currentDelta: this.currentDelta,
+      hourlyVolatilityBps: adaptiveStatus?.volatility?.hourlyVolBps
+    });
+    return status;
+  }
+
+  // ==================== 私有方法 ====================
+
+  /**
+   * 下单并记录网格
+   */
+  private async placeGridOrder(index: number, side: Side, px: number, sz: number): Promise<string> {
+    const normalizedPx = this.normalizePrice(side, px);
+    const normalizedSz = this.normalizeSize(sz);
+    if (normalizedSz <= 0) {
+      throw new Error(`Grid size ${sz} normalized to zero with lot size ${this.lotSize}`);
+    }
+    let attempt = 0;
+    while (attempt < PLACE_RETRY_ATTEMPTS) {
+      const clientId = `grid-${this.config.symbol}-${index}-${Date.now()}-${attempt}`;
+      const order: Order = {
+        clientId,
+        symbol: this.config.symbol,
+        side,
+        px: normalizedPx,
+        sz: normalizedSz,
+        tif: 'GTC',
+        postOnly: true,
+        accountId: this.accountId
+      };
+      try {
+        this.logger.debug({ action: 'place_grid_order', index, side, px: normalizedPx, sz: normalizedSz, clientId, attempt }, 'Placing grid order');
+        const orderId = await this.router.sendLimitChild(order);
+        const trackedClientId = order.clientId;
+        this.grids.set(index, {
+          index,
+          side,
+          px: normalizedPx,
+          sz: normalizedSz,
+          orderId,
+          clientId: trackedClientId,
+          filled: false,
+          timestamp: Date.now()
+        });
+        this.logger.debug({ index, side, px: normalizedPx, sz: normalizedSz, orderId, clientId: trackedClientId }, 'Grid order placed successfully');
+        this.consecutivePlaceFailures = 0;
+        return orderId;
+      } catch (error) {
+        attempt += 1;
+        const normalizedError = normalizeError(error);
+
+        // 增强日志:记录盘口、mid、spread 等详细信息
+        const book = this.shadowBook.snapshot(this.config.symbol);
+        const enhancedLog: any = {
+          error: normalizedError,
+          index,
+          side,
+          px: normalizedPx,
+          sz: normalizedSz,
+          attempt,
+          currentGridStepBps: this.currentGridStepBps
+        };
+        if (book?.bids?.[0] && book?.asks?.[0]) {
+          const mid = (book.bids[0].px + book.asks[0].px) / 2;
+          const spread = book.asks[0].px - book.bids[0].px;
+          const spreadBps = (spread / mid) * 10_000;
+          enhancedLog.marketData = {
+            mid: mid.toFixed(2),
+            bestBid: book.bids[0].px,
+            bestAsk: book.asks[0].px,
+            spreadBps: spreadBps.toFixed(2),
+            pxDeviationFromMid: ((normalizedPx - mid) / mid * 10_000).toFixed(2) + 'bps'
+          };
+        }
+
+        if (attempt >= PLACE_RETRY_ATTEMPTS) {
+          this.consecutivePlaceFailures += 1;
+          this.logger.error({
+            ...enhancedLog,
+            attempts: attempt,
+            consecutivePlaceFailures: this.consecutivePlaceFailures
+          }, 'Failed to place grid order after all retries');
+          throw error;
+        }
+        const delay = PLACE_RETRY_BASE_DELAY_MS * attempt;
+        this.logger.warn({ ...enhancedLog, retryDelayMs: delay }, 'Grid order placement failed, retrying');
+        await sleep(delay);
+      }
+    }
+    throw new Error('unreachable');
+  }
+
+  private async cancelGridLevel(level: GridLevel): Promise<void> {
+    const orderId = level.orderId;
+    if (!orderId) return;
+    this.logger.debug({ orderId }, 'Canceling grid order');
+    let cancelled = false;
+    try {
+      await this.router.cancel(orderId);
+      cancelled = true;
+    } catch (error) {
+      const normalized = normalizeError(error);
+      this.logger.warn({ orderId, error: normalized }, 'Grid cancel by orderId failed');
+      if (level.clientId) {
+        try {
+          await this.router.cancelByClientId(level.clientId);
+          cancelled = true;
+        } catch (fallbackError) {
+          const fallbackNormalized = normalizeError(fallbackError);
+          if (isIgnorableCancelError(fallbackNormalized)) {
+            this.logger.warn({ orderId, clientId: level.clientId, error: fallbackNormalized }, 'Grid cancel by clientId treated as success');
+            cancelled = true;
+          } else {
+            this.logger.error(
+              { orderId, clientId: level.clientId, error: fallbackNormalized },
+              'Grid cancel by clientId failed'
+            );
+          }
+        }
+      }
+      if (!cancelled && isIgnorableCancelError(normalized)) {
+        this.logger.warn({ orderId, error: normalized }, 'Grid cancel considered complete');
+        cancelled = true;
+      }
+      if (!cancelled) {
+        throw error;
+      }
+    } finally {
+      this.grids.delete(level.index);
+      if (cancelled) {
+        this.releaseOrder(orderId, level.clientId);
+        this.logger.debug({ orderId }, 'Grid order cancel confirmed');
+      }
+    }
+  }
+
+  private normalizePrice(side: Side, rawPx: number): number {
+    if (this.tickSize <= 0) return rawPx;
+    const factor = rawPx / this.tickSize;
+    const adjusted = side === 'buy' ? Math.floor(factor) : Math.ceil(factor);
+    const normalized = adjusted * this.tickSize;
+    if (!Number.isFinite(normalized) || normalized <= 0) {
+      throw new Error(`Normalized price ${normalized} invalid for tick size ${this.tickSize}`);
+    }
+    return normalized;
+  }
+
+  private normalizeSize(rawSz: number): number {
+    if (this.lotSize <= 0) return rawSz;
+    const factor = rawSz / this.lotSize;
+    const adjusted = Math.floor(factor);
+    const normalized = adjusted * this.lotSize;
+    return Number(normalized.toFixed(10));
+  }
+
+  async shutdown(): Promise<void> {
+    this.logger.info({ gridSize: this.grids.size, action: 'shutdown_start' }, 'Starting GridMaker shutdown');
+    try {
+      await this.cancelAllOrders(this.config.symbol);
+      this.logger.info({ action: 'shutdown_cancelAll_complete' }, 'cancelAll during shutdown completed');
+    } catch (error) {
+      this.logger.warn({ error: normalizeError(error), action: 'shutdown_cancelAll_failed' }, 'cancel_all during shutdown failed');
+    }
+    const levels = Array.from(this.grids.values());
+    await Promise.all(levels.map(level =>
+      level.orderId
+        ? this.cancelGridLevel(level).catch(error =>
+            this.logger.error({ orderId: level.orderId, clientId: level.clientId, error: normalizeError(error) }, 'Failed to cancel grid order during shutdown')
+          )
+        : Promise.resolve()
+    ));
+    this.grids.clear();
+    this.isInitialized = false;
+    this.pendingHedges.clear();
+    this.logger.info({ action: 'shutdown_complete' }, 'GridMaker shutdown completed');
+  }
+
+  /**
+   * 根据 orderId 查找网格层
+   */
+  private findGridLevel(orderId: string): GridLevel | undefined {
+    for (const grid of this.grids.values()) {
+      if (grid.orderId === orderId) {
+        return grid;
+      }
+    }
+    return undefined;
+  }
+
+  /**
+   * 更新 Delta
+   */
+  private updateDelta(fill: Fill): void {
+    const deltaChange = fill.side === 'buy' ? fill.sz : -fill.sz;
+    this.currentDelta += deltaChange;
+
+    this.logger.info({
+      fillSide: fill.side,
+      fillSz: fill.sz,
+      deltaChange,
+      currentDelta: this.currentDelta
+    }, 'Delta updated');
+  }
+
+  /**
+   * 检查是否需要对冲
+   */
+  private async maybeHedge(): Promise<void> {
+    const absDelta = Math.abs(this.currentDelta);
+
+    if (absDelta >= this.config.hedgeThresholdBase) {
+      this.logger.info({
+        currentDelta: this.currentDelta,
+        threshold: this.config.hedgeThresholdBase,
+        pendingHedges: this.pendingHedges.size
+      }, 'Hedge threshold reached, triggering hedge');
+
+      try {
+        const result = await this.hedgeEngine.maybeHedge(this.config.symbol, this.currentDelta);
+        if (Math.abs(result.hedged) > 1e-8) {
+          const key = result.orderId ?? result.clientId;
+          if (key) {
+            this.pendingHedges.set(key, { qty: result.hedged, ts: Date.now() });
+          }
+          this.logger.info({
+            hedged: result.hedged,
+            orderId: result.orderId,
+            clientId: result.clientId,
+            pendingHedges: this.pendingHedges.size,
+            currentDelta: this.currentDelta
+          }, 'Hedge order placed, awaiting fill for delta correction');
+        }
+      } catch (error) {
+        this.logger.error({ error, currentDelta: this.currentDelta }, 'Hedge failed');
+      }
+    }
+  }
+
+  async onTick(): Promise<void> {
+    if (this.needsReinit && !this.resetting) {
+      this.logger.info({ action: 'onTick_reinit', currentGridStepBps: this.currentGridStepBps }, 'Triggering grid reset due to scheduled reinitialization');
+      await this.reset();
+      return;
+    }
+    if (!this.isInitialized) return;
+    const mid = this.shadowBook.mid(this.config.symbol);
+    if (!mid) return;
+    if (this.volatilityEstimator) {
+      this.volatilityEstimator.update(mid);
+    }
+    this.prunePendingHedges(Date.now());
+    await this.maybeAdjustGridStep();
+    await this.maybeRecenterGrid(mid);
+  }
+
+  private async maybeAdjustGridStep(): Promise<void> {
+    if (!this.adaptiveConfig?.enabled || !this.volatilityEstimator) {
+      return;
+    }
+    const hourlyVolBps = this.volatilityEstimator.getHourlyVolatilityBps();
+    if (hourlyVolBps === undefined) return;
+
+    const {
+      minVolatilityBps,
+      maxVolatilityBps,
+      minGridStepBps,
+      maxGridStepBps,
+      minStepChangeRatio
+    } = this.adaptiveConfig;
+
+    const clampedVol = clamp(hourlyVolBps, minVolatilityBps, maxVolatilityBps);
+    const ratio =
+      (clampedVol - minVolatilityBps) /
+      Math.max(1, maxVolatilityBps - minVolatilityBps);
+    const targetStep =
+      minGridStepBps + ratio * (maxGridStepBps - minGridStepBps);
+
+    // 盘口感知:读取当前最佳买卖价,计算实际可用的最小步长
+    const book = this.shadowBook.snapshot(this.config.symbol);
+    let effectiveMinStep = minGridStepBps;
+    if (book?.bids?.[0] && book?.asks?.[0]) {
+      const mid = (book.bids[0].px + book.asks[0].px) / 2;
+      const topSpreadBps = ((book.asks[0].px - book.bids[0].px) / mid) * 10_000;
+      const cushionBps = this.adaptiveConfig.postOnlyCushionBps ?? 5;
+      const spreadBasedMin = topSpreadBps + cushionBps;
+
+      if (spreadBasedMin > effectiveMinStep) {
+        effectiveMinStep = spreadBasedMin;
+        this.logger.info({
+          topSpreadBps: topSpreadBps.toFixed(2),
+          cushionBps,
+          configMinStepBps: minGridStepBps,
+          effectiveMinStepBps: effectiveMinStep.toFixed(2)
+        }, 'Adjusting min grid step based on current spread (post-only protection)');
+      }
+    }
+
+    // 使用盘口感知的下限
+    let finalTargetStep = Math.max(targetStep, effectiveMinStep);
+    finalTargetStep = clamp(finalTargetStep, minGridStepBps, maxGridStepBps);
+
+    const changeRatio = Math.abs(finalTargetStep - this.currentGridStepBps) / this.currentGridStepBps;
+    if (changeRatio < (minStepChangeRatio ?? 0.2)) {
+      return;
+    }
+
+    this.logger.info({
+      hourlyVolBps,
+      currentGridStepBps: this.currentGridStepBps,
+      targetGridStepBps: finalTargetStep.toFixed(2),
+      changeRatio: changeRatio.toFixed(3),
+      effectiveMinStepBps: effectiveMinStep.toFixed(2)
+    }, 'Adjusting grid step based on volatility');
+
+    this.currentGridStepBps = clamp(finalTargetStep, effectiveMinStep, maxGridStepBps);
+    await this.reset();
+  }
+
+  private async maybeRecenterGrid(mid: number): Promise<void> {
+    if (!this.adaptiveConfig?.recenterEnabled) return;
+    const deviationBps = Math.abs(mid - this.gridCenter) / this.gridCenter * 10_000;
+    if (deviationBps <= this.adaptiveConfig.recenterThresholdBps) return;
+
+    const now = Date.now();
+    const cooldown = this.adaptiveConfig.recenterCooldownMs ?? 300_000;
+    if (now - this.lastRecenterTs < cooldown) {
+      this.logger.debug({
+        deviationBps,
+        cooldownRemainingMs: cooldown - (now - this.lastRecenterTs)
+      }, 'Recenter cooldown active');
+      return;
+    }
+
+    this.logger.warn({
+      deviationBps,
+      previousCenter: this.gridCenter,
+      newCenter: mid
+    }, 'Grid center drift detected, resetting grid');
+
+    this.lastRecenterTs = now;
+    await this.reset();
+  }
+
+  private prunePendingHedges(now: number): void {
+    const timeout =
+      this.adaptiveConfig?.hedgePendingTimeoutMs ?? HEDGE_PENDING_TIMEOUT_MS;
+    for (const [orderId, entry] of this.pendingHedges) {
+      if (now - entry.ts > timeout) {
+        this.pendingHedges.delete(orderId);
+        this.logger.warn({
+          orderId,
+          ageMs: now - entry.ts,
+          timeoutMs: timeout
+        }, 'Pending hedge expired without fill');
+      }
+    }
+  }
+}
+
+export interface AdaptiveGridConfig {
+  enabled: boolean;
+  volatilityWindowMinutes: number;
+  minVolatilityBps: number;
+  maxVolatilityBps: number;
+  minGridStepBps: number;
+  maxGridStepBps: number;
+  recenterEnabled: boolean;
+  recenterThresholdBps: number;
+  recenterCooldownMs: number;
+  minStepChangeRatio?: number;
+  minSamples?: number;
+  maxCadenceMs?: number;
+  hedgePendingTimeoutMs?: number;
+  postOnlyCushionBps?: number;
+}
+
+function clamp(value: number, min: number, max: number): number {
+  return Math.min(Math.max(value, min), max);
+}
+
+function sleep(ms: number): Promise<void> {
+  return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+function normalizeError(error: unknown): { message?: string; name?: string; status?: number; code?: string } | undefined {
+  if (!error) return undefined;
+  if (error instanceof Error) {
+    const normalized: { message: string; name: string; status?: number; code?: string } = {
+      message: error.message,
+      name: error.name
+    };
+    const anyErr = error as any;
+    if (typeof anyErr.status === 'number') {
+      normalized.status = anyErr.status;
+    }
+    if (anyErr.code !== undefined) {
+      normalized.code = String(anyErr.code);
+    }
+    return normalized;
+  }
+  if (typeof error === 'object') {
+    return error as any;
+  }
+  return { message: String(error) };
+}
+
+function isIgnorableCancelError(error?: { status?: number; code?: string }): boolean {
+  if (!error) return false;
+  if (error.status === 404) return true;
+  if (error.status === 422 && (error.code === '5' || error.code === 'order_not_active')) {
+    return true;
+  }
+  return false;
+}

+ 26 - 0
packages/strategies/src/marketMaker.ts

@@ -0,0 +1,26 @@
+import type { Fill, OrderBook } from "../../domain/src/types";
+import { OrderRouter } from "../../execution/src/orderRouter";
+import type pino from "pino";
+
+export interface MMConfig { symbol:string; tickSz:number; clipSz:number; spreadBps:number; }
+export class MarketMaker {
+  private readonly logger: pino.Logger;
+
+  constructor(private cfg:MMConfig, private router:OrderRouter, private getBook:()=>OrderBook|undefined, logger: pino.Logger){
+    this.logger = logger.child({ name: "MarketMaker" });
+  }
+  async onTick(){
+    const b = this.getBook(); if(!b?.bids?.[0] || !b?.asks?.[0]) return;
+    const mid = (b.bids[0].px + b.asks[0].px)/2;
+    const pxBid = Math.floor(mid*(1-this.cfg.spreadBps/1e4)/this.cfg.tickSz)*this.cfg.tickSz;
+    const pxAsk = Math.ceil (mid*(1+this.cfg.spreadBps/1e4)/this.cfg.tickSz)*this.cfg.tickSz;
+    const base = { symbol:this.cfg.symbol, tif:"GTC" as const, postOnly:true };
+    await this.router.sendLimitChild({ ...base, clientId:`mm-bid-${Date.now()}`, side:"buy",  px:pxBid, sz:this.cfg.clipSz });
+    await this.router.sendLimitChild({ ...base, clientId:`mm-ask-${Date.now()}`, side:"sell", px:pxAsk, sz:this.cfg.clipSz });
+  }
+
+  async onFill(fill: Fill): Promise<void> {
+    this.logger.info({ fill }, "MarketMaker fill processed");
+    // Hook for adapting inventory or re-quoting; current implementation simply logs.
+  }
+}

+ 27 - 0
packages/strategies/src/microScalper.ts

@@ -0,0 +1,27 @@
+import type { Fill, OrderBook } from "../../domain/src/types";
+import { OrderRouter } from "../../execution/src/orderRouter";
+import type pino from "pino";
+
+export interface ScalperCfg { symbol:string; clipSz:number; triggerSpreadBps:number; tpBps:number; slBps:number; cooldownMs:number; }
+export class MicroScalper {
+  private last=0;
+  private readonly logger: pino.Logger;
+  constructor(private cfg:ScalperCfg, private router:OrderRouter, private getBook:()=>OrderBook|undefined, logger: pino.Logger){
+    this.logger = logger.child({ name: "MicroScalper" });
+  }
+  async onBook(){
+    const now=Date.now(); if (now-this.last<this.cfg.cooldownMs) return;
+    const b=this.getBook(); if(!b?.bids?.[0]||!b?.asks?.[0]) return;
+    const mid=(b.bids[0].px+b.asks[0].px)/2;
+    const spread=((b.asks[0].px-b.bids[0].px)/mid)*1e4;
+    if (spread>this.cfg.triggerSpreadBps){
+      this.last=now;
+      await this.router.sendLimitChild({ clientId:`scalp-${now}`, symbol:this.cfg.symbol, side:"buy", px:b.bids[0].px, sz:this.cfg.clipSz, tif:"IOC" });
+      // For brevity, OCO/take-profit is handled by TriggerEngine (not shown).
+    }
+  }
+
+  async onFill(fill: Fill): Promise<void> {
+    this.logger.info({ fill }, "MicroScalper fill processed");
+  }
+}

+ 27 - 0
packages/telemetry/src/gridMetrics.ts

@@ -0,0 +1,27 @@
+import {
+  gridCurrentDelta,
+  gridPendingHedges,
+  gridStepBps,
+  gridVolatilityBps
+} from "./metrics";
+
+export interface GridMetricSnapshot {
+  stepBps: number;
+  pendingHedges: number;
+  currentDelta: number;
+  hourlyVolatilityBps?: number;
+}
+
+export function observeGridMetrics(
+  symbol: string,
+  snapshot: GridMetricSnapshot
+): void {
+  if (!symbol) return;
+  gridStepBps.set({ symbol }, snapshot.stepBps);
+  gridPendingHedges.set({ symbol }, snapshot.pendingHedges);
+  gridCurrentDelta.set({ symbol }, snapshot.currentDelta);
+  if (snapshot.hourlyVolatilityBps !== undefined) {
+    gridVolatilityBps.set({ symbol }, snapshot.hourlyVolatilityBps);
+  }
+}
+

+ 46 - 0
packages/telemetry/src/metrics.ts

@@ -0,0 +1,46 @@
+import client from "prom-client";
+
+export const registry = new client.Registry();
+
+export const makerRatio = new client.Gauge({
+  name: "maker_ratio",
+  help: "Maker trade ratio"
+});
+
+export const deltaAbs = new client.Gauge({
+  name: "delta_abs",
+  help: "Absolute delta"
+});
+
+export const gridStepBps = new client.Gauge({
+  name: "grid_step_bps",
+  help: "Current grid step size in basis points",
+  labelNames: ["symbol"]
+});
+
+export const gridVolatilityBps = new client.Gauge({
+  name: "grid_volatility_hourly_bps",
+  help: "Hourly volatility estimate in bps",
+  labelNames: ["symbol"]
+});
+
+export const gridPendingHedges = new client.Gauge({
+  name: "grid_pending_hedges",
+  help: "Number of pending hedge orders awaiting fills",
+  labelNames: ["symbol"]
+});
+
+export const gridCurrentDelta = new client.Gauge({
+  name: "grid_current_delta",
+  help: "Current grid delta (base)",
+  labelNames: ["symbol"]
+});
+
+registry.registerMetric(makerRatio);
+registry.registerMetric(deltaAbs);
+registry.registerMetric(gridStepBps);
+registry.registerMetric(gridVolatilityBps);
+registry.registerMetric(gridPendingHedges);
+registry.registerMetric(gridCurrentDelta);
+
+client.collectDefaultMetrics({ register: registry });

+ 95 - 0
packages/utils/src/marketDataAdapter.ts

@@ -0,0 +1,95 @@
+import { EventEmitter } from "node:events";
+
+import type { OrderBook, OrderBookDelta } from "../../domain/src/types";
+import { ShadowBook } from "./shadowBook";
+
+export interface MarketDataAdapterOptions {
+  symbols: string[];
+  shadowBook: ShadowBook;
+  fetchSnapshot: (symbol: string) => Promise<OrderBook>;
+  pollIntervalMs?: number;
+}
+
+type EventKey = "snapshot" | "delta" | "error";
+
+type TimerMap = Map<string, NodeJS.Timeout>;
+
+export class MarketDataAdapter extends EventEmitter {
+  private readonly symbols: string[];
+  private readonly shadowBook: ShadowBook;
+  private readonly fetchSnapshot: (symbol: string) => Promise<OrderBook>;
+  private readonly pollInterval: number;
+  private readonly timers: TimerMap = new Map();
+  private started = false;
+
+  constructor(options: MarketDataAdapterOptions) {
+    super();
+    this.symbols = options.symbols;
+    this.shadowBook = options.shadowBook;
+    this.fetchSnapshot = options.fetchSnapshot;
+    this.pollInterval = options.pollIntervalMs ?? 1_000;
+  }
+
+  override on<Event extends EventKey>(event: Event, listener: (payload: MarketDataEvents[Event]) => void): this {
+    return super.on(event, listener);
+  }
+
+  override once<Event extends EventKey>(event: Event, listener: (payload: MarketDataEvents[Event]) => void): this {
+    return super.once(event, listener);
+  }
+
+  async start(): Promise<void> {
+    if (this.started) return;
+    this.started = true;
+    await Promise.all(this.symbols.map(symbol => this.refreshSymbol(symbol)));
+    for (const symbol of this.symbols) {
+      const timer = setInterval(() => {
+        this.refreshSymbol(symbol).catch(err => {
+          this.emit("error", { symbol, error: err });
+        });
+      }, this.pollInterval);
+      this.timers.set(symbol, timer);
+    }
+  }
+
+  stop(): void {
+    if (!this.started) return;
+    this.started = false;
+    for (const timer of this.timers.values()) {
+      clearInterval(timer);
+    }
+    this.timers.clear();
+  }
+
+  ingestSnapshot(symbol: string, snapshot: OrderBook, seq?: number): void {
+    this.shadowBook.updateFromSnapshot(symbol, snapshot, seq);
+    this.emit("snapshot", { symbol, snapshot });
+  }
+
+  ingestDelta(symbol: string, delta: OrderBookDelta): void {
+    this.shadowBook.applyIncrement(symbol, delta);
+    this.emit("delta", { symbol, delta });
+  }
+
+  private async refreshSymbol(symbol: string): Promise<void> {
+    try {
+      const snapshot = await this.fetchSnapshot(symbol);
+      this.ingestSnapshot(symbol, snapshot);
+    } catch (error) {
+      this.emit("error", { symbol, error });
+      throw error;
+    }
+  }
+}
+
+export type MarketDataAdapterEventPayloads = {
+  snapshot: { symbol: string; snapshot: OrderBook };
+  delta: { symbol: string; delta: OrderBookDelta };
+  error: { symbol: string; error: unknown };
+};
+
+export interface MarketDataEvents {
+  snapshot: MarketDataAdapterEventPayloads["snapshot"];
+  delta: MarketDataAdapterEventPayloads["delta"];
+  error: MarketDataAdapterEventPayloads["error"];
+}

+ 198 - 0
packages/utils/src/shadowBook.ts

@@ -0,0 +1,198 @@
+import type { OrderBook, OrderBookDelta, OrderBookUpdate } from "../../domain/src/types";
+
+export interface ShadowBookOptions {
+  /**
+   * Maximum depth levels to store per side. Defaults to storing all levels.
+   */
+  maxDepth?: number;
+  /**
+   * Consider data stale if last update is older than this value (ms).
+   */
+  stalenessMs?: number;
+}
+
+interface BookState {
+  bids: OrderBook["bids"];
+  asks: OrderBook["asks"];
+  ts: number;
+  seq?: number;
+  lastUpdate: number;
+}
+
+export class ShadowBook {
+  private readonly books = new Map<string, BookState>();
+  private readonly maxDepth?: number;
+  private readonly stalenessMs: number;
+
+  constructor(options: ShadowBookOptions = {}) {
+    this.maxDepth = options.maxDepth;
+    this.stalenessMs = options.stalenessMs ?? 5_000;
+  }
+
+  updateFromSnapshot(symbol: string, snapshot: OrderBook, seq?: number): void {
+    const bids = this.normalizeSide(snapshot.bids);
+    const asks = this.normalizeSide(snapshot.asks);
+    this.books.set(symbol, {
+      bids,
+      asks,
+      ts: snapshot.ts,
+      seq,
+      lastUpdate: Date.now()
+    });
+  }
+
+  applyIncrement(symbol: string, delta: OrderBookDelta): void {
+    const state = this.books.get(symbol);
+    if (!state) {
+      const bids = this.normalizeSide(delta.bids?.map(convertUpdateToLevel) ?? []);
+      const asks = this.normalizeSide(delta.asks?.map(convertUpdateToLevel) ?? []);
+      this.books.set(symbol, {
+        bids,
+        asks,
+        ts: delta.ts ?? Date.now(),
+        seq: delta.seq,
+        lastUpdate: Date.now()
+      });
+      return;
+    }
+
+    if (delta.bids) {
+      state.bids = this.applyUpdates(state.bids, delta.bids, "bid");
+    }
+    if (delta.asks) {
+      state.asks = this.applyUpdates(state.asks, delta.asks, "ask");
+    }
+    if (delta.ts) {
+      state.ts = delta.ts;
+    }
+    if (delta.seq !== undefined) {
+      state.seq = delta.seq;
+    }
+    state.lastUpdate = Date.now();
+  }
+
+  getState(symbol: string): BookState | undefined {
+    return this.books.get(symbol);
+  }
+
+  snapshot(symbol: string): OrderBook | undefined {
+    const state = this.books.get(symbol);
+    if (!state) return undefined;
+    return { bids: state.bids, asks: state.asks, ts: state.ts };
+  }
+
+  getTop(symbol: string) {
+    const state = this.books.get(symbol);
+    if (!state?.bids.length || !state?.asks.length) {
+      return undefined;
+    }
+    return {
+      bid: state.bids[0],
+      ask: state.asks[0]
+    };
+  }
+
+  mid(symbol: string): number | undefined {
+    const top = this.getTop(symbol);
+    if (!top) return undefined;
+    return (top.bid.px + top.ask.px) / 2;
+  }
+
+  spread(symbol: string): number | undefined {
+    const top = this.getTop(symbol);
+    if (!top) return undefined;
+    return top.ask.px - top.bid.px;
+  }
+
+  spreadBps(symbol: string): number | undefined {
+    const mid = this.mid(symbol);
+    const spread = this.spread(symbol);
+    if (!mid || !spread) return undefined;
+    return (spread / mid) * 10_000;
+  }
+
+  computeObi(symbol: string, depth = 1): number | undefined {
+    const state = this.books.get(symbol);
+    if (!state) return undefined;
+    const maxDepth = Math.min(depth, state.bids.length, state.asks.length);
+    if (maxDepth === 0) return undefined;
+
+    const bidVolume = state.bids.slice(0, maxDepth).reduce((sum, level) => sum + level.sz, 0);
+    const askVolume = state.asks.slice(0, maxDepth).reduce((sum, level) => sum + level.sz, 0);
+    const denom = bidVolume + askVolume;
+    if (denom === 0) return 0;
+    return (bidVolume - askVolume) / denom;
+  }
+
+  topDepthUsd(symbol: string, depth = 5): number | undefined {
+    const state = this.books.get(symbol);
+    if (!state) return undefined;
+    const top = this.getTop(symbol);
+    if (!top) return undefined;
+
+    const bidsUsd = state.bids
+      .slice(0, depth)
+      .reduce((sum, level) => sum + level.sz * level.px, 0);
+    const asksUsd = state.asks
+      .slice(0, depth)
+      .reduce((sum, level) => sum + level.sz * level.px, 0);
+    return bidsUsd + asksUsd;
+  }
+
+  detectDataGap(symbol: string, now = Date.now()): boolean {
+    const state = this.books.get(symbol);
+    if (!state) return true;
+    return now - state.lastUpdate > this.stalenessMs;
+  }
+
+  clear(symbol: string): void {
+    this.books.delete(symbol);
+  }
+
+  reset(): void {
+    this.books.clear();
+  }
+
+  private normalizeSide(levels: OrderBook["bids"]): OrderBook["bids"] {
+    const sorted = [...levels].sort((a, b) => b.px - a.px);
+    if (this.maxDepth !== undefined) {
+      return sorted.slice(0, this.maxDepth);
+    }
+    return sorted;
+  }
+
+  private applyUpdates(
+    current: OrderBook["bids"],
+    updates: OrderBookUpdate[],
+    side: "bid" | "ask"
+  ): OrderBook["bids"] {
+    const map = new Map<number, number>();
+    for (const level of current) {
+      map.set(level.px, level.sz);
+    }
+    for (const update of updates) {
+      const price = update.price;
+      const size = update.size;
+      if (size <= 0) {
+        map.delete(price);
+      } else {
+        map.set(price, size);
+      }
+    }
+    const merged = Array.from(map.entries()).map(([px, sz]) => ({ px, sz }));
+    merged.sort((a, b) => {
+      if (side === "bid") {
+        return b.px - a.px;
+      }
+      return a.px - b.px;
+    });
+    if (this.maxDepth !== undefined) {
+      return merged.slice(0, this.maxDepth);
+    }
+    return merged;
+  }
+}
+
+function convertUpdateToLevel(update: OrderBookUpdate) {
+  return { px: update.price, sz: update.size };
+}

+ 95 - 0
packages/utils/src/volatilityEstimator.ts

@@ -0,0 +1,95 @@
+export interface VolatilityEstimatorOptions {
+  windowMinutes?: number;
+  minSamples?: number;
+  maxCadenceMs?: number;
+}
+
+interface PricePoint {
+  ts: number;
+  price: number;
+}
+
+/**
+ * Estimates short-term volatility based on rolling mid-price snapshots.
+ */
+export class VolatilityEstimator {
+  private readonly windowMinutes: number;
+  private readonly minSamples: number;
+  private readonly maxCadenceMs: number;
+  private readonly history: PricePoint[] = [];
+
+  constructor(options: VolatilityEstimatorOptions = {}) {
+    this.windowMinutes = options.windowMinutes ?? 30;
+    this.minSamples = options.minSamples ?? 10;
+    this.maxCadenceMs = options.maxCadenceMs ?? 5_000;
+  }
+
+  update(price: number, ts: number = Date.now()): void {
+    if (!Number.isFinite(price) || price <= 0) return;
+    const last = this.history[this.history.length - 1];
+    if (last && ts - last.ts < this.maxCadenceMs) {
+      this.history[this.history.length - 1] = { ts, price };
+    } else {
+      this.history.push({ ts, price });
+    }
+
+    const cutoff = ts - this.windowMinutes * 60 * 1000;
+    while (this.history.length > 0 && this.history[0]!.ts < cutoff) {
+      this.history.shift();
+    }
+  }
+
+  getAnnualizedVolatility(): number | undefined {
+    if (this.history.length < this.minSamples) return undefined;
+    const returns = this.computeLogReturns(this.history);
+    if (returns.length === 0) return undefined;
+    const stdDev = this.standardDeviation(returns);
+    const periodsPerYear = (365 * 24 * 60) / this.windowMinutes;
+    return stdDev * Math.sqrt(periodsPerYear);
+  }
+
+  getHourlyVolatilityBps(): number | undefined {
+    if (this.history.length < 2) return undefined;
+    const windowMs = 60 * 60 * 1000;
+    const latest = this.history[this.history.length - 1]!;
+    const cutoff = latest.ts - windowMs;
+    const relevant = this.history.filter(point => point.ts >= cutoff);
+    if (relevant.length < 2) return undefined;
+    const first = relevant[0]!.price;
+    const last = relevant[relevant.length - 1]!.price;
+    const range = Math.abs(last - first) / first;
+    return range * 10_000;
+  }
+
+  getStatus() {
+    const oldest = this.history[0];
+    const latest = this.history[this.history.length - 1];
+    return {
+      historySize: this.history.length,
+      oldestTs: oldest?.ts,
+      latestPrice: latest?.price,
+      annualizedVol: this.getAnnualizedVolatility(),
+      hourlyVolBps: this.getHourlyVolatilityBps()
+    };
+  }
+
+  private computeLogReturns(series: PricePoint[]): number[] {
+    const returns: number[] = [];
+    for (let i = 1; i < series.length; i += 1) {
+      const prev = series[i - 1]!.price;
+      const curr = series[i]!.price;
+      if (prev <= 0 || curr <= 0) continue;
+      returns.push(Math.log(curr / prev));
+    }
+    return returns;
+  }
+
+  private standardDeviation(values: number[]): number {
+    if (values.length === 0) return 0;
+    const mean = values.reduce((sum, v) => sum + v, 0) / values.length;
+    const variance =
+      values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;
+    return Math.sqrt(variance);
+  }
+}
+

+ 2560 - 0
pnpm-lock.yaml

@@ -0,0 +1,2560 @@
+lockfileVersion: '9.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
+importers:
+
+  .:
+    dependencies:
+      bs58:
+        specifier: ^6.0.0
+        version: 6.0.0
+      decimal.js:
+        specifier: ^10.4.3
+        version: 10.6.0
+      dotenv:
+        specifier: ^16.4.5
+        version: 16.6.1
+      pino:
+        specifier: ^9.0.0
+        version: 9.13.1
+      prom-client:
+        specifier: ^15.1.2
+        version: 15.1.3
+      tweetnacl:
+        specifier: ^1.0.3
+        version: 1.0.3
+      undici:
+        specifier: ^6.19.8
+        version: 6.22.0
+      ws:
+        specifier: ^8.18.0
+        version: 8.18.3
+      yaml:
+        specifier: ^2.6.0
+        version: 2.8.1
+    devDependencies:
+      '@types/node':
+        specifier: ^20.11.30
+        version: 20.19.19
+      '@types/ws':
+        specifier: ^8.5.10
+        version: 8.18.1
+      '@typescript-eslint/eslint-plugin':
+        specifier: ^7.17.0
+        version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)
+      '@typescript-eslint/parser':
+        specifier: ^7.17.0
+        version: 7.18.0(eslint@8.57.1)(typescript@5.9.3)
+      eslint:
+        specifier: ^8.57.0
+        version: 8.57.1
+      pino-pretty:
+        specifier: ^11.0.0
+        version: 11.3.0
+      tsx:
+        specifier: ^4.16.2
+        version: 4.20.6
+      typescript:
+        specifier: ^5.6.2
+        version: 5.9.3
+      vitest:
+        specifier: ^2.0.5
+        version: 2.1.9(@types/node@20.19.19)
+      zod:
+        specifier: ^3.23.8
+        version: 3.25.76
+
+packages:
+
+  '@esbuild/aix-ppc64@0.21.5':
+    resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
+    engines: {node: '>=12'}
+    cpu: [ppc64]
+    os: [aix]
+
+  '@esbuild/aix-ppc64@0.25.10':
+    resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [aix]
+
+  '@esbuild/android-arm64@0.21.5':
+    resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [android]
+
+  '@esbuild/android-arm64@0.25.10':
+    resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [android]
+
+  '@esbuild/android-arm@0.21.5':
+    resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
+    engines: {node: '>=12'}
+    cpu: [arm]
+    os: [android]
+
+  '@esbuild/android-arm@0.25.10':
+    resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [android]
+
+  '@esbuild/android-x64@0.21.5':
+    resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [android]
+
+  '@esbuild/android-x64@0.25.10':
+    resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [android]
+
+  '@esbuild/darwin-arm64@0.21.5':
+    resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@esbuild/darwin-arm64@0.25.10':
+    resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@esbuild/darwin-x64@0.21.5':
+    resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@esbuild/darwin-x64@0.25.10':
+    resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@esbuild/freebsd-arm64@0.21.5':
+    resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [freebsd]
+
+  '@esbuild/freebsd-arm64@0.25.10':
+    resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [freebsd]
+
+  '@esbuild/freebsd-x64@0.21.5':
+    resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@esbuild/freebsd-x64@0.25.10':
+    resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@esbuild/linux-arm64@0.21.5':
+    resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@esbuild/linux-arm64@0.25.10':
+    resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@esbuild/linux-arm@0.21.5':
+    resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
+    engines: {node: '>=12'}
+    cpu: [arm]
+    os: [linux]
+
+  '@esbuild/linux-arm@0.25.10':
+    resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [linux]
+
+  '@esbuild/linux-ia32@0.21.5':
+    resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
+    engines: {node: '>=12'}
+    cpu: [ia32]
+    os: [linux]
+
+  '@esbuild/linux-ia32@0.25.10':
+    resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [linux]
+
+  '@esbuild/linux-loong64@0.21.5':
+    resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
+    engines: {node: '>=12'}
+    cpu: [loong64]
+    os: [linux]
+
+  '@esbuild/linux-loong64@0.25.10':
+    resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==}
+    engines: {node: '>=18'}
+    cpu: [loong64]
+    os: [linux]
+
+  '@esbuild/linux-mips64el@0.21.5':
+    resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
+    engines: {node: '>=12'}
+    cpu: [mips64el]
+    os: [linux]
+
+  '@esbuild/linux-mips64el@0.25.10':
+    resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==}
+    engines: {node: '>=18'}
+    cpu: [mips64el]
+    os: [linux]
+
+  '@esbuild/linux-ppc64@0.21.5':
+    resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
+    engines: {node: '>=12'}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@esbuild/linux-ppc64@0.25.10':
+    resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@esbuild/linux-riscv64@0.21.5':
+    resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
+    engines: {node: '>=12'}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@esbuild/linux-riscv64@0.25.10':
+    resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==}
+    engines: {node: '>=18'}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@esbuild/linux-s390x@0.21.5':
+    resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
+    engines: {node: '>=12'}
+    cpu: [s390x]
+    os: [linux]
+
+  '@esbuild/linux-s390x@0.25.10':
+    resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==}
+    engines: {node: '>=18'}
+    cpu: [s390x]
+    os: [linux]
+
+  '@esbuild/linux-x64@0.21.5':
+    resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [linux]
+
+  '@esbuild/linux-x64@0.25.10':
+    resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [linux]
+
+  '@esbuild/netbsd-arm64@0.25.10':
+    resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [netbsd]
+
+  '@esbuild/netbsd-x64@0.21.5':
+    resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [netbsd]
+
+  '@esbuild/netbsd-x64@0.25.10':
+    resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [netbsd]
+
+  '@esbuild/openbsd-arm64@0.25.10':
+    resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openbsd]
+
+  '@esbuild/openbsd-x64@0.21.5':
+    resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [openbsd]
+
+  '@esbuild/openbsd-x64@0.25.10':
+    resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [openbsd]
+
+  '@esbuild/openharmony-arm64@0.25.10':
+    resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openharmony]
+
+  '@esbuild/sunos-x64@0.21.5':
+    resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [sunos]
+
+  '@esbuild/sunos-x64@0.25.10':
+    resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [sunos]
+
+  '@esbuild/win32-arm64@0.21.5':
+    resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@esbuild/win32-arm64@0.25.10':
+    resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@esbuild/win32-ia32@0.21.5':
+    resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
+    engines: {node: '>=12'}
+    cpu: [ia32]
+    os: [win32]
+
+  '@esbuild/win32-ia32@0.25.10':
+    resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [win32]
+
+  '@esbuild/win32-x64@0.21.5':
+    resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [win32]
+
+  '@esbuild/win32-x64@0.25.10':
+    resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [win32]
+
+  '@eslint-community/eslint-utils@4.9.0':
+    resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+    peerDependencies:
+      eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+  '@eslint-community/regexpp@4.12.1':
+    resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
+    engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+  '@eslint/eslintrc@2.1.4':
+    resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+  '@eslint/js@8.57.1':
+    resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+  '@humanwhocodes/config-array@0.13.0':
+    resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
+    engines: {node: '>=10.10.0'}
+    deprecated: Use @eslint/config-array instead
+
+  '@humanwhocodes/module-importer@1.0.1':
+    resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+    engines: {node: '>=12.22'}
+
+  '@humanwhocodes/object-schema@2.0.3':
+    resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
+    deprecated: Use @eslint/object-schema instead
+
+  '@jridgewell/sourcemap-codec@1.5.5':
+    resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+  '@nodelib/fs.scandir@2.1.5':
+    resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+    engines: {node: '>= 8'}
+
+  '@nodelib/fs.stat@2.0.5':
+    resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+    engines: {node: '>= 8'}
+
+  '@nodelib/fs.walk@1.2.8':
+    resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+    engines: {node: '>= 8'}
+
+  '@opentelemetry/api@1.9.0':
+    resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
+    engines: {node: '>=8.0.0'}
+
+  '@rollup/rollup-android-arm-eabi@4.52.4':
+    resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==}
+    cpu: [arm]
+    os: [android]
+
+  '@rollup/rollup-android-arm64@4.52.4':
+    resolution: {integrity: sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==}
+    cpu: [arm64]
+    os: [android]
+
+  '@rollup/rollup-darwin-arm64@4.52.4':
+    resolution: {integrity: sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@rollup/rollup-darwin-x64@4.52.4':
+    resolution: {integrity: sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==}
+    cpu: [x64]
+    os: [darwin]
+
+  '@rollup/rollup-freebsd-arm64@4.52.4':
+    resolution: {integrity: sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==}
+    cpu: [arm64]
+    os: [freebsd]
+
+  '@rollup/rollup-freebsd-x64@4.52.4':
+    resolution: {integrity: sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@rollup/rollup-linux-arm-gnueabihf@4.52.4':
+    resolution: {integrity: sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==}
+    cpu: [arm]
+    os: [linux]
+
+  '@rollup/rollup-linux-arm-musleabihf@4.52.4':
+    resolution: {integrity: sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==}
+    cpu: [arm]
+    os: [linux]
+
+  '@rollup/rollup-linux-arm64-gnu@4.52.4':
+    resolution: {integrity: sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==}
+    cpu: [arm64]
+    os: [linux]
+
+  '@rollup/rollup-linux-arm64-musl@4.52.4':
+    resolution: {integrity: sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==}
+    cpu: [arm64]
+    os: [linux]
+
+  '@rollup/rollup-linux-loong64-gnu@4.52.4':
+    resolution: {integrity: sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==}
+    cpu: [loong64]
+    os: [linux]
+
+  '@rollup/rollup-linux-ppc64-gnu@4.52.4':
+    resolution: {integrity: sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@rollup/rollup-linux-riscv64-gnu@4.52.4':
+    resolution: {integrity: sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@rollup/rollup-linux-riscv64-musl@4.52.4':
+    resolution: {integrity: sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@rollup/rollup-linux-s390x-gnu@4.52.4':
+    resolution: {integrity: sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==}
+    cpu: [s390x]
+    os: [linux]
+
+  '@rollup/rollup-linux-x64-gnu@4.52.4':
+    resolution: {integrity: sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==}
+    cpu: [x64]
+    os: [linux]
+
+  '@rollup/rollup-linux-x64-musl@4.52.4':
+    resolution: {integrity: sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==}
+    cpu: [x64]
+    os: [linux]
+
+  '@rollup/rollup-openharmony-arm64@4.52.4':
+    resolution: {integrity: sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==}
+    cpu: [arm64]
+    os: [openharmony]
+
+  '@rollup/rollup-win32-arm64-msvc@4.52.4':
+    resolution: {integrity: sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==}
+    cpu: [arm64]
+    os: [win32]
+
+  '@rollup/rollup-win32-ia32-msvc@4.52.4':
+    resolution: {integrity: sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==}
+    cpu: [ia32]
+    os: [win32]
+
+  '@rollup/rollup-win32-x64-gnu@4.52.4':
+    resolution: {integrity: sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==}
+    cpu: [x64]
+    os: [win32]
+
+  '@rollup/rollup-win32-x64-msvc@4.52.4':
+    resolution: {integrity: sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==}
+    cpu: [x64]
+    os: [win32]
+
+  '@types/estree@1.0.8':
+    resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+
+  '@types/node@20.19.19':
+    resolution: {integrity: sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==}
+
+  '@types/ws@8.18.1':
+    resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
+
+  '@typescript-eslint/eslint-plugin@7.18.0':
+    resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==}
+    engines: {node: ^18.18.0 || >=20.0.0}
+    peerDependencies:
+      '@typescript-eslint/parser': ^7.0.0
+      eslint: ^8.56.0
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  '@typescript-eslint/parser@7.18.0':
+    resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==}
+    engines: {node: ^18.18.0 || >=20.0.0}
+    peerDependencies:
+      eslint: ^8.56.0
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  '@typescript-eslint/scope-manager@7.18.0':
+    resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==}
+    engines: {node: ^18.18.0 || >=20.0.0}
+
+  '@typescript-eslint/type-utils@7.18.0':
+    resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==}
+    engines: {node: ^18.18.0 || >=20.0.0}
+    peerDependencies:
+      eslint: ^8.56.0
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  '@typescript-eslint/types@7.18.0':
+    resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==}
+    engines: {node: ^18.18.0 || >=20.0.0}
+
+  '@typescript-eslint/typescript-estree@7.18.0':
+    resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==}
+    engines: {node: ^18.18.0 || >=20.0.0}
+    peerDependencies:
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  '@typescript-eslint/utils@7.18.0':
+    resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==}
+    engines: {node: ^18.18.0 || >=20.0.0}
+    peerDependencies:
+      eslint: ^8.56.0
+
+  '@typescript-eslint/visitor-keys@7.18.0':
+    resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==}
+    engines: {node: ^18.18.0 || >=20.0.0}
+
+  '@ungap/structured-clone@1.3.0':
+    resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
+
+  '@vitest/expect@2.1.9':
+    resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
+
+  '@vitest/mocker@2.1.9':
+    resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==}
+    peerDependencies:
+      msw: ^2.4.9
+      vite: ^5.0.0
+    peerDependenciesMeta:
+      msw:
+        optional: true
+      vite:
+        optional: true
+
+  '@vitest/pretty-format@2.1.9':
+    resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==}
+
+  '@vitest/runner@2.1.9':
+    resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==}
+
+  '@vitest/snapshot@2.1.9':
+    resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==}
+
+  '@vitest/spy@2.1.9':
+    resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==}
+
+  '@vitest/utils@2.1.9':
+    resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==}
+
+  abort-controller@3.0.0:
+    resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
+    engines: {node: '>=6.5'}
+
+  acorn-jsx@5.3.2:
+    resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+    peerDependencies:
+      acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+  acorn@8.15.0:
+    resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
+    engines: {node: '>=0.4.0'}
+    hasBin: true
+
+  ajv@6.12.6:
+    resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+
+  ansi-regex@5.0.1:
+    resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+    engines: {node: '>=8'}
+
+  ansi-styles@4.3.0:
+    resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+    engines: {node: '>=8'}
+
+  argparse@2.0.1:
+    resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+  array-union@2.1.0:
+    resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
+    engines: {node: '>=8'}
+
+  assertion-error@2.0.1:
+    resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+    engines: {node: '>=12'}
+
+  atomic-sleep@1.0.0:
+    resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
+    engines: {node: '>=8.0.0'}
+
+  balanced-match@1.0.2:
+    resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+  base-x@5.0.1:
+    resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==}
+
+  base64-js@1.5.1:
+    resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+
+  bintrees@1.0.2:
+    resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==}
+
+  brace-expansion@1.1.12:
+    resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
+
+  brace-expansion@2.0.2:
+    resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
+
+  braces@3.0.3:
+    resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+    engines: {node: '>=8'}
+
+  bs58@6.0.0:
+    resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==}
+
+  buffer@6.0.3:
+    resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
+
+  cac@6.7.14:
+    resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
+    engines: {node: '>=8'}
+
+  callsites@3.1.0:
+    resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+    engines: {node: '>=6'}
+
+  chai@5.3.3:
+    resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
+    engines: {node: '>=18'}
+
+  chalk@4.1.2:
+    resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+    engines: {node: '>=10'}
+
+  check-error@2.1.1:
+    resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
+    engines: {node: '>= 16'}
+
+  color-convert@2.0.1:
+    resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+    engines: {node: '>=7.0.0'}
+
+  color-name@1.1.4:
+    resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+  colorette@2.0.20:
+    resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
+
+  concat-map@0.0.1:
+    resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+  cross-spawn@7.0.6:
+    resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+    engines: {node: '>= 8'}
+
+  dateformat@4.6.3:
+    resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
+
+  debug@4.4.3:
+    resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+    engines: {node: '>=6.0'}
+    peerDependencies:
+      supports-color: '*'
+    peerDependenciesMeta:
+      supports-color:
+        optional: true
+
+  decimal.js@10.6.0:
+    resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+
+  deep-eql@5.0.2:
+    resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
+    engines: {node: '>=6'}
+
+  deep-is@0.1.4:
+    resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+  dir-glob@3.0.1:
+    resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
+    engines: {node: '>=8'}
+
+  doctrine@3.0.0:
+    resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
+    engines: {node: '>=6.0.0'}
+
+  dotenv@16.6.1:
+    resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
+    engines: {node: '>=12'}
+
+  end-of-stream@1.4.5:
+    resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
+
+  es-module-lexer@1.7.0:
+    resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
+
+  esbuild@0.21.5:
+    resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
+    engines: {node: '>=12'}
+    hasBin: true
+
+  esbuild@0.25.10:
+    resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==}
+    engines: {node: '>=18'}
+    hasBin: true
+
+  escape-string-regexp@4.0.0:
+    resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+    engines: {node: '>=10'}
+
+  eslint-scope@7.2.2:
+    resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+  eslint-visitor-keys@3.4.3:
+    resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+  eslint@8.57.1:
+    resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+    deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
+    hasBin: true
+
+  espree@9.6.1:
+    resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+  esquery@1.6.0:
+    resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
+    engines: {node: '>=0.10'}
+
+  esrecurse@4.3.0:
+    resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+    engines: {node: '>=4.0'}
+
+  estraverse@5.3.0:
+    resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+    engines: {node: '>=4.0'}
+
+  estree-walker@3.0.3:
+    resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+  esutils@2.0.3:
+    resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+    engines: {node: '>=0.10.0'}
+
+  event-target-shim@5.0.1:
+    resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
+    engines: {node: '>=6'}
+
+  events@3.3.0:
+    resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
+    engines: {node: '>=0.8.x'}
+
+  expect-type@1.2.2:
+    resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==}
+    engines: {node: '>=12.0.0'}
+
+  fast-copy@3.0.2:
+    resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==}
+
+  fast-deep-equal@3.1.3:
+    resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+  fast-glob@3.3.3:
+    resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
+    engines: {node: '>=8.6.0'}
+
+  fast-json-stable-stringify@2.1.0:
+    resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+  fast-levenshtein@2.0.6:
+    resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+  fast-safe-stringify@2.1.1:
+    resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
+
+  fastq@1.19.1:
+    resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
+
+  file-entry-cache@6.0.1:
+    resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
+    engines: {node: ^10.12.0 || >=12.0.0}
+
+  fill-range@7.1.1:
+    resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+    engines: {node: '>=8'}
+
+  find-up@5.0.0:
+    resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+    engines: {node: '>=10'}
+
+  flat-cache@3.2.0:
+    resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
+    engines: {node: ^10.12.0 || >=12.0.0}
+
+  flatted@3.3.3:
+    resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
+
+  fs.realpath@1.0.0:
+    resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+
+  fsevents@2.3.3:
+    resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+    os: [darwin]
+
+  get-tsconfig@4.11.0:
+    resolution: {integrity: sha512-sNsqf7XKQ38IawiVGPOoAlqZo1DMrO7TU+ZcZwi7yLl7/7S0JwmoBMKz/IkUPhSoXM0Ng3vT0yB1iCe5XavDeQ==}
+
+  glob-parent@5.1.2:
+    resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+    engines: {node: '>= 6'}
+
+  glob-parent@6.0.2:
+    resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+    engines: {node: '>=10.13.0'}
+
+  glob@7.2.3:
+    resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
+    deprecated: Glob versions prior to v9 are no longer supported
+
+  globals@13.24.0:
+    resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
+    engines: {node: '>=8'}
+
+  globby@11.1.0:
+    resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
+    engines: {node: '>=10'}
+
+  graphemer@1.4.0:
+    resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
+
+  has-flag@4.0.0:
+    resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+    engines: {node: '>=8'}
+
+  help-me@5.0.0:
+    resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
+
+  ieee754@1.2.1:
+    resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+
+  ignore@5.3.2:
+    resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+    engines: {node: '>= 4'}
+
+  import-fresh@3.3.1:
+    resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+    engines: {node: '>=6'}
+
+  imurmurhash@0.1.4:
+    resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+    engines: {node: '>=0.8.19'}
+
+  inflight@1.0.6:
+    resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
+    deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
+
+  inherits@2.0.4:
+    resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
+  is-extglob@2.1.1:
+    resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+    engines: {node: '>=0.10.0'}
+
+  is-glob@4.0.3:
+    resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+    engines: {node: '>=0.10.0'}
+
+  is-number@7.0.0:
+    resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+    engines: {node: '>=0.12.0'}
+
+  is-path-inside@3.0.3:
+    resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
+    engines: {node: '>=8'}
+
+  isexe@2.0.0:
+    resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+  joycon@3.1.1:
+    resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
+    engines: {node: '>=10'}
+
+  js-yaml@4.1.0:
+    resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+    hasBin: true
+
+  json-buffer@3.0.1:
+    resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+  json-schema-traverse@0.4.1:
+    resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+  json-stable-stringify-without-jsonify@1.0.1:
+    resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+  keyv@4.5.4:
+    resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+  levn@0.4.1:
+    resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+    engines: {node: '>= 0.8.0'}
+
+  locate-path@6.0.0:
+    resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+    engines: {node: '>=10'}
+
+  lodash.merge@4.6.2:
+    resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+  loupe@3.2.1:
+    resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
+
+  magic-string@0.30.19:
+    resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
+
+  merge2@1.4.1:
+    resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+    engines: {node: '>= 8'}
+
+  micromatch@4.0.8:
+    resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
+    engines: {node: '>=8.6'}
+
+  minimatch@3.1.2:
+    resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+
+  minimatch@9.0.5:
+    resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
+    engines: {node: '>=16 || 14 >=14.17'}
+
+  minimist@1.2.8:
+    resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+
+  ms@2.1.3:
+    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+  nanoid@3.3.11:
+    resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+    hasBin: true
+
+  natural-compare@1.4.0:
+    resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+  on-exit-leak-free@2.1.2:
+    resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
+    engines: {node: '>=14.0.0'}
+
+  once@1.4.0:
+    resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
+  optionator@0.9.4:
+    resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+    engines: {node: '>= 0.8.0'}
+
+  p-limit@3.1.0:
+    resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+    engines: {node: '>=10'}
+
+  p-locate@5.0.0:
+    resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+    engines: {node: '>=10'}
+
+  parent-module@1.0.1:
+    resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+    engines: {node: '>=6'}
+
+  path-exists@4.0.0:
+    resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+    engines: {node: '>=8'}
+
+  path-is-absolute@1.0.1:
+    resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
+    engines: {node: '>=0.10.0'}
+
+  path-key@3.1.1:
+    resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+    engines: {node: '>=8'}
+
+  path-type@4.0.0:
+    resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
+    engines: {node: '>=8'}
+
+  pathe@1.1.2:
+    resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
+
+  pathval@2.0.1:
+    resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
+    engines: {node: '>= 14.16'}
+
+  picocolors@1.1.1:
+    resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+  picomatch@2.3.1:
+    resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+    engines: {node: '>=8.6'}
+
+  pino-abstract-transport@2.0.0:
+    resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
+
+  pino-pretty@11.3.0:
+    resolution: {integrity: sha512-oXwn7ICywaZPHmu3epHGU2oJX4nPmKvHvB/bwrJHlGcbEWaVcotkpyVHMKLKmiVryWYByNp0jpgAcXpFJDXJzA==}
+    hasBin: true
+
+  pino-std-serializers@7.0.0:
+    resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==}
+
+  pino@9.13.1:
+    resolution: {integrity: sha512-Szuj+ViDTjKPQYiKumGmEn3frdl+ZPSdosHyt9SnUevFosOkMY2b7ipxlEctNKPmMD/VibeBI+ZcZCJK+4DPuw==}
+    hasBin: true
+
+  postcss@8.5.6:
+    resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
+    engines: {node: ^10 || ^12 || >=14}
+
+  prelude-ls@1.2.1:
+    resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+    engines: {node: '>= 0.8.0'}
+
+  process-warning@5.0.0:
+    resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
+
+  process@0.11.10:
+    resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
+    engines: {node: '>= 0.6.0'}
+
+  prom-client@15.1.3:
+    resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==}
+    engines: {node: ^16 || ^18 || >=20}
+
+  pump@3.0.3:
+    resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
+
+  punycode@2.3.1:
+    resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+    engines: {node: '>=6'}
+
+  queue-microtask@1.2.3:
+    resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+
+  quick-format-unescaped@4.0.4:
+    resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
+
+  readable-stream@4.7.0:
+    resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+  real-require@0.2.0:
+    resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
+    engines: {node: '>= 12.13.0'}
+
+  resolve-from@4.0.0:
+    resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+    engines: {node: '>=4'}
+
+  resolve-pkg-maps@1.0.0:
+    resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+
+  reusify@1.1.0:
+    resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
+    engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+
+  rimraf@3.0.2:
+    resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
+    deprecated: Rimraf versions prior to v4 are no longer supported
+    hasBin: true
+
+  rollup@4.52.4:
+    resolution: {integrity: sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==}
+    engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+    hasBin: true
+
+  run-parallel@1.2.0:
+    resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+
+  safe-buffer@5.2.1:
+    resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+
+  safe-stable-stringify@2.5.0:
+    resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
+    engines: {node: '>=10'}
+
+  secure-json-parse@2.7.0:
+    resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
+
+  semver@7.7.2:
+    resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
+    engines: {node: '>=10'}
+    hasBin: true
+
+  shebang-command@2.0.0:
+    resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+    engines: {node: '>=8'}
+
+  shebang-regex@3.0.0:
+    resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+    engines: {node: '>=8'}
+
+  siginfo@2.0.0:
+    resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
+  slash@3.0.0:
+    resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
+    engines: {node: '>=8'}
+
+  slow-redact@0.3.1:
+    resolution: {integrity: sha512-NvFvl1GuLZNW4U046Tfi8b26zXo8aBzgCAS2f7yVJR/fArN93mOqSA99cB9uITm92ajSz01bsu1K7SCVVjIMpQ==}
+
+  sonic-boom@4.2.0:
+    resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==}
+
+  source-map-js@1.2.1:
+    resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+    engines: {node: '>=0.10.0'}
+
+  split2@4.2.0:
+    resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
+    engines: {node: '>= 10.x'}
+
+  stackback@0.0.2:
+    resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
+  std-env@3.9.0:
+    resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==}
+
+  string_decoder@1.3.0:
+    resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
+
+  strip-ansi@6.0.1:
+    resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+    engines: {node: '>=8'}
+
+  strip-json-comments@3.1.1:
+    resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+    engines: {node: '>=8'}
+
+  supports-color@7.2.0:
+    resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+    engines: {node: '>=8'}
+
+  tdigest@0.1.2:
+    resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==}
+
+  text-table@0.2.0:
+    resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
+
+  thread-stream@3.1.0:
+    resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
+
+  tinybench@2.9.0:
+    resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
+  tinyexec@0.3.2:
+    resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
+
+  tinypool@1.1.1:
+    resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+
+  tinyrainbow@1.2.0:
+    resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==}
+    engines: {node: '>=14.0.0'}
+
+  tinyspy@3.0.2:
+    resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
+    engines: {node: '>=14.0.0'}
+
+  to-regex-range@5.0.1:
+    resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+    engines: {node: '>=8.0'}
+
+  ts-api-utils@1.4.3:
+    resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==}
+    engines: {node: '>=16'}
+    peerDependencies:
+      typescript: '>=4.2.0'
+
+  tsx@4.20.6:
+    resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==}
+    engines: {node: '>=18.0.0'}
+    hasBin: true
+
+  tweetnacl@1.0.3:
+    resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==}
+
+  type-check@0.4.0:
+    resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+    engines: {node: '>= 0.8.0'}
+
+  type-fest@0.20.2:
+    resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
+    engines: {node: '>=10'}
+
+  typescript@5.9.3:
+    resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+    engines: {node: '>=14.17'}
+    hasBin: true
+
+  undici-types@6.21.0:
+    resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
+
+  undici@6.22.0:
+    resolution: {integrity: sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==}
+    engines: {node: '>=18.17'}
+
+  uri-js@4.4.1:
+    resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+  vite-node@2.1.9:
+    resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+    hasBin: true
+
+  vite@5.4.20:
+    resolution: {integrity: sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+    hasBin: true
+    peerDependencies:
+      '@types/node': ^18.0.0 || >=20.0.0
+      less: '*'
+      lightningcss: ^1.21.0
+      sass: '*'
+      sass-embedded: '*'
+      stylus: '*'
+      sugarss: '*'
+      terser: ^5.4.0
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+      less:
+        optional: true
+      lightningcss:
+        optional: true
+      sass:
+        optional: true
+      sass-embedded:
+        optional: true
+      stylus:
+        optional: true
+      sugarss:
+        optional: true
+      terser:
+        optional: true
+
+  vitest@2.1.9:
+    resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+    hasBin: true
+    peerDependencies:
+      '@edge-runtime/vm': '*'
+      '@types/node': ^18.0.0 || >=20.0.0
+      '@vitest/browser': 2.1.9
+      '@vitest/ui': 2.1.9
+      happy-dom: '*'
+      jsdom: '*'
+    peerDependenciesMeta:
+      '@edge-runtime/vm':
+        optional: true
+      '@types/node':
+        optional: true
+      '@vitest/browser':
+        optional: true
+      '@vitest/ui':
+        optional: true
+      happy-dom:
+        optional: true
+      jsdom:
+        optional: true
+
+  which@2.0.2:
+    resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+    engines: {node: '>= 8'}
+    hasBin: true
+
+  why-is-node-running@2.3.0:
+    resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
+    engines: {node: '>=8'}
+    hasBin: true
+
+  word-wrap@1.2.5:
+    resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+    engines: {node: '>=0.10.0'}
+
+  wrappy@1.0.2:
+    resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+
+  ws@8.18.3:
+    resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
+    engines: {node: '>=10.0.0'}
+    peerDependencies:
+      bufferutil: ^4.0.1
+      utf-8-validate: '>=5.0.2'
+    peerDependenciesMeta:
+      bufferutil:
+        optional: true
+      utf-8-validate:
+        optional: true
+
+  yaml@2.8.1:
+    resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==}
+    engines: {node: '>= 14.6'}
+    hasBin: true
+
+  yocto-queue@0.1.0:
+    resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+    engines: {node: '>=10'}
+
+  zod@3.25.76:
+    resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
+
+snapshots:
+
+  '@esbuild/aix-ppc64@0.21.5':
+    optional: true
+
+  '@esbuild/aix-ppc64@0.25.10':
+    optional: true
+
+  '@esbuild/android-arm64@0.21.5':
+    optional: true
+
+  '@esbuild/android-arm64@0.25.10':
+    optional: true
+
+  '@esbuild/android-arm@0.21.5':
+    optional: true
+
+  '@esbuild/android-arm@0.25.10':
+    optional: true
+
+  '@esbuild/android-x64@0.21.5':
+    optional: true
+
+  '@esbuild/android-x64@0.25.10':
+    optional: true
+
+  '@esbuild/darwin-arm64@0.21.5':
+    optional: true
+
+  '@esbuild/darwin-arm64@0.25.10':
+    optional: true
+
+  '@esbuild/darwin-x64@0.21.5':
+    optional: true
+
+  '@esbuild/darwin-x64@0.25.10':
+    optional: true
+
+  '@esbuild/freebsd-arm64@0.21.5':
+    optional: true
+
+  '@esbuild/freebsd-arm64@0.25.10':
+    optional: true
+
+  '@esbuild/freebsd-x64@0.21.5':
+    optional: true
+
+  '@esbuild/freebsd-x64@0.25.10':
+    optional: true
+
+  '@esbuild/linux-arm64@0.21.5':
+    optional: true
+
+  '@esbuild/linux-arm64@0.25.10':
+    optional: true
+
+  '@esbuild/linux-arm@0.21.5':
+    optional: true
+
+  '@esbuild/linux-arm@0.25.10':
+    optional: true
+
+  '@esbuild/linux-ia32@0.21.5':
+    optional: true
+
+  '@esbuild/linux-ia32@0.25.10':
+    optional: true
+
+  '@esbuild/linux-loong64@0.21.5':
+    optional: true
+
+  '@esbuild/linux-loong64@0.25.10':
+    optional: true
+
+  '@esbuild/linux-mips64el@0.21.5':
+    optional: true
+
+  '@esbuild/linux-mips64el@0.25.10':
+    optional: true
+
+  '@esbuild/linux-ppc64@0.21.5':
+    optional: true
+
+  '@esbuild/linux-ppc64@0.25.10':
+    optional: true
+
+  '@esbuild/linux-riscv64@0.21.5':
+    optional: true
+
+  '@esbuild/linux-riscv64@0.25.10':
+    optional: true
+
+  '@esbuild/linux-s390x@0.21.5':
+    optional: true
+
+  '@esbuild/linux-s390x@0.25.10':
+    optional: true
+
+  '@esbuild/linux-x64@0.21.5':
+    optional: true
+
+  '@esbuild/linux-x64@0.25.10':
+    optional: true
+
+  '@esbuild/netbsd-arm64@0.25.10':
+    optional: true
+
+  '@esbuild/netbsd-x64@0.21.5':
+    optional: true
+
+  '@esbuild/netbsd-x64@0.25.10':
+    optional: true
+
+  '@esbuild/openbsd-arm64@0.25.10':
+    optional: true
+
+  '@esbuild/openbsd-x64@0.21.5':
+    optional: true
+
+  '@esbuild/openbsd-x64@0.25.10':
+    optional: true
+
+  '@esbuild/openharmony-arm64@0.25.10':
+    optional: true
+
+  '@esbuild/sunos-x64@0.21.5':
+    optional: true
+
+  '@esbuild/sunos-x64@0.25.10':
+    optional: true
+
+  '@esbuild/win32-arm64@0.21.5':
+    optional: true
+
+  '@esbuild/win32-arm64@0.25.10':
+    optional: true
+
+  '@esbuild/win32-ia32@0.21.5':
+    optional: true
+
+  '@esbuild/win32-ia32@0.25.10':
+    optional: true
+
+  '@esbuild/win32-x64@0.21.5':
+    optional: true
+
+  '@esbuild/win32-x64@0.25.10':
+    optional: true
+
+  '@eslint-community/eslint-utils@4.9.0(eslint@8.57.1)':
+    dependencies:
+      eslint: 8.57.1
+      eslint-visitor-keys: 3.4.3
+
+  '@eslint-community/regexpp@4.12.1': {}
+
+  '@eslint/eslintrc@2.1.4':
+    dependencies:
+      ajv: 6.12.6
+      debug: 4.4.3
+      espree: 9.6.1
+      globals: 13.24.0
+      ignore: 5.3.2
+      import-fresh: 3.3.1
+      js-yaml: 4.1.0
+      minimatch: 3.1.2
+      strip-json-comments: 3.1.1
+    transitivePeerDependencies:
+      - supports-color
+
+  '@eslint/js@8.57.1': {}
+
+  '@humanwhocodes/config-array@0.13.0':
+    dependencies:
+      '@humanwhocodes/object-schema': 2.0.3
+      debug: 4.4.3
+      minimatch: 3.1.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@humanwhocodes/module-importer@1.0.1': {}
+
+  '@humanwhocodes/object-schema@2.0.3': {}
+
+  '@jridgewell/sourcemap-codec@1.5.5': {}
+
+  '@nodelib/fs.scandir@2.1.5':
+    dependencies:
+      '@nodelib/fs.stat': 2.0.5
+      run-parallel: 1.2.0
+
+  '@nodelib/fs.stat@2.0.5': {}
+
+  '@nodelib/fs.walk@1.2.8':
+    dependencies:
+      '@nodelib/fs.scandir': 2.1.5
+      fastq: 1.19.1
+
+  '@opentelemetry/api@1.9.0': {}
+
+  '@rollup/rollup-android-arm-eabi@4.52.4':
+    optional: true
+
+  '@rollup/rollup-android-arm64@4.52.4':
+    optional: true
+
+  '@rollup/rollup-darwin-arm64@4.52.4':
+    optional: true
+
+  '@rollup/rollup-darwin-x64@4.52.4':
+    optional: true
+
+  '@rollup/rollup-freebsd-arm64@4.52.4':
+    optional: true
+
+  '@rollup/rollup-freebsd-x64@4.52.4':
+    optional: true
+
+  '@rollup/rollup-linux-arm-gnueabihf@4.52.4':
+    optional: true
+
+  '@rollup/rollup-linux-arm-musleabihf@4.52.4':
+    optional: true
+
+  '@rollup/rollup-linux-arm64-gnu@4.52.4':
+    optional: true
+
+  '@rollup/rollup-linux-arm64-musl@4.52.4':
+    optional: true
+
+  '@rollup/rollup-linux-loong64-gnu@4.52.4':
+    optional: true
+
+  '@rollup/rollup-linux-ppc64-gnu@4.52.4':
+    optional: true
+
+  '@rollup/rollup-linux-riscv64-gnu@4.52.4':
+    optional: true
+
+  '@rollup/rollup-linux-riscv64-musl@4.52.4':
+    optional: true
+
+  '@rollup/rollup-linux-s390x-gnu@4.52.4':
+    optional: true
+
+  '@rollup/rollup-linux-x64-gnu@4.52.4':
+    optional: true
+
+  '@rollup/rollup-linux-x64-musl@4.52.4':
+    optional: true
+
+  '@rollup/rollup-openharmony-arm64@4.52.4':
+    optional: true
+
+  '@rollup/rollup-win32-arm64-msvc@4.52.4':
+    optional: true
+
+  '@rollup/rollup-win32-ia32-msvc@4.52.4':
+    optional: true
+
+  '@rollup/rollup-win32-x64-gnu@4.52.4':
+    optional: true
+
+  '@rollup/rollup-win32-x64-msvc@4.52.4':
+    optional: true
+
+  '@types/estree@1.0.8': {}
+
+  '@types/node@20.19.19':
+    dependencies:
+      undici-types: 6.21.0
+
+  '@types/ws@8.18.1':
+    dependencies:
+      '@types/node': 20.19.19
+
+  '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)':
+    dependencies:
+      '@eslint-community/regexpp': 4.12.1
+      '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.9.3)
+      '@typescript-eslint/scope-manager': 7.18.0
+      '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.9.3)
+      '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.9.3)
+      '@typescript-eslint/visitor-keys': 7.18.0
+      eslint: 8.57.1
+      graphemer: 1.4.0
+      ignore: 5.3.2
+      natural-compare: 1.4.0
+      ts-api-utils: 1.4.3(typescript@5.9.3)
+    optionalDependencies:
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3)':
+    dependencies:
+      '@typescript-eslint/scope-manager': 7.18.0
+      '@typescript-eslint/types': 7.18.0
+      '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3)
+      '@typescript-eslint/visitor-keys': 7.18.0
+      debug: 4.4.3
+      eslint: 8.57.1
+    optionalDependencies:
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/scope-manager@7.18.0':
+    dependencies:
+      '@typescript-eslint/types': 7.18.0
+      '@typescript-eslint/visitor-keys': 7.18.0
+
+  '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.9.3)':
+    dependencies:
+      '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3)
+      '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.9.3)
+      debug: 4.4.3
+      eslint: 8.57.1
+      ts-api-utils: 1.4.3(typescript@5.9.3)
+    optionalDependencies:
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/types@7.18.0': {}
+
+  '@typescript-eslint/typescript-estree@7.18.0(typescript@5.9.3)':
+    dependencies:
+      '@typescript-eslint/types': 7.18.0
+      '@typescript-eslint/visitor-keys': 7.18.0
+      debug: 4.4.3
+      globby: 11.1.0
+      is-glob: 4.0.3
+      minimatch: 9.0.5
+      semver: 7.7.2
+      ts-api-utils: 1.4.3(typescript@5.9.3)
+    optionalDependencies:
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.9.3)':
+    dependencies:
+      '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1)
+      '@typescript-eslint/scope-manager': 7.18.0
+      '@typescript-eslint/types': 7.18.0
+      '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3)
+      eslint: 8.57.1
+    transitivePeerDependencies:
+      - supports-color
+      - typescript
+
+  '@typescript-eslint/visitor-keys@7.18.0':
+    dependencies:
+      '@typescript-eslint/types': 7.18.0
+      eslint-visitor-keys: 3.4.3
+
+  '@ungap/structured-clone@1.3.0': {}
+
+  '@vitest/expect@2.1.9':
+    dependencies:
+      '@vitest/spy': 2.1.9
+      '@vitest/utils': 2.1.9
+      chai: 5.3.3
+      tinyrainbow: 1.2.0
+
+  '@vitest/mocker@2.1.9(vite@5.4.20(@types/node@20.19.19))':
+    dependencies:
+      '@vitest/spy': 2.1.9
+      estree-walker: 3.0.3
+      magic-string: 0.30.19
+    optionalDependencies:
+      vite: 5.4.20(@types/node@20.19.19)
+
+  '@vitest/pretty-format@2.1.9':
+    dependencies:
+      tinyrainbow: 1.2.0
+
+  '@vitest/runner@2.1.9':
+    dependencies:
+      '@vitest/utils': 2.1.9
+      pathe: 1.1.2
+
+  '@vitest/snapshot@2.1.9':
+    dependencies:
+      '@vitest/pretty-format': 2.1.9
+      magic-string: 0.30.19
+      pathe: 1.1.2
+
+  '@vitest/spy@2.1.9':
+    dependencies:
+      tinyspy: 3.0.2
+
+  '@vitest/utils@2.1.9':
+    dependencies:
+      '@vitest/pretty-format': 2.1.9
+      loupe: 3.2.1
+      tinyrainbow: 1.2.0
+
+  abort-controller@3.0.0:
+    dependencies:
+      event-target-shim: 5.0.1
+
+  acorn-jsx@5.3.2(acorn@8.15.0):
+    dependencies:
+      acorn: 8.15.0
+
+  acorn@8.15.0: {}
+
+  ajv@6.12.6:
+    dependencies:
+      fast-deep-equal: 3.1.3
+      fast-json-stable-stringify: 2.1.0
+      json-schema-traverse: 0.4.1
+      uri-js: 4.4.1
+
+  ansi-regex@5.0.1: {}
+
+  ansi-styles@4.3.0:
+    dependencies:
+      color-convert: 2.0.1
+
+  argparse@2.0.1: {}
+
+  array-union@2.1.0: {}
+
+  assertion-error@2.0.1: {}
+
+  atomic-sleep@1.0.0: {}
+
+  balanced-match@1.0.2: {}
+
+  base-x@5.0.1: {}
+
+  base64-js@1.5.1: {}
+
+  bintrees@1.0.2: {}
+
+  brace-expansion@1.1.12:
+    dependencies:
+      balanced-match: 1.0.2
+      concat-map: 0.0.1
+
+  brace-expansion@2.0.2:
+    dependencies:
+      balanced-match: 1.0.2
+
+  braces@3.0.3:
+    dependencies:
+      fill-range: 7.1.1
+
+  bs58@6.0.0:
+    dependencies:
+      base-x: 5.0.1
+
+  buffer@6.0.3:
+    dependencies:
+      base64-js: 1.5.1
+      ieee754: 1.2.1
+
+  cac@6.7.14: {}
+
+  callsites@3.1.0: {}
+
+  chai@5.3.3:
+    dependencies:
+      assertion-error: 2.0.1
+      check-error: 2.1.1
+      deep-eql: 5.0.2
+      loupe: 3.2.1
+      pathval: 2.0.1
+
+  chalk@4.1.2:
+    dependencies:
+      ansi-styles: 4.3.0
+      supports-color: 7.2.0
+
+  check-error@2.1.1: {}
+
+  color-convert@2.0.1:
+    dependencies:
+      color-name: 1.1.4
+
+  color-name@1.1.4: {}
+
+  colorette@2.0.20: {}
+
+  concat-map@0.0.1: {}
+
+  cross-spawn@7.0.6:
+    dependencies:
+      path-key: 3.1.1
+      shebang-command: 2.0.0
+      which: 2.0.2
+
+  dateformat@4.6.3: {}
+
+  debug@4.4.3:
+    dependencies:
+      ms: 2.1.3
+
+  decimal.js@10.6.0: {}
+
+  deep-eql@5.0.2: {}
+
+  deep-is@0.1.4: {}
+
+  dir-glob@3.0.1:
+    dependencies:
+      path-type: 4.0.0
+
+  doctrine@3.0.0:
+    dependencies:
+      esutils: 2.0.3
+
+  dotenv@16.6.1: {}
+
+  end-of-stream@1.4.5:
+    dependencies:
+      once: 1.4.0
+
+  es-module-lexer@1.7.0: {}
+
+  esbuild@0.21.5:
+    optionalDependencies:
+      '@esbuild/aix-ppc64': 0.21.5
+      '@esbuild/android-arm': 0.21.5
+      '@esbuild/android-arm64': 0.21.5
+      '@esbuild/android-x64': 0.21.5
+      '@esbuild/darwin-arm64': 0.21.5
+      '@esbuild/darwin-x64': 0.21.5
+      '@esbuild/freebsd-arm64': 0.21.5
+      '@esbuild/freebsd-x64': 0.21.5
+      '@esbuild/linux-arm': 0.21.5
+      '@esbuild/linux-arm64': 0.21.5
+      '@esbuild/linux-ia32': 0.21.5
+      '@esbuild/linux-loong64': 0.21.5
+      '@esbuild/linux-mips64el': 0.21.5
+      '@esbuild/linux-ppc64': 0.21.5
+      '@esbuild/linux-riscv64': 0.21.5
+      '@esbuild/linux-s390x': 0.21.5
+      '@esbuild/linux-x64': 0.21.5
+      '@esbuild/netbsd-x64': 0.21.5
+      '@esbuild/openbsd-x64': 0.21.5
+      '@esbuild/sunos-x64': 0.21.5
+      '@esbuild/win32-arm64': 0.21.5
+      '@esbuild/win32-ia32': 0.21.5
+      '@esbuild/win32-x64': 0.21.5
+
+  esbuild@0.25.10:
+    optionalDependencies:
+      '@esbuild/aix-ppc64': 0.25.10
+      '@esbuild/android-arm': 0.25.10
+      '@esbuild/android-arm64': 0.25.10
+      '@esbuild/android-x64': 0.25.10
+      '@esbuild/darwin-arm64': 0.25.10
+      '@esbuild/darwin-x64': 0.25.10
+      '@esbuild/freebsd-arm64': 0.25.10
+      '@esbuild/freebsd-x64': 0.25.10
+      '@esbuild/linux-arm': 0.25.10
+      '@esbuild/linux-arm64': 0.25.10
+      '@esbuild/linux-ia32': 0.25.10
+      '@esbuild/linux-loong64': 0.25.10
+      '@esbuild/linux-mips64el': 0.25.10
+      '@esbuild/linux-ppc64': 0.25.10
+      '@esbuild/linux-riscv64': 0.25.10
+      '@esbuild/linux-s390x': 0.25.10
+      '@esbuild/linux-x64': 0.25.10
+      '@esbuild/netbsd-arm64': 0.25.10
+      '@esbuild/netbsd-x64': 0.25.10
+      '@esbuild/openbsd-arm64': 0.25.10
+      '@esbuild/openbsd-x64': 0.25.10
+      '@esbuild/openharmony-arm64': 0.25.10
+      '@esbuild/sunos-x64': 0.25.10
+      '@esbuild/win32-arm64': 0.25.10
+      '@esbuild/win32-ia32': 0.25.10
+      '@esbuild/win32-x64': 0.25.10
+
+  escape-string-regexp@4.0.0: {}
+
+  eslint-scope@7.2.2:
+    dependencies:
+      esrecurse: 4.3.0
+      estraverse: 5.3.0
+
+  eslint-visitor-keys@3.4.3: {}
+
+  eslint@8.57.1:
+    dependencies:
+      '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1)
+      '@eslint-community/regexpp': 4.12.1
+      '@eslint/eslintrc': 2.1.4
+      '@eslint/js': 8.57.1
+      '@humanwhocodes/config-array': 0.13.0
+      '@humanwhocodes/module-importer': 1.0.1
+      '@nodelib/fs.walk': 1.2.8
+      '@ungap/structured-clone': 1.3.0
+      ajv: 6.12.6
+      chalk: 4.1.2
+      cross-spawn: 7.0.6
+      debug: 4.4.3
+      doctrine: 3.0.0
+      escape-string-regexp: 4.0.0
+      eslint-scope: 7.2.2
+      eslint-visitor-keys: 3.4.3
+      espree: 9.6.1
+      esquery: 1.6.0
+      esutils: 2.0.3
+      fast-deep-equal: 3.1.3
+      file-entry-cache: 6.0.1
+      find-up: 5.0.0
+      glob-parent: 6.0.2
+      globals: 13.24.0
+      graphemer: 1.4.0
+      ignore: 5.3.2
+      imurmurhash: 0.1.4
+      is-glob: 4.0.3
+      is-path-inside: 3.0.3
+      js-yaml: 4.1.0
+      json-stable-stringify-without-jsonify: 1.0.1
+      levn: 0.4.1
+      lodash.merge: 4.6.2
+      minimatch: 3.1.2
+      natural-compare: 1.4.0
+      optionator: 0.9.4
+      strip-ansi: 6.0.1
+      text-table: 0.2.0
+    transitivePeerDependencies:
+      - supports-color
+
+  espree@9.6.1:
+    dependencies:
+      acorn: 8.15.0
+      acorn-jsx: 5.3.2(acorn@8.15.0)
+      eslint-visitor-keys: 3.4.3
+
+  esquery@1.6.0:
+    dependencies:
+      estraverse: 5.3.0
+
+  esrecurse@4.3.0:
+    dependencies:
+      estraverse: 5.3.0
+
+  estraverse@5.3.0: {}
+
+  estree-walker@3.0.3:
+    dependencies:
+      '@types/estree': 1.0.8
+
+  esutils@2.0.3: {}
+
+  event-target-shim@5.0.1: {}
+
+  events@3.3.0: {}
+
+  expect-type@1.2.2: {}
+
+  fast-copy@3.0.2: {}
+
+  fast-deep-equal@3.1.3: {}
+
+  fast-glob@3.3.3:
+    dependencies:
+      '@nodelib/fs.stat': 2.0.5
+      '@nodelib/fs.walk': 1.2.8
+      glob-parent: 5.1.2
+      merge2: 1.4.1
+      micromatch: 4.0.8
+
+  fast-json-stable-stringify@2.1.0: {}
+
+  fast-levenshtein@2.0.6: {}
+
+  fast-safe-stringify@2.1.1: {}
+
+  fastq@1.19.1:
+    dependencies:
+      reusify: 1.1.0
+
+  file-entry-cache@6.0.1:
+    dependencies:
+      flat-cache: 3.2.0
+
+  fill-range@7.1.1:
+    dependencies:
+      to-regex-range: 5.0.1
+
+  find-up@5.0.0:
+    dependencies:
+      locate-path: 6.0.0
+      path-exists: 4.0.0
+
+  flat-cache@3.2.0:
+    dependencies:
+      flatted: 3.3.3
+      keyv: 4.5.4
+      rimraf: 3.0.2
+
+  flatted@3.3.3: {}
+
+  fs.realpath@1.0.0: {}
+
+  fsevents@2.3.3:
+    optional: true
+
+  get-tsconfig@4.11.0:
+    dependencies:
+      resolve-pkg-maps: 1.0.0
+
+  glob-parent@5.1.2:
+    dependencies:
+      is-glob: 4.0.3
+
+  glob-parent@6.0.2:
+    dependencies:
+      is-glob: 4.0.3
+
+  glob@7.2.3:
+    dependencies:
+      fs.realpath: 1.0.0
+      inflight: 1.0.6
+      inherits: 2.0.4
+      minimatch: 3.1.2
+      once: 1.4.0
+      path-is-absolute: 1.0.1
+
+  globals@13.24.0:
+    dependencies:
+      type-fest: 0.20.2
+
+  globby@11.1.0:
+    dependencies:
+      array-union: 2.1.0
+      dir-glob: 3.0.1
+      fast-glob: 3.3.3
+      ignore: 5.3.2
+      merge2: 1.4.1
+      slash: 3.0.0
+
+  graphemer@1.4.0: {}
+
+  has-flag@4.0.0: {}
+
+  help-me@5.0.0: {}
+
+  ieee754@1.2.1: {}
+
+  ignore@5.3.2: {}
+
+  import-fresh@3.3.1:
+    dependencies:
+      parent-module: 1.0.1
+      resolve-from: 4.0.0
+
+  imurmurhash@0.1.4: {}
+
+  inflight@1.0.6:
+    dependencies:
+      once: 1.4.0
+      wrappy: 1.0.2
+
+  inherits@2.0.4: {}
+
+  is-extglob@2.1.1: {}
+
+  is-glob@4.0.3:
+    dependencies:
+      is-extglob: 2.1.1
+
+  is-number@7.0.0: {}
+
+  is-path-inside@3.0.3: {}
+
+  isexe@2.0.0: {}
+
+  joycon@3.1.1: {}
+
+  js-yaml@4.1.0:
+    dependencies:
+      argparse: 2.0.1
+
+  json-buffer@3.0.1: {}
+
+  json-schema-traverse@0.4.1: {}
+
+  json-stable-stringify-without-jsonify@1.0.1: {}
+
+  keyv@4.5.4:
+    dependencies:
+      json-buffer: 3.0.1
+
+  levn@0.4.1:
+    dependencies:
+      prelude-ls: 1.2.1
+      type-check: 0.4.0
+
+  locate-path@6.0.0:
+    dependencies:
+      p-locate: 5.0.0
+
+  lodash.merge@4.6.2: {}
+
+  loupe@3.2.1: {}
+
+  magic-string@0.30.19:
+    dependencies:
+      '@jridgewell/sourcemap-codec': 1.5.5
+
+  merge2@1.4.1: {}
+
+  micromatch@4.0.8:
+    dependencies:
+      braces: 3.0.3
+      picomatch: 2.3.1
+
+  minimatch@3.1.2:
+    dependencies:
+      brace-expansion: 1.1.12
+
+  minimatch@9.0.5:
+    dependencies:
+      brace-expansion: 2.0.2
+
+  minimist@1.2.8: {}
+
+  ms@2.1.3: {}
+
+  nanoid@3.3.11: {}
+
+  natural-compare@1.4.0: {}
+
+  on-exit-leak-free@2.1.2: {}
+
+  once@1.4.0:
+    dependencies:
+      wrappy: 1.0.2
+
+  optionator@0.9.4:
+    dependencies:
+      deep-is: 0.1.4
+      fast-levenshtein: 2.0.6
+      levn: 0.4.1
+      prelude-ls: 1.2.1
+      type-check: 0.4.0
+      word-wrap: 1.2.5
+
+  p-limit@3.1.0:
+    dependencies:
+      yocto-queue: 0.1.0
+
+  p-locate@5.0.0:
+    dependencies:
+      p-limit: 3.1.0
+
+  parent-module@1.0.1:
+    dependencies:
+      callsites: 3.1.0
+
+  path-exists@4.0.0: {}
+
+  path-is-absolute@1.0.1: {}
+
+  path-key@3.1.1: {}
+
+  path-type@4.0.0: {}
+
+  pathe@1.1.2: {}
+
+  pathval@2.0.1: {}
+
+  picocolors@1.1.1: {}
+
+  picomatch@2.3.1: {}
+
+  pino-abstract-transport@2.0.0:
+    dependencies:
+      split2: 4.2.0
+
+  pino-pretty@11.3.0:
+    dependencies:
+      colorette: 2.0.20
+      dateformat: 4.6.3
+      fast-copy: 3.0.2
+      fast-safe-stringify: 2.1.1
+      help-me: 5.0.0
+      joycon: 3.1.1
+      minimist: 1.2.8
+      on-exit-leak-free: 2.1.2
+      pino-abstract-transport: 2.0.0
+      pump: 3.0.3
+      readable-stream: 4.7.0
+      secure-json-parse: 2.7.0
+      sonic-boom: 4.2.0
+      strip-json-comments: 3.1.1
+
+  pino-std-serializers@7.0.0: {}
+
+  pino@9.13.1:
+    dependencies:
+      atomic-sleep: 1.0.0
+      on-exit-leak-free: 2.1.2
+      pino-abstract-transport: 2.0.0
+      pino-std-serializers: 7.0.0
+      process-warning: 5.0.0
+      quick-format-unescaped: 4.0.4
+      real-require: 0.2.0
+      safe-stable-stringify: 2.5.0
+      slow-redact: 0.3.1
+      sonic-boom: 4.2.0
+      thread-stream: 3.1.0
+
+  postcss@8.5.6:
+    dependencies:
+      nanoid: 3.3.11
+      picocolors: 1.1.1
+      source-map-js: 1.2.1
+
+  prelude-ls@1.2.1: {}
+
+  process-warning@5.0.0: {}
+
+  process@0.11.10: {}
+
+  prom-client@15.1.3:
+    dependencies:
+      '@opentelemetry/api': 1.9.0
+      tdigest: 0.1.2
+
+  pump@3.0.3:
+    dependencies:
+      end-of-stream: 1.4.5
+      once: 1.4.0
+
+  punycode@2.3.1: {}
+
+  queue-microtask@1.2.3: {}
+
+  quick-format-unescaped@4.0.4: {}
+
+  readable-stream@4.7.0:
+    dependencies:
+      abort-controller: 3.0.0
+      buffer: 6.0.3
+      events: 3.3.0
+      process: 0.11.10
+      string_decoder: 1.3.0
+
+  real-require@0.2.0: {}
+
+  resolve-from@4.0.0: {}
+
+  resolve-pkg-maps@1.0.0: {}
+
+  reusify@1.1.0: {}
+
+  rimraf@3.0.2:
+    dependencies:
+      glob: 7.2.3
+
+  rollup@4.52.4:
+    dependencies:
+      '@types/estree': 1.0.8
+    optionalDependencies:
+      '@rollup/rollup-android-arm-eabi': 4.52.4
+      '@rollup/rollup-android-arm64': 4.52.4
+      '@rollup/rollup-darwin-arm64': 4.52.4
+      '@rollup/rollup-darwin-x64': 4.52.4
+      '@rollup/rollup-freebsd-arm64': 4.52.4
+      '@rollup/rollup-freebsd-x64': 4.52.4
+      '@rollup/rollup-linux-arm-gnueabihf': 4.52.4
+      '@rollup/rollup-linux-arm-musleabihf': 4.52.4
+      '@rollup/rollup-linux-arm64-gnu': 4.52.4
+      '@rollup/rollup-linux-arm64-musl': 4.52.4
+      '@rollup/rollup-linux-loong64-gnu': 4.52.4
+      '@rollup/rollup-linux-ppc64-gnu': 4.52.4
+      '@rollup/rollup-linux-riscv64-gnu': 4.52.4
+      '@rollup/rollup-linux-riscv64-musl': 4.52.4
+      '@rollup/rollup-linux-s390x-gnu': 4.52.4
+      '@rollup/rollup-linux-x64-gnu': 4.52.4
+      '@rollup/rollup-linux-x64-musl': 4.52.4
+      '@rollup/rollup-openharmony-arm64': 4.52.4
+      '@rollup/rollup-win32-arm64-msvc': 4.52.4
+      '@rollup/rollup-win32-ia32-msvc': 4.52.4
+      '@rollup/rollup-win32-x64-gnu': 4.52.4
+      '@rollup/rollup-win32-x64-msvc': 4.52.4
+      fsevents: 2.3.3
+
+  run-parallel@1.2.0:
+    dependencies:
+      queue-microtask: 1.2.3
+
+  safe-buffer@5.2.1: {}
+
+  safe-stable-stringify@2.5.0: {}
+
+  secure-json-parse@2.7.0: {}
+
+  semver@7.7.2: {}
+
+  shebang-command@2.0.0:
+    dependencies:
+      shebang-regex: 3.0.0
+
+  shebang-regex@3.0.0: {}
+
+  siginfo@2.0.0: {}
+
+  slash@3.0.0: {}
+
+  slow-redact@0.3.1: {}
+
+  sonic-boom@4.2.0:
+    dependencies:
+      atomic-sleep: 1.0.0
+
+  source-map-js@1.2.1: {}
+
+  split2@4.2.0: {}
+
+  stackback@0.0.2: {}
+
+  std-env@3.9.0: {}
+
+  string_decoder@1.3.0:
+    dependencies:
+      safe-buffer: 5.2.1
+
+  strip-ansi@6.0.1:
+    dependencies:
+      ansi-regex: 5.0.1
+
+  strip-json-comments@3.1.1: {}
+
+  supports-color@7.2.0:
+    dependencies:
+      has-flag: 4.0.0
+
+  tdigest@0.1.2:
+    dependencies:
+      bintrees: 1.0.2
+
+  text-table@0.2.0: {}
+
+  thread-stream@3.1.0:
+    dependencies:
+      real-require: 0.2.0
+
+  tinybench@2.9.0: {}
+
+  tinyexec@0.3.2: {}
+
+  tinypool@1.1.1: {}
+
+  tinyrainbow@1.2.0: {}
+
+  tinyspy@3.0.2: {}
+
+  to-regex-range@5.0.1:
+    dependencies:
+      is-number: 7.0.0
+
+  ts-api-utils@1.4.3(typescript@5.9.3):
+    dependencies:
+      typescript: 5.9.3
+
+  tsx@4.20.6:
+    dependencies:
+      esbuild: 0.25.10
+      get-tsconfig: 4.11.0
+    optionalDependencies:
+      fsevents: 2.3.3
+
+  tweetnacl@1.0.3: {}
+
+  type-check@0.4.0:
+    dependencies:
+      prelude-ls: 1.2.1
+
+  type-fest@0.20.2: {}
+
+  typescript@5.9.3: {}
+
+  undici-types@6.21.0: {}
+
+  undici@6.22.0: {}
+
+  uri-js@4.4.1:
+    dependencies:
+      punycode: 2.3.1
+
+  vite-node@2.1.9(@types/node@20.19.19):
+    dependencies:
+      cac: 6.7.14
+      debug: 4.4.3
+      es-module-lexer: 1.7.0
+      pathe: 1.1.2
+      vite: 5.4.20(@types/node@20.19.19)
+    transitivePeerDependencies:
+      - '@types/node'
+      - less
+      - lightningcss
+      - sass
+      - sass-embedded
+      - stylus
+      - sugarss
+      - supports-color
+      - terser
+
+  vite@5.4.20(@types/node@20.19.19):
+    dependencies:
+      esbuild: 0.21.5
+      postcss: 8.5.6
+      rollup: 4.52.4
+    optionalDependencies:
+      '@types/node': 20.19.19
+      fsevents: 2.3.3
+
+  vitest@2.1.9(@types/node@20.19.19):
+    dependencies:
+      '@vitest/expect': 2.1.9
+      '@vitest/mocker': 2.1.9(vite@5.4.20(@types/node@20.19.19))
+      '@vitest/pretty-format': 2.1.9
+      '@vitest/runner': 2.1.9
+      '@vitest/snapshot': 2.1.9
+      '@vitest/spy': 2.1.9
+      '@vitest/utils': 2.1.9
+      chai: 5.3.3
+      debug: 4.4.3
+      expect-type: 1.2.2
+      magic-string: 0.30.19
+      pathe: 1.1.2
+      std-env: 3.9.0
+      tinybench: 2.9.0
+      tinyexec: 0.3.2
+      tinypool: 1.1.1
+      tinyrainbow: 1.2.0
+      vite: 5.4.20(@types/node@20.19.19)
+      vite-node: 2.1.9(@types/node@20.19.19)
+      why-is-node-running: 2.3.0
+    optionalDependencies:
+      '@types/node': 20.19.19
+    transitivePeerDependencies:
+      - less
+      - lightningcss
+      - msw
+      - sass
+      - sass-embedded
+      - stylus
+      - sugarss
+      - supports-color
+      - terser
+
+  which@2.0.2:
+    dependencies:
+      isexe: 2.0.0
+
+  why-is-node-running@2.3.0:
+    dependencies:
+      siginfo: 2.0.0
+      stackback: 0.0.2
+
+  word-wrap@1.2.5: {}
+
+  wrappy@1.0.2: {}
+
+  ws@8.18.3: {}
+
+  yaml@2.8.1: {}
+
+  yocto-queue@0.1.0: {}
+
+  zod@3.25.76: {}

+ 3 - 0
pnpm-workspace.yaml

@@ -0,0 +1,3 @@
+packages:
+  - apps/*
+  - packages/*

+ 83 - 0
tests/adapterRegistry.test.ts

@@ -0,0 +1,83 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+
+import { AdapterRegistry } from "../packages/connectors/pacifica/src/adapterRegistry";
+import type { PositionSnapshot } from "../packages/domain/src/types";
+
+type PositionMap = Record<string, PositionSnapshot>;
+
+function createMockAdapter(map: PositionMap) {
+  let currentAccountId: string | undefined;
+  return {
+    setAccountId: vi.fn((id: string) => {
+      currentAccountId = id;
+    }),
+    getPosition: vi.fn(async (symbol: string) => {
+      const snapshot = map[symbol];
+      if (snapshot) {
+        return { ...snapshot, accountId: currentAccountId };
+      }
+      return {
+        symbol,
+        base: 0,
+        quote: 0,
+        ts: Date.now(),
+        accountId: currentAccountId
+      };
+    })
+  } as any;
+}
+
+describe("AdapterRegistry", () => {
+  const now = 1_700_000_000_000;
+
+  beforeEach(() => {
+    vi.useFakeTimers();
+    vi.setSystemTime(now);
+  });
+
+  afterEach(() => {
+    vi.useRealTimers();
+  });
+
+  it("attaches adapters and resolves them by id and role", () => {
+    const registry = new AdapterRegistry();
+    const makerAdapter = createMockAdapter({});
+    const hedgerAdapter = createMockAdapter({});
+
+    registry.attach("maker", makerAdapter, "maker");
+    registry.attach("hedger", hedgerAdapter, "hedger");
+
+    expect(registry.get("maker")).toBe(makerAdapter);
+
+    const hedgerEntry = registry.findEntryByRole("hedger");
+    expect(hedgerEntry).toBeDefined();
+    expect(hedgerEntry?.id).toBe("hedger");
+    expect(hedgerEntry?.adapter).toBe(hedgerAdapter);
+    expect(makerAdapter.setAccountId).toHaveBeenCalledWith("maker");
+    expect(hedgerAdapter.setAccountId).toHaveBeenCalledWith("hedger");
+  });
+
+  it("collects positions with account attribution", async () => {
+    const registry = new AdapterRegistry();
+
+    const makerAdapter = createMockAdapter({
+      BTC: { symbol: "BTC", base: 0.25, quote: 12_500, ts: now }
+    });
+    const hedgerAdapter = createMockAdapter({
+      BTC: { symbol: "BTC", base: -0.2, quote: -10_000, ts: now }
+    });
+
+    registry.attach("maker", makerAdapter, "maker");
+    registry.attach("hedger", hedgerAdapter, "hedger");
+
+    const snapshots = await registry.collectPositions("BTC");
+
+    expect(snapshots).toEqual([
+      { symbol: "BTC", base: 0.25, quote: 12_500, ts: now, accountId: "maker" },
+      { symbol: "BTC", base: -0.2, quote: -10_000, ts: now, accountId: "hedger" }
+    ]);
+
+    expect(makerAdapter.getPosition).toHaveBeenCalledWith("BTC");
+    expect(hedgerAdapter.getPosition).toHaveBeenCalledWith("BTC");
+  });
+});

+ 97 - 0
tests/globalOrderCoordinator.test.ts

@@ -0,0 +1,97 @@
+import { describe, it, expect, vi } from "vitest";
+
+import {
+  GlobalOrderCoordinator,
+  type GlobalOrderSnapshot,
+  type ValidationIntent
+} from "../packages/execution/src/globalOrderCoordinator";
+
+function makeSnapshot(partial: Partial<GlobalOrderSnapshot>): GlobalOrderSnapshot {
+  return {
+    orderId: partial.orderId ?? "order-1",
+    clientOrderId: partial.clientOrderId,
+    accountId: partial.accountId ?? "maker",
+    symbol: partial.symbol ?? "BTC",
+    side: partial.side ?? "buy",
+    price: partial.price ?? 50_000,
+    timestamp: partial.timestamp ?? Date.now()
+  };
+}
+
+describe("GlobalOrderCoordinator", () => {
+  it("registers and releases orders", () => {
+    const coordinator = new GlobalOrderCoordinator();
+    const snapshot = makeSnapshot({ clientOrderId: "client-1" });
+
+    coordinator.register(snapshot);
+    expect(coordinator.list()).toHaveLength(1);
+    expect(coordinator.peek(snapshot.orderId)).toEqual(snapshot);
+    expect(coordinator.peekByClientId("client-1")).toEqual(snapshot);
+
+    coordinator.release(snapshot.orderId);
+    expect(coordinator.list()).toHaveLength(0);
+    expect(coordinator.peek(snapshot.orderId)).toBeUndefined();
+    expect(coordinator.peekByClientId("client-1")).toBeUndefined();
+  });
+
+  it("detects cross-account conflicts", () => {
+    const coordinator = new GlobalOrderCoordinator();
+    coordinator.register(makeSnapshot({ orderId: "sell-1", side: "sell", price: 50_100 }));
+
+    const onConflict = vi.fn();
+    coordinator.on("conflict", onConflict);
+
+    const intent: ValidationIntent = {
+      accountId: "hedger",
+      symbol: "BTC",
+      side: "buy",
+      price: 50_200
+    };
+
+    expect(() => coordinator.validate(intent)).toThrow(/STP violation/);
+    expect(onConflict).toHaveBeenCalledTimes(1);
+    const payload = onConflict.mock.calls[0][0];
+    expect(payload.intent).toEqual(intent);
+    expect(payload.conflicts).toHaveLength(1);
+    expect(payload.conflicts[0].orderId).toBe("sell-1");
+  });
+
+  it("ignores orders from the same account", () => {
+    const coordinator = new GlobalOrderCoordinator();
+    coordinator.register(makeSnapshot({ orderId: "sell-1", accountId: "maker", side: "sell" }));
+
+    expect(() =>
+      coordinator.validate({
+        accountId: "maker",
+        symbol: "BTC",
+        side: "buy",
+        price: 49_900
+      })
+    ).not.toThrow();
+  });
+
+  it("respects tolerance when evaluating potential crosses", () => {
+    const coordinator = new GlobalOrderCoordinator({ stpToleranceBps: 5 });
+    coordinator.register(makeSnapshot({ orderId: "ask-1", side: "sell", price: 50_000 }));
+
+    // Within tolerance -> treated as conflict.
+    expect(() =>
+      coordinator.validate({
+        accountId: "hedger",
+        symbol: "BTC",
+        side: "buy",
+        price: 50_020
+      })
+    ).toThrow();
+
+    // Far away (non-cross) -> no conflict.
+    expect(() =>
+      coordinator.validate({
+        accountId: "hedger",
+        symbol: "BTC",
+        side: "buy",
+        price: 49_000
+      })
+    ).not.toThrow();
+  });
+});

+ 44 - 0
tests/marketDataAdapter.test.ts

@@ -0,0 +1,44 @@
+import { describe, expect, it, vi } from 'vitest';
+
+import { MarketDataAdapter } from '../packages/utils/src/marketDataAdapter';
+import { ShadowBook } from '../packages/utils/src/shadowBook';
+import type { OrderBook } from '../packages/domain/src/types';
+
+describe('MarketDataAdapter', () => {
+  it('updates shadow book and emits events for snapshot/delta', async () => {
+    const shadow = new ShadowBook();
+    const snapshot: OrderBook = {
+      bids: [{ px: 100, sz: 1 }],
+      asks: [{ px: 101, sz: 1 }],
+      ts: Date.now()
+    };
+
+    const fetchSnapshot = vi.fn(async () => snapshot);
+    const adapter = new MarketDataAdapter({
+      symbols: ['BTC'],
+      shadowBook: shadow,
+      fetchSnapshot,
+      pollIntervalMs: 10
+    });
+
+    const events: string[] = [];
+    adapter.on('snapshot', payload => {
+      events.push(`snapshot:${payload.symbol}`);
+    });
+
+    await adapter.start();
+    expect(fetchSnapshot).toHaveBeenCalled();
+    const stored = shadow.snapshot('BTC');
+    expect(stored?.bids[0]?.px).toBe(100);
+
+    adapter.ingestDelta('BTC', {
+      bids: [{ price: 100.5, size: 2 }],
+      seq: 1
+    });
+    const updated = shadow.snapshot('BTC');
+    expect(updated?.bids[0]?.px).toBe(100.5);
+
+    adapter.stop();
+    expect(events).toContain('snapshot:BTC');
+  });
+});

+ 95 - 0
tests/orderRouter.test.ts

@@ -0,0 +1,95 @@
+import { describe, expect, it, vi } from 'vitest';
+
+import { OrderRouter } from '../packages/execution/src/orderRouter';
+import type { Order, OrderBook } from '../packages/domain/src/types';
+
+const sampleBook: OrderBook = {
+  bids: [{ px: 100, sz: 1 }],
+  asks: [{ px: 100.5, sz: 1 }],
+  ts: Date.now()
+};
+
+describe('OrderRouter', () => {
+  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
+
+  it('sends limit order within slip guard', async () => {
+    const send = vi.fn(async (order: Order) => ({ id: order.clientId }));
+    const router = new OrderRouter(send, () => sampleBook, { maxBps: 100 });
+
+    const order: Order = {
+      clientId: 'o-1',
+      symbol: 'BTC',
+      side: 'buy',
+      px: 100.55,
+      sz: 0.1,
+      tif: 'GTC'
+    };
+
+    const id = await router.sendLimit(order);
+    expect(id).toMatch(uuidRegex);
+    expect(send).toHaveBeenCalledWith(expect.objectContaining({ clientId: id }));
+    expect(send).toHaveBeenCalledTimes(1);
+  });
+
+  it('rejects orders exceeding slip guard', async () => {
+    const send = vi.fn(async (order: Order) => ({ id: order.clientId }));
+    const router = new OrderRouter(send, () => sampleBook, { maxBps: 10 });
+
+    const order: Order = {
+      clientId: 'o-2',
+      symbol: 'BTC',
+      side: 'buy',
+      px: 102,
+      sz: 0.1,
+      tif: 'GTC'
+    };
+
+    await expect(router.sendLimit(order)).rejects.toThrow(/slippage/);
+    expect(send).not.toHaveBeenCalled();
+  });
+
+  it('rejects post-only orders that would cross', async () => {
+    const send = vi.fn(async (order: Order) => ({ id: order.clientId }));
+    const router = new OrderRouter(send, () => sampleBook, { maxBps: 100 });
+
+    const order: Order = {
+      clientId: 'o-3',
+      symbol: 'BTC',
+      side: 'buy',
+      px: 100.5,
+      sz: 0.1,
+      tif: 'GTC',
+      postOnly: true
+    };
+
+    await expect(router.sendLimit(order)).rejects.toThrow(/post-only/);
+    expect(send).not.toHaveBeenCalled();
+  });
+
+  it('throttles orders based on min interval and enforces unique client ids', async () => {
+    const send = vi.fn(async (order: Order) => ({ id: order.clientId }));
+    const router = new OrderRouter(send, () => sampleBook, { maxBps: 100, minIntervalMs: 10 });
+
+    const orderA: Order = {
+      clientId: 'o-4',
+      symbol: 'BTC',
+      side: 'buy',
+      px: 100.4,
+      sz: 0.1,
+      tif: 'GTC'
+    };
+
+    const orderB: Order = { ...orderA, clientId: 'o-5', px: 100.45 };
+
+    await router.sendLimit(orderA);
+    expect(send).toHaveBeenCalledTimes(1);
+
+    const start = Date.now();
+    await router.sendLimit(orderB);
+    const elapsed = Date.now() - start;
+    expect(send).toHaveBeenCalledTimes(2);
+    expect(elapsed).toBeGreaterThanOrEqual(8);
+
+    await expect(router.sendLimit(orderB)).rejects.toThrow(/duplicate clientId/);
+  });
+});

+ 131 - 0
tests/pacificaWsClient.test.ts

@@ -0,0 +1,131 @@
+import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
+import { WebSocketServer } from 'ws';
+import { PacificaWebSocket } from '../packages/connectors/pacifica/src/wsClient';
+import nacl from 'tweetnacl';
+import bs58 from 'bs58';
+import { signRequest } from '../packages/connectors/pacifica/src/signing';
+
+describe('PacificaWebSocket', () => {
+  let server: WebSocketServer;
+  let port: number;
+
+  beforeEach(() => {
+    server = new WebSocketServer({ port: 0 });
+    const address = server.address();
+    if (typeof address === 'string' || !address) {
+      throw new Error('Failed to obtain server address');
+    }
+    port = address.port;
+  });
+
+  afterEach(async () => {
+    for (const client of server.clients) {
+      client.close();
+    }
+    await new Promise(resolve => server.close(resolve));
+    vi.restoreAllMocks();
+  });
+
+  it('connects, subscribes, and receives heartbeat responses', async () => {
+    const messages: string[] = [];
+
+    const client = new PacificaWebSocket({
+      url: `ws://127.0.0.1:${port}`,
+      heartbeatIntervalMs: 10
+    });
+
+    server.on('connection', socket => {
+      socket.on('message', data => {
+        messages.push(data.toString());
+        socket.send(JSON.stringify({ method: 'pong' }));
+      });
+    });
+
+    const messagePromise = new Promise<string>(resolve => {
+      client.once('message', data => resolve(data.toString()));
+    });
+
+    client.subscribe('book.BTC', { depth: 1 });
+    client.connect();
+
+    // wait for server to receive login & subscribe
+    await new Promise(resolve => setTimeout(resolve, 50));
+
+    expect(messages.length).toBeGreaterThanOrEqual(1);
+    const subscribe = JSON.parse(messages[0]!);
+    expect(subscribe.method).toBe('subscribe');
+    expect(subscribe.params.channel).toBe('book.BTC');
+
+    const serverMessage = await messagePromise;
+    expect(serverMessage).toContain('pong');
+
+    client.disconnect();
+  });
+
+  it('throws when subscribing to private channel without credentials', () => {
+    const client = new PacificaWebSocket({
+      url: `ws://127.0.0.1:${port}`
+    });
+
+    expect(() => client.subscribe('orders.sub-1')).toThrow(
+      'Missing credentials for private Pacifica channel orders.sub-1'
+    );
+  });
+
+  it('injects auth params for private channel subscriptions', async () => {
+    const seed = Uint8Array.from({ length: 32 }, (_, i) => i + 1);
+    const { secretKey, publicKey } = nacl.sign.keyPair.fromSeed(seed);
+    const secret = Buffer.from(secretKey).toString('base64');
+    const apiKey = bs58.encode(publicKey);
+
+    vi.spyOn(Date, 'now').mockReturnValue(1_700_000_000_000);
+
+    const messages: string[] = [];
+
+    const client = new PacificaWebSocket({
+      url: `ws://127.0.0.1:${port}`,
+      apiKey,
+      secret,
+      subaccount: 'sub-1',
+      heartbeatIntervalMs: 10
+    });
+
+    server.on('connection', socket => {
+      socket.on('message', data => {
+        messages.push(data.toString());
+      });
+    });
+
+    const basePayload = {
+      method: 'subscribe' as const,
+      params: { channel: 'orders.sub-1' }
+    };
+
+    const { headers, timestamp } = signRequest(
+      { apiKey, secret, subaccount: 'sub-1' },
+      'SUBSCRIBE',
+      '/ws',
+      basePayload
+    );
+
+    const expectedSignature = headers['X-Pacific-Signature'];
+
+    client.subscribe('orders.sub-1');
+    client.connect();
+
+    await new Promise(resolve => setTimeout(resolve, 50));
+
+    expect(messages.length).toBeGreaterThanOrEqual(1);
+    const subscribe = JSON.parse(messages[0]!);
+    expect(subscribe.method).toBe('subscribe');
+    expect(subscribe.params.channel).toBe('orders.sub-1');
+    expect(subscribe.params.auth).toEqual({
+      key: apiKey,
+      timestamp,
+      signature: expectedSignature,
+      subaccount: 'sub-1'
+    });
+
+    client.disconnect();
+  });
+});

+ 19 - 0
tests/rateLimiter.test.ts

@@ -0,0 +1,19 @@
+import { describe, expect, it } from 'vitest';
+
+import { RateLimiter } from '../packages/connectors/pacifica/src/rateLimiter';
+
+describe('RateLimiter', () => {
+  it('limits the number of requests per interval', async () => {
+    const limiter = new RateLimiter({ capacity: 2, refillAmount: 2, refillIntervalMs: 50 });
+
+    const start = Date.now();
+    await limiter.acquire();
+    await limiter.acquire();
+    const p = limiter.acquire();
+    const elapsed = Date.now() - start;
+    expect(elapsed).toBeLessThan(10);
+    await p;
+    const totalElapsed = Date.now() - start;
+    expect(totalElapsed).toBeGreaterThanOrEqual(50);
+  });
+});

+ 76 - 0
tests/riskAllocatorAllocation.test.ts

@@ -0,0 +1,76 @@
+import { describe, it, expect } from "vitest";
+
+import { RiskAllocator } from "../packages/registry/src/riskAllocator";
+import type { SymbolRuntimeState } from "../packages/registry/src/types";
+
+function state(params: {
+  symbol: string;
+  maxNotional: number;
+  maxBase: number;
+  score?: number;
+  status?: SymbolRuntimeState["status"];
+}): SymbolRuntimeState {
+  return {
+    config: {
+      symbol: params.symbol,
+      maxNotional: params.maxNotional,
+      maxBase: params.maxBase
+    },
+    status: params.status ?? "active",
+    updatedAt: Date.now(),
+    score: params.score
+  };
+}
+
+describe("RiskAllocator", () => {
+  it("allocates proportionally by score across active symbols", () => {
+    const allocator = new RiskAllocator({ totalNotional: 1_000_000, totalBase: 10 });
+    const allocations = allocator.allocate([
+      state({ symbol: "BTC", maxNotional: 1_000_000, maxBase: 10, score: 2 }),
+      state({ symbol: "ETH", maxNotional: 800_000, maxBase: 8, score: 1 }),
+      state({ symbol: "SOL", maxNotional: 500_000, maxBase: 5, score: 0.5 })
+    ]);
+
+    expect(allocations.size).toBe(3);
+
+    const btc = allocations.get("BTC");
+    const eth = allocations.get("ETH");
+    const sol = allocations.get("SOL");
+    expect(btc).toBeDefined();
+    expect(eth).toBeDefined();
+    expect(sol).toBeDefined();
+
+    expect(btc!.notionalLimit).toBeCloseTo(571_428.571, 2);
+    expect(btc!.baseLimit).toBeCloseTo(5.714, 3);
+    expect(eth!.notionalLimit).toBeCloseTo(285_714.285, 2);
+    expect(eth!.baseLimit).toBeCloseTo(2.857, 3);
+    expect(sol!.notionalLimit).toBeCloseTo(142_857.142, 2);
+    expect(sol!.baseLimit).toBeCloseTo(1.428, 2);
+  });
+
+  it("omits inactive or zero-score symbols and enforces per-symbol limits", () => {
+    const allocator = new RiskAllocator({ totalNotional: 100_000, totalBase: 10 });
+    const allocations = allocator.allocate([
+      state({ symbol: "BTC", maxNotional: 40_000, maxBase: 4, score: 10 }),
+      state({ symbol: "ETH", maxNotional: 80_000, maxBase: 8, score: 5, status: "paused" }),
+      state({ symbol: "SOL", maxNotional: 10_000, maxBase: 1, score: 0 })
+    ]);
+
+    expect(allocations.size).toBe(1);
+    expect(allocations.get("BTC")).toEqual({ notionalLimit: 40_000, baseLimit: 4 });
+  });
+
+  it("respects minimum thresholds configured in options", () => {
+    const allocator = new RiskAllocator(
+      { totalNotional: 100_000, totalBase: 10 },
+      { minNotionalPerSymbol: 30_000, minBasePerSymbol: 3 }
+    );
+    const allocations = allocator.allocate([
+      state({ symbol: "BTC", maxNotional: 25_000, maxBase: 5, score: 1 }),
+      state({ symbol: "ETH", maxNotional: 80_000, maxBase: 8, score: 3 })
+    ]);
+
+    expect(allocations.get("BTC")).toEqual({ notionalLimit: 25_000, baseLimit: 3 });
+    expect(allocations.get("ETH")).toEqual({ notionalLimit: 75_000, baseLimit: 7.5 });
+  });
+});

+ 72 - 0
tests/riskEngine.test.ts

@@ -0,0 +1,72 @@
+import { describe, expect, it } from 'vitest';
+
+import { RiskEngine } from '../packages/risk/src/riskEngine';
+import type { Order, PositionSnapshot } from '../packages/domain/src/types';
+
+const baseOrder: Order = {
+  clientId: 'risk-1',
+  symbol: 'BTC',
+  side: 'buy',
+  px: 100,
+  sz: 0.5,
+  tif: 'GTC'
+};
+
+const position: PositionSnapshot = {
+  symbol: 'BTC',
+  base: 0,
+  quote: 1000,
+  ts: Date.now()
+};
+
+describe('RiskEngine', () => {
+  it('blocks orders that exceed size or limits', () => {
+    const engine = new RiskEngine({
+      maxBaseAbs: 1,
+      maxNotionalAbs: 150,
+      maxOrderSz: 0.6
+    });
+
+    expect(() => engine.preCheck(baseOrder, position, 100)).not.toThrow();
+
+    expect(() =>
+      engine.preCheck({ ...baseOrder, sz: 0.7 }, position, 100)
+    ).toThrow(/order size/);
+
+    expect(() =>
+      engine.preCheck({ ...baseOrder, sz: 0.5 }, { ...position, base: 0.7 }, 100)
+    ).toThrow(/inventory/);
+  });
+
+  it('triggers kill switch on drawdown and resets when requested', () => {
+    const engine = new RiskEngine(
+      {
+        maxBaseAbs: 2,
+        maxNotionalAbs: 1_000,
+        maxOrderSz: 1
+      },
+      {
+        drawdownPct: 0.5,
+        triggers: [
+          { type: 'delta_abs', threshold: 1.6 },
+          { type: 'hedge_failure_count', threshold: 3 }
+        ]
+      }
+    );
+
+    engine.updateEquity(100);
+    engine.updateEquity(45); // 55% drawdown
+    expect(engine.shouldHalt()).toBe(true);
+    engine.resetKillSwitch();
+    expect(engine.shouldHalt()).toBe(false);
+
+    engine.updateDeltaAbs(2);
+    expect(engine.shouldHalt()).toBe(true);
+    engine.resetKillSwitch();
+
+    engine.recordHedgeFailure();
+    engine.recordHedgeFailure();
+    engine.recordHedgeFailure();
+    expect(engine.shouldHalt()).toBe(true);
+  });
+});

+ 51 - 0
tests/signing.test.ts

@@ -0,0 +1,51 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import nacl from 'tweetnacl';
+import bs58 from 'bs58';
+
+import { signRequest } from '../packages/connectors/pacifica/src/signing';
+
+const seed = new Uint8Array(32).fill(7);
+const { secretKey, publicKey } = nacl.sign.keyPair.fromSeed(seed);
+const secretBase64 = Buffer.from(secretKey).toString('base64');
+
+describe('signRequest', () => {
+  beforeEach(() => {
+    vi.restoreAllMocks();
+  });
+
+  it('signs payloads according to Pacifica spec', () => {
+    const body = { foo: 'bar', amount: 1 };
+    vi.spyOn(Date, 'now').mockReturnValue(1_700_000_000_000);
+
+    const apiKey = bs58.encode(publicKey);
+    const { headers, body: serialized } = signRequest(
+      {
+        apiKey,
+        secret: secretBase64
+      },
+      'POST',
+      '/orders',
+      body
+    );
+
+    expect(headers['X-Pacific-Key']).toBe(apiKey);
+    expect(headers['X-Pacific-Signature']).toBeTruthy();
+    expect(headers['X-Pacific-Timestamp']).toBe('1700000000000');
+    expect(serialized).toBe(JSON.stringify(body));
+
+    const baseString = `${headers['X-Pacific-Timestamp']}:POST:/orders:${serialized}`;
+    const signatureBytes = Buffer.from(headers['X-Pacific-Signature'], 'base64');
+    const valid = nacl.sign.detached.verify(
+      new TextEncoder().encode(baseString),
+      signatureBytes,
+      publicKey
+    );
+    expect(valid).toBe(true);
+  });
+
+  it('omits signature headers when api credentials are missing', () => {
+    const { headers, body } = signRequest({}, 'GET', '/products/BTC', undefined);
+    expect(headers).toEqual({});
+    expect(body).toBe('');
+  });
+});

+ 102 - 0
tests/symbolRegistry.test.ts

@@ -0,0 +1,102 @@
+import { describe, it, expect, vi } from "vitest";
+
+import { SymbolRegistry } from "../packages/registry/src/symbolRegistry";
+import type { SymbolConfig, SymbolRuntimeState } from "../packages/registry/src/types";
+
+function createConfig(overrides: Partial<SymbolConfig> = {}): SymbolConfig {
+  return {
+    symbol: overrides.symbol ?? "BTC",
+    maxNotional: overrides.maxNotional ?? 100_000,
+    maxBase: overrides.maxBase ?? 1,
+    enabled: overrides.enabled,
+    tags: overrides.tags,
+    metadata: overrides.metadata
+  };
+}
+
+function createState(overrides: Partial<SymbolRuntimeState> = {}): SymbolRuntimeState {
+  return {
+    config: overrides.config ?? createConfig({ symbol: "BTC" }),
+    status: overrides.status ?? "inactive",
+    updatedAt: overrides.updatedAt ?? Date.now(),
+    reason: overrides.reason,
+    allocation: overrides.allocation,
+    score: overrides.score
+  };
+}
+
+describe("SymbolRegistry", () => {
+  it("registers symbols and emits lifecycle events", () => {
+    const registry = new SymbolRegistry();
+    const onRegister = vi.fn();
+    registry.on("registered", onRegister);
+
+    const state = registry.register(createConfig({ symbol: "ETH" }));
+
+    expect(state.status).toBe("inactive");
+    expect(registry.get("ETH")).toBe(state);
+    expect(onRegister).toHaveBeenCalledTimes(1);
+    expect(onRegister).toHaveBeenCalledWith(state);
+  });
+
+  it("applies enabled=false as disabled status", () => {
+    const registry = new SymbolRegistry();
+    const state = registry.register(createConfig({ symbol: "SOL", enabled: false }));
+    expect(state.status).toBe("disabled");
+  });
+
+  it("updates configuration and tracks allocation/score", () => {
+    const registry = new SymbolRegistry();
+    registry.register(createConfig({ symbol: "BTC" }));
+
+    const updated = registry.updateConfig("BTC", { maxNotional: 200_000 });
+    expect(updated.config.maxNotional).toBe(200_000);
+
+    const allocation = { notionalLimit: 50_000, baseLimit: 0.5 };
+    const scored = registry.setAllocation("BTC", allocation);
+    expect(scored.allocation).toEqual(allocation);
+
+    registry.setScore("BTC", 0.8);
+    expect(registry.get("BTC")?.score).toBeCloseTo(0.8);
+  });
+
+  it("changes status and emits statusChanged events", () => {
+    const registry = new SymbolRegistry();
+    const onStatus = vi.fn();
+    registry.on("statusChanged", onStatus);
+
+    registry.register(createConfig({ symbol: "BTC" }));
+    const activated = registry.activate("BTC");
+    expect(activated.status).toBe("active");
+
+    registry.pause("BTC", "volatility");
+    expect(registry.get("BTC")?.status).toBe("paused");
+    expect(registry.get("BTC")?.reason).toBe("volatility");
+
+    registry.disable("BTC", "maintenance");
+    expect(registry.get("BTC")?.status).toBe("disabled");
+    expect(onStatus).toHaveBeenCalledTimes(3);
+  });
+
+  it("lists states by status and removes symbols", () => {
+    const registry = new SymbolRegistry();
+    registry.register(createConfig({ symbol: "BTC" }));
+    registry.register(createConfig({ symbol: "ETH" }));
+    registry.activate("BTC");
+
+    const active = registry.list("active");
+    expect(active).toHaveLength(1);
+    expect(active[0].config.symbol).toBe("BTC");
+
+    const inactive = registry.list("inactive");
+    expect(inactive).toHaveLength(1);
+    expect(inactive[0].config.symbol).toBe("ETH");
+
+    const onRemoved = vi.fn();
+    registry.on("removed", onRemoved);
+    registry.remove("ETH");
+    expect(registry.get("ETH")).toBeUndefined();
+    expect(onRemoved).toHaveBeenCalledWith({ symbol: "ETH" });
+  });
+});
+

+ 41 - 0
tests/volatilityEstimator.test.ts

@@ -0,0 +1,41 @@
+import { describe, it, expect } from "vitest";
+
+import { VolatilityEstimator } from "../packages/utils/src/volatilityEstimator";
+
+describe("VolatilityEstimator", () => {
+  it("tracks hourly volatility in bps", () => {
+    const estimator = new VolatilityEstimator({ windowMinutes: 60, minSamples: 2 });
+    const start = Date.now();
+    estimator.update(100, start - 60 * 60 * 1000);
+    estimator.update(110, start);
+
+    const hourlyBps = estimator.getHourlyVolatilityBps();
+    expect(hourlyBps).toBeDefined();
+    expect(hourlyBps).toBeCloseTo(1000);
+  });
+
+  it("computes annualized volatility from returns", () => {
+    const estimator = new VolatilityEstimator({ windowMinutes: 30, minSamples: 2 });
+    const base = Date.now();
+    estimator.update(100, base - 3 * 60 * 1000);
+    estimator.update(102, base - 2 * 60 * 1000);
+    estimator.update(98, base - 60 * 1000);
+    estimator.update(101, base);
+
+    const annualized = estimator.getAnnualizedVolatility();
+    expect(annualized).toBeDefined();
+    expect(annualized).toBeGreaterThan(0);
+  });
+
+  it("exposes status snapshot", () => {
+    const estimator = new VolatilityEstimator({ windowMinutes: 5, minSamples: 2 });
+    const now = Date.now();
+    estimator.update(50_000, now - 10_000);
+    estimator.update(50_500, now);
+
+    const status = estimator.getStatus();
+    expect(status.historySize).toBeGreaterThanOrEqual(2);
+    expect(status.latestPrice).toBeCloseTo(50_500);
+    expect(status.hourlyVolBps).toBeGreaterThan(0);
+  });
+});

+ 61 - 0
tests/wsOrderGateway.test.ts

@@ -0,0 +1,61 @@
+import { describe, it, expect, vi } from "vitest";
+import nacl from "tweetnacl";
+
+import { PacificaWsOrderGateway } from "../packages/connectors/pacifica/src/wsOrderGateway";
+import type { SigningConfig } from "../packages/connectors/pacifica/src/signing";
+
+describe("PacificaWsOrderGateway", () => {
+  const seed = Uint8Array.from({ length: 32 }, (_, i) => i + 1);
+  const { secretKey, publicKey } = nacl.sign.keyPair.fromSeed(seed);
+  const secret = Buffer.from(secretKey).toString("base64");
+  const apiKey = Buffer.from(publicKey).toString("base64");
+
+  const signing: SigningConfig = {
+    apiKey,
+    secret,
+    subaccount: "sub-1"
+  };
+
+  it("extracts numeric order id from websocket response", async () => {
+    const mockWs = {
+      connect: vi.fn(),
+      waitForOpen: vi.fn(),
+      sendRpc: vi.fn().mockResolvedValue({ s: "BTC", i: 377323582, I: "client-1" })
+    } as any;
+
+    const gateway = new PacificaWsOrderGateway(mockWs, signing);
+    const result = await gateway.createOrder({
+      symbol: "BTC",
+      side: "bid",
+      price: "50000",
+      amount: "0.1",
+      tif: "IOC",
+      clientOrderId: "client-1"
+    });
+
+    expect(result.orderId).toBe("377323582");
+    expect(mockWs.sendRpc).toHaveBeenCalled();
+  });
+
+  it("throws when order id missing", async () => {
+    const mockWs = {
+      connect: vi.fn(),
+      waitForOpen: vi.fn(),
+      sendRpc: vi.fn().mockResolvedValue({ status: "ok" })
+    } as any;
+
+    const gateway = new PacificaWsOrderGateway(mockWs, signing);
+
+    await expect(
+      gateway.createOrder({
+        symbol: "BTC",
+        side: "bid",
+        price: "50000",
+        amount: "0.1",
+        tif: "IOC",
+        clientOrderId: "client-1"
+      })
+    ).rejects.toThrow(/missing order id/);
+  });
+});
+

+ 18 - 0
tsconfig.base.json

@@ -0,0 +1,18 @@
+{
+  "compilerOptions": {
+    "target": "ES2022",
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "outDir": "dist",
+    "rootDir": ".",
+    "strict": true,
+    "skipLibCheck": true,
+    "resolveJsonModule": true,
+    "esModuleInterop": true,
+    "types": [
+      "node",
+      "vitest/globals"
+    ],
+    "verbatimModuleSyntax": true
+  }
+}

+ 9 - 0
vitest.config.ts

@@ -0,0 +1,9 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  test: {
+    environment: 'node',
+    include: ['tests/**/*.test.ts'],
+    globals: true
+  }
+});