|
|
@@ -3,6 +3,8 @@ import type { OrderRouter } from '../../execution/src/orderRouter';
|
|
|
import type { HedgeEngine } from '../../hedge/src/hedgeEngine';
|
|
|
import type { ShadowBook } from '../../utils/src/shadowBook';
|
|
|
import { VolatilityEstimator } from '../../utils/src/volatilityEstimator';
|
|
|
+import { FillRateMonitor } from '../../utils/src/fillRateMonitor';
|
|
|
+import { FillRateController, type FillRateControllerConfig } from '../../utils/src/fillRateController';
|
|
|
import pino, { type Logger } from 'pino';
|
|
|
import { observeGridMetrics } from '../../telemetry/src/gridMetrics';
|
|
|
const PLACE_RETRY_ATTEMPTS = 3;
|
|
|
@@ -51,6 +53,9 @@ export class GridMaker {
|
|
|
private readonly pendingHedges = new Map<string, { qty: number; ts: number }>();
|
|
|
private readonly adaptiveConfig?: AdaptiveGridConfig;
|
|
|
private readonly volatilityEstimator?: VolatilityEstimator;
|
|
|
+ private readonly fillRateMonitor?: FillRateMonitor;
|
|
|
+ private readonly fillRateController?: FillRateController;
|
|
|
+ private readonly fillRateControlEnabled: boolean;
|
|
|
private readonly cancelAllOrders: (symbol: string) => Promise<void>;
|
|
|
private readonly releaseOrder: (orderId: string, clientOrderId?: string) => void;
|
|
|
private readonly logger: Logger;
|
|
|
@@ -70,7 +75,8 @@ export class GridMaker {
|
|
|
logger?: Logger,
|
|
|
adaptiveConfig?: AdaptiveGridConfig,
|
|
|
cancelAllOrders?: (symbol: string) => Promise<void>,
|
|
|
- releaseOrder?: (orderId: string, clientOrderId?: string) => void
|
|
|
+ releaseOrder?: (orderId: string, clientOrderId?: string) => void,
|
|
|
+ fillRateControlConfig?: FillRateControllerConfig
|
|
|
) {
|
|
|
this.accountId = config.accountId ?? "maker";
|
|
|
this.tickSize = config.tickSize ?? 1;
|
|
|
@@ -82,14 +88,58 @@ export class GridMaker {
|
|
|
this.releaseOrder = releaseOrder ?? (() => {});
|
|
|
const baseLogger = logger ?? pino({ name: 'GridMaker' });
|
|
|
this.logger = baseLogger.child({ component: 'GridMaker' });
|
|
|
- if (adaptiveConfig?.enabled) {
|
|
|
+
|
|
|
+ // 成交率闭环控制(优先级高于价差/波动率自适应)
|
|
|
+ this.fillRateControlEnabled = !!fillRateControlConfig;
|
|
|
+ if (fillRateControlConfig) {
|
|
|
+ this.fillRateMonitor = new FillRateMonitor({
|
|
|
+ windowSeconds: 300, // 5 分钟窗口
|
|
|
+ minSamples: 2 // 降低到2次成交即可启动(快速响应)
|
|
|
+ });
|
|
|
+ this.fillRateController = new FillRateController(fillRateControlConfig);
|
|
|
+ this.logger.info({ fillRateControlConfig }, 'Fill rate closed-loop control enabled (hybrid mode with spread-based fallback)');
|
|
|
+ } else if (adaptiveConfig?.enabled) {
|
|
|
+ // 如果没有成交率控制,使用传统的自适应逻辑
|
|
|
this.adaptiveConfig = adaptiveConfig;
|
|
|
this.volatilityEstimator = new VolatilityEstimator({
|
|
|
windowMinutes: adaptiveConfig.volatilityWindowMinutes,
|
|
|
minSamples: adaptiveConfig.minSamples,
|
|
|
maxCadenceMs: adaptiveConfig.maxCadenceMs
|
|
|
});
|
|
|
- this.logger.info({ adaptiveConfig }, 'Adaptive grid enabled');
|
|
|
+ this.logger.info({ adaptiveConfig }, 'Adaptive grid enabled (spread/volatility based)');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理订单状态更新(从 WebSocket 推送)
|
|
|
+ */
|
|
|
+ handleOrderUpdate(update: any): void {
|
|
|
+ const orderId = update?.order_id ?? update?.orderId;
|
|
|
+ if (!orderId) return;
|
|
|
+
|
|
|
+ const statusRaw = (update?.status ?? update?.state ?? '').toString().toLowerCase();
|
|
|
+
|
|
|
+ // 如果订单被拒绝或取消,从网格中移除
|
|
|
+ if (statusRaw === 'rejected' || statusRaw === 'canceled' || statusRaw === 'cancelled' || statusRaw === 'expired') {
|
|
|
+ // 查找该订单对应的网格层
|
|
|
+ for (const [index, level] of this.grids.entries()) {
|
|
|
+ if (level.orderId === orderId) {
|
|
|
+ this.logger.warn({
|
|
|
+ index,
|
|
|
+ orderId,
|
|
|
+ status: statusRaw,
|
|
|
+ price: level.px,
|
|
|
+ side: level.side
|
|
|
+ }, 'Order rejected/cancelled by exchange - removing from grid');
|
|
|
+
|
|
|
+ // 从网格中删除这个层级
|
|
|
+ this.grids.delete(index);
|
|
|
+
|
|
|
+ // 标记需要重新初始化(在下次 tick 时补单)
|
|
|
+ this.needsReinit = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -106,7 +156,28 @@ export class GridMaker {
|
|
|
if (this.volatilityEstimator) {
|
|
|
this.volatilityEstimator.update(mid);
|
|
|
}
|
|
|
- this.logger.info({ mid, config: this.config, currentGridStepBps: this.currentGridStepBps }, 'Initializing grid');
|
|
|
+
|
|
|
+ // 获取盘口数据用于诊断
|
|
|
+ const book = this.shadowBook.snapshot(this.config.symbol);
|
|
|
+ const bestBid = book?.bids?.[0]?.px;
|
|
|
+ const bestAsk = book?.asks?.[0]?.px;
|
|
|
+ let marketContext: any = { mid, currentGridStepBps: this.currentGridStepBps };
|
|
|
+
|
|
|
+ if (bestBid && bestAsk) {
|
|
|
+ const spread = bestAsk - bestBid;
|
|
|
+ const spreadBps = (spread / mid) * 10_000;
|
|
|
+ marketContext = {
|
|
|
+ ...marketContext,
|
|
|
+ bestBid,
|
|
|
+ bestAsk,
|
|
|
+ spread: spread.toFixed(2),
|
|
|
+ spreadBps: spreadBps.toFixed(2),
|
|
|
+ bidDepth: book.bids?.slice(0, 3).map(l => ({ px: l.px, sz: l.sz })),
|
|
|
+ askDepth: book.asks?.slice(0, 3).map(l => ({ px: l.px, sz: l.sz }))
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ this.logger.info({ ...marketContext, config: this.config }, 'Initializing grid with market context');
|
|
|
|
|
|
const { gridRangeBps, maxLayers } = this.config;
|
|
|
const minLayers = this.adaptiveConfig?.minLayers;
|
|
|
@@ -165,6 +236,27 @@ export class GridMaker {
|
|
|
const targetLevels = this.buildTargetLevels(mid, stepRatio, actualLayers, normalizedBaseSz);
|
|
|
const expectedOrders = targetLevels.length;
|
|
|
|
|
|
+ // 诊断:输出第1层订单离盘口的距离
|
|
|
+ if (bestBid && bestAsk && targetLevels.length > 0) {
|
|
|
+ const buyOrders = targetLevels.filter(l => l.side === 'buy').sort((a, b) => b.px - a.px);
|
|
|
+ const sellOrders = targetLevels.filter(l => l.side === 'sell').sort((a, b) => a.px - b.px);
|
|
|
+
|
|
|
+ if (buyOrders.length > 0 && sellOrders.length > 0) {
|
|
|
+ const topBuyPx = buyOrders[0].px;
|
|
|
+ const topSellPx = sellOrders[0].px;
|
|
|
+ const buyDistanceBps = ((bestBid - topBuyPx) / bestBid) * 10_000;
|
|
|
+ const sellDistanceBps = ((topSellPx - bestAsk) / bestAsk) * 10_000;
|
|
|
+
|
|
|
+ this.logger.info({
|
|
|
+ topBuyOrder: { px: topBuyPx, distanceFromBestBid: (bestBid - topBuyPx).toFixed(2), distanceBps: buyDistanceBps.toFixed(2) },
|
|
|
+ topSellOrder: { px: topSellPx, distanceFromBestAsk: (topSellPx - bestAsk).toFixed(2), distanceBps: sellDistanceBps.toFixed(2) },
|
|
|
+ totalLayers: actualLayers,
|
|
|
+ buyOrderRange: { highest: buyOrders[0].px, lowest: buyOrders[buyOrders.length - 1].px },
|
|
|
+ sellOrderRange: { lowest: sellOrders[0].px, highest: sellOrders[sellOrders.length - 1].px }
|
|
|
+ }, 'Grid order placement analysis');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
if (this.incrementalMode && this.isInitialized) {
|
|
|
try {
|
|
|
const stats = await this.reconcileGrid(targetLevels);
|
|
|
@@ -220,21 +312,51 @@ export class GridMaker {
|
|
|
|
|
|
const startTs = Date.now();
|
|
|
let successCount = 0;
|
|
|
+ let failedCount = 0;
|
|
|
+ let rateLimitEncountered = false;
|
|
|
+ const maxConcurrency = Math.max(1, Math.floor(this.adaptiveConfig?.maxPlacementConcurrency ?? 4));
|
|
|
+ const batchDelayMs = this.adaptiveConfig?.placementBatchDelayMs ?? 200;
|
|
|
+ const rateLimitBackoffMs = this.adaptiveConfig?.rateLimitBackoffMs ?? Math.max(batchDelayMs * 2, 500);
|
|
|
+
|
|
|
+ this.logger.info({ totalOrders: levels.length, maxConcurrency }, 'Placing grid orders in batches');
|
|
|
|
|
|
- const placementPromises = levels.map(level =>
|
|
|
- this.placeGridOrder(level.index, level.side, level.px, level.sz)
|
|
|
- .then(() => {
|
|
|
+ for (let offset = 0; offset < levels.length; offset += maxConcurrency) {
|
|
|
+ const batch = levels.slice(offset, offset + maxConcurrency);
|
|
|
+ let batchHitRateLimit = false;
|
|
|
+
|
|
|
+ await Promise.allSettled(batch.map(async level => {
|
|
|
+ try {
|
|
|
+ await this.placeGridOrder(level.index, level.side, level.px, level.sz);
|
|
|
successCount += 1;
|
|
|
- })
|
|
|
- .catch(err => {
|
|
|
- this.logger.warn({ index: level.index, side: level.side, px: level.px, error: normalizeError(err) }, 'Skipping failed grid level');
|
|
|
- })
|
|
|
- );
|
|
|
-
|
|
|
- this.logger.info({ totalOrders: placementPromises.length }, 'Placing grid orders in parallel');
|
|
|
- await Promise.allSettled(placementPromises);
|
|
|
+ } catch (err) {
|
|
|
+ failedCount += 1;
|
|
|
+ const normalized = normalizeError(err);
|
|
|
+ const message = normalized?.message?.toLowerCase() ?? '';
|
|
|
+ if (message.includes('ratelimiter') || normalized?.code === 'rate_limit') {
|
|
|
+ batchHitRateLimit = true;
|
|
|
+ }
|
|
|
+ this.logger.warn(
|
|
|
+ { index: level.index, side: level.side, px: level.px, error: normalized },
|
|
|
+ 'Skipping failed grid level'
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }));
|
|
|
+
|
|
|
+ if (batchHitRateLimit) {
|
|
|
+ rateLimitEncountered = true;
|
|
|
+ this.logger.warn(
|
|
|
+ { rateLimitBackoffMs, processedLevels: offset + batch.length },
|
|
|
+ 'Encountered rate limiter while placing batch, backing off before continuing'
|
|
|
+ );
|
|
|
+ await sleep(rateLimitBackoffMs);
|
|
|
+ } else if (batchDelayMs > 0 && offset + batch.length < levels.length) {
|
|
|
+ await sleep(batchDelayMs);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
const elapsedMs = Date.now() - startTs;
|
|
|
- const avg = placementPromises.length > 0 ? (elapsedMs / placementPromises.length).toFixed(0) : '0';
|
|
|
+ const totalAttempts = levels.length;
|
|
|
+ const avg = totalAttempts > 0 ? (elapsedMs / totalAttempts).toFixed(0) : '0';
|
|
|
|
|
|
if (typeof (this.router as any).disableBulkInitMode === 'function') {
|
|
|
(this.router as any).disableBulkInitMode();
|
|
|
@@ -242,10 +364,12 @@ export class GridMaker {
|
|
|
}
|
|
|
|
|
|
this.logger.info({
|
|
|
- totalOrders: placementPromises.length,
|
|
|
+ totalOrders: levels.length,
|
|
|
successfulOrders: successCount,
|
|
|
+ failedOrders: failedCount,
|
|
|
elapsedMs,
|
|
|
- averageMs: avg
|
|
|
+ averageMs: avg,
|
|
|
+ rateLimitEncountered
|
|
|
}, 'Parallel grid placement completed');
|
|
|
|
|
|
if (this.consecutivePlaceFailures >= 5) {
|
|
|
@@ -314,19 +438,55 @@ export class GridMaker {
|
|
|
|
|
|
this.logger.info({ remainingTargets: targetMap.size, reused, cancelled }, 'Reconcile phase 1 complete - now placing new orders');
|
|
|
|
|
|
- for (const target of targetMap.values()) {
|
|
|
+ if (targetMap.size > 0) {
|
|
|
+ const latestMid = this.shadowBook.mid(this.config.symbol);
|
|
|
+ if (latestMid) {
|
|
|
+ const stepRatio = this.currentGridStepBps / 10_000;
|
|
|
+ const recalculatedBaseSz = this.normalizeSize(this.baseClipUsd / latestMid);
|
|
|
+ if (recalculatedBaseSz > 0) {
|
|
|
+ for (const target of targetMap.values()) {
|
|
|
+ const distance = Math.abs(target.index) * stepRatio;
|
|
|
+ const rawPx = target.side === 'buy'
|
|
|
+ ? latestMid * (1 - distance)
|
|
|
+ : latestMid * (1 + distance);
|
|
|
+ target.px = rawPx;
|
|
|
+ target.sz = recalculatedBaseSz;
|
|
|
+ }
|
|
|
+ this.gridCenter = latestMid;
|
|
|
+ this.logger.info({
|
|
|
+ latestMid,
|
|
|
+ recalculatedBaseSz,
|
|
|
+ targetCount: targetMap.size
|
|
|
+ }, 'Updated placement targets with latest mid before re-hanging orders');
|
|
|
+ } else {
|
|
|
+ this.logger.warn({
|
|
|
+ latestMid,
|
|
|
+ recalculatedBaseSz
|
|
|
+ }, 'Recalculated base size invalid, keeping previous placement targets');
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.logger.warn('No latest mid price available during reconcile placement; using previously computed targets');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 并行放置所有新订单(与初始化相同的方式)
|
|
|
+ const placementPromises = Array.from(targetMap.values()).map(async (target) => {
|
|
|
this.logger.info({ index: target.index, side: target.side, px: target.px, sz: target.sz }, 'Placing new grid order in reconcile');
|
|
|
try {
|
|
|
await this.placeGridOrder(target.index, target.side, target.px, target.sz);
|
|
|
- placed += 1;
|
|
|
+ return { success: true, index: target.index };
|
|
|
} catch (error) {
|
|
|
this.logger.error({ index: target.index, error: error instanceof Error ? error.message : String(error) }, 'Failed to place order in reconcile - continuing');
|
|
|
+ return { success: false, index: target.index };
|
|
|
}
|
|
|
- }
|
|
|
+ });
|
|
|
+
|
|
|
+ const results = await Promise.all(placementPromises);
|
|
|
+ placed = results.filter(r => r.success).length;
|
|
|
|
|
|
const elapsedMs = Date.now() - start;
|
|
|
this.consecutivePlaceFailures = Math.max(0, this.consecutivePlaceFailures);
|
|
|
- this.logger.info({ placed, reused, cancelled, elapsedMs }, 'Reconcile phase 2 complete - all orders placed');
|
|
|
+ this.logger.info({ placed, reused, cancelled, elapsedMs, totalTargets: targetMap.size, successRate: `${(placed / targetMap.size * 100).toFixed(1)}%` }, 'Reconcile phase 2 complete - all orders placed');
|
|
|
return { placed, reused, cancelled, elapsedMs };
|
|
|
}
|
|
|
|
|
|
@@ -356,12 +516,23 @@ export class GridMaker {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ // 记录成交事件到成交率监控器
|
|
|
+ if (this.fillRateMonitor) {
|
|
|
+ this.fillRateMonitor.recordFill({
|
|
|
+ isMaker: fill.liquidity === 'maker',
|
|
|
+ isSelfTrade: false, // TODO: 实现自成交检测
|
|
|
+ symbol: fill.symbol,
|
|
|
+ ts: fill.ts
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
this.logger.info({
|
|
|
gridIndex: gridLevel.index,
|
|
|
side: fill.side,
|
|
|
px: fill.px,
|
|
|
sz: fill.sz,
|
|
|
- fee: fill.fee
|
|
|
+ fee: fill.fee,
|
|
|
+ liquidity: fill.liquidity
|
|
|
}, 'Grid order filled');
|
|
|
|
|
|
// 标记该层已成交
|
|
|
@@ -483,8 +654,20 @@ export class GridMaker {
|
|
|
try {
|
|
|
this.logger.info({ action: 'initialize_start' }, 'Re-initializing grid after reset');
|
|
|
await this.initialize();
|
|
|
- this.logger.info({ action: 'reset_complete', gridSize: this.grids.size }, 'Grid reset and re-initialized successfully');
|
|
|
+ const gridReady = this.isInitialized && !this.needsReinit && this.grids.size > 0;
|
|
|
+ if (gridReady) {
|
|
|
+ this.logger.info({ action: 'reset_complete', gridSize: this.grids.size }, 'Grid reset and re-initialized successfully');
|
|
|
+ } else {
|
|
|
+ this.needsReinit = true;
|
|
|
+ this.logger.warn({
|
|
|
+ action: 'reset_incomplete',
|
|
|
+ gridSize: this.grids.size,
|
|
|
+ isInitialized: this.isInitialized,
|
|
|
+ needsReinit: this.needsReinit
|
|
|
+ }, 'Grid reset completed without restoring full grid; will retry on next tick');
|
|
|
+ }
|
|
|
} catch (error) {
|
|
|
+ this.needsReinit = true;
|
|
|
this.logger.error({ error: normalizeError(error), action: 'initialize_failed', gridSize: this.grids.size }, 'Failed to re-initialize grid after reset');
|
|
|
// 不抛出异常,允许下次 onTick 重试
|
|
|
} finally {
|
|
|
@@ -738,8 +921,8 @@ export class GridMaker {
|
|
|
throw error;
|
|
|
}
|
|
|
} finally {
|
|
|
- this.grids.delete(level.index);
|
|
|
if (cancelled) {
|
|
|
+ this.grids.delete(level.index);
|
|
|
this.releaseOrder(orderId, level.clientId);
|
|
|
this.logger.debug({ orderId }, 'Grid order cancel confirmed');
|
|
|
}
|
|
|
@@ -876,59 +1059,114 @@ export class GridMaker {
|
|
|
}
|
|
|
|
|
|
private async maybeAdjustGridStep(): Promise<void> {
|
|
|
- if (!this.adaptiveConfig?.enabled || !this.volatilityEstimator) {
|
|
|
+ // 优先使用成交率闭环控制
|
|
|
+ if (this.fillRateControlEnabled && this.fillRateMonitor && this.fillRateController) {
|
|
|
+ const metrics = this.fillRateMonitor.getMetrics(this.config.symbol);
|
|
|
+
|
|
|
+ // 如果没有足够成交数据,回退到价差自适应(混合模式)
|
|
|
+ if (!metrics) {
|
|
|
+ this.logger.debug({}, 'Insufficient fill rate data, falling back to spread-based adaptive');
|
|
|
+ // 继续执行后面的价差自适应逻辑(不return)
|
|
|
+ } else {
|
|
|
+ // 有成交数据,使用成交率控制
|
|
|
+ return await this.executeFillRateControl(metrics);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 价差自适应(作为后备或冷启动策略)
|
|
|
+ await this.executeSpreadBasedAdaptive();
|
|
|
+ }
|
|
|
+
|
|
|
+ private async executeFillRateControl(metrics: any): Promise<void> {
|
|
|
+ if (!this.fillRateController) return;
|
|
|
+
|
|
|
+ // 使用 PI 控制器计算目标参数
|
|
|
+ const controlOutput = this.fillRateController.compute(metrics);
|
|
|
+
|
|
|
+ // 检查是否需要调整
|
|
|
+ const stepChangeRatio = Math.abs(controlOutput.gridStepBps - this.currentGridStepBps) / this.currentGridStepBps;
|
|
|
+ const clipChangeRatio = Math.abs(controlOutput.clipUsd - this.baseClipUsd) / this.baseClipUsd;
|
|
|
+
|
|
|
+ if (stepChangeRatio > 0.05 || clipChangeRatio > 0.05 || controlOutput.emergencyMode) {
|
|
|
+ this.logger.info({
|
|
|
+ metrics: {
|
|
|
+ fillsPerMinute: metrics.fillsPerMinute.toFixed(2),
|
|
|
+ makerRatio: (metrics.makerRatio * 100).toFixed(1) + '%',
|
|
|
+ selfTradeRatio: (metrics.selfTradeRatio * 100).toFixed(1) + '%'
|
|
|
+ },
|
|
|
+ before: {
|
|
|
+ gridStepBps: this.currentGridStepBps.toFixed(2),
|
|
|
+ clipUsd: this.baseClipUsd.toFixed(1)
|
|
|
+ },
|
|
|
+ after: {
|
|
|
+ gridStepBps: controlOutput.gridStepBps.toFixed(2),
|
|
|
+ clipUsd: controlOutput.clipUsd.toFixed(1)
|
|
|
+ },
|
|
|
+ emergencyMode: controlOutput.emergencyMode,
|
|
|
+ reason: controlOutput.reason
|
|
|
+ }, 'Adjusting grid parameters based on fill rate KPI');
|
|
|
+
|
|
|
+ this.currentGridStepBps = controlOutput.gridStepBps;
|
|
|
+ this.baseClipUsd = controlOutput.clipUsd;
|
|
|
+
|
|
|
+ // 重新初始化网格
|
|
|
+ if (this.incrementalMode && this.isInitialized && !this.resetting) {
|
|
|
+ await this.initialize();
|
|
|
+ } else {
|
|
|
+ await this.reset();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.logger.debug({
|
|
|
+ metrics: {
|
|
|
+ fillsPerMinute: metrics.fillsPerMinute.toFixed(2),
|
|
|
+ makerRatio: (metrics.makerRatio * 100).toFixed(1) + '%'
|
|
|
+ },
|
|
|
+ stepChangeRatio: stepChangeRatio.toFixed(3),
|
|
|
+ clipChangeRatio: clipChangeRatio.toFixed(3)
|
|
|
+ }, 'Fill rate control - no adjustment needed');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async executeSpreadBasedAdaptive(): Promise<void> {
|
|
|
+ if (!this.adaptiveConfig?.enabled) {
|
|
|
return;
|
|
|
}
|
|
|
- const hourlyVolBps = this.volatilityEstimator.getHourlyVolatilityBps();
|
|
|
- if (hourlyVolBps === undefined) return;
|
|
|
|
|
|
const {
|
|
|
- minVolatilityBps,
|
|
|
- maxVolatilityBps,
|
|
|
minGridStepBps,
|
|
|
maxGridStepBps,
|
|
|
minStepChangeRatio,
|
|
|
minLayers
|
|
|
} = this.adaptiveConfig;
|
|
|
|
|
|
- const clampedVol = clamp(hourlyVolBps, minVolatilityBps, maxVolatilityBps);
|
|
|
- const ratio =
|
|
|
- (clampedVol - minVolatilityBps) /
|
|
|
- Math.max(1, maxVolatilityBps - minVolatilityBps);
|
|
|
- const targetStep =
|
|
|
- minGridStepBps + ratio * (maxGridStepBps - minGridStepBps);
|
|
|
-
|
|
|
- // 盘口感知:读取当前最佳买卖价,计算实际可用的最小步长
|
|
|
+ // 基于价差的自适应:读取当前盘口价差,按固定比例调整网格间距
|
|
|
const book = this.shadowBook.snapshot(this.config.symbol);
|
|
|
- let effectiveMinStep = minGridStepBps;
|
|
|
- if (book?.bids?.[0] && book?.asks?.[0]) {
|
|
|
- const mid = (book.bids[0].px + book.asks[0].px) / 2;
|
|
|
- const topSpreadBps = ((book.asks[0].px - book.bids[0].px) / mid) * 10_000;
|
|
|
- const cushionBps = this.adaptiveConfig.postOnlyCushionBps ?? 5;
|
|
|
- const spreadBasedMin = topSpreadBps + cushionBps;
|
|
|
-
|
|
|
- if (spreadBasedMin > effectiveMinStep) {
|
|
|
- effectiveMinStep = spreadBasedMin;
|
|
|
- this.logger.info({
|
|
|
- topSpreadBps: topSpreadBps.toFixed(2),
|
|
|
- cushionBps,
|
|
|
- configMinStepBps: minGridStepBps,
|
|
|
- effectiveMinStepBps: effectiveMinStep.toFixed(2)
|
|
|
- }, 'Adjusting min grid step based on current spread (post-only protection)');
|
|
|
- }
|
|
|
+ if (!book?.bids?.[0] || !book?.asks?.[0]) {
|
|
|
+ this.logger.debug({}, 'No book data available for spread-based adaptive');
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
- // 使用盘口感知的下限
|
|
|
- let finalTargetStep = Math.max(targetStep, effectiveMinStep);
|
|
|
- finalTargetStep = clamp(finalTargetStep, minGridStepBps, maxGridStepBps);
|
|
|
+ const bestBid = book.bids[0].px;
|
|
|
+ const bestAsk = book.asks[0].px;
|
|
|
+ const mid = (bestBid + bestAsk) / 2;
|
|
|
+ const spread = bestAsk - bestBid;
|
|
|
+ const spreadBps = (spread / mid) * 10_000;
|
|
|
+
|
|
|
+ // 目标网格间距 = 价差 × 固定比例(0.4-0.6x spread 适合微网格做市)
|
|
|
+ const spreadRatio = 0.5; // 50% of spread
|
|
|
+ const targetStepFromSpread = spreadBps * spreadRatio;
|
|
|
+
|
|
|
+ // 应用配置的上下限
|
|
|
+ let finalTargetStep = clamp(targetStepFromSpread, minGridStepBps, maxGridStepBps);
|
|
|
|
|
|
+ // 保证最小层数约束
|
|
|
if (minLayers && minLayers > 0) {
|
|
|
const maxStepForMinLayers = this.config.gridRangeBps / minLayers;
|
|
|
if (finalTargetStep > maxStepForMinLayers) {
|
|
|
const adjusted = Math.max(minGridStepBps, Math.min(maxStepForMinLayers, maxGridStepBps));
|
|
|
if (adjusted < finalTargetStep - 1e-6) {
|
|
|
this.logger.info({
|
|
|
- requestedStepBps: finalTargetStep,
|
|
|
+ requestedStepBps: finalTargetStep.toFixed(2),
|
|
|
adjustedStepBps: adjusted,
|
|
|
minLayers,
|
|
|
gridRangeBps: this.config.gridRangeBps
|
|
|
@@ -938,29 +1176,31 @@ export class GridMaker {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // 计算变化幅度,避免频繁微调
|
|
|
const changeRatio = Math.abs(finalTargetStep - this.currentGridStepBps) / this.currentGridStepBps;
|
|
|
if (changeRatio < (minStepChangeRatio ?? 0.2)) {
|
|
|
this.logger.debug({
|
|
|
- hourlyVolBps,
|
|
|
+ spreadBps: spreadBps.toFixed(2),
|
|
|
+ spreadRatio,
|
|
|
currentGridStepBps: this.currentGridStepBps,
|
|
|
targetGridStepBps: finalTargetStep.toFixed(2),
|
|
|
changeRatio: changeRatio.toFixed(3),
|
|
|
minStepChangeRatio,
|
|
|
- effectiveMinStepBps: effectiveMinStep.toFixed(2),
|
|
|
reason: 'change_ratio_below_threshold'
|
|
|
- }, 'Skipping grid step adjustment - change too small');
|
|
|
+ }, 'Skipping grid step adjustment - change too small (spread-based)');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
this.logger.info({
|
|
|
- hourlyVolBps,
|
|
|
+ spreadBps: spreadBps.toFixed(2),
|
|
|
+ spreadRatio,
|
|
|
+ targetStepFromSpread: targetStepFromSpread.toFixed(2),
|
|
|
currentGridStepBps: this.currentGridStepBps,
|
|
|
targetGridStepBps: finalTargetStep.toFixed(2),
|
|
|
- changeRatio: changeRatio.toFixed(3),
|
|
|
- effectiveMinStepBps: effectiveMinStep.toFixed(2)
|
|
|
- }, 'Adjusting grid step based on volatility');
|
|
|
+ changeRatio: changeRatio.toFixed(3)
|
|
|
+ }, 'Adjusting grid step based on spread (micro-grid adaptive)');
|
|
|
|
|
|
- this.currentGridStepBps = clamp(finalTargetStep, effectiveMinStep, maxGridStepBps);
|
|
|
+ this.currentGridStepBps = finalTargetStep;
|
|
|
if (this.incrementalMode && this.isInitialized && !this.resetting) {
|
|
|
await this.initialize(); // initialize() 内部会调用 reconcileGrid 进行增量更新
|
|
|
} else {
|
|
|
@@ -1031,6 +1271,9 @@ export interface AdaptiveGridConfig {
|
|
|
hedgePendingTimeoutMs?: number;
|
|
|
postOnlyCushionBps?: number;
|
|
|
minLayers?: number;
|
|
|
+ maxPlacementConcurrency?: number;
|
|
|
+ placementBatchDelayMs?: number;
|
|
|
+ rateLimitBackoffMs?: number;
|
|
|
}
|
|
|
|
|
|
function clamp(value: number, min: number, max: number): number {
|