From ccc3a2bf8943bcb48185bef2f35124098a65f6e1 Mon Sep 17 00:00:00 2001 From: discountry Date: Fri, 3 Oct 2025 16:16:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=20OffsetMakerEngine?= =?UTF-8?q?=20=E8=AE=A2=E5=8D=95=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=BF=AB=E9=80=9F=E9=87=8D=E4=BB=B7?= =?UTF-8?q?=E6=8A=91=E5=88=B6=E6=9C=BA=E5=88=B6=E4=BB=A5=E5=87=8F=E5=B0=91?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E4=B8=8B=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/strategy/offset-maker-engine.ts | 41 ++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/strategy/offset-maker-engine.ts b/src/strategy/offset-maker-engine.ts index 5b5e517..7f89e51 100644 --- a/src/strategy/offset-maker-engine.ts +++ b/src/strategy/offset-maker-engine.ts @@ -80,11 +80,21 @@ export class OffsetMakerEngine { private lastSkipSell = false; private lastImbalance: "balanced" | "buy_dominant" | "sell_dominant" = "balanced"; + // Reprice suppression for fast-ticking Lighter order book + private readonly repriceDwellMs: number; + private readonly minRepriceTicks: number = 2; + private lastEntryOrderBySide: Record<"BUY" | "SELL", { price: string; ts: number } | null> = { + BUY: null, + SELL: null, + }; + constructor(private readonly config: MakerConfig, private readonly exchange: ExchangeAdapter) { this.tradeLog = createTradeLog(this.config.maxLogEntries); this.rateLimit = new RateLimitController(this.config.refreshIntervalMs, (type, detail) => this.tradeLog.push(type, detail) ); + // Debounce window defaults to 3x refresh interval, min 1s + this.repriceDwellMs = Math.max(1000, this.config.refreshIntervalMs * 3); this.bootstrap(); } @@ -439,7 +449,32 @@ export class OffsetMakerEngine { private async syncOrders(targets: DesiredOrder[]): Promise { const availableOrders = this.openOrders.filter((o) => !this.pendingCancelOrders.has(String(o.orderId))); - const { toCancel, toPlace } = makeOrderPlan(availableOrders, targets); + + // Coalesce reprices for entry orders: if within tick threshold or within dwell window, keep existing order + const adjustedTargets: DesiredOrder[] = targets.map((t) => ({ ...t })); + for (let i = 0; i < adjustedTargets.length; i++) { + const t = adjustedTargets[i]; + if (!t || t.reduceOnly) continue; // only suppress entry orders + const existing = availableOrders.find((o) => o.side === t.side && o.reduceOnly !== true); + if (!existing) continue; + const newPrice = Number(t.price); + const oldPrice = Number(existing.price); + if (!Number.isFinite(newPrice) || !Number.isFinite(oldPrice)) continue; + const ticksDiff = Math.abs(newPrice - oldPrice) / this.config.priceTick; + const recentPlaced = this.lastEntryOrderBySide[t.side]?.ts ?? 0; + const withinDwell = Date.now() - recentPlaced < this.repriceDwellMs; + if (ticksDiff < this.minRepriceTicks || withinDwell) { + // Keep the existing resting order to avoid cancel/place churn + adjustedTargets[i] = { + side: t.side, + price: String(existing.price), + amount: t.amount, + reduceOnly: false, + }; + } + } + + const { toCancel, toPlace } = makeOrderPlan(availableOrders, adjustedTargets); for (const order of toCancel) { if (this.pendingCancelOrders.has(String(order.orderId))) continue; @@ -494,6 +529,10 @@ export class OffsetMakerEngine { qtyStep: 0.001, // 默认数量步长 } ); + // Record last placed entry order timing and price + if (!target.reduceOnly) { + this.lastEntryOrderBySide[target.side] = { price: target.price, ts: Date.now() }; + } } catch (error) { this.tradeLog.push("error", `挂单失败(${target.side} ${target.price}): ${String(error)}`); }