From da3dacb549321ab34ad34cfe8144446b8251e337 Mon Sep 17 00:00:00 2001 From: discountry Date: Wed, 8 Oct 2025 04:56:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E7=BD=91=E6=A0=BC?= =?UTF-8?q?=E5=BC=95=E6=93=8E=EF=BC=8C=E6=B7=BB=E5=8A=A0=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E6=88=B3=E4=BB=A5=E8=B7=9F=E8=B8=AA=E7=AD=89=E5=BE=85=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E7=8A=B6=E6=80=81=EF=BC=8C=E4=BC=98=E5=8C=96=E6=96=B0?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E4=B8=8B=E5=8D=95=E9=80=BB=E8=BE=91=E4=BB=A5?= =?UTF-8?q?=E9=81=BF=E5=85=8D=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/grid-engine.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/strategy/grid-engine.ts b/src/strategy/grid-engine.ts index f9a3eed..2a8ae39 100644 --- a/src/strategy/grid-engine.ts +++ b/src/strategy/grid-engine.ts @@ -141,7 +141,7 @@ export class GridEngine { private stopReason: string | null = null; private lastUpdated: number | null = null; private accountVersion = 0; - private awaitingByLevel = new Map(); + private awaitingByLevel = new Map(); constructor(private readonly config: GridConfig, private readonly exchange: ExchangeAdapter, options: EngineOptions = {}) { this.tradeLog = createTradeLog(this.config.maxLogEntries); @@ -619,7 +619,7 @@ export class GridEngine { const level = meta.intent === "EXIT" ? (meta.sourceLevel ?? this.findSourceForCloseTarget(meta.level, meta.side)) : meta.level; - this.awaitingByLevel.set(level, { accountVerAtStart: this.accountVersion, absAtStart: this.lastAbsPositionAmt }); + this.awaitingByLevel.set(level, { accountVerAtStart: this.accountVersion, absAtStart: this.lastAbsPositionAmt, ts: this.now() }); // skip side effects for this disappeared id in this tick this.orderIntentById.delete(id); continue; @@ -669,6 +669,11 @@ export class GridEngine { // Resolve deferred unknown classifications after a new account snapshot if (this.awaitingByLevel.size) { for (const [level, info] of Array.from(this.awaitingByLevel.entries())) { + // timeout fallback: if no account delta for long time, treat as canceled/no-op + if (this.now() - info.ts > 8000) { + this.awaitingByLevel.delete(level); + continue; + } if (this.accountVersion <= info.accountVerAtStart) continue; const absNow = Math.abs(this.position.positionAmt); if (absNow > info.absAtStart + EPSILON) { @@ -835,9 +840,17 @@ export class GridEngine { } } - // Place desired orders + // Place desired orders (rate-limited per tick to avoid dedupe race) this.desiredOrders = desired; + let newOrdersPlaced = 0; + const MAX_NEW_ORDERS_PER_TICK = 1; for (const d of desired) { + if (newOrdersPlaced >= MAX_NEW_ORDERS_PER_TICK) break; + // If a LIMIT operation is already pending (coordinator lock), skip issuing more this tick + if (this.pendings["LIMIT"]) { + this.log("info", "存在未完成的 LIMIT 操作,本轮不再下新单"); + break; + } const isClose = d.intent === "EXIT" || (d.side === "SELL" && this.isTargetOfPendingLong(d.level)) || (d.side === "BUY" && this.isTargetOfPendingShort(d.level)); const intent: "ENTRY" | "EXIT" = isClose ? "EXIT" : "ENTRY"; // Cap quantities: EXIT by remaining position; ENTRY by maxPositionSize guard @@ -859,7 +872,9 @@ export class GridEngine { d.amount = capped; } const key = this.getOrderKey(d.side, d.price, intent); - if ((activeKeyCounts.get(key) ?? 0) >= 1) { + // Strong local dedupe: skip if any active LIMIT exists with same side+price + const hasSameSidePrice = this.openOrders.some(o => this.isActiveLimitOrder(o) && o.side === d.side && this.normalizePrice(o.price) === d.price); + if (hasSameSidePrice || (activeKeyCounts.get(key) ?? 0) >= 1) { this.log("info", `已存在挂单,跳过 ${intent} ${d.side} @ ${d.price}`); continue; } @@ -880,6 +895,7 @@ export class GridEngine { { priceTick: this.config.priceTick, qtyStep: this.config.qtyStep } ); if (placed) { + newOrdersPlaced += 1; plannedKeyCounts.set(key, (plannedKeyCounts.get(key) ?? 0) + 1); activeKeyCounts.set(key, (activeKeyCounts.get(key) ?? 0) + 1); // ensure suppression window persists after success