M16_FILL_DRIVEN_TIGHTENING_DESIGN.md 59 KB

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)中发现:

{"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 成交缺失的根本原因

原因一:步长与波动率的反向关系失效

当前自适应逻辑:

const targetStep = mapVolatilityToStep(hourlyVolBps, minVolBps, maxVolBps, minStepBps, maxStepBps);

问题

  • 波动率计算基于 30 分钟滚动窗口的价格范围
  • 当价格进入盘整期(窄幅震荡)时,hourlyVolBps 下降 → targetStep 增大
  • 步长增大导致订单远离当前价格,成交概率进一步降低
  • 形成负反馈循环:无成交 → 盘整 → 步长扩大 → 更无成交

原因二:缺少基于实际成交的反馈机制

现有系统仅根据市场波动率调整参数,未考虑策略自身的成交表现

  • 无成交时,系统不知道是"挂单距离过远"还是"市场流动性不足"
  • 成交恢复后,系统无法快速调整回紧密网格
  • 缺少"探索-利用"平衡:无成交时应该收紧探测成交区域,而非扩大

原因三:Post-Only 保护与贴盘口的矛盾

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 成交计数

interface FillMetrics {
  tickIndex: number;              // 当前 tick 序号(从启动开始递增)
  fillCountThisTick: number;      // 本 tick 内成交订单数
  fillVolumeUsd: number;          // 本 tick 内成交金额(USD)
  avgFillIntervalSec: number;     // 滚动平均成交间隔(秒)
  lastFillTimestamp: number;      // 上次成交时间戳
  noFillDurationSec: number;      // 距上次成交经过的秒数
  consecutiveEmptyTicks: number;  // 连续无成交 tick 数
}

2.1.2 滚动窗口计算

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 触发条件

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 压缩执行逻辑

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 压缩次数限制

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 硬性参数下限

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 批次执行压缩动作

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 预算检查

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 配置定义

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 验证

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 配置加载与验证

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

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

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 集成

// 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 指标定义

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 更新逻辑

// 在 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 定义

{
  "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 告警规则

# 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 单元测试

// 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 集成测试

// 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 回测验证

// 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 启用/禁用功能

# 禁用 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
compression_step_size: 0.9
max_compression_factor: 0.7
快速响应无成交,但保留 30% 缓冲避免过度贴盘
中低频策略 fill_starvation_threshold: 5
compression_step_size: 0.85
max_compression_factor: 0.5
平衡探索与保守,避免频繁调整
极端贴盘口模式 min_absolute_step_bps: 0.5
min_absolute_cushion_bps: 0
fill_starvation_threshold: 2
激进模式,需配合高 burst throttling
盘整市(低波动) fill_starvation_threshold: 7
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

    curl http://localhost:4000/api/config/current | jq '.grid.adaptive.fill_driven.enabled'
    
  2. 检查是否触发速率限制

    curl http://localhost:9090/api/v1/query?query=grid_compression_rate_limited_total
    
  3. 检查盘口价差是否低于阈值

    curl http://localhost:9090/api/v1/query?query=orderbook_spread_bps
    
  4. 查看日志

    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_bpsmin_absolute_cushion_bps

    fill_driven:
     min_absolute_step_bps: 2
     min_absolute_cushion_bps: 1
    
  2. 降低压缩强度

    fill_driven:
     compression_step_size: 0.9  # 从 0.85 提高到 0.9
     max_compression_factor: 0.6  # 从 0.5 提高到 0.6
    
  3. 增加压缩触发延迟

    fill_driven:
     fill_starvation_threshold: 7  # 从 5 提高到 7
    

问题 3:扩张速度过慢

症状

成交恢复后 20 分钟,compression_factor 仍为 0.7

解决方案

  1. 提高扩张步长

    fill_driven:
     expansion_step_size: 1.1  # 从 1.05 提高到 1.1
    
  2. 检查是否有持续的零星成交阻止完全恢复

    curl http://localhost:9090/api/v1/query?query=rate(grid_fill_total[5m])
    
  3. 手动重置压缩状态

    curl -X POST http://localhost:4000/api/grid/fill-driven/reset
    

8.4 手动干预接口

// 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. 确认是否为市场流动性问题(检查交易所订单簿深度)

    curl "https://api.pacifica.fi/api/v1/orderbook?symbol=BTC"
    
  2. 检查 fill-driven 是否已触发压缩

    curl http://localhost:9090/api/v1/query?query=grid_compression_factor{symbol="BTC"}
    
  3. 若未压缩且配置正确,检查日志中的限流或错误

    tail -100 logs/runner-*.log | grep -E "rate.limit|compression|error"
    
  4. 若市场流动性正常但策略无成交,考虑手动触发一次压缩

    # 降低步长到当前的 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. 确认是否确实无成交

    curl http://localhost:9090/api/v1/query?query=rate(grid_fill_total{symbol="BTC"}[10m])
    
  2. 若有成交但未扩张,检查扩张逻辑是否被阻塞

    tail -50 logs/runner-*.log | grep "expansion"
    
  3. 手动重置压缩状态

    curl -X POST http://localhost:4000/api/grid/fill-driven/reset -d '{"symbol": "BTC"}'
    
  4. 若问题持续,禁用 fill-driven 并恢复到基础自适应模式

    curl -X POST http://localhost:4000/api/config/update \
     -d '{"grid": {"adaptive": {"fill_driven": {"enabled": false}}}}'
    

9. 多实例协调

9.1 跨账户/标的隔离

// 每个 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 检测

// 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 差异化策略(内圈/外圈账号)

# 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 自动降级触发条件

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 手动回滚脚本

#!/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)

  • 实现 FillRateTracker 模块
  • 实现 FillDrivenAdaptation 核心逻辑
  • 实现 CompressionRateLimiter
  • 集成到 GridMaker
  • 单元测试覆盖率 > 80%

Phase 2: Integration & Config (Week 1-2)

  • Zod schema 定义与验证
  • 配置热更新支持
  • Prometheus 指标集成
  • Grafana dashboard 创建
  • 告警规则配置

Phase 3: Testing & Validation (Week 2)

  • 集成测试(模拟无成交场景)
  • 回测验证(低波动数据集)
  • 压力测试(极端压缩/扩张)
  • 多实例协调测试

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. 参考资料


文档状态: ✅ Ready for Development 审批人: [待填写] 最后更新: 2025-10-09