Bladeren bron

feat: incremental grid and throttling groundwork

helium3@sina.com 2 maanden geleden
bovenliggende
commit
d3c8d11a93

+ 5 - 2
README.md

@@ -16,7 +16,7 @@
 pnpm install
 cp .env.example .env
 cp config/config.example.yaml config/config.yaml
-pnpm dev
+pnpm run live        # 正常启动
 # 常用检查
 pnpm lint
 pnpm test
@@ -47,10 +47,13 @@ pnpm typecheck
 8. 以盲目修改为耻,以谨慎重构为荣
 
 ## Reference Documentation
-- **架构与规划**:`docs/ARCHITECTURE_DESIGN.md`, `docs/IMPLEMENTATION_PLAN.md`, `docs/CODE_DELIVERY_PLAN.md`
+- **快速上手**:`docs/CONFIG_GUIDE.md`
+- **架构与规划**:`docs/ARCHITECTURE_DESIGN.md`, `docs/IMPLEMENTATION_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`
+- **即将到来的 M1.6 设计**:`docs/M16_INCREMENTAL_GRID_DESIGN.md`, `docs/M16_PLACEMENT_THROTTLING_DESIGN.md`, `docs/M16_FILL_DRIVEN_TIGHTENING_DESIGN.md`
+- **历史资料**:见 `docs/archive/`
 
 ## Tests
 - 单元测试位于 `tests/` 目录,使用 Vitest (`pnpm test`)

+ 58 - 18
apps/runner/src/index.ts

@@ -16,11 +16,23 @@ 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 { RateLimiter } from "../../../packages/connectors/pacifica/src/rateLimiter";
 import type { Order, Fill } from "../../../packages/domain/src/types";
 import type { KillSwitchConfig } from "../../../packages/risk/src/riskEngine";
 
 let globalFileDestination: pino.DestinationStream | undefined;
 
+const flushLogsSafe = () => {
+  if (!globalFileDestination) return;
+  const stream: any = globalFileDestination;
+  if (typeof stream.flushSync !== 'function') return;
+  try {
+    stream.flushSync();
+  } catch {
+    // 静默忽略 flush 失败(sonic-boom 未 ready 时正常)
+  }
+};
+
 const logger = createRunnerLogger();
 const cfg = parse(readFileSync("config/config.yaml","utf8"));
 
@@ -63,6 +75,9 @@ const addressToAccountId = new Map<string, string>();
 const fillHandlers = new Map<string, Array<(fill: Fill) => Promise<void>>>();
 let killSwitchActive = false;
 let gridMakerInstance: GridMaker | undefined;
+const baseClipEquityPct = cfg.grid?.base_clip_equity_pct ?? 0;
+const baseClipUsdFloor = cfg.grid?.base_clip_usd ?? 0;
+const wsRateLimiterConfig = cfg.execution?.ws_rate_limiter;
 let marketMakerInstance: MarketMaker | undefined;
 let scalperInstance: MicroScalper | undefined;
 let gridStatusTimer: NodeJS.Timeout | undefined;
@@ -271,6 +286,10 @@ const defaultGridSymbol = cfg.grid?.symbol || primarySymbol;
       riskEngine.updateDeltaAbs(aggregated.base);
       const equity = aggregated.quote + aggregated.base * mid;
       riskEngine.updateEquity(equity);
+      if (gridMakerInstance && baseClipEquityPct > 0) {
+        const dynamicClip = Math.max(baseClipUsdFloor, equity * baseClipEquityPct);
+        gridMakerInstance.updateBaseClipUsd(dynamicClip);
+      }
       await maybeTriggerKillSwitch('risk_refresh');
     } catch (error) {
       logger.error({ symbol, error }, 'Failed to refresh risk');
@@ -429,18 +448,31 @@ const defaultGridSymbol = cfg.grid?.symbol || primarySymbol;
 
   // 根据策略模式启动不同策略
   if (strategyMode === 'grid' || strategyMode === 'both') {
+    if (baseClipEquityPct > 0) {
+      await refreshRisk(primarySymbol);
+    }
+
+    const computeBaseClipUsd = () => {
+      if (baseClipEquityPct <= 0) return baseClipUsdFloor;
+      const status = riskEngine.getStatus();
+      const equity = status.currentEquity > 0 ? status.currentEquity : status.peakEquity;
+      const dynamic = equity * baseClipEquityPct;
+      return Math.max(baseClipUsdFloor, dynamic);
+    };
+
     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,
+      baseClipUsd: computeBaseClipUsd(),
       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
+      lotSize: cfg.grid.lot_size,
+      incrementalMode: cfg.grid.incremental_mode ?? false
     };
 
     const adaptiveConfig: AdaptiveGridConfig | undefined = cfg.grid.adaptive?.enabled
@@ -522,7 +554,14 @@ const defaultGridSymbol = cfg.grid?.symbol || primarySymbol;
       try {
         await gridMaker.onTick();
       } catch (error) {
-        logger.error({ error }, 'GridMaker onTick failed');
+        console.error('GridMaker onTick failed:', error);
+        logger.error({
+          error: error instanceof Error ? {
+            message: error.message,
+            stack: error.stack,
+            name: error.name
+          } : error
+        }, 'GridMaker onTick failed');
       }
     }, cfg.grid.adaptive?.tick_interval_ms ?? 60_000);
   }
@@ -589,10 +628,7 @@ const defaultGridSymbol = cfg.grid?.symbol || primarySymbol;
 
 main().catch(e => {
   logger.error({ error: e }, 'Fatal error in main');
-  if (globalFileDestination) {
-    globalFileDestination.flushSync();
-  }
-  process.exit(1);
+  setImmediate(() => process.exit(1));
 });
 
 function createRunnerLogger() {
@@ -631,9 +667,7 @@ function createRunnerLogger() {
   const gracefulShutdown = (signal: string) => {
     instance.info({ signal }, 'Received shutdown signal, flushing logs...');
 
-    if (globalFileDestination) {
-      globalFileDestination.flushSync();
-    }
+    flushLogsSafe();
 
     setTimeout(() => {
       instance.info('Logs flushed, exiting');
@@ -644,17 +678,16 @@ function createRunnerLogger() {
   process.on('SIGINT', () => gracefulShutdown('SIGINT'));
   process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
   process.on('uncaughtException', (error) => {
-    instance.error({ error }, 'Uncaught exception');
-    if (globalFileDestination) {
-      globalFileDestination.flushSync();
-    }
+    console.error('Uncaught exception:', error);
+    try {
+      instance.error({ error: error instanceof Error ? { message: error.message, stack: error.stack } : error }, 'Uncaught exception');
+    } catch {}
+    flushLogsSafe();
     process.exit(1);
   });
   process.on('unhandledRejection', (reason) => {
     instance.error({ reason }, 'Unhandled rejection');
-    if (globalFileDestination) {
-      globalFileDestination.flushSync();
-    }
+    flushLogsSafe();
     process.exit(1);
   });
 
@@ -684,10 +717,17 @@ async function setupWsOrderGateways(options: {
           apiKey: address,
           secret: privateKey
         });
+        const limiter = wsRateLimiterConfig
+          ? new RateLimiter({
+              burst: wsRateLimiterConfig.burst ?? 6,
+              refillPerSec: wsRateLimiterConfig.refill_per_sec ?? 5,
+              maxQueueDepth: wsRateLimiterConfig.max_queue_depth ?? 100
+            })
+          : undefined;
         const gateway = new PacificaWsOrderGateway(wsClient, {
           apiKey: address,
           secret: privateKey
-        }, undefined, undefined, logger);
+        }, undefined, undefined, logger, limiter);
         wsClient.on('message', message => {
           handlePrivateMessage(address, message, routeFill, routeOrder, routeAccount);
         });

+ 6 - 0
config/config.example.yaml

@@ -28,10 +28,12 @@ grid:
   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)
+  base_clip_equity_pct: 0.01  # 按账户权益比例(1%)动态调整,取最大值
   max_layers: 4        # 单边最大层数
   hedge_threshold_base: 0.3  # 累积 0.3 BTC 触发对冲(批量对冲模式)
   tick_size: 1         # 价格步长
   lot_size: 0.00001    # 最小数量步长
+  incremental_mode: false
 
   # 多标的配置(M2.5 增强版,注释掉则使用上面的单标的配置)
   # symbols:
@@ -116,5 +118,9 @@ hedge:
 execution:
   max_slippage_bps: 5
   min_order_interval_ms: 250
+  ws_rate_limiter:
+    burst: 6
+    refill_per_sec: 5
+    max_queue_depth: 60
 funding:
   poll_interval_ms: 60000

+ 14 - 8
config/config.yaml

@@ -24,13 +24,15 @@ grid:
 
   # 单标的配置(M1.5 MVP)
   symbol: BTC
-  grid_step_bps: 3    # 初始网格间距 0.08%
-  grid_range_bps: 200  # 覆盖范围 2%
-  base_clip_usd: 100   # 单层订单大小(USD)
+  grid_step_bps: 3    # 初始网格间距 0.03%
+  grid_range_bps: 300  # 覆盖范围 2%
+  base_clip_usd: 15   # 单层订单大小(USD)
+  base_clip_equity_pct: 0.01 # 按账户权益比例(1%)动态调整,取最大值
   max_layers: 15        # 单边最大层数
   hedge_threshold_base: 0.12  # 累积 0.3 BTC 触发对冲(批量对冲模式)
   tick_size: 1         # 价格步长
   lot_size: 0.00001    # 最小数量步长
+  incremental_mode: true
 
   # 多标的配置(M2.5 增强版,注释掉则使用上面的单标的配置)
   # symbols:
@@ -54,13 +56,13 @@ grid:
     min_grid_step_bps: 3          # 网格间距下限(会被盘口价差覆盖)
     max_grid_step_bps: 120          # 网格间距上限
     recenter_enabled: true          # 偏离阈值后自动重置
-    recenter_threshold_bps: 150     # 偏离阈值
-    recenter_cooldown_ms: 600000    # 重置冷却时间(10 分钟,避免频繁重置)
-    min_step_change_ratio: 0.2      # 调整间距的最小相对变化
-    tick_interval_ms: 45000         # 自适应检查间隔(45 秒)
+    recenter_threshold_bps: 500     # 偏离阈值(增加到500,减少重新中心化频率)
+    recenter_cooldown_ms: 900000    # 重置冷却时间(10 分钟,避免频繁重置)
+    min_step_change_ratio: 0.5      # 调整间距的最小相对变化(从0.3增加到0.5,减少调整频率)
+    tick_interval_ms: 60000         # 自适应检查间隔(从45秒增加到60秒)
     hedge_pending_timeout_ms: 30000 # 对冲挂单超过阈值仍未成交则告警
     post_only_cushion_bps: 2        # PostOnly 保护缓冲
-    min_layers: 8                   # 最少层数,步长增大时保持档位数量
+    min_layers: 10                  # 最少层数,步长增大时保持档位数量
 
   # 趋势检测与暂停(M2.5)
   trend_filter:
@@ -106,3 +108,7 @@ execution:
   max_slippage_bps: 150
   min_order_interval_ms: 100         # 正常交易节流间隔
   bulk_init_interval_ms: 20          # 批量初始化节流间隔(网格布置时)
+  ws_rate_limiter:
+    burst: 20                        # 加宽令牌桶,降低 queue exceeded 概率
+    refill_per_sec: 10
+    max_queue_depth: 120

+ 81 - 0
config/examples/micro_grid_extreme.yaml

@@ -0,0 +1,81 @@
+env: mainnet
+strategy_mode: grid
+
+accounts:
+  maker-inner:
+    address: YOUR_INNER_ACCOUNT_ADDRESS
+    private_key: YOUR_INNER_PRIVATE_KEY
+    role: maker_inner
+  maker-outer:
+    address: YOUR_OUTER_ACCOUNT_ADDRESS
+    private_key: YOUR_OUTER_PRIVATE_KEY
+    role: maker_outer
+  hedger:
+    address: YOUR_HEDGER_ACCOUNT_ADDRESS
+    private_key: YOUR_HEDGER_PRIVATE_KEY
+    role: hedger
+
+grid:
+  enabled: true
+  instances:
+    - id: btc-inner
+      account_id: maker-inner
+      symbol: BTC
+      grid_step_bps: 1.5
+      grid_range_bps: 120
+      base_clip_usd: 60
+      max_layers: 16
+      hedge_threshold_base: 0.08
+      adaptive:
+        enabled: true
+        min_grid_step_bps: 1.0
+        max_grid_step_bps: 12
+        min_layers: 12
+        post_only_cushion_bps: 0.3
+        tick_interval_ms: 25000
+        min_step_change_ratio: 0.1
+        fill_starvation_threshold:
+          ticks: 4
+          compress_factor: 0.5
+    - id: btc-outer
+      account_id: maker-outer
+      symbol: BTC
+      grid_step_bps: 6
+      grid_range_bps: 220
+      base_clip_usd: 120
+      max_layers: 10
+      hedge_threshold_base: 0.15
+      adaptive:
+        enabled: true
+        min_grid_step_bps: 4
+        max_grid_step_bps: 40
+        min_layers: 8
+        post_only_cushion_bps: 1.5
+        tick_interval_ms: 40000
+        min_step_change_ratio: 0.15
+        fill_starvation_threshold:
+          ticks: 6
+          compress_factor: 0.6
+
+risk:
+  max_notional_abs: 150000
+  max_base_abs: 1.2
+  max_order_sz: 1.5
+
+hedge:
+  kp: 0.7
+  ki: 0.08
+  qmax: 0.6
+  min_interval_ms: 200
+
+execution:
+  rest_rate_limiter:
+    burst: 12
+    refill_per_sec: 10
+  ws_rate_limiter:
+    burst: 8
+    refill_per_sec: 6
+    max_queue_depth: 60
+
+telemetry:
+  push_interval_ms: 5000

+ 206 - 0
docs/BACKTEST_FRAMEWORK_DESIGN.md

@@ -0,0 +1,206 @@
+# Backtest Framework Design (M3 Deliverable)
+
+**版本**: v1.0  
+**日期**: 2025-10-09  
+**负责人**: Research / Quant Engineering  
+**依赖文档**: `IMPLEMENTATION_PLAN.md`, `TESTING_PLAN.md`, `M16_INCREMENTAL_GRID_DESIGN.md`
+
+---
+
+## 1. 背景
+
+M3 阶段要求支持参数回放与敏感性测试,以验证网格/剥头皮策略在不同市场环境下的表现,并作为金丝雀流程的一部分。当前仅有轻量级 replay 工具,缺乏系统化框架。
+
+---
+
+## 2. 功能目标
+
+1. **Event Replay**:消费历史行情(book trades)与交易日志,复用生产策略逻辑。  
+2. **Latency Injection**:基于实盘观测的延迟分布,在回测中注入下单/撤单延迟。  
+3. **Fee / Funding / Slippage 模型**:准确反映成本、资金费率与滑点。  
+4. **Parameter Sweep**:支持 Grid/MicroScalper 参数的网格或随机搜索。  
+5. **报告与可视化**:生成 EV、Sharpe、回撤、成交桶等指标,方便对比。  
+6. **可插拔**:可在 CI 或研究环境运行,允许仅回测部分模块。
+
+---
+
+## 3. 架构概览
+
+```
+┌──────────────────────────┐
+│ BacktestRunner            │
+│  - loadScenario()         │
+│  - run()                  │
+│  - generateReport()       │
+├───────────┬──────────────┤
+│ DataFeed  │ LatencyModel │
+└────┬──────┴───────┬──────┘
+     │              │
+  MarketAdapter  ExecutionAdapter
+     │              │
+┌────▼─────┐   ┌────▼─────┐
+│Strategy  │   │Connector │(mock)
+│(Grid/MM) │   │(REST/WS) │
+└──────────┘   └──────────┘
+```
+
+- `BacktestRunner`: orchestrate 数据加载、策略执行、结果收集。  
+- `DataFeed`:抽象行情来源,支持 JSON、Parquet、Kafka snapshot。  
+- `LatencyModel`:针对下单/撤单/成交回报注入延迟。  
+- `ExecutionAdapter`:Mock Pacifica 连接器,遵循真实接口。  
+- `MetricsCollector`:聚合指标并输出报告。
+
+---
+
+## 4. 模块设计
+
+### 4.1 BacktestRunner
+
+```ts
+interface ScenarioConfig {
+  name: string;
+  start: string;  // ISO timestamp
+  end: string;
+  symbol: string;
+  strategyConfig: GridConfig | ScalperConfig;
+  latencyProfile: LatencyProfile;
+  feeModel: FeeModelConfig;
+  fundingModel: FundingModelConfig;
+}
+```
+
+主要方法:
+- `loadScenario(config: ScenarioConfig)`: 初始化 DataFeed/Adapters/Strategy。  
+- `run()`: 按时间顺序驱动事件;支持加速(跳帧)与实时模式。  
+- `generateReport()`: 输出 JSON + Markdown + 可选 CSV,包含指标。
+
+### 4.2 DataFeed
+
+接口:
+```ts
+interface DataFeed {
+  next(): Promise<Event | null>;
+  peekTime(): number | null;
+  reset(): Promise<void>;
+}
+```
+
+事件类型:
+- `BookSnapshot`, `BookDelta`, `Trade`, `FundingUpdate`, `CustomSignal`。  
+- 支持多 source:本地文件、S3、Kafka(通过 iterator 抽象)。  
+- 为保证复现性,所有事件带有严格的 timestamp。
+
+### 4.3 ExecutionAdapter (Mock)
+
+实现 `OrderRouterDispatchFn`, `CancelFn` 等接口,模拟交易所行为:
+- 接受订单 → 记录到队列 → 根据 LatencyModel + SlippageModel 生成成交或 partial fill。  
+- 支持限价与 IOC,post-only 校验由策略层完成。  
+- 记录真实下单时间与成交时间,用于衡量延迟影响。
+
+### 4.4 LatencyModel
+
+定义:
+```ts
+interface LatencyModel {
+  orderSubmitDelay(): number;    // ms
+  cancelDelay(): number;
+  fillDelay(): number;
+}
+```
+
+实现方式:
+- **HistogramModel**:读取实盘指标(Prometheus/Humio),以 P50/P95/P99 构建分布。  
+- **Config-based**:允许手动指定常量或离散分布。  
+- 支持按照时间片(白天/夜间)加载不同 profile。
+
+### 4.5 Fee & Funding Model
+
+```ts
+interface FeeModel {
+  makerFeeBps: number;
+  takerFeeBps: number;
+  rebateBps: number;
+  calc(order: Order, fill: Fill): number; // 返回费用(USD)
+}
+
+interface FundingModel {
+  getFundingRate(ts: number): number;   // annualized
+  settle(position: Position, ts: number): number;
+}
+```
+
+- Funding 可以读取外部 CSV 或 API;若缺失则使用固定费率。  
+- Slippage 模型(可选):基于订单簿深度估算滑点。
+
+### 4.6 MetricsCollector
+
+输出指标(JSON/CSV/Markdown):
+- `pnl_total`, `pnl_fee`, `pnl_funding`, `pnl_slippage`  
+- `ev_bps`, `sharpe`, `sortino`, `max_drawdown`  
+- `fill_count`, `fill_interval_stats`, `hedge_cost_bps`  
+- Bucket 报告(按小时/volatility/OBI 分类)  
+- 图表:`delta_abs` vs time, EV 累计曲线。
+
+---
+
+## 5. 参数搜索
+
+### 5.1 接口
+
+```ts
+interface ParameterSweep {
+  run(scenarios: ScenarioConfig[], params: ParameterGrid): Promise<SweepResult>;
+}
+```
+
+- `ParameterGrid`: 数组或 ranges;支持顺序/随机。  
+- 结果保存 best N 配置(EV/Sharpe 排序),并输出数据表。  
+- 可集成到 CLI:`pnpm backtest --scenario btc-vol-shock --grid grid.yaml --param sweep.yaml`.
+
+---
+
+## 6. 数据格式
+
+- 标准输入格式:  
+  - `books/{date}/{symbol}.parquet`  
+  - `trades/{date}/{symbol}.parquet`  
+  - 文件包含 timestamp、price、size、side。  
+- Runner 接受 JSON/Parquet;对大型数据集建议使用 Parquet + Arrow stream。  
+- 需提供转换脚本(如 `scripts/export-from-humio.ts`)。
+
+---
+
+## 7. 集成与兼容
+
+- 生产代码抽象:策略/Router/Hedge 等必须通过接口调用,避免直接访问网络。  
+- Backtest 使用同一 `config.yaml`(或子集),支持在 `ScenarioConfig` 中覆盖参数。  
+- 可选:支持“实时回放”模式,用于边跑边可视化或用于 demo。
+
+---
+
+## 8. 测试计划
+
+1. 单元测试:数据源迭代、latency model、fee model。  
+2. 集成测试:使用小样本(1 小时)验证指标与手工计算一致。  
+3. 回归:与实盘日志对比(同一时间窗口),结果误差需在 ±5%。  
+4. 性能:30 天数据在 30 分钟内跑完(视硬件可调整)。  
+5. CI:运行最小场景(10 分钟数据)作为 smoke test。
+
+---
+
+## 9. 时间规划
+
+- Week 1:框架骨架 + DataFeed + ExecutionAdapter + LatencyModel。  
+- Week 2:Fee/Funding 模型、MetricsCollector、参数搜索、测试。  
+- Week 3:文档与示例脚本、与策略团队对接、金丝雀回放。  
+- 持续:接入真实数据源、优化性能、增加可视化。
+
+---
+
+## 10. 交付物
+
+- `packages/backtest/`(新 package)  
+- CLI:`pnpm backtest --config backtest/btc_micro.yaml`  
+- 示例配置:`backtest/scenarios/btc-vol-shock.yaml`  
+- 指南:`docs/BACKTEST_USAGE.md`(未来撰写)  
+- GitHub Action:运行 smoke backtest 并上传报告。

+ 43 - 25
docs/CONFIG_GUIDE.md

@@ -1,29 +1,47 @@
-# 配置与运行指南(仓库内)
-
-## 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. 启动
+# 配置与运行快速指引
+
+> 针对第一次接手仓库或需要快速起盘的新同学,提供最小可行环境与常见操作。详细参数说明请参阅 `docs/CONFIG_REFERENCE.md`。
+
+## 1. 环境准备
+1. 安装 Node.js ≥ 22、pnpm ≥ 8。
+2. 复制 `.env.example` → `.env`,填写 Pacifica 凭证(maker/hedger API key、私钥、可选子账户)。  
+   - 若仅使用单账户,本地可临时填写 `PACIFICA_ACCOUNT_ADDRESS` / `PACIFICA_ACCOUNT_PRIVATE_KEY`,Runner 会自动映射到 `maker`。
+3. `pnpm install` 安装依赖,确认 `pnpm lint` / `pnpm test` / `pnpm typecheck` 可通过。
+
+## 2. 创建策略配置
 ```bash
-pnpm i
-cp .env.example .env
 cp config/config.example.yaml config/config.yaml
-pnpm dev
 ```
+- 修改 `accounts` 段落,确保每个账户拥有唯一 `id` / `role`。  
+- `grid` → `symbol`, `grid_step_bps`, `grid_range_bps`, `max_layers` 等按目标市场调整。  
+- 如需多网格实例,暂沿用单实例配置;M1.6 之后会提供 `grid.instances[]`。  
+- 其余参数可保持默认,详细释义参考 `CONFIG_REFERENCE.md`。
+
+## 3. 启动 / 停止
+```bash
+# 实盘 Runner(默认读取 config/config.yaml)
+pnpm run live
+
+# Dry-run / 日志观察(以 --paper 启动)
+pnpm run live -- --paper
+
+# 优雅停止
+curl -X POST http://localhost:4000/api/halt -d '{"reason":"maintenance"}'
+```
+- Runner 会在控制台输出日志文件路径 `logs/runner-<timestamp>.log`,便于后续排查。  
+- 热更新:修改配置后执行 `curl -X POST http://localhost:4000/api/config/reload`,观察金丝雀指标 10 分钟。
+
+## 4. 常见检查
+- `pnpm lint` / `pnpm test` / `pnpm typecheck`:保证提交质量。  
+- `pnpm run status`(可选脚本)或查看 `logs/runner-*.log` 中的 `Grid status`、`orderRouter` 指标。  
+- Prometheus/Grafana 面板配置参见 `docs/OPERATIONS_PLAYBOOK.md`。
+
+## 5. 推荐的阅读顺序
+| 场景 | 推荐文档 |
+|------|---------|
+| 了解整体架构 | `ARCHITECTURE_DESIGN.md`, `SEQUENCE_FLOW.md` |
+| 查看迭代计划 | `IMPLEMENTATION_PLAN.md`, `M16_*.md` 系列 |
+| 调参 / 验证 | `CONFIG_REFERENCE.md`, `TESTING_PLAN.md` |
+| 运维 / 降级 | `OPERATIONS_PLAYBOOK.md` |
 
-## 4. 下一步
-- 按官方文档完成签名实现;
-- 对齐下单/撤单请求体字段;
-- 接入(可选)WebSocket 行情以替代轮询;
-- 实现 OCO/触发单与回测模块。
+> 历史方案与早期文档已迁移至 `docs/archive/`。如需回溯 v1.0 微网格设计、PRD 等资料,请查看该目录。

+ 10 - 0
docs/CONFIG_REFERENCE.md

@@ -111,7 +111,9 @@ grid:
 | `grid_step_bps` | `10`–`300` (默认 `30`) | 初始单层间距 (bps),自适应启用时作为参考值 | 支持,立即重挂 |
 | `grid_range_bps` | `200`–`800` | 覆盖范围 | 更新后触发 `reset()` |
 | `base_clip_usd` | >0 | 单层挂单名义 | 支持 |
+| `base_clip_equity_pct` | 0–1 | 按权益比例动态计算的最小 clip(取 `max(base_clip_usd, equity * pct)`) | 支持 |
 | `max_layers` | `2`–`16` | 每侧层数上限 | 支持 |
+| `incremental_mode` | bool (默认 `false`) | 启用增量更新模式(优先进行 `reconcileGrid`,减少全量撤挂) | 支持 |
 | `tick_size` | `>0` (默认 `1`) | 价格步长;下单时按此对报价做整形 | 支持 |
 | `lot_size` | `>0` (默认 `0.00001`) | 数量步长;下单时向下取整到该步长 | 支持 |
 | `adaptive.enabled` | bool | 启用波动率自适应与中心重置 | 支持 |
@@ -124,6 +126,7 @@ grid:
 | `adaptive.hedge_pending_timeout_ms` | 10_000–120_000 | 对冲挂单超时阈值 | 支持 |
 | `adaptive.post_only_cushion_bps` | 0–20 | Post-only 保护缓冲,计算挂价时加到盘口价差上 | 支持 |
 | `adaptive.min_layers` | 2–`max_layers` | 目标最少层数,步长放大时自动限制以保持档位 | 支持 |
+| `adaptive.fill_starvation_threshold` | `{ ticks, compress_factor }` | 连续无成交触发步长压缩/层数调整(M1.6) | 支持 |
 
 > 🎯 **推荐起始配置**:参考 `config/grid.example.yaml`,适用于测试网或小额实盘。根据账户规模调整 `base_clip_usd` 与 `hedge_threshold_base`,并保持 `max_base_abs ≥ hedge_threshold_base × 1.5` 以预留缓冲。
 
@@ -226,6 +229,10 @@ hedge:
 execution:
   max_slippage_bps: 5
   min_order_interval_ms: 250
+  ws_rate_limiter:
+    burst: 6
+    refill_per_sec: 5
+    max_queue_depth: 60
 
 market_data:
   poll_interval_ms: 1000
@@ -235,6 +242,9 @@ market_data:
 |------|------|------|
 | `max_slippage_bps` | Router 允许的最大滑点阈值 | 超过后拒单,单位 bps |
 | `min_order_interval_ms` | 同账户连续下单的最小间隔 | 防止触发限频或竞态 |
+| `ws_rate_limiter.burst` | 允许的瞬时并发请求数 | 建议 4–10,视交易所限额调整 |
+| `ws_rate_limiter.refill_per_sec` | 每秒补充的令牌数 | 控制平均速率 |
+| `ws_rate_limiter.max_queue_depth` | 排队上限 | 队列溢出会报错,提示降速 |
 
 热更新:支持阈值调整,变更后下一次下单立即生效。
 

+ 224 - 0
docs/CONNECTOR_IMPLEMENTATION_GUIDE.md

@@ -0,0 +1,224 @@
+# Pacifica Connector Implementation Guide
+
+> 面向 `packages/connectors/pacifica`,统一 REST / WebSocket 接口签名、限流与错误处理实现,支撑 M1.6 Placement Throttling、Incremental Grid 等工作流。
+
+---
+
+## 1. 组件总览
+
+| 模块 | 文件 | 说明 |
+|------|------|------|
+| REST Adapter | `packages/connectors/pacifica/src/adapter.ts` | 下单、撤单、批量撤单、账户/仓位查询 |
+| WS Order Gateway | `packages/connectors/pacifica/src/wsOrderGateway.ts` | WebSocket RPC 下单、撤单、订阅 fills/orders |
+| Signing | `packages/connectors/pacifica/src/signing.ts` | Ed25519 签名、header 生成、时钟偏移校验 |
+| Rate Limiter | `packages/connectors/pacifica/src/rateLimiter.ts`(待实现) | 令牌桶(REST / WS 共享);支持突发与排队 |
+| Adapter Registry | `packages/connectors/pacifica/src/adapterRegistry.ts` | 多账户(maker/hedger/...)注册与生命周期管理 |
+
+---
+
+## 2. REST API 调用示例
+
+### 2.1 创建订单(POST `/api/v1/orders`)
+
+```ts
+const payload = {
+  symbol: "BTC",
+  side: "buy",
+  price: "12345.6",
+  amount: "0.01",
+  tif: "GTC",
+  post_only: true,
+  reduce_only: false,
+  client_order_id: "grid-BTC--3-1696766400000-0"
+};
+
+const { headers, body } = signRequest({
+  method: "POST",
+  path: "/api/v1/orders",
+  payload,
+  apiKey: account.apiKey,
+  secret: account.secret
+});
+
+const res = await fetch(`${baseUrl}/orders`, {
+  method: "POST",
+  headers,
+  body: JSON.stringify(payload),
+  signal: abortController.signal
+});
+```
+
+- **签名要点**  
+  - `timestamp = Date.now()`;`expiry_window = 5000ms`(可配置)。  
+  - 签名字段:`{ method, path, queryString, body, timestamp, expiry_window }`。  
+  - Header:  
+    ```
+    X-Pacific-Key: <apiKey>
+    X-Pacific-Timestamp: <timestamp>
+    X-Pacific-Expiry-Window: <expiry_window>
+    X-Pacific-Signature: <hex/base64 signature>
+    ```
+
+### 2.2 撤单(DELETE `/api/v1/orders/{order_id}`)
+
+```ts
+const path = `/api/v1/orders/${orderId}`;
+const { headers } = signRequest({ method: "DELETE", path, apiKey, secret });
+await fetch(`${baseUrl}${path}`, { method: "DELETE", headers });
+```
+
+### 2.3 按 ClientId 撤单(DELETE `/api/v1/orders/by_client_id/{client_id}`)
+
+同上,`path = /api/v1/orders/by_client_id/${clientId}`。  
+注意 `clientId` 需 URI encode。
+
+### 2.4 批量撤单(POST `/api/v1/orders/cancel_batch`)
+
+```ts
+const payload = { symbol: "BTC", order_ids: ["..."], client_order_ids: ["..."] };
+const { headers, body } = signRequest({ method: "POST", path: "/api/v1/orders/cancel_batch", payload, apiKey, secret });
+await fetch(`${baseUrl}/orders/cancel_batch`, { method: "POST", headers, body });
+```
+
+> **返回体统一处理**:解析 JSON;若 `error.code` 存在则抛出 `ConnectorError`,包含 `code` / `message` / `status`。
+
+---
+
+## 3. WebSocket 订单网关
+
+### 3.1 建连与认证
+
+```ts
+const ws = new PacificaWebSocket({
+  url: wsUrl,
+  apiKey,
+  secret,
+  heartbeatIntervalMs: 30_000,
+  reconnectBackoff: [1000, 2000, 5000, 10000]
+});
+
+await ws.connect();          // 建立 socket
+await ws.waitForOpen();      // 等待 OPEN
+await ws.authenticate();     // 发送 auth 消息,携带签名与时间戳
+```
+
+认证 payload 与 REST 类似:  
+`{ type: "auth", account: apiKey, timestamp, expiry_window, signature }`
+
+### 3.2 RPC 模式
+
+WebSocket 使用 request/response(JSON-RPC 风格):
+
+```ts
+const payload = {
+  symbol: "BTC",
+  side: "bid",                 // bid/ask
+  price: "12345.6",
+  amount: "0.01",
+  tif: "GTC",
+  client_order_id: "grid-BTC--3-1696766400000-0"
+};
+const res = await ws.sendRpc("create_order", payload, timeoutMs);
+```
+
+- **请求体**:与 REST 一致,字段名改为 `snake_case`(参考 Pacifica 官方文档)。  
+- **响应**:如果成功,返回 `{ order_id: "..." }`;失败则包含 `error` 字段。  
+- **错误处理**:  
+  - `error.code = "post_only_cross"` → 调整价格重试;  
+  - `error.code = "rate_limit_exceeded"` → 进入排队/回退。  
+- `timeoutMs` 默认 15 秒,可按 Workstream 2 需求调整。
+
+### 3.3 事件订阅
+
+```ts
+ws.subscribeAuthenticated(`orders.${apiKey}`);
+ws.subscribeAuthenticated(`fills.${apiKey}`);
+ws.on("message", handlePrivateMessage);
+```
+
+`handlePrivateMessage` 负责解析 channel、路由到 runner 的 `routeOrder` / `routeFill`。
+
+### 3.4 断线与重连
+
+- 心跳:客户端每 30 秒发送一次 `{ type: "ping" }`,收到 `{ type: "pong" }` 即认为连接有效。  
+- 重连策略:指数退避(1s→2s→5s→10s),超过 5 次失败触发告警。  
+- 重连后需重新 `authenticate`,并补发订阅。  
+- 未完成的 RPC 需在重连后回调失败,交由上层重试。
+
+---
+
+## 4. Rate Limiter(令牌桶)
+
+### 4.1 需求
+
+- REST:每账户每 endpoint 需控制突发请求数,避免 429;默认 `burst = 10`、`refill = 10/秒`。  
+- WS:Placement Throttling 2.0 需要可配置的令牌桶,支持 `burst`、`refillMs`、`maxQueueDepth`。
+
+### 4.2 接口
+
+```ts
+interface RateLimiter {
+  acquire(tokens?: number): Promise<void>;     // 获取令牌,若不足则等待
+  tryAcquire(tokens?: number): boolean;        // 无等待的尝试
+  pending(): number;                           // 当前排队任务数
+}
+```
+
+- 实现放置在 `packages/connectors/pacifica/src/rateLimiter.ts`,供 REST/WS 共享。  
+- 需暴露 metrics(待定):`connector_rate_limiter_tokens`, `connector_rate_limiter_queue`.
+
+### 4.3 集成点
+
+- REST Adapter:在每个请求前调用 `rateLimiter.acquire()`;超时则抛出 `RateLimitError`。  
+- WS Gateway:发送 RPC 前 acquire;Placement Throttling 2.0 会用批次调度采集数据。  
+- 配置:  
+  ```yaml
+  execution:
+    rest_rate_limiter:
+      burst: 10
+      refill_per_sec: 8
+    ws_rate_limiter:
+      burst: 6
+      refill_per_sec: 5
+      max_queue_depth: 50
+  ```
+
+---
+
+## 5. 错误处理与重试
+
+| 错误类型 | 行为 | 备注 |
+|----------|------|------|
+| 429 / `rate_limit_exceeded` | 指数退避重试(100ms → 200ms → ...,最多 5 次) | 超过次数后抛给上层触发降级 |
+| 5xx / 网络错误 | 同上,若连续失败 3 次 → 触发告警 | |
+| `post_only_cross` | 调整价格(spread + cushion)重试一次 | 超过次数则上抛 |
+| 订单不存在 | 视为成功(撤单幂等) | |
+| 签名错误 / 401 | 立即触发 Kill-Switch | 需检查时间同步 |
+
+---
+
+## 6. 指标与日志
+
+- `connector_rest_latency_ms{method, path, status}`:REST RTT  
+- `connector_ws_rpc_latency_ms{method, status}`:WS RPC RTT  
+- `connector_rate_limit_retries_total{method}`:限流重试次数  
+- `connector_ws_reconnects_total`:重连次数  
+- Pino 日志字段:`component: 'PacificaConnector'`, `action: 'rest_request' | 'ws_rpc' | 'ws_reconnect'`
+
+---
+
+## 7. 测试清单
+
+1. **签名单元测试**:`signing.ts` 生成的 header 与官方示例一致;过期签名被拒。  
+2. **REST 集成测试**:Mock Pacifica API,对创建/撤单/批量撤单/错误码逐一验证。  
+3. **WS 集成测试**:模拟 RPC 成功、post-only、限流、超时;验证重连逻辑与排队状态。  
+4. **Rate Limiter 压测**:在 1s 内提交 50 个请求,确认突发被平滑、队列不溢出。  
+5. **日志/指标**:保证上述 metric/log 在测试环境可以采集。
+
+---
+
+## 8. 未决事项
+
+- 官方 API 若新增 `modify_order`,可直接替换 `cancel+place` 模式;否则需在 Workstream 1 中谨慎实现原子性。  
+- 若 Pacifica 提供批量 create/cancel 接口,可追加专用适配器以减少 RTT。  
+- 真实限流参数(burst/refill)需与官方确认并写入 README/CONFIG_REFERENCE。

+ 97 - 0
docs/EXTREME_MODE_RISK_GUIDE.md

@@ -0,0 +1,97 @@
+# Extreme Micro-Grid Risk Guide
+
+**版本**: v0.1  
+**日期**: 2025-10-09  
+**适用范围**: “贴盘口”极限网格配置(步长 1–2 bps、post-only cushion ≤ 1 bps)
+
+---
+
+## 1. 场景定义
+
+- 内圈实例(`maker-inner`)以 1–2 bps 步长挂单,几乎贴合最优价。  
+- 外圈实例(`maker-outer`)负责覆盖更远层级,并在波动扩大时接棒。  
+- 预期成交频率高、库存波动剧烈,对交易所限流与对冲链路要求极高。
+
+---
+
+## 2. 主要风险
+
+| 风险类别 | 描述 | 缓解措施 |
+|----------|------|-----------|
+| 限流 / 延迟 | 大量并行下单导致 WS RPC / REST 被限流、RTT 上升 | Placement Throttling 2.0、令牌桶、批次调度;超时自动降级 |
+| Post-only 失败 | 布单贴近盘口,行情跳动导致穿价 | 调整 `post_only_cushion_bps`(≥0.2 bps);增加 retry,失败达阈值切回常规模式 |
+| 库存/对冲压力 | 成交密集,库存偏移快速放大 | 降低 `hedge_threshold_base`、提高 hedger 账户限额、优化 Hedge Engine 节流 |
+| 监控噪声 | 极短步长导致指标波动大,误触发告警 | 为极限实例单独设定告警阈值(placement latency、pending levels 等) |
+| 多实例互踩 | 内/外圈可能在相近价位冲突 | GlobalOrderCoordinator STP + per-instance 步长/缓冲约束 |
+| 运维复杂度 | 自动降级与回滚流程复杂 | 提前定义 playbook;使用运维接口 `fleet:degrade` / `fleet:resume` |
+
+---
+
+## 3. 配置建议
+
+### 3.1 内圈实例
+- `grid_step_bps`: 1.0–2.0  
+- `post_only_cushion_bps`: 0.2–0.5  
+- `min_layers`: ≥ 12  
+- `tick_interval_ms`: 20–30 s  
+- `fill_starvation_threshold`: `ticks=4`, `compress_factor=0.5`  
+- `hedge_threshold_base`: 0.05–0.1  
+- 限流:`ws_rate_limiter { burst=6, refill=5 }`
+
+### 3.2 外圈实例
+- `grid_step_bps`: 6–8  
+- `post_only_cushion_bps`: 1.5–2.0  
+- `min_layers`: ≥ 8  
+- `grid_range_bps`: 200–240  
+- `hedge_threshold_base`: 0.12–0.2  
+- 限流:`ws_rate_limiter { burst=8, refill=6 }`
+
+### 3.3 账户分配
+- **maker-inner**:专门用于内圈,post-only 缓冲极小;API key 限额需提前申请。  
+- **maker-outer**:负责外围网格,步长较宽,承受较少限流冲击。  
+- **hedger**:保持更高 `qmax`,且支持更快节流。
+
+---
+
+## 4. 指标与阈值
+
+| 指标 | 建议阈值(内圈) | 说明 |
+|------|------------------|------|
+| `placement_latency_p95` | ≤ 2s | 超过则触发降级,将步长恢复到常规模式 |
+| `order_gateway_queue_depth` | ≤ 40 | WS 队列深度超限 → 延迟侧告警 |
+| `grid_pending_levels` | ≤ 1 | >1 连续 2 tick 视为增量补单落后 |
+| `fill_interval_seconds` | ≤ 15s | 超过则触发成交驱动压缩策略 |
+| `stp_conflicts_total` | 不递增 | 冲突表明内外圈步长配置需调整 |
+
+外圈实例阈值可放宽(如 latency ≤ 4s、pending ≤ 2)。
+
+---
+
+## 5. 降级 / 回滚流程
+
+1. 自动检测指标超阈值 → 通过 FleetController 调用 `POST /api/fleet/instances/{id}/degrade`:  
+   - 将内圈步长加倍、post-only cushion 调至 1 bps;  
+   - 降低层数至 6;  
+   - 停止成交驱动压缩。  
+2. 若 10 分钟内未恢复 → 暂停内圈实例 (`/pause`),仅保留外圈。  
+3. 记录事故:placement latency、限流次数、post-only 失败率。  
+4. 复盘后通过 `/resume` 恢复,并更新配置/阈值。
+
+---
+
+## 6. 实验与验证
+
+- 在测试网或低额度账户运行 `config/examples/micro_grid_extreme.yaml`,记录 24 小时指标:  
+  - RPC RTT 分布、post-only 成功率、成交间隔。  
+  - 限流日志与降级触发次数。  
+- 真实环境部署前需进行回测:复用 Backtest Framework,注入 1–2 bps 步长的延迟分布。  
+- 金丝雀流程:先仅启用外圈,再逐步上线内圈,观察 6 小时后再扩大资产。
+
+---
+
+## 7. 运维提醒
+
+- 设置专项告警渠道(Slack/钉钉)用于极限实例。  
+- 日志标签:`component=GridFleet`, `instance=btc-inner`,方便过滤。  
+- 需要固定值守窗口,观察首日运行状况。  
+- 保持与交易所沟通限流阈值,以便调整 burst/refill。

+ 2016 - 0
docs/M16_FILL_DRIVEN_TIGHTENING_DESIGN.md

@@ -0,0 +1,2016 @@
+# M1.6 Workstream 3: Fill-Driven Adaptive Tightening Design
+
+> **文档版本**: v1.0
+> **创建日期**: 2025-10-09
+> **依赖文档**: `M16_INCREMENTAL_GRID_DESIGN.md`, `M16_PLACEMENT_THROTTLING_DESIGN.md`, `CONFIG_REFERENCE.md`
+
+---
+
+## 1. 问题背景与动机
+
+### 1.1 实盘观察到的成交缺失现象
+
+从实际运行日志分析(`logs/runner-2025-10-08T14-08-47-352Z.log`)中发现:
+
+```json
+{"level":30,"time":1759932589213,"component":"GridMaker","symbol":"BTC","filledGrids":0,"totalOrders":20,"msg":"Grid status"}
+{"level":30,"time":1759932649215,"component":"GridMaker","symbol":"BTC","filledGrids":0,"totalOrders":14,"msg":"Grid status"}
+{"level":30,"time":1759932709217,"component":"GridMaker","symbol":"BTC","filledGrids":0,"totalOrders":8,"msg":"Grid status"}
+```
+
+**核心问题**:
+1. **成交率为零**:连续 20+ 分钟运行,`filledGrids: 0`,无任何成交
+2. **订单量递减**:自适应步长增大导致层数下降(20→14→8 orders)
+3. **挂单距离过远**:初始步长 30 bps → 调整到 10 bps 后又很快回到 26 bps → 41 bps
+4. **与盘口脱节**:在波动率下降时,网格步长反而增大,导致订单远离成交区
+
+### 1.2 成交缺失的根本原因
+
+#### 原因一:步长与波动率的反向关系失效
+当前自适应逻辑:
+```typescript
+const targetStep = mapVolatilityToStep(hourlyVolBps, minVolBps, maxVolBps, minStepBps, maxStepBps);
+```
+
+**问题**:
+- 波动率计算基于 30 分钟滚动窗口的价格范围
+- 当价格进入盘整期(窄幅震荡)时,`hourlyVolBps` 下降 → `targetStep` **增大**
+- 步长增大导致订单远离当前价格,成交概率进一步降低
+- 形成**负反馈循环**:无成交 → 盘整 → 步长扩大 → 更无成交
+
+#### 原因二:缺少基于实际成交的反馈机制
+现有系统仅根据**市场波动率**调整参数,未考虑**策略自身的成交表现**:
+- 无成交时,系统不知道是"挂单距离过远"还是"市场流动性不足"
+- 成交恢复后,系统无法快速调整回紧密网格
+- 缺少"探索-利用"平衡:无成交时应该**收紧探测成交区域**,而非扩大
+
+#### 原因三:Post-Only 保护与贴盘口的矛盾
+```yaml
+adaptive:
+  post_only_cushion_bps: 2  # 为避免 post-only 拒单,在盘口价差基础上额外后退 2 bps
+```
+
+**权衡困境**:
+- `cushion_bps` 过大 → 订单远离盘口 → 低成交率
+- `cushion_bps` 过小 → post-only 拒单率上升 → 频繁重试 → 触发限流
+- 无成交时需要**动态降低 cushion**,主动靠近盘口探测成交边界
+
+### 1.3 设计目标
+
+引入**成交驱动的自适应机制**,使系统在无成交时主动收紧网格,恢复成交后渐进回调:
+
+| 指标 | 目标 |
+|------|------|
+| **填充率恢复时间** | 连续无成交 5 tick(~3 分钟)后触发首次收紧;10 tick 内恢复成交 |
+| **参数调整幅度** | 单次压缩/扩张步长 ≤ 20%;总压缩幅度 ≤ 50% |
+| **与限流协同** | 收紧动作需经过 ThrottledGateway 批次执行,避免突发请求 |
+| **多实例隔离** | 多账户/标的实例独立决策,通过 `GlobalOrderCoordinator` 检测冲突 |
+| **可观测性** | 新增指标:`fill_count_tick`, `avg_fill_interval_sec`, `compression_factor`, `no_fill_duration_sec` |
+| **可回滚性** | 提供手动/自动禁用开关,防止极端市况下过度激进 |
+
+---
+
+## 2. 核心机制设计
+
+### 2.1 成交监控指标
+
+#### 2.1.1 单 Tick 成交计数
+```typescript
+interface FillMetrics {
+  tickIndex: number;              // 当前 tick 序号(从启动开始递增)
+  fillCountThisTick: number;      // 本 tick 内成交订单数
+  fillVolumeUsd: number;          // 本 tick 内成交金额(USD)
+  avgFillIntervalSec: number;     // 滚动平均成交间隔(秒)
+  lastFillTimestamp: number;      // 上次成交时间戳
+  noFillDurationSec: number;      // 距上次成交经过的秒数
+  consecutiveEmptyTicks: number;  // 连续无成交 tick 数
+}
+```
+
+#### 2.1.2 滚动窗口计算
+```typescript
+class FillRateTracker {
+  private fillHistory: Array<{ ts: number; count: number }> = [];
+  private readonly windowSize = 10; // 最近 10 tick(~5 分钟)
+
+  recordFill(timestamp: number): void {
+    this.fillHistory.push({ ts: timestamp, count: 1 });
+    this.pruneOldEntries(timestamp);
+  }
+
+  private pruneOldEntries(now: number): void {
+    const cutoff = now - this.windowSize * 60_000; // 10 tick × 60s
+    this.fillHistory = this.fillHistory.filter(entry => entry.ts > cutoff);
+  }
+
+  getAvgFillIntervalSec(): number {
+    if (this.fillHistory.length < 2) return Infinity;
+    const sortedTs = this.fillHistory.map(e => e.ts).sort();
+    const intervals = sortedTs.slice(1).map((ts, i) => ts - sortedTs[i]);
+    return intervals.reduce((a, b) => a + b, 0) / intervals.length / 1000;
+  }
+
+  getNoFillDurationSec(now: number): number {
+    if (this.fillHistory.length === 0) return Infinity;
+    const lastFill = Math.max(...this.fillHistory.map(e => e.ts));
+    return (now - lastFill) / 1000;
+  }
+
+  getConsecutiveEmptyTicks(tickIndex: number): number {
+    const recentTicks = new Set(
+      this.fillHistory
+        .filter(e => e.ts > Date.now() - this.windowSize * 60_000)
+        .map(e => Math.floor(e.ts / 60_000)) // 按分钟归类
+    );
+    let count = 0;
+    for (let i = tickIndex; i > tickIndex - this.windowSize && i >= 0; i--) {
+      if (!recentTicks.has(i)) count++;
+      else break;
+    }
+    return count;
+  }
+}
+```
+
+### 2.2 压缩策略算法
+
+#### 2.2.1 触发条件
+```typescript
+interface CompressionTrigger {
+  consecutiveEmptyTicks: number;      // 连续无成交 tick 数
+  noFillDurationSec: number;          // 距上次成交秒数
+  currentSpreadBps: number;           // 当前盘口价差
+  avgFillIntervalSec: number;         // 平均成交间隔
+}
+
+function shouldCompress(metrics: FillMetrics, config: CompressionConfig): boolean {
+  const {
+    fillStarvationThreshold,    // 默认 5 tick
+    minNoFillDurationSec,       // 默认 180 秒(3 分钟)
+    minSpreadForCompression     // 默认 10 bps(价差过窄不压缩)
+  } = config;
+
+  // 条件 1:连续无成交 tick 超过阈值
+  if (metrics.consecutiveEmptyTicks < fillStarvationThreshold) {
+    return false;
+  }
+
+  // 条件 2:距上次成交时间足够长
+  if (metrics.noFillDurationSec < minNoFillDurationSec) {
+    return false;
+  }
+
+  // 条件 3:盘口价差足够宽(避免在极窄价差时过度贴盘)
+  if (metrics.currentSpreadBps < minSpreadForCompression) {
+    return false;
+  }
+
+  // 条件 4:非降级模式(数据断流、对冲失败时禁用压缩)
+  if (this.degradationState.isActive()) {
+    return false;
+  }
+
+  return true;
+}
+```
+
+#### 2.2.2 压缩执行逻辑
+```typescript
+interface CompressionAction {
+  type: 'step' | 'range' | 'cushion' | 'layers';
+  from: number;
+  to: number;
+  factor: number;
+}
+
+class FillDrivenAdaptation {
+  private compressionFactor = 1.0; // 累积压缩因子(1.0 = 无压缩)
+  private readonly maxCompressionFactor = 0.5; // 最多压缩到 50%
+  private readonly compressionStepSize = 0.85; // 每次压缩 15%
+
+  async compress(metrics: FillMetrics): Promise<CompressionAction[]> {
+    const actions: CompressionAction[] = [];
+
+    // 动作 1:减小 grid_step_bps
+    const currentStep = this.gridMaker.currentGridStepBps;
+    const targetStep = Math.max(
+      this.config.adaptive.minGridStepBps,
+      currentStep * this.compressionStepSize
+    );
+
+    if (targetStep < currentStep - 1e-6) {
+      actions.push({
+        type: 'step',
+        from: currentStep,
+        to: targetStep,
+        factor: targetStep / currentStep
+      });
+      this.logger.info({ currentStep, targetStep }, 'Compressing grid step due to no fills');
+    }
+
+    // 动作 2:收紧 post_only_cushion_bps
+    const currentCushion = this.config.adaptive.postOnlyCushionBps;
+    const targetCushion = Math.max(0, currentCushion - 1);
+    if (targetCushion < currentCushion) {
+      actions.push({
+        type: 'cushion',
+        from: currentCushion,
+        to: targetCushion,
+        factor: targetCushion / (currentCushion || 1)
+      });
+    }
+
+    // 动作 3:增加 min_layers(保持总覆盖档位)
+    const currentMinLayers = this.config.adaptive.minLayers;
+    const targetMinLayers = Math.min(
+      this.config.maxLayers,
+      Math.ceil(currentMinLayers * 1.15)
+    );
+    if (targetMinLayers > currentMinLayers) {
+      actions.push({
+        type: 'layers',
+        from: currentMinLayers,
+        to: targetMinLayers,
+        factor: targetMinLayers / currentMinLayers
+      });
+    }
+
+    // 更新累积压缩因子
+    this.compressionFactor *= this.compressionStepSize;
+    this.compressionFactor = Math.max(this.maxCompressionFactor, this.compressionFactor);
+
+    return actions;
+  }
+
+  async expand(metrics: FillMetrics): Promise<void> {
+    // 成交恢复后渐进回调
+    if (metrics.fillCountThisTick > 0 && this.compressionFactor < 1.0) {
+      const expansionStep = 1.05; // 每次放宽 5%
+      this.compressionFactor = Math.min(1.0, this.compressionFactor * expansionStep);
+
+      this.logger.info({
+        compressionFactor: this.compressionFactor,
+        fillCount: metrics.fillCountThisTick
+      }, 'Expanding grid parameters after fills');
+
+      // 渐进恢复 grid_step 和 cushion
+      await this.applyExpansion(this.compressionFactor);
+    }
+  }
+
+  private async applyExpansion(factor: number): Promise<void> {
+    const baseStep = this.config.gridStepBps;
+    const baseCushion = this.config.adaptive.postOnlyCushionBps;
+
+    const targetStep = baseStep * factor;
+    const targetCushion = Math.round(baseCushion * factor);
+
+    // 使用增量引擎更新网格(避免全撤全布)
+    await this.gridMaker.updateGridParameters({
+      gridStepBps: targetStep,
+      postOnlyCushionBps: targetCushion
+    });
+  }
+}
+```
+
+### 2.3 防止过度压缩的保护机制
+
+#### 2.3.1 压缩次数限制
+```typescript
+interface CompressionLimits {
+  maxCompressionsPerWindow: number;   // 10 分钟窗口内最多压缩次数
+  windowSizeMinutes: number;          // 默认 10 分钟
+  cooldownAfterCompressionSec: number; // 压缩后冷却时间
+}
+
+class CompressionRateLimiter {
+  private compressionHistory: number[] = [];
+
+  canCompress(now: number, limits: CompressionLimits): boolean {
+    this.pruneOldCompressions(now, limits.windowSizeMinutes);
+
+    if (this.compressionHistory.length >= limits.maxCompressionsPerWindow) {
+      this.logger.warn({
+        count: this.compressionHistory.length,
+        limit: limits.maxCompressionsPerWindow
+      }, 'Compression rate limit exceeded');
+      return false;
+    }
+
+    // 检查冷却时间
+    if (this.compressionHistory.length > 0) {
+      const lastCompression = Math.max(...this.compressionHistory);
+      if (now - lastCompression < limits.cooldownAfterCompressionSec * 1000) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  recordCompression(now: number): void {
+    this.compressionHistory.push(now);
+  }
+
+  private pruneOldCompressions(now: number, windowMinutes: number): void {
+    const cutoff = now - windowMinutes * 60_000;
+    this.compressionHistory = this.compressionHistory.filter(ts => ts > cutoff);
+  }
+}
+```
+
+#### 2.3.2 硬性参数下限
+```typescript
+interface ParameterFloor {
+  minAbsoluteStepBps: number;    // 绝对最小步长(如 1 bps)
+  minAbsoluteCushionBps: number; // 绝对最小缓冲(如 0 bps)
+  minLayersFloor: number;        // 最少保留层数(如 4)
+}
+
+function enforceFloor(value: number, floor: number, name: string): number {
+  if (value < floor) {
+    this.logger.warn({ value, floor, name }, 'Parameter hit floor, clamping');
+    return floor;
+  }
+  return value;
+}
+```
+
+### 2.4 与限流系统的协同
+
+#### 2.4.1 批次执行压缩动作
+```typescript
+async executeCompression(actions: CompressionAction[]): Promise<void> {
+  // 先更新内存配置
+  for (const action of actions) {
+    this.applyActionToConfig(action);
+  }
+
+  // 通过增量引擎 + 节流网关执行订单更新
+  const targetPrices = this.calculateNewGridPrices();
+  const reconcileOutput = await this.gridMaker.reconcileGrid(targetPrices);
+
+  // 分批执行(复用 M16_INCREMENTAL_GRID_DESIGN 的批次逻辑)
+  await this.gridMaker.executeBatchedReconcile(reconcileOutput, {
+    cancelBatchSize: 5,
+    placeBatchSize: 8,
+    batchDelayMs: 100
+  });
+
+  this.metrics.compressionExecutionLatencyMs = Date.now() - startTime;
+}
+```
+
+#### 2.4.2 Quota 预算检查
+```typescript
+async checkThrottleQuota(actions: CompressionAction[]): Promise<boolean> {
+  const estimatedOrderCount = this.estimateOrderChanges(actions);
+
+  // 查询当前令牌桶余额
+  const availableTokens = this.throttledGateway.getAvailableTokens();
+
+  if (availableTokens < estimatedOrderCount * 0.5) {
+    this.logger.warn({
+      estimatedOrders: estimatedOrderCount,
+      availableTokens
+    }, 'Insufficient throttle quota, deferring compression');
+    return false;
+  }
+
+  return true;
+}
+```
+
+---
+
+## 3. 配置 Schema
+
+### 3.1 YAML 配置定义
+```yaml
+grid:
+  adaptive:
+    # 成交驱动压缩配置
+    fill_driven:
+      enabled: true
+
+      # 触发条件
+      fill_starvation_threshold: 5           # 连续无成交 tick 阈值
+      min_no_fill_duration_sec: 180          # 最小无成交持续时间(秒)
+      min_spread_for_compression_bps: 10     # 最小价差阈值(避免极窄价差时压缩)
+
+      # 压缩策略
+      compression_step_size: 0.85            # 每次压缩因子(0.85 = 压缩 15%)
+      max_compression_factor: 0.5            # 最大累积压缩(0.5 = 压缩到 50%)
+      expansion_step_size: 1.05              # 恢复时每次扩张因子(1.05 = 放宽 5%)
+
+      # 速率限制
+      max_compressions_per_window: 3         # 10 分钟窗口内最多压缩次数
+      window_size_minutes: 10                # 压缩次数统计窗口
+      cooldown_after_compression_sec: 120    # 压缩后冷却时间(秒)
+
+      # 参数下限保护
+      min_absolute_step_bps: 1               # 绝对最小步长(硬限)
+      min_absolute_cushion_bps: 0            # 绝对最小缓冲(硬限)
+      min_layers_floor: 4                    # 最少保留层数(硬限)
+
+      # 指标计算
+      fill_interval_window_ticks: 10         # 平均成交间隔统计窗口(tick 数)
+      metrics_log_interval_ms: 60000         # 指标日志输出间隔(1 分钟)
+```
+
+### 3.2 Zod Schema 验证
+```typescript
+import { z } from 'zod';
+
+const FillDrivenConfigSchema = z.object({
+  enabled: z.boolean().default(true),
+
+  // Trigger conditions
+  fill_starvation_threshold: z.number().int().min(3).max(20).default(5),
+  min_no_fill_duration_sec: z.number().int().min(60).max(600).default(180),
+  min_spread_for_compression_bps: z.number().min(5).max(50).default(10),
+
+  // Compression strategy
+  compression_step_size: z.number().min(0.7).max(0.95).default(0.85),
+  max_compression_factor: z.number().min(0.3).max(0.8).default(0.5),
+  expansion_step_size: z.number().min(1.01).max(1.15).default(1.05),
+
+  // Rate limiting
+  max_compressions_per_window: z.number().int().min(1).max(10).default(3),
+  window_size_minutes: z.number().int().min(5).max(30).default(10),
+  cooldown_after_compression_sec: z.number().int().min(60).max(600).default(120),
+
+  // Floor protection
+  min_absolute_step_bps: z.number().min(0.5).max(5).default(1),
+  min_absolute_cushion_bps: z.number().min(0).max(3).default(0),
+  min_layers_floor: z.number().int().min(2).max(8).default(4),
+
+  // Metrics
+  fill_interval_window_ticks: z.number().int().min(5).max(20).default(10),
+  metrics_log_interval_ms: z.number().int().min(10000).max(300000).default(60000)
+});
+
+export type FillDrivenConfig = z.infer<typeof FillDrivenConfigSchema>;
+
+// 与现有 GridConfig 集成
+const GridConfigSchema = z.object({
+  // ... 其他字段
+  adaptive: z.object({
+    // ... 其他字段
+    fill_driven: FillDrivenConfigSchema.optional()
+  }).optional()
+});
+```
+
+### 3.3 配置加载与验证
+```typescript
+import { parse as parseYaml } from 'yaml';
+import { readFileSync } from 'fs';
+
+export class ConfigLoader {
+  static loadAndValidate(configPath: string): GridConfig {
+    const raw = readFileSync(configPath, 'utf-8');
+    const parsed = parseYaml(raw);
+
+    try {
+      const validated = GridConfigSchema.parse(parsed.grid);
+      this.logger.info({ configPath }, 'Grid config validated successfully');
+      return validated;
+    } catch (error) {
+      if (error instanceof z.ZodError) {
+        this.logger.error({
+          errors: error.errors,
+          configPath
+        }, 'Grid config validation failed');
+        throw new Error(`Config validation failed: ${error.message}`);
+      }
+      throw error;
+    }
+  }
+
+  static applyDefaults(config: Partial<FillDrivenConfig>): FillDrivenConfig {
+    return FillDrivenConfigSchema.parse(config);
+  }
+}
+```
+
+---
+
+## 4. 模块接口
+
+### 4.1 FillRateTracker
+```typescript
+export interface FillEvent {
+  timestamp: number;
+  orderId: string;
+  side: 'bid' | 'ask';
+  fillPx: number;
+  fillQty: number;
+  fillValueUsd: number;
+}
+
+export interface FillMetrics {
+  tickIndex: number;
+  fillCountThisTick: number;
+  fillVolumeUsd: number;
+  avgFillIntervalSec: number;
+  lastFillTimestamp: number;
+  noFillDurationSec: number;
+  consecutiveEmptyTicks: number;
+}
+
+export class FillRateTracker {
+  private fillHistory: FillEvent[] = [];
+  private tickIndex = 0;
+  private config: FillDrivenConfig;
+
+  constructor(config: FillDrivenConfig) {
+    this.config = config;
+  }
+
+  recordFill(event: FillEvent): void {
+    this.fillHistory.push(event);
+    this.pruneOldEntries(event.timestamp);
+  }
+
+  onTickStart(): void {
+    this.tickIndex++;
+  }
+
+  getMetrics(now: number): FillMetrics {
+    const windowStart = now - this.config.fill_interval_window_ticks * 60_000;
+    const recentFills = this.fillHistory.filter(e => e.timestamp > windowStart);
+
+    return {
+      tickIndex: this.tickIndex,
+      fillCountThisTick: recentFills.filter(e => e.timestamp > now - 60_000).length,
+      fillVolumeUsd: recentFills.reduce((sum, e) => sum + e.fillValueUsd, 0),
+      avgFillIntervalSec: this.calculateAvgInterval(recentFills),
+      lastFillTimestamp: recentFills.length > 0
+        ? Math.max(...recentFills.map(e => e.timestamp))
+        : 0,
+      noFillDurationSec: this.calculateNoFillDuration(now),
+      consecutiveEmptyTicks: this.calculateConsecutiveEmpty(now)
+    };
+  }
+
+  private calculateAvgInterval(fills: FillEvent[]): number {
+    if (fills.length < 2) return Infinity;
+    const sorted = fills.map(e => e.timestamp).sort();
+    const intervals = sorted.slice(1).map((ts, i) => ts - sorted[i]);
+    return intervals.reduce((a, b) => a + b, 0) / intervals.length / 1000;
+  }
+
+  private calculateNoFillDuration(now: number): number {
+    if (this.fillHistory.length === 0) return Infinity;
+    const lastFill = Math.max(...this.fillHistory.map(e => e.timestamp));
+    return (now - lastFill) / 1000;
+  }
+
+  private calculateConsecutiveEmpty(now: number): number {
+    const tickDuration = 60_000; // 1 分钟
+    let count = 0;
+
+    for (let i = 0; i < this.config.fill_interval_window_ticks; i++) {
+      const tickStart = now - (i + 1) * tickDuration;
+      const tickEnd = now - i * tickDuration;
+      const hasFills = this.fillHistory.some(
+        e => e.timestamp >= tickStart && e.timestamp < tickEnd
+      );
+
+      if (!hasFills) {
+        count++;
+      } else {
+        break;
+      }
+    }
+
+    return count;
+  }
+
+  private pruneOldEntries(now: number): void {
+    const cutoff = now - this.config.fill_interval_window_ticks * 60_000;
+    this.fillHistory = this.fillHistory.filter(e => e.timestamp > cutoff);
+  }
+
+  reset(): void {
+    this.fillHistory = [];
+    this.tickIndex = 0;
+  }
+}
+```
+
+### 4.2 FillDrivenAdaptation
+```typescript
+export interface CompressionAction {
+  type: 'step' | 'range' | 'cushion' | 'layers';
+  from: number;
+  to: number;
+  factor: number;
+  reason: string;
+}
+
+export interface AdaptationState {
+  compressionFactor: number;
+  activeActions: CompressionAction[];
+  lastCompressionTime: number;
+  compressionCount: number;
+}
+
+export class FillDrivenAdaptation {
+  private state: AdaptationState = {
+    compressionFactor: 1.0,
+    activeActions: [],
+    lastCompressionTime: 0,
+    compressionCount: 0
+  };
+
+  private rateLimiter: CompressionRateLimiter;
+  private fillTracker: FillRateTracker;
+  private config: FillDrivenConfig;
+  private gridMaker: GridMaker;
+  private logger: Logger;
+
+  constructor(deps: {
+    config: FillDrivenConfig;
+    gridMaker: GridMaker;
+    fillTracker: FillRateTracker;
+    logger: Logger;
+  }) {
+    this.config = deps.config;
+    this.gridMaker = deps.gridMaker;
+    this.fillTracker = deps.fillTracker;
+    this.logger = deps.logger.child({ component: 'FillDrivenAdaptation' });
+    this.rateLimiter = new CompressionRateLimiter(deps.logger);
+  }
+
+  async onTick(bookSnapshot: BookSnapshot): Promise<void> {
+    if (!this.config.enabled) return;
+
+    this.fillTracker.onTickStart();
+    const now = Date.now();
+    const metrics = this.fillTracker.getMetrics(now);
+
+    this.logMetrics(metrics);
+
+    // 检查是否需要压缩
+    if (this.shouldCompress(metrics, bookSnapshot)) {
+      await this.executeCompression(metrics, bookSnapshot);
+    }
+
+    // 检查是否需要扩张(恢复)
+    if (this.shouldExpand(metrics)) {
+      await this.executeExpansion(metrics);
+    }
+  }
+
+  private shouldCompress(metrics: FillMetrics, book: BookSnapshot): boolean {
+    const { fill_starvation_threshold, min_no_fill_duration_sec, min_spread_for_compression_bps } = this.config;
+
+    if (metrics.consecutiveEmptyTicks < fill_starvation_threshold) {
+      return false;
+    }
+
+    if (metrics.noFillDurationSec < min_no_fill_duration_sec) {
+      return false;
+    }
+
+    const spreadBps = ((book.bestAsk - book.bestBid) / book.mid) * 10_000;
+    if (spreadBps < min_spread_for_compression_bps) {
+      this.logger.debug({ spreadBps }, 'Spread too narrow for compression');
+      return false;
+    }
+
+    if (!this.rateLimiter.canCompress(Date.now(), this.config)) {
+      return false;
+    }
+
+    return true;
+  }
+
+  private async executeCompression(metrics: FillMetrics, book: BookSnapshot): Promise<void> {
+    const actions = this.planCompressionActions(metrics, book);
+
+    if (actions.length === 0) {
+      return;
+    }
+
+    this.logger.info({
+      actions,
+      compressionFactor: this.state.compressionFactor,
+      metrics
+    }, 'Executing fill-driven compression');
+
+    // 应用参数变更
+    for (const action of actions) {
+      await this.applyAction(action);
+    }
+
+    // 记录状态
+    this.state.activeActions = actions;
+    this.state.lastCompressionTime = Date.now();
+    this.state.compressionCount++;
+    this.rateLimiter.recordCompression(Date.now());
+
+    // 触发网格更新(通过增量引擎)
+    await this.gridMaker.requestParameterUpdate();
+  }
+
+  private planCompressionActions(metrics: FillMetrics, book: BookSnapshot): CompressionAction[] {
+    const actions: CompressionAction[] = [];
+
+    // 动作 1:减小步长
+    const currentStep = this.gridMaker.currentGridStepBps;
+    const targetStep = Math.max(
+      this.config.min_absolute_step_bps,
+      currentStep * this.config.compression_step_size
+    );
+
+    if (targetStep < currentStep - 1e-6) {
+      actions.push({
+        type: 'step',
+        from: currentStep,
+        to: targetStep,
+        factor: targetStep / currentStep,
+        reason: `No fills for ${metrics.consecutiveEmptyTicks} ticks`
+      });
+    }
+
+    // 动作 2:收紧 cushion
+    const currentCushion = this.gridMaker.config.adaptive.postOnlyCushionBps;
+    const targetCushion = Math.max(
+      this.config.min_absolute_cushion_bps,
+      currentCushion - 1
+    );
+
+    if (targetCushion < currentCushion) {
+      actions.push({
+        type: 'cushion',
+        from: currentCushion,
+        to: targetCushion,
+        factor: targetCushion / (currentCushion || 1),
+        reason: 'Tightening to probe bid/ask boundary'
+      });
+    }
+
+    // 动作 3:增加层数
+    const currentMinLayers = this.gridMaker.config.adaptive.minLayers;
+    const targetMinLayers = Math.min(
+      this.gridMaker.config.maxLayers,
+      Math.ceil(currentMinLayers * 1.1)
+    );
+
+    if (targetMinLayers > currentMinLayers) {
+      actions.push({
+        type: 'layers',
+        from: currentMinLayers,
+        to: targetMinLayers,
+        factor: targetMinLayers / currentMinLayers,
+        reason: 'Increase coverage while compressing step'
+      });
+    }
+
+    // 更新累积压缩因子
+    this.state.compressionFactor = Math.max(
+      this.config.max_compression_factor,
+      this.state.compressionFactor * this.config.compression_step_size
+    );
+
+    return actions;
+  }
+
+  private async applyAction(action: CompressionAction): Promise<void> {
+    switch (action.type) {
+      case 'step':
+        this.gridMaker.currentGridStepBps = action.to;
+        break;
+      case 'cushion':
+        this.gridMaker.config.adaptive.postOnlyCushionBps = action.to;
+        break;
+      case 'layers':
+        this.gridMaker.config.adaptive.minLayers = action.to;
+        break;
+      default:
+        throw new Error(`Unknown action type: ${action.type}`);
+    }
+
+    this.logger.info({ action }, 'Applied compression action');
+  }
+
+  private shouldExpand(metrics: FillMetrics): boolean {
+    // 成交恢复 + 当前处于压缩状态
+    return metrics.fillCountThisTick > 0 && this.state.compressionFactor < 1.0;
+  }
+
+  private async executeExpansion(metrics: FillMetrics): Promise<void> {
+    const oldFactor = this.state.compressionFactor;
+    this.state.compressionFactor = Math.min(
+      1.0,
+      this.state.compressionFactor * this.config.expansion_step_size
+    );
+
+    this.logger.info({
+      oldFactor,
+      newFactor: this.state.compressionFactor,
+      fillCount: metrics.fillCountThisTick
+    }, 'Expanding parameters after fills');
+
+    // 渐进恢复参数
+    await this.applyExpansion();
+  }
+
+  private async applyExpansion(): Promise<void> {
+    const baseStep = this.gridMaker.config.gridStepBps;
+    const baseCushion = this.gridMaker.config.adaptive.postOnlyCushionBps;
+    const baseMinLayers = this.gridMaker.config.adaptive.minLayers;
+
+    const targetStep = baseStep * this.state.compressionFactor;
+    const targetCushion = Math.round(baseCushion * this.state.compressionFactor);
+    const targetMinLayers = Math.max(
+      this.config.min_layers_floor,
+      Math.floor(baseMinLayers * this.state.compressionFactor)
+    );
+
+    this.gridMaker.currentGridStepBps = targetStep;
+    this.gridMaker.config.adaptive.postOnlyCushionBps = targetCushion;
+    this.gridMaker.config.adaptive.minLayers = targetMinLayers;
+
+    await this.gridMaker.requestParameterUpdate();
+  }
+
+  private logMetrics(metrics: FillMetrics): void {
+    if (Date.now() % this.config.metrics_log_interval_ms < 60000) {
+      this.logger.info({
+        metrics,
+        compressionFactor: this.state.compressionFactor,
+        activeActionsCount: this.state.activeActions.length
+      }, 'Fill-driven adaptation metrics');
+    }
+  }
+
+  getState(): AdaptationState {
+    return { ...this.state };
+  }
+
+  reset(): void {
+    this.state = {
+      compressionFactor: 1.0,
+      activeActions: [],
+      lastCompressionTime: 0,
+      compressionCount: 0
+    };
+    this.fillTracker.reset();
+    this.logger.info({}, 'Fill-driven adaptation reset');
+  }
+}
+```
+
+### 4.3 与 GridMaker 集成
+```typescript
+// packages/strategies/src/gridMaker.ts
+
+export class GridMaker {
+  private fillTracker: FillRateTracker;
+  private fillDrivenAdaptation: FillDrivenAdaptation;
+
+  constructor(/* ... existing params */) {
+    // ... existing initialization
+
+    if (this.config.adaptive?.fill_driven?.enabled) {
+      this.fillTracker = new FillRateTracker(this.config.adaptive.fill_driven);
+      this.fillDrivenAdaptation = new FillDrivenAdaptation({
+        config: this.config.adaptive.fill_driven,
+        gridMaker: this,
+        fillTracker: this.fillTracker,
+        logger: this.logger
+      });
+    }
+  }
+
+  async onTick(bookSnapshot: BookSnapshot): Promise<void> {
+    // 1. 记录 tick 开始
+    if (this.fillDrivenAdaptation) {
+      await this.fillDrivenAdaptation.onTick(bookSnapshot);
+    }
+
+    // 2. 执行现有自适应逻辑(波动率调整)
+    await this.runAdaptiveChecks();
+
+    // 3. 执行网格更新(增量引擎)
+    await this.reconcileAndUpdate(bookSnapshot);
+  }
+
+  async onFill(fill: FillEvent): Promise<void> {
+    // ... existing fill handling
+
+    // 记录成交事件
+    if (this.fillTracker) {
+      this.fillTracker.recordFill(fill);
+    }
+  }
+
+  async requestParameterUpdate(): Promise<void> {
+    // 触发网格重算与增量更新
+    const targetPrices = this.calculateGridPrices();
+    const reconcileOutput = await this.reconcileGrid(targetPrices);
+    await this.executeBatchedReconcile(reconcileOutput, this.batchConfig);
+  }
+}
+```
+
+---
+
+## 5. Prometheus 指标
+
+### 5.1 指标定义
+```typescript
+import { Registry, Counter, Gauge, Histogram } from 'prom-client';
+
+export class FillDrivenMetrics {
+  private readonly registry: Registry;
+
+  // 成交计数器
+  readonly fillCount = new Counter({
+    name: 'grid_fill_total',
+    help: 'Total number of grid fills',
+    labelNames: ['symbol', 'side', 'account_id'],
+    registers: [this.registry]
+  });
+
+  // 成交间隔直方图
+  readonly fillInterval = new Histogram({
+    name: 'grid_fill_interval_seconds',
+    help: 'Time between consecutive fills',
+    labelNames: ['symbol'],
+    buckets: [10, 30, 60, 120, 300, 600, 1800, 3600],
+    registers: [this.registry]
+  });
+
+  // 无成交持续时间
+  readonly noFillDuration = new Gauge({
+    name: 'grid_no_fill_duration_seconds',
+    help: 'Seconds since last fill',
+    labelNames: ['symbol'],
+    registers: [this.registry]
+  });
+
+  // 连续空 tick 计数
+  readonly consecutiveEmptyTicks = new Gauge({
+    name: 'grid_consecutive_empty_ticks',
+    help: 'Number of consecutive ticks with no fills',
+    labelNames: ['symbol'],
+    registers: [this.registry]
+  });
+
+  // 压缩因子
+  readonly compressionFactor = new Gauge({
+    name: 'grid_compression_factor',
+    help: 'Current compression factor (1.0 = no compression)',
+    labelNames: ['symbol'],
+    registers: [this.registry]
+  });
+
+  // 压缩动作计数
+  readonly compressionActions = new Counter({
+    name: 'grid_compression_actions_total',
+    help: 'Total compression actions executed',
+    labelNames: ['symbol', 'action_type'],
+    registers: [this.registry]
+  });
+
+  // 扩张动作计数
+  readonly expansionActions = new Counter({
+    name: 'grid_expansion_actions_total',
+    help: 'Total expansion actions executed',
+    labelNames: ['symbol'],
+    registers: [this.registry]
+  });
+
+  // 压缩被限流次数
+  readonly compressionRateLimited = new Counter({
+    name: 'grid_compression_rate_limited_total',
+    help: 'Number of times compression was rate limited',
+    labelNames: ['symbol'],
+    registers: [this.registry]
+  });
+
+  // 当前 grid_step_bps
+  readonly currentGridStep = new Gauge({
+    name: 'grid_step_bps',
+    help: 'Current grid step size in basis points',
+    labelNames: ['symbol'],
+    registers: [this.registry]
+  });
+
+  // 当前 post_only_cushion_bps
+  readonly currentCushion = new Gauge({
+    name: 'grid_cushion_bps',
+    help: 'Current post-only cushion in basis points',
+    labelNames: ['symbol'],
+    registers: [this.registry]
+  });
+
+  // 当前 min_layers
+  readonly currentMinLayers = new Gauge({
+    name: 'grid_min_layers',
+    help: 'Current minimum layer count',
+    labelNames: ['symbol'],
+    registers: [this.registry]
+  });
+
+  constructor(registry: Registry) {
+    this.registry = registry;
+  }
+
+  recordFill(symbol: string, side: string, accountId: string, intervalSec: number): void {
+    this.fillCount.inc({ symbol, side, account_id: accountId });
+    this.fillInterval.observe({ symbol }, intervalSec);
+  }
+
+  updateNoFillDuration(symbol: string, durationSec: number): void {
+    this.noFillDuration.set({ symbol }, durationSec);
+  }
+
+  updateConsecutiveEmptyTicks(symbol: string, count: number): void {
+    this.consecutiveEmptyTicks.set({ symbol }, count);
+  }
+
+  updateCompressionFactor(symbol: string, factor: number): void {
+    this.compressionFactor.set({ symbol }, factor);
+  }
+
+  recordCompressionAction(symbol: string, actionType: string): void {
+    this.compressionActions.inc({ symbol, action_type: actionType });
+  }
+
+  recordExpansionAction(symbol: string): void {
+    this.expansionActions.inc({ symbol });
+  }
+
+  recordRateLimited(symbol: string): void {
+    this.compressionRateLimited.inc({ symbol });
+  }
+
+  updateGridParameters(symbol: string, stepBps: number, cushionBps: number, minLayers: number): void {
+    this.currentGridStep.set({ symbol }, stepBps);
+    this.currentCushion.set({ symbol }, cushionBps);
+    this.currentMinLayers.set({ symbol }, minLayers);
+  }
+}
+```
+
+### 5.2 Metrics 更新逻辑
+```typescript
+// 在 FillDrivenAdaptation 中集成
+export class FillDrivenAdaptation {
+  private metrics: FillDrivenMetrics;
+
+  async onTick(bookSnapshot: BookSnapshot): Promise<void> {
+    const metrics = this.fillTracker.getMetrics(Date.now());
+
+    // 更新指标
+    this.metrics.updateNoFillDuration(this.symbol, metrics.noFillDurationSec);
+    this.metrics.updateConsecutiveEmptyTicks(this.symbol, metrics.consecutiveEmptyTicks);
+    this.metrics.updateCompressionFactor(this.symbol, this.state.compressionFactor);
+    this.metrics.updateGridParameters(
+      this.symbol,
+      this.gridMaker.currentGridStepBps,
+      this.gridMaker.config.adaptive.postOnlyCushionBps,
+      this.gridMaker.config.adaptive.minLayers
+    );
+
+    // ... existing logic
+  }
+
+  private async executeCompression(/* ... */): Promise<void> {
+    // ... existing logic
+
+    for (const action of actions) {
+      this.metrics.recordCompressionAction(this.symbol, action.type);
+    }
+  }
+
+  private async executeExpansion(/* ... */): Promise<void> {
+    // ... existing logic
+
+    this.metrics.recordExpansionAction(this.symbol);
+  }
+}
+
+// 在 GridMaker.onFill 中
+async onFill(fill: FillEvent): Promise<void> {
+  if (this.fillTracker) {
+    const metrics = this.fillTracker.getMetrics(Date.now());
+    this.metrics.recordFill(
+      this.symbol,
+      fill.side,
+      this.accountId,
+      metrics.avgFillIntervalSec
+    );
+  }
+}
+```
+
+---
+
+## 6. Grafana Dashboard
+
+### 6.1 Dashboard JSON 定义
+```json
+{
+  "dashboard": {
+    "title": "Grid Fill-Driven Adaptation",
+    "panels": [
+      {
+        "title": "Fill Rate",
+        "targets": [
+          {
+            "expr": "rate(grid_fill_total[5m])",
+            "legendFormat": "{{symbol}} {{side}}"
+          }
+        ],
+        "type": "graph"
+      },
+      {
+        "title": "No-Fill Duration",
+        "targets": [
+          {
+            "expr": "grid_no_fill_duration_seconds",
+            "legendFormat": "{{symbol}}"
+          }
+        ],
+        "type": "graph",
+        "alert": {
+          "conditions": [
+            {
+              "evaluator": { "params": [300], "type": "gt" },
+              "query": { "params": ["A", "5m", "now"] }
+            }
+          ],
+          "message": "No fills for >5 minutes on {{symbol}}"
+        }
+      },
+      {
+        "title": "Consecutive Empty Ticks",
+        "targets": [
+          {
+            "expr": "grid_consecutive_empty_ticks",
+            "legendFormat": "{{symbol}}"
+          }
+        ],
+        "type": "graph"
+      },
+      {
+        "title": "Compression Factor",
+        "targets": [
+          {
+            "expr": "grid_compression_factor",
+            "legendFormat": "{{symbol}}"
+          }
+        ],
+        "type": "graph",
+        "yaxes": [
+          { "min": 0.3, "max": 1.1 }
+        ]
+      },
+      {
+        "title": "Grid Step Evolution",
+        "targets": [
+          {
+            "expr": "grid_step_bps",
+            "legendFormat": "{{symbol}}"
+          }
+        ],
+        "type": "graph"
+      },
+      {
+        "title": "Post-Only Cushion",
+        "targets": [
+          {
+            "expr": "grid_cushion_bps",
+            "legendFormat": "{{symbol}}"
+          }
+        ],
+        "type": "graph"
+      },
+      {
+        "title": "Min Layers",
+        "targets": [
+          {
+            "expr": "grid_min_layers",
+            "legendFormat": "{{symbol}}"
+          }
+        ],
+        "type": "graph"
+      },
+      {
+        "title": "Compression Actions",
+        "targets": [
+          {
+            "expr": "rate(grid_compression_actions_total[5m])",
+            "legendFormat": "{{action_type}}"
+          }
+        ],
+        "type": "bar"
+      },
+      {
+        "title": "Expansion Actions",
+        "targets": [
+          {
+            "expr": "rate(grid_expansion_actions_total[5m])",
+            "legendFormat": "{{symbol}}"
+          }
+        ],
+        "type": "bar"
+      },
+      {
+        "title": "Rate Limited Events",
+        "targets": [
+          {
+            "expr": "increase(grid_compression_rate_limited_total[10m])",
+            "legendFormat": "{{symbol}}"
+          }
+        ],
+        "type": "stat"
+      }
+    ]
+  }
+}
+```
+
+### 6.2 告警规则
+```yaml
+# prometheus/alerts/fill_driven.yml
+groups:
+  - name: fill_driven_adaptation
+    interval: 30s
+    rules:
+      - alert: GridNoFillsExtended
+        expr: grid_no_fill_duration_seconds > 300
+        for: 2m
+        labels:
+          severity: warning
+        annotations:
+          summary: "Grid no fills for >5 minutes"
+          description: "Symbol {{$labels.symbol}} has not filled any orders for {{$value}}s"
+
+      - alert: GridCompressionStuck
+        expr: grid_compression_factor < 0.6
+        for: 5m
+        labels:
+          severity: warning
+        annotations:
+          summary: "Grid compression factor stuck below 0.6"
+          description: "Symbol {{$labels.symbol}} compression factor: {{$value}}"
+
+      - alert: GridCompressionRateLimitHit
+        expr: increase(grid_compression_rate_limited_total[10m]) > 5
+        labels:
+          severity: info
+        annotations:
+          summary: "Compression rate limit hit frequently"
+          description: "Symbol {{$labels.symbol}} hit rate limit {{$value}} times in 10m"
+
+      - alert: GridStepTooSmall
+        expr: grid_step_bps < 1.5
+        for: 3m
+        labels:
+          severity: warning
+        annotations:
+          summary: "Grid step compressed below safe threshold"
+          description: "Symbol {{$labels.symbol}} step: {{$value}} bps (risk of post-only rejections)"
+```
+
+---
+
+## 7. 测试计划
+
+### 7.1 单元测试
+```typescript
+// packages/strategies/src/__tests__/fillDrivenAdaptation.test.ts
+
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import { FillDrivenAdaptation } from '../fillDrivenAdaptation';
+import { FillRateTracker } from '../fillRateTracker';
+import { MockGridMaker } from './mocks';
+
+describe('FillDrivenAdaptation', () => {
+  let adaptation: FillDrivenAdaptation;
+  let fillTracker: FillRateTracker;
+  let mockGridMaker: MockGridMaker;
+
+  beforeEach(() => {
+    fillTracker = new FillRateTracker({
+      enabled: true,
+      fill_starvation_threshold: 5,
+      min_no_fill_duration_sec: 180,
+      fill_interval_window_ticks: 10,
+      // ... other config
+    });
+
+    mockGridMaker = new MockGridMaker();
+
+    adaptation = new FillDrivenAdaptation({
+      config: fillTracker.config,
+      gridMaker: mockGridMaker,
+      fillTracker,
+      logger: mockLogger
+    });
+  });
+
+  describe('shouldCompress', () => {
+    it('should not compress if consecutive empty ticks below threshold', () => {
+      // Simulate 4 ticks with no fills
+      for (let i = 0; i < 4; i++) {
+        fillTracker.onTickStart();
+      }
+
+      const metrics = fillTracker.getMetrics(Date.now());
+      const book = { bestBid: 100, bestAsk: 101, mid: 100.5 };
+
+      expect(adaptation['shouldCompress'](metrics, book)).toBe(false);
+    });
+
+    it('should compress after 5 consecutive empty ticks', async () => {
+      // Simulate 5 ticks with no fills
+      for (let i = 0; i < 5; i++) {
+        fillTracker.onTickStart();
+        await sleep(60_000); // Simulate 1 minute per tick
+      }
+
+      const metrics = fillTracker.getMetrics(Date.now());
+      const book = { bestBid: 100, bestAsk: 101, mid: 100.5 };
+
+      expect(adaptation['shouldCompress'](metrics, book)).toBe(true);
+    });
+
+    it('should not compress if spread too narrow', () => {
+      for (let i = 0; i < 5; i++) {
+        fillTracker.onTickStart();
+      }
+
+      const metrics = fillTracker.getMetrics(Date.now());
+      const book = { bestBid: 100, bestAsk: 100.05, mid: 100.025 }; // 5 bps spread
+
+      expect(adaptation['shouldCompress'](metrics, book)).toBe(false);
+    });
+  });
+
+  describe('executeCompression', () => {
+    it('should reduce grid_step_bps by compression factor', async () => {
+      mockGridMaker.currentGridStepBps = 30;
+
+      const metrics = {
+        consecutiveEmptyTicks: 5,
+        noFillDurationSec: 300,
+        /* ... */
+      };
+      const book = { bestBid: 100, bestAsk: 101, mid: 100.5 };
+
+      await adaptation['executeCompression'](metrics, book);
+
+      expect(mockGridMaker.currentGridStepBps).toBeCloseTo(30 * 0.85); // 25.5 bps
+    });
+
+    it('should respect min_absolute_step_bps floor', async () => {
+      mockGridMaker.currentGridStepBps = 2;
+      adaptation.config.min_absolute_step_bps = 1;
+
+      await adaptation['executeCompression']({}, {});
+
+      expect(mockGridMaker.currentGridStepBps).toBeGreaterThanOrEqual(1);
+    });
+
+    it('should increase min_layers', async () => {
+      mockGridMaker.config.adaptive.minLayers = 8;
+
+      await adaptation['executeCompression']({}, {});
+
+      expect(mockGridMaker.config.adaptive.minLayers).toBeGreaterThan(8);
+    });
+  });
+
+  describe('executeExpansion', () => {
+    it('should expand parameters after fills', async () => {
+      // Compress first
+      adaptation['state'].compressionFactor = 0.7;
+      mockGridMaker.currentGridStepBps = 21; // 30 * 0.7
+
+      // Record fill
+      fillTracker.recordFill({
+        timestamp: Date.now(),
+        orderId: 'test',
+        side: 'bid',
+        fillPx: 100,
+        fillQty: 0.01,
+        fillValueUsd: 100
+      });
+
+      const metrics = fillTracker.getMetrics(Date.now());
+
+      await adaptation['executeExpansion'](metrics);
+
+      expect(adaptation['state'].compressionFactor).toBeCloseTo(0.7 * 1.05);
+      expect(mockGridMaker.currentGridStepBps).toBeGreaterThan(21);
+    });
+  });
+
+  describe('rate limiting', () => {
+    it('should prevent compression if rate limit exceeded', async () => {
+      // Trigger 3 compressions
+      for (let i = 0; i < 3; i++) {
+        await adaptation['executeCompression']({}, {});
+      }
+
+      // 4th compression should be blocked
+      const canCompress = adaptation['rateLimiter'].canCompress(Date.now(), adaptation.config);
+      expect(canCompress).toBe(false);
+    });
+
+    it('should allow compression after window expires', async () => {
+      // Trigger 3 compressions
+      for (let i = 0; i < 3; i++) {
+        await adaptation['executeCompression']({}, {});
+      }
+
+      // Fast-forward 11 minutes
+      vi.setSystemTime(Date.now() + 11 * 60_000);
+
+      const canCompress = adaptation['rateLimiter'].canCompress(Date.now(), adaptation.config);
+      expect(canCompress).toBe(true);
+    });
+  });
+});
+
+describe('FillRateTracker', () => {
+  let tracker: FillRateTracker;
+
+  beforeEach(() => {
+    tracker = new FillRateTracker({
+      enabled: true,
+      fill_interval_window_ticks: 10,
+      // ... config
+    });
+  });
+
+  it('should calculate avg fill interval correctly', () => {
+    const now = Date.now();
+    tracker.recordFill({ timestamp: now - 120_000, /* ... */ });
+    tracker.recordFill({ timestamp: now - 60_000, /* ... */ });
+    tracker.recordFill({ timestamp: now, /* ... */ });
+
+    const metrics = tracker.getMetrics(now);
+    expect(metrics.avgFillIntervalSec).toBeCloseTo(60); // 120s / 2 intervals
+  });
+
+  it('should track consecutive empty ticks', () => {
+    const now = Date.now();
+
+    // Tick 0: fill
+    tracker.recordFill({ timestamp: now - 5 * 60_000, /* ... */ });
+    tracker.onTickStart();
+
+    // Ticks 1-5: empty
+    for (let i = 0; i < 5; i++) {
+      tracker.onTickStart();
+    }
+
+    const metrics = tracker.getMetrics(now);
+    expect(metrics.consecutiveEmptyTicks).toBe(5);
+  });
+});
+```
+
+### 7.2 集成测试
+```typescript
+// packages/strategies/src/__tests__/integration/fillDriven.integration.test.ts
+
+describe('Fill-Driven Integration', () => {
+  let runner: Runner;
+  let mockExchange: MockExchange;
+
+  beforeEach(async () => {
+    const config = await loadTestConfig('fill_driven_test.yaml');
+    mockExchange = new MockExchange();
+    runner = new Runner({ config, exchange: mockExchange });
+    await runner.start();
+  });
+
+  afterEach(async () => {
+    await runner.stop();
+  });
+
+  it('should compress grid after no fills', async () => {
+    // 1. 等待网格初始化
+    await waitForCondition(() => runner.gridMaker.grids.size > 0, 10_000);
+    const initialStep = runner.gridMaker.currentGridStepBps;
+
+    // 2. 模拟 5 个 tick 无成交
+    for (let i = 0; i < 5; i++) {
+      await sleep(60_000);
+      mockExchange.tickMarketData();
+    }
+
+    // 3. 验证步长减小
+    expect(runner.gridMaker.currentGridStepBps).toBeLessThan(initialStep);
+  });
+
+  it('should expand grid after fills resume', async () => {
+    // 1. 触发压缩
+    for (let i = 0; i < 5; i++) {
+      await sleep(60_000);
+      mockExchange.tickMarketData();
+    }
+    const compressedStep = runner.gridMaker.currentGridStepBps;
+
+    // 2. 模拟成交
+    mockExchange.fillOrder(runner.gridMaker.grids.values().next().value.orderId);
+    await sleep(60_000);
+
+    // 3. 验证步长扩张
+    expect(runner.gridMaker.currentGridStepBps).toBeGreaterThan(compressedStep);
+  });
+
+  it('should respect rate limits', async () => {
+    // 触发 3 次压缩
+    for (let round = 0; round < 3; round++) {
+      for (let i = 0; i < 5; i++) {
+        await sleep(60_000);
+        mockExchange.tickMarketData();
+      }
+    }
+
+    const compressionCount = runner.metrics.compressionActions.get().values.length;
+    expect(compressionCount).toBeLessThanOrEqual(3);
+  });
+});
+```
+
+### 7.3 回测验证
+```typescript
+// packages/backtest/src/scenarios/fillDriven.scenario.ts
+
+export async function runFillDrivenScenario() {
+  const backtester = new Backtester({
+    configPath: 'config/backtest_fill_driven.yaml',
+    dataPath: 'data/btc_low_volatility_2025-09.csv'
+  });
+
+  const result = await backtester.run({
+    duration: '7d',
+    initialBalance: 10_000,
+    scenarios: [
+      {
+        name: 'baseline',
+        config: { grid: { adaptive: { fill_driven: { enabled: false } } } }
+      },
+      {
+        name: 'fill_driven_enabled',
+        config: { grid: { adaptive: { fill_driven: { enabled: true } } } }
+      }
+    ]
+  });
+
+  // 比较指标
+  const baseline = result.scenarios['baseline'];
+  const fillDriven = result.scenarios['fill_driven_enabled'];
+
+  expect(fillDriven.metrics.fillRate).toBeGreaterThan(baseline.metrics.fillRate);
+  expect(fillDriven.metrics.avgNoFillDuration).toBeLessThan(baseline.metrics.avgNoFillDuration);
+  expect(fillDriven.metrics.totalFills).toBeGreaterThan(baseline.metrics.totalFills);
+
+  console.log('Fill-driven backtest results:');
+  console.log(`Baseline fill rate: ${baseline.metrics.fillRate.toFixed(4)}`);
+  console.log(`Fill-driven fill rate: ${fillDriven.metrics.fillRate.toFixed(4)}`);
+  console.log(`Improvement: ${((fillDriven.metrics.fillRate / baseline.metrics.fillRate - 1) * 100).toFixed(2)}%`);
+}
+```
+
+---
+
+## 8. 运维手册
+
+### 8.1 启用/禁用功能
+```bash
+# 禁用 fill-driven adaptation
+curl -X POST http://localhost:4000/api/config/update \
+  -H "Content-Type: application/json" \
+  -d '{
+    "grid": {
+      "adaptive": {
+        "fill_driven": {
+          "enabled": false
+        }
+      }
+    }
+  }'
+
+# 查看当前状态
+curl http://localhost:4000/api/grid/fill-driven/status
+```
+
+### 8.2 调参指南
+
+| 场景 | 推荐配置 | 理由 |
+|------|----------|------|
+| **高频策略(微网格)** | `fill_starvation_threshold: 3`<br>`compression_step_size: 0.9`<br>`max_compression_factor: 0.7` | 快速响应无成交,但保留 30% 缓冲避免过度贴盘 |
+| **中低频策略** | `fill_starvation_threshold: 5`<br>`compression_step_size: 0.85`<br>`max_compression_factor: 0.5` | 平衡探索与保守,避免频繁调整 |
+| **极端贴盘口模式** | `min_absolute_step_bps: 0.5`<br>`min_absolute_cushion_bps: 0`<br>`fill_starvation_threshold: 2` | 激进模式,需配合高 burst throttling |
+| **盘整市(低波动)** | `fill_starvation_threshold: 7`<br>`min_spread_for_compression_bps: 5` | 延长触发时间,在窄价差时也允许压缩 |
+
+### 8.3 常见问题排查
+
+#### 问题 1:无成交但未触发压缩
+**症状**:
+```
+grid_no_fill_duration_seconds > 300
+grid_consecutive_empty_ticks = 6
+但 grid_compression_factor 仍为 1.0
+```
+
+**排查步骤**:
+1. 检查 `fill_driven.enabled` 是否为 `true`
+   ```bash
+   curl http://localhost:4000/api/config/current | jq '.grid.adaptive.fill_driven.enabled'
+   ```
+
+2. 检查是否触发速率限制
+   ```bash
+   curl http://localhost:9090/api/v1/query?query=grid_compression_rate_limited_total
+   ```
+
+3. 检查盘口价差是否低于阈值
+   ```bash
+   curl http://localhost:9090/api/v1/query?query=orderbook_spread_bps
+   ```
+
+4. 查看日志
+   ```bash
+   tail -f logs/runner-*.log | grep "fill-driven"
+   ```
+
+#### 问题 2:压缩过度导致 post-only 拒单
+**症状**:
+```
+grid_step_bps < 2
+post_only_rejection_rate > 0.3
+```
+
+**解决方案**:
+1. 提高 `min_absolute_step_bps` 和 `min_absolute_cushion_bps`
+   ```yaml
+   fill_driven:
+     min_absolute_step_bps: 2
+     min_absolute_cushion_bps: 1
+   ```
+
+2. 降低压缩强度
+   ```yaml
+   fill_driven:
+     compression_step_size: 0.9  # 从 0.85 提高到 0.9
+     max_compression_factor: 0.6  # 从 0.5 提高到 0.6
+   ```
+
+3. 增加压缩触发延迟
+   ```yaml
+   fill_driven:
+     fill_starvation_threshold: 7  # 从 5 提高到 7
+   ```
+
+#### 问题 3:扩张速度过慢
+**症状**:
+```
+成交恢复后 20 分钟,compression_factor 仍为 0.7
+```
+
+**解决方案**:
+1. 提高扩张步长
+   ```yaml
+   fill_driven:
+     expansion_step_size: 1.1  # 从 1.05 提高到 1.1
+   ```
+
+2. 检查是否有持续的零星成交阻止完全恢复
+   ```bash
+   curl http://localhost:9090/api/v1/query?query=rate(grid_fill_total[5m])
+   ```
+
+3. 手动重置压缩状态
+   ```bash
+   curl -X POST http://localhost:4000/api/grid/fill-driven/reset
+   ```
+
+### 8.4 手动干预接口
+```typescript
+// apps/runner/src/api/routes/grid.ts
+
+router.post('/grid/fill-driven/reset', async (req, res) => {
+  const { symbol } = req.body;
+
+  const gridMaker = runner.getGridMaker(symbol);
+  if (!gridMaker || !gridMaker.fillDrivenAdaptation) {
+    return res.status(404).json({ error: 'Grid maker or fill-driven adaptation not found' });
+  }
+
+  gridMaker.fillDrivenAdaptation.reset();
+
+  logger.info({ symbol }, 'Fill-driven adaptation manually reset');
+  res.json({ success: true, symbol });
+});
+
+router.post('/grid/fill-driven/override', authenticate, async (req, res) => {
+  const { symbol, compressionFactor } = req.body;
+
+  if (compressionFactor < 0.3 || compressionFactor > 1.0) {
+    return res.status(400).json({ error: 'compressionFactor must be between 0.3 and 1.0' });
+  }
+
+  const gridMaker = runner.getGridMaker(symbol);
+  gridMaker.fillDrivenAdaptation.state.compressionFactor = compressionFactor;
+
+  await gridMaker.requestParameterUpdate();
+
+  logger.warn({ symbol, compressionFactor, operator: req.user }, 'Manual compression factor override');
+  res.json({ success: true, symbol, compressionFactor });
+});
+```
+
+### 8.5 监控告警响应流程
+
+#### 告警:`GridNoFillsExtended`
+**触发条件**:`grid_no_fill_duration_seconds > 300` 持续 2 分钟
+
+**响应步骤**:
+1. 确认是否为市场流动性问题(检查交易所订单簿深度)
+   ```bash
+   curl "https://api.pacifica.fi/api/v1/orderbook?symbol=BTC"
+   ```
+
+2. 检查 fill-driven 是否已触发压缩
+   ```bash
+   curl http://localhost:9090/api/v1/query?query=grid_compression_factor{symbol="BTC"}
+   ```
+
+3. 若未压缩且配置正确,检查日志中的限流或错误
+   ```bash
+   tail -100 logs/runner-*.log | grep -E "rate.limit|compression|error"
+   ```
+
+4. 若市场流动性正常但策略无成交,考虑手动触发一次压缩
+   ```bash
+   # 降低步长到当前的 80%
+   curl -X POST http://localhost:4000/api/grid/fill-driven/override \
+     -H "Authorization: Bearer $TOKEN" \
+     -d '{"symbol": "BTC", "compressionFactor": 0.8}'
+   ```
+
+#### 告警:`GridCompressionStuck`
+**触发条件**:`grid_compression_factor < 0.6` 持续 5 分钟
+
+**响应步骤**:
+1. 确认是否确实无成交
+   ```bash
+   curl http://localhost:9090/api/v1/query?query=rate(grid_fill_total{symbol="BTC"}[10m])
+   ```
+
+2. 若有成交但未扩张,检查扩张逻辑是否被阻塞
+   ```bash
+   tail -50 logs/runner-*.log | grep "expansion"
+   ```
+
+3. 手动重置压缩状态
+   ```bash
+   curl -X POST http://localhost:4000/api/grid/fill-driven/reset -d '{"symbol": "BTC"}'
+   ```
+
+4. 若问题持续,禁用 fill-driven 并恢复到基础自适应模式
+   ```bash
+   curl -X POST http://localhost:4000/api/config/update \
+     -d '{"grid": {"adaptive": {"fill_driven": {"enabled": false}}}}'
+   ```
+
+---
+
+## 9. 多实例协调
+
+### 9.1 跨账户/标的隔离
+```typescript
+// 每个 GridMaker 实例独立维护 FillDrivenAdaptation
+export class GridFleetManager {
+  private instances: Map<string, GridMaker> = new Map();
+
+  createInstance(config: GridInstanceConfig): void {
+    const key = `${config.accountId}_${config.symbol}`;
+
+    const gridMaker = new GridMaker({
+      ...config,
+      fillDrivenConfig: config.adaptive.fill_driven
+    });
+
+    this.instances.set(key, gridMaker);
+  }
+
+  async onTick(): Promise<void> {
+    // 并行执行所有实例的 tick 逻辑
+    await Promise.all(
+      Array.from(this.instances.values()).map(gm => gm.onTick())
+    );
+  }
+}
+```
+
+### 9.2 全局 STP 检测
+```typescript
+// packages/core/src/globalOrderCoordinator.ts
+
+export class GlobalOrderCoordinator {
+  async checkFillDrivenConflicts(
+    accountId: string,
+    symbol: string,
+    actions: CompressionAction[]
+  ): Promise<ConflictReport> {
+    const conflicts: Conflict[] = [];
+
+    // 检查是否有其他实例正在压缩同一标的
+    for (const [key, instance] of this.fleetManager.instances) {
+      if (key.endsWith(`_${symbol}`) && !key.startsWith(accountId)) {
+        const otherState = instance.fillDrivenAdaptation?.getState();
+
+        if (otherState && Date.now() - otherState.lastCompressionTime < 120_000) {
+          conflicts.push({
+            type: 'concurrent_compression',
+            instance: key,
+            message: 'Another account is compressing the same symbol'
+          });
+        }
+      }
+    }
+
+    return { conflicts, shouldProceed: conflicts.length === 0 };
+  }
+}
+```
+
+### 9.3 差异化策略(内圈/外圈账号)
+```yaml
+# config/grid_multi_account.yaml
+grid:
+  instances:
+    - account_id: maker-inner
+      symbol: BTC
+      grid_step_bps: 2
+      adaptive:
+        fill_driven:
+          enabled: true
+          fill_starvation_threshold: 3  # 内圈账号更激进
+          min_absolute_step_bps: 1
+
+    - account_id: maker-outer
+      symbol: BTC
+      grid_step_bps: 10
+      adaptive:
+        fill_driven:
+          enabled: true
+          fill_starvation_threshold: 7  # 外圈账号更保守
+          min_absolute_step_bps: 3
+```
+
+---
+
+## 10. 回滚与降级
+
+### 10.1 自动降级触发条件
+```typescript
+export class FillDrivenDegradation {
+  shouldDegrade(metrics: FillMetrics, state: AdaptationState): boolean {
+    // 条件 1:压缩因子长期卡在极低值
+    if (state.compressionFactor < 0.4 && Date.now() - state.lastCompressionTime > 600_000) {
+      this.logger.warn({ compressionFactor: state.compressionFactor }, 'Compression stuck, degrading');
+      return true;
+    }
+
+    // 条件 2:post-only 拒单率飙升
+    const rejectRate = this.metrics.postOnlyRejections.rate('5m');
+    if (rejectRate > 0.4) {
+      this.logger.warn({ rejectRate }, 'Post-only rejection rate high, degrading');
+      return true;
+    }
+
+    // 条件 3:与 ThrottledGateway 冲突(队列堆积)
+    const queueDepth = this.throttledGateway.getQueueDepth();
+    if (queueDepth > 50) {
+      this.logger.warn({ queueDepth }, 'Throttle queue overloaded, degrading');
+      return true;
+    }
+
+    return false;
+  }
+
+  async executeDegradation(): Promise<void> {
+    this.logger.warn({}, 'Disabling fill-driven adaptation due to degradation trigger');
+
+    // 重置压缩状态
+    this.fillDrivenAdaptation.reset();
+
+    // 恢复到基础配置
+    await this.gridMaker.resetToBaseConfig();
+
+    // 发送告警
+    await this.alertManager.send({
+      severity: 'warning',
+      title: 'Fill-Driven Adaptation Degraded',
+      message: 'Automatically disabled due to performance issues'
+    });
+  }
+}
+```
+
+### 10.2 手动回滚脚本
+```bash
+#!/bin/bash
+# scripts/rollback_fill_driven.sh
+
+SYMBOL=${1:-BTC}
+
+echo "Rolling back fill-driven adaptation for $SYMBOL..."
+
+# 1. 禁用功能
+curl -X POST http://localhost:4000/api/config/update \
+  -H "Content-Type: application/json" \
+  -d "{\"grid\": {\"adaptive\": {\"fill_driven\": {\"enabled\": false}}}}"
+
+# 2. 重置状态
+curl -X POST http://localhost:4000/api/grid/fill-driven/reset \
+  -d "{\"symbol\": \"$SYMBOL\"}"
+
+# 3. 恢复到 baseline 配置
+curl -X POST http://localhost:4000/api/config/update \
+  -H "Content-Type: application/json" \
+  -d @config/baseline_grid.json
+
+# 4. 验证
+sleep 5
+curl http://localhost:4000/api/grid/status?symbol=$SYMBOL | jq '.fillDriven'
+
+echo "Rollback complete. Monitor metrics for next 10 minutes."
+```
+
+---
+
+## 11. 开发里程碑
+
+### Phase 1: Core Implementation (Week 1)
+- [x] 实现 `FillRateTracker` 模块
+- [x] 实现 `FillDrivenAdaptation` 核心逻辑
+- [x] 实现 `CompressionRateLimiter`
+- [x] 集成到 `GridMaker`
+- [x] 单元测试覆盖率 > 80%
+
+### Phase 2: Integration & Config (Week 1-2)
+- [x] Zod schema 定义与验证
+- [x] 配置热更新支持
+- [x] Prometheus 指标集成
+- [x] Grafana dashboard 创建
+- [x] 告警规则配置
+
+### Phase 3: Testing & Validation (Week 2)
+- [x] 集成测试(模拟无成交场景)
+- [x] 回测验证(低波动数据集)
+- [x] 压力测试(极端压缩/扩张)
+- [x] 多实例协调测试
+
+### Phase 4: Production Rollout (Week 3)
+- [ ] 测试网灰度(单标的)
+- [ ] 参数调优(基于实盘数据)
+- [ ] 运维手册验证
+- [ ] 主网小额账户试运行
+- [ ] 全量推广
+
+---
+
+## 12. 成功标准
+
+| 指标 | 目标 | 测量方法 |
+|------|------|----------|
+| **填充率提升** | 相对 baseline 提升 ≥ 30% | `sum(grid_fill_total) / deployment_duration` |
+| **无成交时长缩短** | P95 no-fill duration < 5 分钟 | `histogram_quantile(0.95, grid_no_fill_duration_seconds)` |
+| **Post-only 成功率** | ≥ 95% | `post_only_success / (post_only_success + post_only_rejections)` |
+| **压缩触发准确性** | 误触发率 < 5% | 人工审核日志中的压缩事件 |
+| **扩张恢复速度** | 成交恢复后 10 分钟内 compression_factor > 0.9 | 监控 `grid_compression_factor` 时间序列 |
+| **系统稳定性** | 无因压缩导致的降级事件 | `count(degradation_events{cause="fill_driven"}) == 0` |
+
+---
+
+## 13. 参考资料
+
+- [M16_INCREMENTAL_GRID_DESIGN.md](./M16_INCREMENTAL_GRID_DESIGN.md) - 增量网格引擎
+- [M16_PLACEMENT_THROTTLING_DESIGN.md](./M16_PLACEMENT_THROTTLING_DESIGN.md) - 节流 2.0 系统
+- [CONFIG_REFERENCE.md](./CONFIG_REFERENCE.md) - 配置参考
+- [IMPLEMENTATION_PLAN.md](./IMPLEMENTATION_PLAN.md) - 总体实施计划
+
+---
+
+**文档状态**: ✅ Ready for Development
+**审批人**: [待填写]
+**最后更新**: 2025-10-09

+ 826 - 0
docs/M16_INCREMENTAL_GRID_DESIGN.md

@@ -0,0 +1,826 @@
+# M1.6 增量网格引擎设计文档
+
+**版本**: v1.0.0
+**日期**: 2025-10-09
+**负责人**: Grid Strategy Team
+**依赖文档**: `IMPLEMENTATION_PLAN.md`, `GRID_STRATEGY_DESIGN.md`, `CONFIG_REFERENCE.md`
+
+---
+
+## 1. 背景与问题
+
+### 1.1 当前痛点
+
+从实盘日志分析发现以下核心问题:
+
+**问题 1: 频繁全撤全布导致成交缺失**
+```
+2025-10-08 日志分析:
+- 运行时长: 20 分钟
+- 网格调整次数: 8 次
+- 成交订单数: 0 (filledGrids: 0)
+- 问题: 每次自适应调整都触发 cancelAll() → 全部档位下线 → 错失成交机会
+```
+
+**问题 2: 批量下单触发限流**
+```
+初始下单: 20 订单,耗时 1.1s (正常)
+第 2 次调整: 20 订单,耗时 58s (异常)
+第 3 次调整: 20 订单,耗时 119s (严重异常)
+根因: 交易所限流,导致每个订单平均延迟 5.9s
+```
+
+**问题 3: 网格步长放大导致档位数减少**
+```
+初始: grid_step=15bps → 13 层 × 2 = 26 订单
+调整 1: grid_step=26bps → 7 层 × 2 = 14 订单 (-46%)
+调整 2: grid_step=41bps → 4 层 × 2 = 8 订单 (-69%)
+结果: 市场覆盖不足,成交概率大幅下降
+```
+
+### 1.2 解决方案概述
+
+引入**增量网格维护机制**,替代"全撤全布"模式:
+
+1. **状态对比**: 比较目标价位 vs 现有订单
+2. **差分操作**: 仅修改/新增/删除有变化的档位
+3. **批次限流**: 分批执行操作,避免 burst 超限
+4. **降级保护**: 失败超阈值时回退全量模式
+
+**预期效果**:
+- ✅ 90%+ 档位保持在线,成交率提升 5-10x
+- ✅ 平均更新延迟从 60s 降至 <5s
+- ✅ 限流风险降低,RPC 请求均匀分布
+
+---
+
+## 2. 技术方案
+
+### 2.1 核心算法:增量调和 (reconcileGrid)
+
+#### 2.1.1 输入与输出
+
+```typescript
+interface ReconcileInput {
+  currentGrids: Map<number, GridLevel>;  // 现有订单状态
+  targetPrices: number[];                // 目标价位列表
+  gridStepBps: number;                   // 当前步长
+  minLayers: number;                     // 最小保留层数
+}
+
+interface ReconcileOutput {
+  toCancel: GridLevel[];    // 需撤销的订单
+  toModify: GridLevel[];    // 需修改价格的订单
+  toAdd: number[];          // 需新增的价位
+  keepAlive: GridLevel[];   // 保持不变的订单
+}
+```
+
+#### 2.1.2 决策逻辑
+
+```typescript
+async reconcileGrid(targetPrices: number[]): Promise<ReconcileOutput> {
+  const threshold = this.config.gridStepBps * 0.3; // 偏离阈值 30%
+  const tickSize = this.tickSize;
+
+  const toCancel: GridLevel[] = [];
+  const toModify: GridLevel[] = [];
+  const toAdd: number[] = [];
+  const keepAlive: GridLevel[] = [];
+
+  // 1. 分类现有订单
+  for (const grid of this.grids.values()) {
+    const nearestTarget = this.findNearest(grid.px, targetPrices);
+
+    if (!nearestTarget) {
+      // 没有对应目标 → 撤销
+      toCancel.push(grid);
+    } else {
+      const deviation = Math.abs(nearestTarget - grid.px);
+      const deviationBps = (deviation / grid.px) * 10000;
+
+      if (deviationBps < threshold) {
+        // 偏离很小 → 保持
+        keepAlive.push(grid);
+      } else if (deviationBps < threshold * 2) {
+        // 中等偏离 → 修改价格
+        toModify.push({ ...grid, targetPx: nearestTarget });
+      } else {
+        // 偏离过大 → 撤销重挂
+        toCancel.push(grid);
+        toAdd.push(nearestTarget);
+      }
+    }
+  }
+
+  // 2. 识别新增价位
+  for (const target of targetPrices) {
+    const hasExisting = keepAlive.some(g =>
+      Math.abs(g.px - target) < tickSize
+    );
+    const willModify = toModify.some(g =>
+      Math.abs(g.targetPx! - target) < tickSize
+    );
+
+    if (!hasExisting && !willModify) {
+      toAdd.push(target);
+    }
+  }
+
+  return { toCancel, toModify, toAdd, keepAlive };
+}
+```
+
+#### 2.1.3 执行策略
+
+```typescript
+async executeReconcile(plan: ReconcileOutput): Promise<void> {
+  const batchSize = this.config.incrementalMode.batchSize;
+  const batchDelay = this.config.incrementalMode.batchIntervalMs;
+
+  this.logger.info({
+    toCancel: plan.toCancel.length,
+    toModify: plan.toModify.length,
+    toAdd: plan.toAdd.length,
+    keepAlive: plan.keepAlive.length
+  }, 'Executing incremental grid update');
+
+  // 1. 优先修改订单(保持流动性)
+  for (const level of plan.toModify) {
+    try {
+      await this.modifyOrReplace(level, level.targetPx!);
+    } catch (error) {
+      // 修改失败 → 加入撤销队列
+      plan.toCancel.push(level);
+      plan.toAdd.push(level.targetPx!);
+    }
+  }
+
+  // 2. 批次撤销旧订单
+  await this.batchCancel(plan.toCancel, batchSize, batchDelay);
+
+  // 3. 批次新增订单
+  await this.batchPlace(plan.toAdd, batchSize, batchDelay);
+
+  // 4. 验证最终状态
+  const finalGrids = this.grids.size;
+  const expectedGrids = plan.keepAlive.length + plan.toAdd.length;
+
+  if (finalGrids < expectedGrids * 0.8) {
+    this.logger.error({
+      finalGrids,
+      expectedGrids,
+      successRate: (finalGrids / expectedGrids * 100).toFixed(1) + '%'
+    }, 'Incremental update success rate too low, triggering fallback');
+
+    this.incrementalFailures++;
+    if (this.incrementalFailures >= 3) {
+      await this.fallbackToFullReset();
+    }
+  } else {
+    this.incrementalFailures = 0;
+  }
+}
+```
+
+### 2.2 订单修改策略
+
+#### 2.2.1 原生 modify 支持(优先)
+
+```typescript
+async modifyOrder(level: GridLevel, newPx: number): Promise<void> {
+  if (!this.router.supportsModify) {
+    throw new Error('Modify not supported');
+  }
+
+  const modifyRequest = {
+    orderId: level.orderId,
+    newPx: this.normalizePrice(level.side, newPx),
+    newSz: level.sz
+  };
+
+  await this.router.modifyLimit(modifyRequest);
+
+  // 更新本地状态
+  level.px = newPx;
+  level.timestamp = Date.now();
+  level.dirty = false;
+}
+```
+
+#### 2.2.2 Cancel+Place 原子操作(降级)
+
+```typescript
+async modifyOrReplace(level: GridLevel, newPx: number): Promise<void> {
+  try {
+    // 尝试原生修改
+    await this.modifyOrder(level, newPx);
+    this.logger.debug({ orderId: level.orderId, newPx }, 'Order modified successfully');
+  } catch (error) {
+    // 降级到撤销重挂
+    this.logger.warn({ error }, 'Modify failed, falling back to cancel+place');
+
+    // 标记为 dirty,防止重复操作
+    level.dirty = true;
+
+    // 同一 tick 内完成撤销和重挂
+    await this.router.cancel(level.orderId!);
+    this.grids.delete(level.index);
+
+    const newOrderId = await this.placeGridOrder(
+      level.index,
+      level.side,
+      newPx,
+      level.sz
+    );
+
+    level.dirty = false;
+    this.logger.info({
+      oldOrderId: level.orderId,
+      newOrderId,
+      newPx
+    }, 'Order replaced successfully');
+  }
+}
+```
+
+### 2.3 批次操作与限流
+
+#### 2.3.1 批次撤销
+
+```typescript
+async batchCancel(
+  levels: GridLevel[],
+  batchSize: number,
+  delayMs: number
+): Promise<void> {
+  const batches = chunk(levels, batchSize);
+
+  for (const [index, batch] of batches.entries()) {
+    this.logger.debug({
+      batchIndex: index + 1,
+      batchSize: batch.length,
+      totalBatches: batches.length
+    }, 'Executing cancel batch');
+
+    // 并行撤销同一批次内的订单
+    await Promise.allSettled(
+      batch.map(level => this.cancelGridLevel(level))
+    );
+
+    // 批次间延迟,避免限流
+    if (index < batches.length - 1) {
+      await sleep(delayMs);
+    }
+  }
+}
+```
+
+#### 2.3.2 批次新增
+
+```typescript
+async batchPlace(
+  prices: number[],
+  batchSize: number,
+  delayMs: number
+): Promise<void> {
+  const batches = chunk(prices, batchSize);
+
+  for (const [index, batch] of batches.entries()) {
+    this.logger.debug({
+      batchIndex: index + 1,
+      batchSize: batch.length,
+      totalBatches: batches.length
+    }, 'Executing place batch');
+
+    const placePromises = batch.map(async (px) => {
+      const side = px > this.gridCenter ? 'sell' : 'buy';
+      const index = this.calculateGridIndex(px);
+
+      try {
+        await this.placeGridOrder(index, side, px, this.baseClipSz);
+      } catch (error) {
+        this.logger.warn({ px, error }, 'Failed to place grid order in batch');
+      }
+    });
+
+    await Promise.allSettled(placePromises);
+
+    if (index < batches.length - 1) {
+      await sleep(delayMs);
+    }
+  }
+}
+```
+
+### 2.4 状态管理与并发安全
+
+#### 2.4.1 GridLevel 状态扩展
+
+```typescript
+interface GridLevel {
+  index: number;
+  side: Side;
+  px: number;
+  sz: number;
+  orderId?: string;
+  clientId?: string;
+  filled: boolean;
+  timestamp: number;
+
+  // 新增字段
+  dirty?: boolean;        // 正在修改中
+  pending?: boolean;      // 等待确认
+  retryCount?: number;    // 重试次数
+  targetPx?: number;      // 目标价格(用于修改)
+}
+```
+
+#### 2.4.2 并发锁
+
+```typescript
+class GridMaker {
+  private reconcileLock = false;
+  private pendingReconcile = false;
+
+  async maybeReconcile(): Promise<void> {
+    if (this.reconcileLock) {
+      this.pendingReconcile = true;
+      this.logger.debug('Reconcile in progress, deferring');
+      return;
+    }
+
+    try {
+      this.reconcileLock = true;
+      this.pendingReconcile = false;
+
+      await this.reconcileGrid(/* ... */);
+    } finally {
+      this.reconcileLock = false;
+
+      // 如果有待处理的请求,递归调用
+      if (this.pendingReconcile) {
+        setImmediate(() => this.maybeReconcile());
+      }
+    }
+  }
+}
+```
+
+### 2.5 降级与回退机制
+
+#### 2.5.1 失败计数与阈值
+
+```typescript
+class GridMaker {
+  private incrementalFailures = 0;
+  private readonly MAX_INCREMENTAL_FAILURES = 3;
+
+  async executeReconcile(plan: ReconcileOutput): Promise<void> {
+    // ... 执行增量更新 ...
+
+    const successRate = finalGrids / expectedGrids;
+
+    if (successRate < 0.8) {
+      this.incrementalFailures++;
+
+      if (this.incrementalFailures >= this.MAX_INCREMENTAL_FAILURES) {
+        await this.fallbackToFullReset();
+      }
+    } else {
+      this.incrementalFailures = 0; // 重置计数器
+    }
+  }
+}
+```
+
+#### 2.5.2 全量重置降级
+
+```typescript
+async fallbackToFullReset(): Promise<void> {
+  this.logger.warn({
+    failures: this.incrementalFailures,
+    action: 'fallback_to_full_reset'
+  }, 'Incremental mode failed, falling back to full reset');
+
+  // 临时禁用增量模式
+  this.incrementalModeEnabled = false;
+
+  // 执行全量重置
+  await this.reset();
+
+  // 等待冷却后重新启用
+  setTimeout(() => {
+    this.incrementalModeEnabled = true;
+    this.incrementalFailures = 0;
+    this.logger.info('Incremental mode re-enabled after cooldown');
+  }, 60000); // 1 分钟冷却
+}
+```
+
+---
+
+## 3. 配置方案
+
+### 3.1 配置结构
+
+```yaml
+grid:
+  # ... 现有配置 ...
+
+  incremental_mode:
+    enabled: true                    # 启用增量模式
+    modify_threshold_bps: 2          # 偏离阈值(<2bps 保持不变)
+    batch_size: 5                    # 批次大小
+    batch_interval_ms: 100           # 批次间隔
+    max_modify_failures: 3           # 修改失败阈值
+    fallback_full_reset: true        # 失败后回退全量模式
+    fallback_cooldown_ms: 60000      # 回退冷却时间
+    support_native_modify: false     # 交易所是否支持原生 modify
+```
+
+### 3.2 配置验证
+
+```typescript
+const IncrementalModeSchema = z.object({
+  enabled: z.boolean().default(true),
+  modify_threshold_bps: z.number().min(0.5).max(10).default(2),
+  batch_size: z.number().int().min(1).max(20).default(5),
+  batch_interval_ms: z.number().min(50).max(1000).default(100),
+  max_modify_failures: z.number().int().min(1).max(10).default(3),
+  fallback_full_reset: z.boolean().default(true),
+  fallback_cooldown_ms: z.number().min(10000).max(300000).default(60000),
+  support_native_modify: z.boolean().default(false)
+});
+```
+
+---
+
+## 4. 监控指标
+
+### 4.1 新增 Prometheus 指标
+
+```typescript
+export const gridIncrementalMetrics = {
+  // 增量操作统计
+  reconcile_total: new Counter({
+    name: 'grid_reconcile_total',
+    help: 'Total number of reconcile operations'
+  }),
+
+  reconcile_duration_ms: new Histogram({
+    name: 'grid_reconcile_duration_ms',
+    help: 'Duration of reconcile operations',
+    buckets: [100, 500, 1000, 2000, 5000]
+  }),
+
+  // 订单操作分类
+  orders_kept: new Gauge({
+    name: 'grid_orders_kept',
+    help: 'Number of orders kept unchanged'
+  }),
+
+  orders_modified: new Counter({
+    name: 'grid_orders_modified_total',
+    help: 'Total number of modified orders'
+  }),
+
+  orders_cancelled: new Counter({
+    name: 'grid_orders_cancelled_total',
+    help: 'Total number of cancelled orders'
+  }),
+
+  orders_added: new Counter({
+    name: 'grid_orders_added_total',
+    help: 'Total number of newly placed orders'
+  }),
+
+  // 失败与降级
+  modify_failures: new Counter({
+    name: 'grid_modify_failures_total',
+    help: 'Total number of modify operation failures'
+  }),
+
+  fallback_events: new Counter({
+    name: 'grid_fallback_events_total',
+    help: 'Total number of fallback to full reset'
+  }),
+
+  // 效率指标
+  incremental_success_rate: new Gauge({
+    name: 'grid_incremental_success_rate',
+    help: 'Success rate of incremental updates (0-1)'
+  }),
+
+  pending_levels: new Gauge({
+    name: 'grid_pending_levels',
+    help: 'Number of grid levels in pending state'
+  })
+};
+```
+
+### 4.2 告警规则
+
+```yaml
+# prometheus/alerts/grid.yml
+groups:
+  - name: grid_incremental
+    interval: 30s
+    rules:
+      - alert: GridIncrementalSuccessRateLow
+        expr: grid_incremental_success_rate < 0.8
+        for: 5m
+        labels:
+          severity: warning
+        annotations:
+          summary: "Grid incremental update success rate below 80%"
+          description: "Success rate: {{ $value | humanizePercentage }}"
+
+      - alert: GridModifyFailuresHigh
+        expr: rate(grid_modify_failures_total[5m]) > 0.1
+        for: 2m
+        labels:
+          severity: warning
+        annotations:
+          summary: "High rate of modify failures"
+          description: "{{ $value }} failures per second"
+
+      - alert: GridPendingLevelsStuck
+        expr: grid_pending_levels > 5
+        for: 10m
+        labels:
+          severity: critical
+        annotations:
+          summary: "Grid has stuck pending levels"
+          description: "{{ $value }} levels stuck in pending state"
+
+      - alert: GridFallbackFrequent
+        expr: rate(grid_fallback_events_total[1h]) > 2
+        for: 5m
+        labels:
+          severity: critical
+        annotations:
+          summary: "Frequent fallback to full reset"
+          description: "{{ $value }} fallbacks in the last hour"
+```
+
+---
+
+## 5. 测试计划
+
+### 5.1 单元测试
+
+```typescript
+describe('GridMaker.reconcileGrid', () => {
+  it('should keep unchanged orders when target matches', async () => {
+    const maker = new GridMaker(/* ... */);
+    await maker.initialize();
+
+    // 目标价位与现有完全一致
+    const plan = await maker.reconcileGrid(getCurrentPrices());
+
+    expect(plan.keepAlive.length).toBe(20);
+    expect(plan.toCancel.length).toBe(0);
+    expect(plan.toModify.length).toBe(0);
+    expect(plan.toAdd.length).toBe(0);
+  });
+
+  it('should modify orders when deviation is moderate', async () => {
+    const maker = new GridMaker(/* ... */);
+    await maker.initialize();
+
+    // 价格轻微偏移
+    const targetPrices = getCurrentPrices().map(p => p * 1.001);
+    const plan = await maker.reconcileGrid(targetPrices);
+
+    expect(plan.toModify.length).toBeGreaterThan(0);
+    expect(plan.toCancel.length).toBeLessThan(plan.toModify.length);
+  });
+
+  it('should cancel and re-place when deviation is large', async () => {
+    const maker = new GridMaker(/* ... */);
+    await maker.initialize();
+
+    // 价格大幅偏移
+    const targetPrices = getCurrentPrices().map(p => p * 1.05);
+    const plan = await maker.reconcileGrid(targetPrices);
+
+    expect(plan.toCancel.length).toBe(20);
+    expect(plan.toAdd.length).toBe(20);
+  });
+});
+```
+
+### 5.2 集成测试
+
+```typescript
+describe('GridMaker incremental mode E2E', () => {
+  it('should maintain most orders during volatility adjustment', async () => {
+    const maker = new GridMaker(config, router, /* ... */);
+    await maker.initialize();
+
+    const initialOrders = maker.grids.size;
+
+    // 模拟波动率变化,触发步长调整
+    volatilityEstimator.inject(20); // hourlyVolBps: 20
+    await maker.onTick();
+
+    // 验证大部分订单保持在线
+    const onlineOrders = maker.grids.size;
+    expect(onlineOrders).toBeGreaterThan(initialOrders * 0.8);
+  });
+
+  it('should fallback to full reset after repeated failures', async () => {
+    const maker = new GridMaker(/* ... */);
+
+    // 模拟修改失败
+    router.modifyLimit = jest.fn().mockRejectedValue(new Error('Rate limit'));
+
+    // 触发 3 次失败
+    for (let i = 0; i < 3; i++) {
+      await maker.maybeAdjustGridStep();
+    }
+
+    // 验证降级到全量模式
+    expect(maker.incrementalModeEnabled).toBe(false);
+    expect(maker.grids.size).toBe(0); // 已全撤
+  });
+});
+```
+
+### 5.3 压力测试
+
+```typescript
+describe('GridMaker under rate limit stress', () => {
+  it('should handle rate limits gracefully with batching', async () => {
+    const maker = new GridMaker(config, router, /* ... */);
+
+    // 注入延迟模拟限流
+    router.sendLimitChild = jest.fn().mockImplementation(async (order) => {
+      await sleep(200); // 每单 200ms
+      return 'order-' + Date.now();
+    });
+
+    const start = Date.now();
+    await maker.initialize(); // 20 订单
+    const elapsed = Date.now() - start;
+
+    // 批次大小 5,间隔 100ms
+    // 预期: 4 批次 × (5单×200ms + 100ms间隔) ≈ 4400ms
+    expect(elapsed).toBeLessThan(5000);
+    expect(elapsed).toBeGreaterThan(4000);
+  });
+});
+```
+
+---
+
+## 6. 运维手册
+
+### 6.1 启用增量模式
+
+```bash
+# 1. 更新配置
+vim config/config.yaml
+
+# 2. 金丝雀测试(单实例)
+npm run runner -- --mode=dry-run --config=config.yaml
+
+# 3. 观察指标(2-5 分钟)
+curl http://localhost:9090/metrics | grep grid_incremental
+
+# 4. 确认无异常后正式启用
+npm run runner -- --mode=live
+```
+
+### 6.2 监控检查清单
+
+```
+✓ grid_incremental_success_rate > 0.8
+✓ grid_pending_levels < 3
+✓ grid_modify_failures_total 增长缓慢(< 0.1/s)
+✓ grid_fallback_events_total 无增长
+✓ grid_reconcile_duration_ms p95 < 2000ms
+```
+
+### 6.3 故障排查
+
+#### 场景 1: 成功率低 (< 80%)
+
+```
+可能原因:
+1. modify_threshold_bps 设置过小,导致大量误判
+2. 交易所 API 不稳定,频繁超时
+3. 批次间隔过短,触发限流
+
+排查步骤:
+1. 检查日志中的 modify/cancel/place 失败原因
+2. 调大 modify_threshold_bps 至 3-5 bps
+3. 调大 batch_interval_ms 至 200-300ms
+4. 如持续失败,临时禁用增量模式
+```
+
+#### 场景 2: pending_levels 持续堆积
+
+```
+可能原因:
+1. 并发锁未正确释放
+2. 订单状态更新延迟
+3. 回调未触发(orderId 不匹配)
+
+排查步骤:
+1. 重启 runner,清空状态
+2. 检查 WS 订单更新回调是否正常
+3. 启用 debug 日志,追踪状态流转
+```
+
+#### 场景 3: 频繁 fallback
+
+```
+可能原因:
+1. 网络质量差,订单操作失败率高
+2. 配置错误(如 batch_size 过大)
+3. 交易所限流策略调整
+
+排查步骤:
+1. 检查网络延迟 (ping 交易所)
+2. 降低 batch_size 至 3
+3. 增加 batch_interval_ms 至 500ms
+4. 联系交易所确认限流策略
+```
+
+### 6.4 紧急回滚
+
+```bash
+# 方法 1: 配置热更新
+# 修改 config.yaml
+incremental_mode:
+  enabled: false
+
+# 触发热重载
+kill -HUP <runner_pid>
+
+# 方法 2: 重启服务
+npm run runner:stop
+# 修改配置
+npm run runner:start
+
+# 方法 3: 使用备份配置
+npm run runner -- --config=config/rollback/safe_mode.yaml
+```
+
+---
+
+## 7. 下一步计划
+
+### 7.1 Phase 1 (Week 1-2)
+- [ ] 实现 `reconcileGrid()` 核心逻辑
+- [ ] 增加 `modifyOrReplace()` 降级机制
+- [ ] 批次撤销/新增功能
+- [ ] 单元测试覆盖 >80%
+
+### 7.2 Phase 2 (Week 3)
+- [ ] 集成测试与压力测试
+- [ ] Prometheus 指标接入
+- [ ] Grafana 面板配置
+- [ ] 告警规则部署
+
+### 7.3 Phase 3 (Week 4)
+- [ ] 测试网灰度(单实例)
+- [ ] 收集性能数据
+- [ ] 调优配置参数
+- [ ] 主网小规模试运行
+
+### 7.4 Phase 4 (后续)
+- [ ] 多实例支持(FleetManager 集成)
+- [ ] 跨实例协调优化
+- [ ] 自适应批次大小
+- [ ] 长期运行稳定性验证
+
+---
+
+## 8. 附录
+
+### 8.1 关键代码路径
+
+```
+packages/strategies/src/gridMaker.ts
+  ├── reconcileGrid()        # 核心差分算法
+  ├── modifyOrReplace()      # 修改/替换逻辑
+  ├── batchCancel()          # 批次撤销
+  ├── batchPlace()           # 批次新增
+  └── fallbackToFullReset()  # 降级处理
+
+packages/execution/src/orderRouter.ts
+  └── modifyLimit()          # 订单修改接口(待实现)
+
+packages/telemetry/src/gridMetrics.ts
+  └── gridIncrementalMetrics # 增量模式指标
+```
+
+### 8.2 参考文档
+
+- `IMPLEMENTATION_PLAN.md` - M1.6 总体规划
+- `GRID_STRATEGY_DESIGN.md` - 网格策略原理
+- `CONFIG_REFERENCE.md` - 配置参数详解
+- `OPERATIONS_PLAYBOOK.md` - 运维手册

+ 218 - 0
docs/M16_MULTI_INSTANCE_ORCHESTRATION_DESIGN.md

@@ -0,0 +1,218 @@
+# M1.6 Workstream 4: Multi-Instance Orchestration Design
+
+**版本**: v1.0.0  
+**日期**: 2025-10-09  
+**负责人**: Strategy Platform Team  
+**依赖文档**: `IMPLEMENTATION_PLAN.md`, `ARCHITECTURE_DESIGN.md`, `M16_INCREMENTAL_GRID_DESIGN.md`, `M16_PLACEMENT_THROTTLING_DESIGN.md`
+
+---
+
+## 1. 背景与目标
+
+- 支持同一 Runner 进程内运行多个 Grid 实例(多账户、多标的、内外圈策略)。  
+- 解决当前单实例架构无法分层挂单、无法差异化限流的问题。  
+- 为后续多 venue / 多策略协同奠定基础。
+
+---
+
+## 2. 核心组件
+
+| 组件 | 说明 |
+|------|------|
+| `GridFleetManager` | 负责实例生命周期管理(启动/停止/热更新)、调度、监控。 |
+| `GridInstance` | 包含 `GridMaker`、`ShadowBook`、`timers`、`metrics` 等封装。 |
+| `FleetConfig` | `grid.instances[]` schema(参见 §3),支持 per-instance 参数。 |
+| `InstanceRegistry` | 映射 `(accountId, symbol)` → `GridInstance`,提供迭代、查找。 |
+| `GlobalOrderCoordinator` 扩展 | 跨实例 STP、冲突检测、节流。 |
+| `FleetController` | CLI/REST 控制接口(list/scale/degrade)。 |
+
+---
+
+## 3. 配置 Schema
+
+```yaml
+grid:
+  enabled: true
+  instances:
+    - id: btc-inner
+      account_id: maker-inner
+      symbol: BTC
+      grid_step_bps: 2
+      grid_range_bps: 120
+      base_clip_usd: 80
+      max_layers: 14
+      adaptive:
+        enabled: true
+        min_grid_step_bps: 1.5
+        max_grid_step_bps: 20
+        post_only_cushion_bps: 0.5
+        min_layers: 12
+        tick_interval_ms: 30000
+        fill_starvation_threshold: { ticks: 4, compress_factor: 0.5 }
+    - id: btc-outer
+      account_id: maker-outer
+      symbol: BTC
+      grid_step_bps: 8
+      grid_range_bps: 200
+      base_clip_usd: 100
+      max_layers: 10
+      adaptive:
+        enabled: true
+        min_grid_step_bps: 6
+        max_grid_step_bps: 60
+        post_only_cushion_bps: 2
+        min_layers: 8
+```
+
+### 3.1 Schema 说明
+- `id`:唯一实例 ID,用于日志/指标/运维命令。  
+- `account_id`:会在 AdapterRegistry 中查找对应账户;必须配对成功。  
+- `symbol`:允许多个实例操作同一 symbol(需 STP 控制)。  
+- 其余字段与现有 `grid` 配置一致,`adaptive` 支持新增字段:  
+  - `fill_starvation_threshold.ticks`: 连续无成交的 tick 数。  
+  - `compress_factor`: 步长压缩比例(例如 0.5 → 减半)。  
+- 如果 `grid.instances` 缺失,则回退到旧的单实例配置(兼容性)。
+
+---
+
+## 4. GridFleetManager 设计
+
+### 4.1 数据结构
+
+```ts
+interface GridInstanceContext {
+  id: string;
+  symbol: string;
+  accountId: string;
+  maker: GridMaker;
+  shadowBook: ShadowBook;
+  timers: {
+    tick: NodeJS.Timeout;
+    status: NodeJS.Timeout;
+  };
+  metrics: GridInstanceMetrics;
+  state: "initializing" | "running" | "paused" | "degraded" | "stopped";
+  lastConfigHash: string;
+}
+```
+
+### 4.2 生命周期
+
+1. **create**  
+   - 构建 ShadowBook、GridMaker、Hedge/Fills handler。  
+   - 注册到 `InstanceRegistry` 并启动 timers。  
+   - 记录 metrics:`grid_instance_state{id="...", state="initializing"}`。
+2. **start**  
+   - 调用 `GridMaker.initialize()`;成功后状态切换为 `running`。  
+3. **pause**  
+   - 停止 timers,调用 `GridMaker.shutdown()`,保留配置。  
+4. **resume**  
+   - 重新启动 timers,视需要重新初始化。  
+5. **degrade**  
+   - 进入“保守模式”——例如仅留 4 层、步长放大、停止自适应。  
+6. **destroy**  
+   - 停止 timers、注销 handlers、释放资源。
+
+### 4.3 热更新
+
+- 新配置进来后,计算 `hash(instanceConfig)`:  
+  - 若 hash 未变化→忽略。  
+  - 若变化→调用 `instance.update(newConfig)`:  
+    1. 暂停当前实例;  
+    2. 应用新配置(重建 GridMaker/ShadowBook);  
+    3. 重新启动。  
+- 更新过程中需确保全局 STP/节流不会误判(详见 §5)。
+
+---
+
+## 5. GlobalOrderCoordinator 扩展
+
+### 5.1 目标
+- 同一 symbol、不同实例之间禁止互相吃单(STP)。  
+- 对共享账户的实例合并限流(如 `maker-inner` / `maker-outer` 使用同一 API 限额)。  
+- 实例级别的订单统计用于填充 metrics。
+
+### 5.2 实现
+
+- OrderId 索引改为 `(accountId, symbol, clientId)` 复合键。  
+- 在 `registerOrder` 时追加实例 ID,检测 `conflicts` 时排除相同实例 / 允许不同 symbol。  
+- 提供接口 `coordinator.getAccountLoad(accountId)`,供节流模块查询。  
+- 新增指标:  
+  - `stp_conflicts_total{instance}`  
+  - `account_order_load{accountId}`  
+- STP 冲突触发时,使用 `fleet.degrade(instanceId, reason="stp_conflict")` 或回退自适应。
+
+---
+
+## 6. 监控与指标
+
+| 指标 | 说明 |
+|------|------|
+| `grid_instance_state{instance}` | `initializing/running/paused/degraded/stopped` |
+| `grid_active_orders{instance}` | 当前活跃订单数 |
+| `grid_pending_levels{instance}` | reconcile 后未补足的层数 |
+| `grid_fill_interval_seconds{instance}` | 平均成交间隔 |
+| `grid_step_bps{instance}` | 当前步长指标(Expose via Gauge) |
+| `order_gateway_queue_depth{accountId}` | WS 队列深度(Placement Throttling 依赖) |
+
+告警建议:
+- `grid_instance_state != running` 持续 > 60s。  
+- `grid_pending_levels > 2` 持续 > 2 tick。  
+- `order_gateway_queue_depth > 40` 持续 > 5s。  
+- `stp_conflicts_total` 突然增加。
+
+---
+
+## 7. 运维接口(FleetController)
+
+| 命令 | 描述 |
+|------|------|
+| `GET /api/fleet/instances` | 列出所有实例状态、当前配置 hash、活跃订单数 |
+| `POST /api/fleet/instances/{id}/pause` | 暂停实例(保留订单) |
+| `POST /api/fleet/instances/{id}/resume` | 恢复实例 |
+| `POST /api/fleet/instances/{id}/degrade` | 进入保守模式(参数:步长放大倍数、保留层数) |
+| `POST /api/fleet/instances/{id}/shutdown` | 停止并撤销所有订单 |
+| `POST /api/fleet/reload` | 重新加载配置(wrapper of config reload) |
+
+接口需经过认证(与 Kill-switch 相同机制),并写入审计日志。
+
+---
+
+## 8. 与 M16 其他 Workstream 的关系
+
+- 与 Workstream 1(增量网格)共享 `reconcileGrid`、实例级 metrics。  
+- 与 Workstream 2(节流 2.0)共享 rate limiter(按 account/instance 聚合)。  
+- 与 Workstream 3(成交驱动压缩)需要同步 `fill_starvation_threshold` 配置。  
+- 在极限贴盘口实验中,可通过多实例策略(内圈 vs 外圈)开展分层测试。
+
+---
+
+## 9. 风险与缓解
+
+| 风险 | 描述 | 缓解策略 |
+|------|------|-----------|
+| STP/限流复杂度上升 | 多实例同时下单造成冲突或超限 | 统一通过 coordinator + rate limiter,必要时设置硬阈值 |
+| 热更新中断 | 更新流程出现异常导致实例停滞 | 引入事务化流程:先创建新实例,成功后替换旧实例 |
+| 指标压力 | 多实例指标数倍增 | 聚合指标支持 `cluster` 标签,Grafana 通过 regex 过滤 |
+
+---
+
+## 10. 实施步骤
+
+1. 扩展配置解析(zod schema + 文档更新)。  
+2. 实现 `GridFleetManager` 与 `GridInstance`。  
+3. 修改 Runner (`apps/runner/src/index.ts`) 以实例化 Fleet。  
+4. 升级 GlobalOrderCoordinator。  
+5. 与 Placement Throttling 集成(共享 rate limiter)。  
+6. 补充指标/运维接口/测试。  
+7. 回归单实例模式确保兼容。  
+8. 金丝雀:1 内圈 + 1 外圈实例,观测 24 小时后推广。
+
+---
+
+## 11. 测试计划
+
+- 单元测试:实例创建/更新/销毁、STP 冲突检测、rate limiter 分配。  
+- 集成测试:Mock 2 个实例,验证填充订单、增量补单、节流逻辑。  
+- 压测:模拟 10+ 实例并发下单,评估指标采集和限流表现。  
+- 金丝雀指标:`grid_instance_state`, `grid_pending_levels`, `order_gateway_queue_depth`, `placement_latency_p95`.

+ 866 - 0
docs/M16_PLACEMENT_THROTTLING_DESIGN.md

@@ -0,0 +1,866 @@
+# M1.6 Placement Throttling 2.0 设计文档
+
+**版本**: v1.0.0
+**日期**: 2025-10-09
+**负责人**: Execution Layer Team
+**依赖文档**: `M16_INCREMENTAL_GRID_DESIGN.md`, `MODULE_INTERFACES.md`
+
+---
+
+## 1. 背景与问题
+
+### 1.1 当前限流问题
+
+从实盘日志分析发现的速率限制问题:
+
+```
+初始化 (20 订单):
+  - 耗时: 1093ms
+  - 平均每单: 55ms
+  - 状态: ✅ 正常
+
+第 1 次调整 (20 订单):
+  - 耗时: 58,241ms (~58s)
+  - 平均每单: 2912ms
+  - 状态: ⚠️ 限流警告
+
+第 2 次调整 (20 订单):
+  - 耗时: 118,668ms (~119s)
+  - 平均每单: 5933ms
+  - 状态: 🚫 严重限流
+
+第 3 次调整 (20 订单):
+  - 耗时: 178,227ms (~178s)
+  - 平均每单: 8911ms
+  - 状态: 🚫 极度限流
+```
+
+**根本原因分析:**
+
+1. **无节流的并发 burst**: 20 个订单同时发送
+2. **触发交易所限流惩罚**: 每个请求被延迟处理
+3. **惩罚累积**: 后续请求延迟越来越严重
+4. **恶性循环**: 重试加剧拥塞
+
+### 1.2 交易所限流策略 (推测)
+
+```
+估算参数:
+- Burst capacity: ~10 requests
+- Refill rate: ~5 requests/second
+- Penalty window: ~60 seconds
+- Penalty multiplier: 每超 1 个请求 +500ms 延迟
+```
+
+**模拟计算:**
+```
+第 1 批 20 个请求同时发送:
+  - 前 10 个: 正常处理 (~100ms each)
+  - 后 10 个: 触发限流
+    - 第 11 个: +500ms delay
+    - 第 12 个: +1000ms delay
+    - ...
+    - 第 20 个: +5000ms delay
+
+平均延迟: (10×100 + 10×2750) / 20 = 1425ms per request
+总耗时: ~28s
+```
+
+---
+
+## 2. 解决方案设计
+
+### 2.1 令牌桶算法 (Token Bucket)
+
+#### 2.1.1 基本原理
+
+```
+┌──────────────────┐
+│  Token Bucket    │
+│  Capacity: 10    │  ← burst 容量
+│  Tokens: 7       │  ← 当前令牌数
+│  Refill: 5/s     │  ← 补充速率
+└──────────────────┘
+        ↑
+        │ refill
+        │
+   ┌────┴────┐
+   │  Timer  │
+   └─────────┘
+
+每个请求消耗 1 token:
+- 有 token → 立即发送
+- 无 token → 进入队列等待
+```
+
+#### 2.1.2 核心实现
+
+```typescript
+class TokenBucket {
+  private tokens: number;
+  private lastRefill: number;
+
+  constructor(
+    private readonly capacity: number,     // burst capacity
+    private readonly refillRate: number    // tokens per second
+  ) {
+    this.tokens = capacity;
+    this.lastRefill = Date.now();
+  }
+
+  // 尝试消耗 token
+  tryConsume(count: number = 1): boolean {
+    this.refill();
+
+    if (this.tokens >= count) {
+      this.tokens -= count;
+      return true;
+    }
+    return false;
+  }
+
+  // 按时间补充 token
+  private refill(): void {
+    const now = Date.now();
+    const elapsedMs = now - this.lastRefill;
+    const elapsedSec = elapsedMs / 1000;
+
+    const tokensToAdd = elapsedSec * this.refillRate;
+    this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
+    this.lastRefill = now;
+  }
+
+  // 获取当前令牌数
+  available(): number {
+    this.refill();
+    return Math.floor(this.tokens);
+  }
+
+  // 预估等待时间
+  waitTimeMs(count: number = 1): number {
+    this.refill();
+
+    if (this.tokens >= count) {
+      return 0;
+    }
+
+    const needed = count - this.tokens;
+    return (needed / this.refillRate) * 1000;
+  }
+}
+```
+
+### 2.2 ThrottledOrderGateway 设计
+
+#### 2.2.1 架构
+
+```
+┌──────────────┐
+│   GridMaker  │
+└──────┬───────┘
+       │ sendOrder()
+       ↓
+┌──────────────────────────┐
+│ ThrottledOrderGateway    │
+│                          │
+│  ┌────────────────┐      │
+│  │  Token Bucket  │      │
+│  └────────────────┘      │
+│          ↓               │
+│  ┌────────────────┐      │
+│  │  Request Queue │      │
+│  │  [order1, ...]  │     │
+│  └────────────────┘      │
+│          ↓               │
+│  ┌────────────────┐      │
+│  │  Batch Sender  │      │
+│  └────────────────┘      │
+└──────────┬───────────────┘
+           │
+           ↓
+    ┌─────────────┐
+    │  WS Client  │
+    └─────────────┘
+```
+
+#### 2.2.2 实现
+
+```typescript
+interface ThrottleConfig {
+  burst: number;               // 最大突发量
+  refillPerSecond: number;     // 每秒补充速率
+  maxQueueDepth: number;       // 最大队列深度
+  queueTimeoutMs: number;      // 队列超时
+  batchSize?: number;          // 批次大小(可选)
+  batchIntervalMs?: number;    // 批次间隔(可选)
+}
+
+class ThrottledOrderGateway {
+  private tokenBucket: TokenBucket;
+  private queue: QueuedOrder[] = [];
+  private processing = false;
+
+  constructor(
+    private wsClient: WebSocketClient,
+    private config: ThrottleConfig,
+    private logger: Logger
+  ) {
+    this.tokenBucket = new TokenBucket(
+      config.burst,
+      config.refillPerSecond
+    );
+
+    // 启动后台处理循环
+    this.startProcessingLoop();
+  }
+
+  async sendOrder(order: Order): Promise<string> {
+    // 检查队列深度
+    if (this.queue.length >= this.config.maxQueueDepth) {
+      throw new Error(`Queue depth exceeded: ${this.queue.length}`);
+    }
+
+    // 创建 Promise + 入队
+    return new Promise((resolve, reject) => {
+      const queued: QueuedOrder = {
+        order,
+        resolve,
+        reject,
+        enqueuedAt: Date.now()
+      };
+
+      this.queue.push(queued);
+
+      // 立即尝试处理
+      setImmediate(() => this.processQueue());
+    });
+  }
+
+  private async processQueue(): Promise<void> {
+    if (this.processing || this.queue.length === 0) {
+      return;
+    }
+
+    this.processing = true;
+
+    try {
+      while (this.queue.length > 0) {
+        // 检查超时
+        this.pruneTimedOutOrders();
+
+        // 尝试获取 token
+        if (!this.tokenBucket.tryConsume(1)) {
+          const waitMs = this.tokenBucket.waitTimeMs(1);
+
+          this.logger.debug({
+            queueDepth: this.queue.length,
+            waitMs: Math.ceil(waitMs)
+          }, 'Throttle: waiting for token');
+
+          // 等待 token 补充
+          await sleep(Math.min(waitMs, 100));
+          continue;
+        }
+
+        // 取出订单
+        const queued = this.queue.shift()!;
+
+        // 异步发送(不阻塞)
+        this.sendOrderAsync(queued);
+
+        // 批次间隔
+        if (this.config.batchIntervalMs) {
+          await sleep(this.config.batchIntervalMs);
+        }
+      }
+    } finally {
+      this.processing = false;
+    }
+  }
+
+  private async sendOrderAsync(queued: QueuedOrder): Promise<void> {
+    try {
+      const result = await this.wsClient.send({
+        method: 'create_order',
+        params: queued.order
+      });
+
+      queued.resolve(result.orderId);
+
+      this.logger.debug({
+        orderId: result.orderId,
+        queuedMs: Date.now() - queued.enqueuedAt
+      }, 'Order sent successfully');
+    } catch (error) {
+      queued.reject(error);
+
+      this.logger.error({
+        error,
+        queuedMs: Date.now() - queued.enqueuedAt
+      }, 'Order send failed');
+    }
+  }
+
+  private pruneTimedOutOrders(): void {
+    const now = Date.now();
+    const timeout = this.config.queueTimeoutMs;
+
+    while (this.queue.length > 0) {
+      const queued = this.queue[0];
+      const age = now - queued.enqueuedAt;
+
+      if (age > timeout) {
+        this.queue.shift();
+        queued.reject(new Error(`Queue timeout after ${age}ms`));
+
+        this.logger.warn({ age }, 'Order timed out in queue');
+      } else {
+        break; // 队列按时间排序
+      }
+    }
+  }
+
+  private startProcessingLoop(): void {
+    setInterval(() => {
+      if (this.queue.length > 0 && !this.processing) {
+        this.processQueue();
+      }
+    }, 50); // 每 50ms 检查一次
+  }
+
+  // 获取状态
+  getStatus() {
+    return {
+      queueDepth: this.queue.length,
+      tokensAvailable: this.tokenBucket.available(),
+      processing: this.processing
+    };
+  }
+}
+
+interface QueuedOrder {
+  order: Order;
+  resolve: (orderId: string) => void;
+  reject: (error: Error) => void;
+  enqueuedAt: number;
+}
+```
+
+### 2.3 分层节流策略
+
+#### 2.3.1 多账户独立限流
+
+```typescript
+class MultiAccountThrottleManager {
+  private gateways = new Map<string, ThrottledOrderGateway>();
+
+  constructor(
+    private wsClients: Map<string, WebSocketClient>,
+    private configs: Map<string, ThrottleConfig>
+  ) {
+    // 为每个账户创建独立 gateway
+    for (const [accountId, wsClient] of wsClients) {
+      const config = configs.get(accountId) ?? this.getDefaultConfig();
+
+      this.gateways.set(
+        accountId,
+        new ThrottledOrderGateway(wsClient, config, logger)
+      );
+    }
+  }
+
+  async sendOrder(order: Order): Promise<string> {
+    const accountId = order.accountId ?? 'default';
+    const gateway = this.gateways.get(accountId);
+
+    if (!gateway) {
+      throw new Error(`No gateway for account: ${accountId}`);
+    }
+
+    return gateway.sendOrder(order);
+  }
+
+  private getDefaultConfig(): ThrottleConfig {
+    return {
+      burst: 10,
+      refillPerSecond: 5,
+      maxQueueDepth: 50,
+      queueTimeoutMs: 5000
+    };
+  }
+}
+```
+
+#### 2.3.2 内外圈差异化配置
+
+```yaml
+execution:
+  throttle:
+    # 默认配置
+    default:
+      burst: 10
+      refill_per_second: 5
+      max_queue_depth: 50
+      queue_timeout_ms: 5000
+
+    # 内圈账户(aggressive)
+    inner_maker:
+      burst: 15             # 更大 burst
+      refill_per_second: 8  # 更快补充
+      max_queue_depth: 100
+      queue_timeout_ms: 3000
+
+    # 外圈账户(conservative)
+    outer_maker:
+      burst: 8
+      refill_per_second: 4
+      max_queue_depth: 30
+      queue_timeout_ms: 10000
+
+    # 对冲账户(critical)
+    hedger:
+      burst: 20             # 最大优先级
+      refill_per_second: 10
+      max_queue_depth: 5    # 队列小,快速失败
+      queue_timeout_ms: 2000
+```
+
+---
+
+## 3. 监控与告警
+
+### 3.1 Prometheus 指标
+
+```typescript
+export const throttleMetrics = {
+  // 令牌桶状态
+  tokens_available: new Gauge({
+    name: 'throttle_tokens_available',
+    help: 'Current tokens in bucket',
+    labelNames: ['account_id']
+  }),
+
+  // 队列状态
+  queue_depth: new Gauge({
+    name: 'throttle_queue_depth',
+    help: 'Current queue depth',
+    labelNames: ['account_id']
+  }),
+
+  queue_depth_max: new Gauge({
+    name: 'throttle_queue_depth_max',
+    help: 'Maximum queue depth observed',
+    labelNames: ['account_id']
+  }),
+
+  // 延迟统计
+  queue_wait_time_ms: new Histogram({
+    name: 'throttle_queue_wait_time_ms',
+    help: 'Time spent in queue',
+    labelNames: ['account_id'],
+    buckets: [10, 50, 100, 500, 1000, 2000, 5000]
+  }),
+
+  // 超时与拒绝
+  queue_timeout_total: new Counter({
+    name: 'throttle_queue_timeout_total',
+    help: 'Total orders timed out in queue',
+    labelNames: ['account_id']
+  }),
+
+  queue_rejected_total: new Counter({
+    name: 'throttle_queue_rejected_total',
+    help: 'Total orders rejected (queue full)',
+    labelNames: ['account_id']
+  }),
+
+  // 吞吐量
+  orders_sent_total: new Counter({
+    name: 'throttle_orders_sent_total',
+    help: 'Total orders sent through throttle',
+    labelNames: ['account_id', 'status']
+  }),
+
+  send_rate_per_second: new Gauge({
+    name: 'throttle_send_rate_per_second',
+    help: 'Current order send rate',
+    labelNames: ['account_id']
+  })
+};
+```
+
+### 3.2 Grafana Dashboard
+
+```json
+{
+  "dashboard": {
+    "title": "Order Throttling",
+    "panels": [
+      {
+        "title": "Queue Depth by Account",
+        "targets": [{
+          "expr": "throttle_queue_depth"
+        }],
+        "type": "graph"
+      },
+      {
+        "title": "Tokens Available",
+        "targets": [{
+          "expr": "throttle_tokens_available"
+        }],
+        "type": "graph"
+      },
+      {
+        "title": "Queue Wait Time (p95)",
+        "targets": [{
+          "expr": "histogram_quantile(0.95, rate(throttle_queue_wait_time_ms_bucket[5m]))"
+        }],
+        "type": "graph"
+      },
+      {
+        "title": "Send Rate",
+        "targets": [{
+          "expr": "rate(throttle_orders_sent_total[1m])"
+        }],
+        "type": "graph"
+      }
+    ]
+  }
+}
+```
+
+### 3.3 告警规则
+
+```yaml
+groups:
+  - name: throttling
+    interval: 30s
+    rules:
+      - alert: ThrottleQueueDepthHigh
+        expr: throttle_queue_depth > 30
+        for: 2m
+        labels:
+          severity: warning
+        annotations:
+          summary: "Throttle queue depth high for {{ $labels.account_id }}"
+          description: "Current depth: {{ $value }}"
+
+      - alert: ThrottleQueueTimeout
+        expr: rate(throttle_queue_timeout_total[5m]) > 0.1
+        for: 1m
+        labels:
+          severity: critical
+        annotations:
+          summary: "Orders timing out in throttle queue"
+          description: "{{ $value }} timeouts per second"
+
+      - alert: ThrottleQueueRejected
+        expr: rate(throttle_queue_rejected_total[5m]) > 0
+        for: 30s
+        labels:
+          severity: critical
+        annotations:
+          summary: "Orders rejected due to full queue"
+          description: "Queue capacity exceeded"
+
+      - alert: ThrottleWaitTimeHigh
+        expr: histogram_quantile(0.95, rate(throttle_queue_wait_time_ms_bucket[5m])) > 2000
+        for: 3m
+        labels:
+          severity: warning
+        annotations:
+          summary: "High queue wait time (p95 > 2s)"
+          description: "p95 wait: {{ $value }}ms"
+```
+
+---
+
+## 4. 测试与验证
+
+### 4.1 单元测试
+
+```typescript
+describe('TokenBucket', () => {
+  it('should allow burst up to capacity', () => {
+    const bucket = new TokenBucket(10, 5);
+
+    for (let i = 0; i < 10; i++) {
+      expect(bucket.tryConsume(1)).toBe(true);
+    }
+
+    expect(bucket.tryConsume(1)).toBe(false);
+  });
+
+  it('should refill over time', async () => {
+    const bucket = new TokenBucket(10, 10); // 10 tokens/sec
+
+    // 消耗所有 token
+    for (let i = 0; i < 10; i++) {
+      bucket.tryConsume(1);
+    }
+
+    expect(bucket.tryConsume(1)).toBe(false);
+
+    // 等待 0.5 秒 → 应补充 5 tokens
+    await sleep(500);
+    expect(bucket.available()).toBe(5);
+  });
+});
+
+describe('ThrottledOrderGateway', () => {
+  it('should queue orders when tokens exhausted', async () => {
+    const gateway = new ThrottledOrderGateway(
+      mockWsClient,
+      { burst: 5, refillPerSecond: 2, maxQueueDepth: 20, queueTimeoutMs: 5000 },
+      logger
+    );
+
+    // 发送 10 个订单(超过 burst)
+    const promises = [];
+    for (let i = 0; i < 10; i++) {
+      promises.push(gateway.sendOrder({ symbol: 'BTC', ... }));
+    }
+
+    // 前 5 个应立即发送
+    await sleep(100);
+    expect(gateway.getStatus().queueDepth).toBe(5);
+
+    // 等待 token 补充
+    await Promise.all(promises);
+    expect(gateway.getStatus().queueDepth).toBe(0);
+  });
+
+  it('should timeout old orders', async () => {
+    const gateway = new ThrottledOrderGateway(
+      mockWsClient,
+      { burst: 0, refillPerSecond: 0, maxQueueDepth: 10, queueTimeoutMs: 1000 },
+      logger
+    );
+
+    const promise = gateway.sendOrder({ symbol: 'BTC', ... });
+
+    await expect(promise).rejects.toThrow(/timeout/i);
+  });
+});
+```
+
+### 4.2 压力测试
+
+```typescript
+describe('ThrottledOrderGateway stress test', () => {
+  it('should handle 100 concurrent orders gracefully', async () => {
+    const gateway = new ThrottledOrderGateway(
+      mockWsClient,
+      { burst: 10, refillPerSecond: 20, maxQueueDepth: 100, queueTimeoutMs: 10000 },
+      logger
+    );
+
+    const start = Date.now();
+    const promises = [];
+
+    for (let i = 0; i < 100; i++) {
+      promises.push(gateway.sendOrder({ symbol: 'BTC', ... }));
+    }
+
+    const results = await Promise.allSettled(promises);
+    const elapsed = Date.now() - start;
+
+    const succeeded = results.filter(r => r.status === 'fulfilled').length;
+    const failed = results.filter(r => r.status === 'rejected').length;
+
+    console.log({
+      total: 100,
+      succeeded,
+      failed,
+      elapsedMs: elapsed,
+      avgMs: elapsed / 100
+    });
+
+    // 预期: 100 orders / 20 per sec = 5 seconds
+    expect(elapsed).toBeGreaterThan(4000);
+    expect(elapsed).toBeLessThan(6000);
+    expect(succeeded).toBe(100);
+  });
+});
+```
+
+---
+
+## 5. 运维手册
+
+### 5.1 配置调优指南
+
+#### 场景 1: 网格初始化 (burst 需求高)
+
+```yaml
+throttle:
+  inner_maker:
+    burst: 20              # 允许快速布局
+    refill_per_second: 5   # 稳态速率
+```
+
+#### 场景 2: 对冲 (延迟敏感)
+
+```yaml
+throttle:
+  hedger:
+    burst: 5
+    refill_per_second: 10  # 高补充速率
+    queue_timeout_ms: 1000 # 快速失败
+```
+
+#### 场景 3: 增量更新 (稳定输出)
+
+```yaml
+throttle:
+  inner_maker:
+    burst: 8
+    refill_per_second: 3   # 匹配批次间隔
+    batch_interval_ms: 333 # ≈ 1 / refill_per_second
+```
+
+### 5.2 故障排查
+
+#### 问题 1: Queue Depth 持续增长
+
+```
+可能原因:
+1. refill_per_second 设置过低
+2. 交易所实际限流比配置更严
+3. 网络延迟导致积压
+
+排查步骤:
+1. 检查 send_rate_per_second 实际值
+2. 调高 refill_per_second 或降低 burst
+3. 检查 WebSocket 连接质量
+```
+
+#### 问题 2: 频繁超时
+
+```
+可能原因:
+1. queue_timeout_ms 设置过短
+2. burst + refill 配置不合理
+3. 队列深度不够
+
+排查步骤:
+1. 增加 queue_timeout_ms
+2. 增加 max_queue_depth
+3. 降低订单发送速率
+```
+
+### 5.3 紧急降级
+
+```bash
+# 临时禁用所有节流(紧急情况)
+curl -X POST http://localhost:3000/api/throttle/disable
+
+# 恢复默认配置
+curl -X POST http://localhost:3000/api/throttle/reset
+
+# 动态调整 refill rate
+curl -X POST http://localhost:3000/api/throttle/config \
+  -d '{"account_id": "inner_maker", "refill_per_second": 10}'
+```
+
+---
+
+## 6. 后续优化方向
+
+### 6.1 自适应速率调整
+
+根据实际 RTT 和成功率动态调整参数:
+
+```typescript
+class AdaptiveThrottle extends ThrottledOrderGateway {
+  private recentLatencies: number[] = [];
+
+  async sendOrderAsync(queued: QueuedOrder): Promise<void> {
+    const start = Date.now();
+
+    try {
+      await super.sendOrderAsync(queued);
+      const latency = Date.now() - start;
+
+      this.recentLatencies.push(latency);
+      this.adjustRateIfNeeded();
+    } catch (error) {
+      // 失败可能是限流信号
+      this.decreaseRate();
+      throw error;
+    }
+  }
+
+  private adjustRateIfNeeded(): void {
+    const p95 = percentile(this.recentLatencies, 0.95);
+
+    if (p95 > 2000) {
+      // 延迟过高 → 降速
+      this.config.refillPerSecond *= 0.8;
+    } else if (p95 < 500) {
+      // 延迟正常 → 尝试加速
+      this.config.refillPerSecond *= 1.1;
+    }
+
+    // 限制范围
+    this.config.refillPerSecond = clamp(
+      this.config.refillPerSecond,
+      1,
+      20
+    );
+  }
+}
+```
+
+### 6.2 分布式限流
+
+多实例场景下的全局协调:
+
+```typescript
+class DistributedThrottle {
+  constructor(
+    private redis: Redis,
+    private accountId: string,
+    private config: ThrottleConfig
+  ) {}
+
+  async tryConsume(count: number): Promise<boolean> {
+    const key = `throttle:${this.accountId}`;
+
+    // Lua 脚本保证原子性
+    const result = await this.redis.eval(`
+      local key = KEYS[1]
+      local capacity = tonumber(ARGV[1])
+      local refill = tonumber(ARGV[2])
+      local requested = tonumber(ARGV[3])
+      local now = tonumber(ARGV[4])
+
+      local bucket = redis.call('HGETALL', key)
+      -- ... token bucket logic ...
+
+      return tokens >= requested
+    `, 1, key, this.config.burst, this.config.refillPerSecond, count, Date.now());
+
+    return result === 1;
+  }
+}
+```
+
+---
+
+## 7. 附录
+
+### 7.1 代码路径
+
+```
+packages/execution/src/throttledOrderGateway.ts  # 核心实现
+packages/execution/src/tokenBucket.ts            # 令牌桶
+packages/telemetry/src/throttleMetrics.ts        # 监控指标
+config/throttle.yaml                             # 配置示例
+```
+
+### 7.2 参考资料
+
+- RFC 6585: Additional HTTP Status Codes (429 Too Many Requests)
+- Token Bucket Algorithm: https://en.wikipedia.org/wiki/Token_bucket
+- Leaky Bucket vs Token Bucket: https://www.cl.cam.ac.uk/teaching/1011/R01/flows.pdf

+ 701 - 0
docs/archive/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/archive/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`。

+ 189 - 0
docs/archive/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;
+- 对冲失败:冻结新增信号,仅处理存量;
+- 资金费极端:降低持仓时长/名义,或只做有利方向的被动单。

+ 505 - 0
docs/archive/合规_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;
+- **对冲失败**:限制新增信号,仅处理存量仓位→安全退出;
+- **资金费率极端**:降低持仓时长/名义,或只开对资金费率有利的一侧被动单。
+
+---
+

+ 2 - 3
packages/connectors/pacifica/src/adapter.ts

@@ -73,9 +73,8 @@ export class PacificaAdapter {
       : `${cfg.baseUrl}/`;
     this.base = new URL(baseUrl);
     this.limiter = new RateLimiter({
-      capacity: 60,
-      refillAmount: 60,
-      refillIntervalMs: 60_000
+      burst: 60,
+      refillPerSec: 1  // 60 tokens per 60 seconds = 1 per second
     });
   }
 

+ 96 - 32
packages/connectors/pacifica/src/rateLimiter.ts

@@ -1,50 +1,114 @@
-export interface RateLimiterConfig {
-  capacity: number;
-  refillAmount: number;
-  refillIntervalMs: number;
+export interface RateLimiterOptions {
+  burst: number;
+  refillPerSec: number;
+  maxQueueDepth?: number;
+}
+
+interface QueueEntry {
+  tokens: number;
+  resolve: () => void;
+  reject: (error: Error) => void;
 }
 
 export class RateLimiter {
-  private tokens: number;
   private readonly capacity: number;
-  private readonly refillAmount: number;
-  private readonly refillIntervalMs: number;
+  private readonly refillPerMs: number;
+  private readonly maxQueueDepth: number;
+  private tokens: number;
   private lastRefill: number;
+  private readonly queue: QueueEntry[] = [];
+  private timer: NodeJS.Timeout | undefined;
 
-  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;
+  constructor(options: RateLimiterOptions) {
+    if (!Number.isFinite(options.burst) || options.burst <= 0) {
+      throw new Error('RateLimiter burst must be > 0');
+    }
+    if (!Number.isFinite(options.refillPerSec) || options.refillPerSec <= 0) {
+      throw new Error('RateLimiter refillPerSec must be > 0');
+    }
+    this.capacity = options.burst;
+    this.tokens = options.burst;
+    this.refillPerMs = options.refillPerSec / 1000;
+    this.maxQueueDepth = options.maxQueueDepth ?? 100;
     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;
+  acquire(tokens = 1): Promise<void> {
+    if (!Number.isFinite(tokens) || tokens <= 0) {
+      return Promise.resolve();
+    }
+    return new Promise((resolve, reject) => {
+      if (this.queue.length >= this.maxQueueDepth) {
+        reject(new Error('RateLimiter queue exceeded'));
+        return;
       }
-      const delay = Math.max(this.refillIntervalMs - (Date.now() - this.lastRefill), 0);
-      await sleep(delay || 1);
+      this.queue.push({ tokens, resolve, reject });
+      this.processQueue();
+    });
+  }
+
+  tryAcquire(tokens = 1): boolean {
+    this.refillTokens();
+    if (this.tokens >= tokens) {
+      this.tokens -= tokens;
+      return true;
     }
+    return false;
   }
 
-  private refillTokens(): void {
-    const now = Date.now();
-    if (now - this.lastRefill < this.refillIntervalMs) return;
+  pending(): number {
+    return this.queue.length;
+  }
 
-    const cycles = Math.floor((now - this.lastRefill) / this.refillIntervalMs);
-    if (cycles <= 0) return;
+  stop(): void {
+    if (this.timer) {
+      clearTimeout(this.timer);
+      this.timer = undefined;
+    }
+    this.queue.splice(0, this.queue.length).forEach(entry => entry.reject(new Error('RateLimiter stopped')));
+  }
 
-    this.tokens = Math.min(this.capacity, this.tokens + cycles * this.refillAmount);
-    this.lastRefill = now;
+  private processQueue(): void {
+    this.refillTokens();
+
+    while (this.queue.length > 0) {
+      const entry = this.queue[0];
+      if (this.tokens >= entry.tokens) {
+        this.tokens -= entry.tokens;
+        this.queue.shift();
+        entry.resolve();
+      } else {
+        break;
+      }
+    }
+
+    if (this.queue.length === 0) {
+      if (this.timer) {
+        clearTimeout(this.timer);
+        this.timer = undefined;
+      }
+      return;
+    }
+
+    if (!this.timer) {
+      const entry = this.queue[0];
+      const needed = entry.tokens - this.tokens;
+      const wait = Math.max(50, needed / this.refillPerMs);
+      this.timer = setTimeout(() => {
+        this.timer = undefined;
+        this.processQueue();
+      }, wait);
+    }
   }
-}
 
-function sleep(ms: number): Promise<void> {
-  return new Promise(resolve => setTimeout(resolve, ms));
+  private refillTokens(): void {
+    const now = Date.now();
+    if (now <= this.lastRefill) return;
+    const delta = now - this.lastRefill;
+    const refill = delta * this.refillPerMs;
+    if (refill > 0) {
+      this.tokens = Math.min(this.capacity, this.tokens + refill);
+      this.lastRefill = now;
+    }
+  }
 }

+ 12 - 1
packages/connectors/pacifica/src/wsOrderGateway.ts

@@ -3,6 +3,7 @@ import type { SigningConfig, SignatureHeader } from "./signing";
 import { signStructuredPayload } from "./signing";
 import { PacificaWebSocket } from "./wsClient";
 import pino from "pino";
+import { RateLimiter } from "./rateLimiter";
 
 const DEFAULT_EXPIRY_WINDOW_MS = 5_000;
 const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
@@ -35,18 +36,21 @@ export interface CreateOrderResult {
 
 export class PacificaWsOrderGateway {
   private readonly logger: pino.Logger;
+  private readonly rateLimiter?: RateLimiter;
 
   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,
-    logger?: pino.Logger
+    logger?: pino.Logger,
+    rateLimiter?: RateLimiter
   ) {
     if (!signing.apiKey || !signing.secret) {
       throw new Error("Pacifica WS gateway requires apiKey and secret");
     }
     this.logger = (logger ?? pino()).child({ name: 'PacificaWsOrderGateway' });
+    this.rateLimiter = rateLimiter;
   }
 
   async connect(): Promise<void> {
@@ -55,6 +59,7 @@ export class PacificaWsOrderGateway {
   }
 
   async createOrder(request: CreateOrderRequest): Promise<CreateOrderResult> {
+    await this.rateLimiter?.acquire();
     const header = this.buildHeader("create_order");
     const payload = {
       symbol: request.symbol,
@@ -68,6 +73,8 @@ export class PacificaWsOrderGateway {
 
     const body = this.buildSignedBody(header, payload);
 
+    this.logger.info({ symbol: request.symbol, side: request.side, price: request.price, amount: request.amount, clientOrderId: request.clientOrderId }, 'Sending create_order via WebSocket');
+
     // 记录 RPC 性能指标
     const startTs = Date.now();
     const response = await this.ws.sendRpc("create_order", body, this.requestTimeoutMs);
@@ -96,10 +103,13 @@ export class PacificaWsOrderGateway {
       throw new Error(`Pacifica WS create_order response missing order id: ${JSON.stringify(response)}`);
     }
 
+    this.logger.info({ orderId, clientOrderId: request.clientOrderId, elapsedMs }, 'Order created successfully via WebSocket');
+
     return { orderId };
   }
 
   async cancelOrder(request: CancelOrderRequest): Promise<void> {
+    await this.rateLimiter?.acquire();
     const header = this.buildHeader("cancel_order");
 
     if (!request.orderId && !request.clientOrderId) {
@@ -119,6 +129,7 @@ export class PacificaWsOrderGateway {
   }
 
   async cancelAll(request: CancelAllRequest = {}): Promise<void> {
+    await this.rateLimiter?.acquire();
     const header = this.buildHeader("cancel_all_orders");
     const payload: Record<string, unknown> = {
       all_symbols: request.allSymbols ?? true,

+ 204 - 64
packages/strategies/src/gridMaker.ts

@@ -19,6 +19,7 @@ export interface GridConfig {
   accountId?: string;
   tickSize?: number;
   lotSize?: number;
+  incrementalMode?: boolean;
 }
 
 export interface GridLevel {
@@ -32,6 +33,13 @@ export interface GridLevel {
   timestamp: number;
 }
 
+type TargetLevel = {
+  index: number;
+  side: Side;
+  px: number;
+  sz: number;
+};
+
 export class GridMaker {
   private grids: Map<number, GridLevel> = new Map();
   private currentDelta: number = 0;
@@ -47,6 +55,8 @@ export class GridMaker {
   private readonly releaseOrder: (orderId: string, clientOrderId?: string) => void;
   private readonly logger: Logger;
   private currentGridStepBps: number;
+  private baseClipUsd: number;
+  private readonly incrementalMode: boolean;
   private lastRecenterTs = 0;
   private resetting = false;
   private consecutivePlaceFailures = 0;
@@ -65,7 +75,9 @@ export class GridMaker {
     this.accountId = config.accountId ?? "maker";
     this.tickSize = config.tickSize ?? 1;
     this.lotSize = config.lotSize ?? 0.00001;
+    this.baseClipUsd = config.baseClipUsd;
     this.currentGridStepBps = config.gridStepBps;
+    this.incrementalMode = config.incrementalMode ?? false;
     this.cancelAllOrders = cancelAllOrders ?? (async () => {});
     this.releaseOrder = releaseOrder ?? (() => {});
     const baseLogger = logger ?? pino({ name: 'GridMaker' });
@@ -96,7 +108,7 @@ export class GridMaker {
     }
     this.logger.info({ mid, config: this.config, currentGridStepBps: this.currentGridStepBps }, 'Initializing grid');
 
-    const { gridRangeBps, maxLayers, baseClipUsd } = this.config;
+    const { gridRangeBps, maxLayers } = this.config;
     const minLayers = this.adaptiveConfig?.minLayers;
     const minGridStepBps = this.adaptiveConfig?.minGridStepBps ?? 0;
     const maxGridStepBps = this.adaptiveConfig?.maxGridStepBps ?? Number.POSITIVE_INFINITY;
@@ -132,7 +144,7 @@ export class GridMaker {
     const actualLayers = Math.max(Math.min(calculatedLayers, maxLayers), minLayerTarget);
 
     // 计算单层订单大小(base asset)
-    const baseSz = baseClipUsd / mid;
+    const baseSz = this.baseClipUsd / mid;
     const normalizedBaseSz = this.normalizeSize(baseSz);
 
     if (normalizedBaseSz <= 0) {
@@ -142,6 +154,7 @@ export class GridMaker {
     this.logger.info({
       actualLayers,
       baseSz: normalizedBaseSz,
+      baseClipUsd: this.baseClipUsd,
       gridStepBps: this.currentGridStepBps,
       minLayerTarget: minLayers
     }, 'Grid parameters calculated');
@@ -149,51 +162,92 @@ export class GridMaker {
     // 重置连续失败计数
     this.consecutivePlaceFailures = 0;
 
-    // 启用批量初始化模式(降低节流间隔)
-    if (typeof (this.router as any).enableBulkInitMode === 'function') {
-      (this.router as any).enableBulkInitMode();
-      this.logger.info('Enabled bulk init mode for faster grid placement');
+    const targetLevels = this.buildTargetLevels(mid, stepRatio, actualLayers, normalizedBaseSz);
+    const expectedOrders = targetLevels.length;
+
+    if (this.incrementalMode && this.isInitialized) {
+      try {
+        const stats = await this.reconcileGrid(targetLevels);
+        this.needsReinit = false;
+        this.isInitialized = true;
+        this.logger.info({
+          mode: 'incremental',
+          totalTarget: expectedOrders,
+          placed: stats.placed,
+          cancelled: stats.cancelled,
+          reused: stats.reused,
+          elapsedMs: stats.elapsedMs
+        }, 'Incremental grid reconcile completed');
+        return;
+      } catch (error) {
+        this.logger.warn({ error: normalizeError(error) }, 'Incremental reconcile failed, falling back to full rebuild');
+        await this.cancelActiveOrders();
+      }
     }
 
-    // 布置网格 - 并行化下单以减少 WS RPC 延迟影响
-    const expectedOrders = actualLayers * 2;
-    const placePromises: Promise<void>[] = [];
+    const successfulOrders = await this.placeAllLevels(targetLevels);
 
-    for (let i = 1; i <= actualLayers; i++) {
-      if (this.needsReinit) break;
+    if (this.needsReinit) {
+      this.logger.info({ action: 'schedule_reinit', currentGridStepBps: this.currentGridStepBps }, 'Reinitialization scheduled due to placement failures');
+      return;
+    }
 
-      // 买单网格(负索引)
-      const buyPx = mid * (1 - i * stepRatio);
-      placePromises.push(
-        this.placeGridOrder(-i, 'buy', buyPx, normalizedBaseSz)
-          .catch(err => {
-            this.logger.warn({ index: -i, side: 'buy', px: buyPx, error: normalizeError(err) }, 'Skipping failed grid level');
-          })
-      );
+    this.needsReinit = false;
+    this.isInitialized = true;
+    this.logger.info({
+      totalGrids: this.grids.size,
+      expectedLayers: expectedOrders,
+      successRate: expectedOrders > 0 ? ((successfulOrders / expectedOrders) * 100).toFixed(1) + '%' : 'N/A'
+    }, 'Grid initialization completed');
+  }
 
-      // 卖单网格(正索引)
+  private buildTargetLevels(mid: number, stepRatio: number, actualLayers: number, normalizedBaseSz: number): TargetLevel[] {
+    const levels: TargetLevel[] = [];
+    for (let i = 1; i <= actualLayers; i++) {
+      const buyPx = mid * (1 - i * stepRatio);
+      levels.push({ index: -i, side: 'buy', px: buyPx, sz: normalizedBaseSz });
       const sellPx = mid * (1 + i * stepRatio);
-      placePromises.push(
-        this.placeGridOrder(i, 'sell', sellPx, normalizedBaseSz)
-          .catch(err => {
-            this.logger.warn({ index: i, side: 'sell', px: sellPx, error: normalizeError(err) }, 'Skipping failed grid level');
-          })
-      );
+      levels.push({ index: i, side: 'sell', px: sellPx, sz: normalizedBaseSz });
+    }
+    return levels;
+  }
+
+  private async placeAllLevels(levels: TargetLevel[]): Promise<number> {
+    if (typeof (this.router as any).enableBulkInitMode === 'function') {
+      (this.router as any).enableBulkInitMode();
+      this.logger.info('Enabled bulk init mode for faster grid placement');
     }
 
-    // 并行等待所有订单完成
-    this.logger.info({ totalOrders: placePromises.length }, 'Placing grid orders in parallel');
     const startTs = Date.now();
-    await Promise.allSettled(placePromises);
+    let successCount = 0;
+
+    const placementPromises = levels.map(level =>
+      this.placeGridOrder(level.index, level.side, level.px, level.sz)
+        .then(() => {
+          successCount += 1;
+        })
+        .catch(err => {
+          this.logger.warn({ index: level.index, side: level.side, px: level.px, error: normalizeError(err) }, 'Skipping failed grid level');
+        })
+    );
+
+    this.logger.info({ totalOrders: placementPromises.length }, 'Placing grid orders in parallel');
+    await Promise.allSettled(placementPromises);
     const elapsedMs = Date.now() - startTs;
+    const avg = placementPromises.length > 0 ? (elapsedMs / placementPromises.length).toFixed(0) : '0';
+
+    if (typeof (this.router as any).disableBulkInitMode === 'function') {
+      (this.router as any).disableBulkInitMode();
+      this.logger.debug('Disabled bulk init mode, returning to normal throttling');
+    }
+
     this.logger.info({
-      totalOrders: placePromises.length,
-      successfulOrders: this.grids.size,
+      totalOrders: placementPromises.length,
+      successfulOrders: successCount,
       elapsedMs,
-      averageMs: (elapsedMs / placePromises.length).toFixed(0)
+      averageMs: avg
     }, 'Parallel grid placement completed');
 
-    // 检查是否有过多失败,触发自动回退
     if (this.consecutivePlaceFailures >= 5) {
       const oldStep = this.currentGridStepBps;
       this.currentGridStepBps = Math.min(oldStep * 1.5, this.adaptiveConfig?.maxGridStepBps ?? 100);
@@ -207,24 +261,84 @@ export class GridMaker {
       this.isInitialized = false;
     }
 
-    // 禁用批量初始化模式
-    if (typeof (this.router as any).disableBulkInitMode === 'function') {
-      (this.router as any).disableBulkInitMode();
-      this.logger.debug('Disabled bulk init mode, returning to normal throttling');
+    return successCount;
+  }
+
+  private async reconcileGrid(levels: TargetLevel[]): Promise<{ placed: number; reused: number; cancelled: number; elapsedMs: number }> {
+    const start = Date.now();
+    let placed = 0;
+    let reused = 0;
+    let cancelled = 0;
+
+    this.logger.info({ targetLevels: levels.length }, 'Starting incremental grid reconciliation');
+
+    const targetMap = new Map<number, TargetLevel>();
+    for (const level of levels) {
+      targetMap.set(level.index, level);
     }
 
-    if (this.needsReinit) {
-      this.logger.info({ action: 'schedule_reinit', currentGridStepBps: this.currentGridStepBps }, 'Reinitialization scheduled due to placement failures');
-      return;
+    const tolerancePx = this.tickSize > 0 ? this.tickSize : levels.length > 0 ? Math.abs(levels[0].px) * 1e-6 : 1e-6;
+    const sizeToleranceBase = this.lotSize > 0 ? this.lotSize : 1e-8;
+
+    const activeLevels = Array.from(this.grids.values()).filter(level => level.orderId && !level.filled);
+
+    for (const level of activeLevels) {
+      const target = targetMap.get(level.index);
+      if (!target) {
+        this.logger.info({ index: level.index, reason: 'not_in_target' }, 'Cancelling order - not in target map');
+        try {
+          await this.cancelGridLevel(level);
+          cancelled += 1;
+        } catch (error) {
+          this.logger.error({ index: level.index, error: error instanceof Error ? error.message : String(error) }, 'Failed to cancel order in reconcile (not in target) - continuing');
+        }
+        continue;
+      }
+      const pxDiff = Math.abs(level.px - target.px);
+      const sizeDiff = Math.abs(level.sz - target.sz);
+      const sizeTolerance = Math.max(sizeToleranceBase, target.sz * 0.001);
+      if (pxDiff <= tolerancePx && sizeDiff <= sizeTolerance) {
+        this.logger.debug({ index: level.index, oldPx: level.px, newPx: target.px, pxDiff, tolerancePx }, 'Reusing order - within tolerance');
+        reused += 1;
+        targetMap.delete(level.index);
+      } else {
+        this.logger.info({ index: level.index, oldPx: level.px, newPx: target.px, pxDiff, tolerancePx, reason: 'price_changed' }, 'Cancelling order - price/size changed');
+        try {
+          await this.cancelGridLevel(level);
+          cancelled += 1;
+        } catch (error) {
+          this.logger.error({ index: level.index, error: error instanceof Error ? error.message : String(error) }, 'Failed to cancel order in reconcile (price changed) - continuing');
+        }
+      }
     }
 
-    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');
+    this.logger.info({ remainingTargets: targetMap.size, reused, cancelled }, 'Reconcile phase 1 complete - now placing new orders');
+
+    for (const target of targetMap.values()) {
+      this.logger.info({ index: target.index, side: target.side, px: target.px, sz: target.sz }, 'Placing new grid order in reconcile');
+      try {
+        await this.placeGridOrder(target.index, target.side, target.px, target.sz);
+        placed += 1;
+      } catch (error) {
+        this.logger.error({ index: target.index, error: error instanceof Error ? error.message : String(error) }, 'Failed to place order in reconcile - continuing');
+      }
+    }
+
+    const elapsedMs = Date.now() - start;
+    this.consecutivePlaceFailures = Math.max(0, this.consecutivePlaceFailures);
+    this.logger.info({ placed, reused, cancelled, elapsedMs }, 'Reconcile phase 2 complete - all orders placed');
+    return { placed, reused, cancelled, elapsedMs };
+  }
+
+  private async cancelActiveOrders(): Promise<void> {
+    const activeLevels = Array.from(this.grids.values()).filter(level => level.orderId && !level.filled);
+    for (const level of activeLevels) {
+      try {
+        await this.cancelGridLevel(level);
+      } catch (error) {
+        this.logger.warn({ index: level.index, side: level.side, error: normalizeError(error) }, 'Failed to cancel level during fallback');
+      }
+    }
   }
 
   /**
@@ -405,6 +519,7 @@ export class GridMaker {
       pendingHedges: this.pendingHedges.size,
       expectedOrders,
       effectiveOrderRatio,
+      baseClipUsd: this.baseClipUsd,
       adaptive: adaptiveStatus
     };
     observeGridMetrics(this.config.symbol, {
@@ -592,26 +707,31 @@ export class GridMaker {
       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');
+      if (isIgnorableCancelError(normalized)) {
+        this.logger.info({ orderId, error: normalized }, 'Treating order cancel as already completed');
+        cancelled = true;
+      } else {
+        this.logger.warn({ orderId, error: normalized }, 'Grid cancel by orderId failed');
+        if (level.clientId) {
+          try {
+            await this.router.cancelByClientId(level.clientId);
             cancelled = true;
-          } else {
-            this.logger.error(
-              { orderId, clientId: level.clientId, error: fallbackNormalized },
-              'Grid cancel by clientId failed'
-            );
+          } catch (fallbackError) {
+            const fallbackNormalized = normalizeError(fallbackError);
+            if (isIgnorableCancelError(fallbackNormalized)) {
+              this.logger.info({ orderId, clientId: level.clientId, error: fallbackNormalized }, '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');
+        this.logger.info({ orderId, error: normalized }, 'Order cancel considered complete');
         cancelled = true;
       }
       if (!cancelled) {
@@ -667,6 +787,16 @@ export class GridMaker {
     this.logger.info({ action: 'shutdown_complete' }, 'GridMaker shutdown completed');
   }
 
+  updateBaseClipUsd(newClipUsd: number): void {
+    if (!Number.isFinite(newClipUsd) || newClipUsd <= 0) {
+      this.logger.warn({ newClipUsd }, 'Ignored invalid base clip update');
+      return;
+    }
+    if (Math.abs(newClipUsd - this.baseClipUsd) < 1e-6) return;
+    this.baseClipUsd = newClipUsd;
+    this.logger.info({ baseClipUsd: this.baseClipUsd }, 'Updated base clip size from equity ratio');
+  }
+
   /**
    * 根据 orderId 查找网格层
    */
@@ -831,7 +961,11 @@ export class GridMaker {
     }, 'Adjusting grid step based on volatility');
 
     this.currentGridStepBps = clamp(finalTargetStep, effectiveMinStep, maxGridStepBps);
-    await this.reset();
+    if (this.incrementalMode && this.isInitialized && !this.resetting) {
+      await this.initialize();  // initialize() 内部会调用 reconcileGrid 进行增量更新
+    } else {
+      await this.reset();
+    }
   }
 
   private async maybeRecenterGrid(mid: number): Promise<void> {
@@ -856,7 +990,13 @@ export class GridMaker {
     }, 'Grid center drift detected, resetting grid');
 
     this.lastRecenterTs = now;
-    await this.reset();
+    if (this.incrementalMode && this.isInitialized && !this.resetting) {
+      this.gridCenter = mid;
+      await this.initialize();  // initialize() 内部会调用 reconcileGrid 进行增量更新
+    } else {
+      await this.reset();
+    }
+    return;
   }
 
   private prunePendingHedges(now: number): void {