文档版本: v1.0 创建日期: 2025-10-09 依赖文档:
M16_INCREMENTAL_GRID_DESIGN.md,M16_PLACEMENT_THROTTLING_DESIGN.md,CONFIG_REFERENCE.md
从实际运行日志分析(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"}
核心问题:
filledGrids: 0,无任何成交当前自适应逻辑:
const targetStep = mapVolatilityToStep(hourlyVolBps, minVolBps, maxVolBps, minStepBps, maxStepBps);
问题:
hourlyVolBps 下降 → targetStep 增大现有系统仅根据市场波动率调整参数,未考虑策略自身的成交表现:
adaptive:
post_only_cushion_bps: 2 # 为避免 post-only 拒单,在盘口价差基础上额外后退 2 bps
权衡困境:
cushion_bps 过大 → 订单远离盘口 → 低成交率cushion_bps 过小 → post-only 拒单率上升 → 频繁重试 → 触发限流引入成交驱动的自适应机制,使系统在无成交时主动收紧网格,恢复成交后渐进回调:
| 指标 | 目标 |
|---|---|
| 填充率恢复时间 | 连续无成交 5 tick(~3 分钟)后触发首次收紧;10 tick 内恢复成交 |
| 参数调整幅度 | 单次压缩/扩张步长 ≤ 20%;总压缩幅度 ≤ 50% |
| 与限流协同 | 收紧动作需经过 ThrottledGateway 批次执行,避免突发请求 |
| 多实例隔离 | 多账户/标的实例独立决策,通过 GlobalOrderCoordinator 检测冲突 |
| 可观测性 | 新增指标:fill_count_tick, avg_fill_interval_sec, compression_factor, no_fill_duration_sec |
| 可回滚性 | 提供手动/自动禁用开关,防止极端市况下过度激进 |
interface FillMetrics {
tickIndex: number; // 当前 tick 序号(从启动开始递增)
fillCountThisTick: number; // 本 tick 内成交订单数
fillVolumeUsd: number; // 本 tick 内成交金额(USD)
avgFillIntervalSec: number; // 滚动平均成交间隔(秒)
lastFillTimestamp: number; // 上次成交时间戳
noFillDurationSec: number; // 距上次成交经过的秒数
consecutiveEmptyTicks: number; // 连续无成交 tick 数
}
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;
}
}
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;
}
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
});
}
}
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);
}
}
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;
}
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;
}
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;
}
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 分钟)
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()
});
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);
}
}
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;
}
}
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');
}
}
// 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);
}
}
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);
}
}
// 在 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
);
}
}
{
"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"
}
]
}
}
# 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)"
// 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);
});
});
// 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);
});
});
// 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)}%`);
}
# 禁用 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
| 场景 | 推荐配置 | 理由 |
|---|---|---|
| 高频策略(微网格) | fill_starvation_threshold: 3compression_step_size: 0.9max_compression_factor: 0.7 |
快速响应无成交,但保留 30% 缓冲避免过度贴盘 |
| 中低频策略 | fill_starvation_threshold: 5compression_step_size: 0.85max_compression_factor: 0.5 |
平衡探索与保守,避免频繁调整 |
| 极端贴盘口模式 | min_absolute_step_bps: 0.5min_absolute_cushion_bps: 0fill_starvation_threshold: 2 |
激进模式,需配合高 burst throttling |
| 盘整市(低波动) | fill_starvation_threshold: 7min_spread_for_compression_bps: 5 |
延长触发时间,在窄价差时也允许压缩 |
症状:
grid_no_fill_duration_seconds > 300
grid_consecutive_empty_ticks = 6
但 grid_compression_factor 仍为 1.0
排查步骤:
检查 fill_driven.enabled 是否为 true
curl http://localhost:4000/api/config/current | jq '.grid.adaptive.fill_driven.enabled'
检查是否触发速率限制
curl http://localhost:9090/api/v1/query?query=grid_compression_rate_limited_total
检查盘口价差是否低于阈值
curl http://localhost:9090/api/v1/query?query=orderbook_spread_bps
查看日志
tail -f logs/runner-*.log | grep "fill-driven"
症状:
grid_step_bps < 2
post_only_rejection_rate > 0.3
解决方案:
提高 min_absolute_step_bps 和 min_absolute_cushion_bps
fill_driven:
min_absolute_step_bps: 2
min_absolute_cushion_bps: 1
降低压缩强度
fill_driven:
compression_step_size: 0.9 # 从 0.85 提高到 0.9
max_compression_factor: 0.6 # 从 0.5 提高到 0.6
增加压缩触发延迟
fill_driven:
fill_starvation_threshold: 7 # 从 5 提高到 7
症状:
成交恢复后 20 分钟,compression_factor 仍为 0.7
解决方案:
提高扩张步长
fill_driven:
expansion_step_size: 1.1 # 从 1.05 提高到 1.1
检查是否有持续的零星成交阻止完全恢复
curl http://localhost:9090/api/v1/query?query=rate(grid_fill_total[5m])
手动重置压缩状态
curl -X POST http://localhost:4000/api/grid/fill-driven/reset
// 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 });
});
GridNoFillsExtended触发条件:grid_no_fill_duration_seconds > 300 持续 2 分钟
响应步骤:
确认是否为市场流动性问题(检查交易所订单簿深度)
curl "https://api.pacifica.fi/api/v1/orderbook?symbol=BTC"
检查 fill-driven 是否已触发压缩
curl http://localhost:9090/api/v1/query?query=grid_compression_factor{symbol="BTC"}
若未压缩且配置正确,检查日志中的限流或错误
tail -100 logs/runner-*.log | grep -E "rate.limit|compression|error"
若市场流动性正常但策略无成交,考虑手动触发一次压缩
# 降低步长到当前的 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 分钟
响应步骤:
确认是否确实无成交
curl http://localhost:9090/api/v1/query?query=rate(grid_fill_total{symbol="BTC"}[10m])
若有成交但未扩张,检查扩张逻辑是否被阻塞
tail -50 logs/runner-*.log | grep "expansion"
手动重置压缩状态
curl -X POST http://localhost:4000/api/grid/fill-driven/reset -d '{"symbol": "BTC"}'
若问题持续,禁用 fill-driven 并恢复到基础自适应模式
curl -X POST http://localhost:4000/api/config/update \
-d '{"grid": {"adaptive": {"fill_driven": {"enabled": false}}}}'
// 每个 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())
);
}
}
// 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 };
}
}
# 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
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'
});
}
}
#!/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."
FillRateTracker 模块FillDrivenAdaptation 核心逻辑CompressionRateLimiterGridMaker| 指标 | 目标 | 测量方法 |
|---|---|---|
| 填充率提升 | 相对 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 |
文档状态: ✅ Ready for Development 审批人: [待填写] 最后更新: 2025-10-09