# 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
