From 95f07145a972e52d3774740830135736de177204 Mon Sep 17 00:00:00 2001 From: discountry Date: Wed, 8 Oct 2025 04:19:23 +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=E9=94=AE=E7=BA=A7?= =?UTF-8?q?=E6=8A=91=E5=88=B6=E6=9C=BA=E5=88=B6=E4=BB=A5=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E5=A4=84=E7=90=86=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=B7=B2=E7=9F=A5=E4=B8=8E=E6=9C=AA=E7=9F=A5?= =?UTF-8?q?=E6=84=8F=E5=9B=BE=E7=9A=84=E8=AE=A2=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/strategy/grid-engine.ts | 58 ++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/src/strategy/grid-engine.ts b/src/strategy/grid-engine.ts index 8a92fce..6a69216 100644 --- a/src/strategy/grid-engine.ts +++ b/src/strategy/grid-engine.ts @@ -99,6 +99,10 @@ export class GridEngine { // Track levels awaiting classification after disappearance until next account snapshot confirms + // Key-level suppression for (side:price:intent) to bridge WS latency windows + private readonly pendingKeyUntil = new Map(); + static readonly PENDING_TTL_MS = 10_000; + private sidesLocked = false; private startupCleaned = false; private initialCloseHandled = false; @@ -486,9 +490,22 @@ export class GridEngine { const id = String(o.orderId); currIds.add(id); const meta = this.orderIntentById.get(id); - if (!meta) continue; // unknown intent from legacy/startup orders - const k = this.getOrderKey(o.side, this.normalizePrice(o.price), meta.intent); - activeKeyCounts.set(k, (activeKeyCounts.get(k) ?? 0) + 1); + const priceStr = this.normalizePrice(o.price); + if (meta) { + const k = this.getOrderKey(o.side, priceStr, meta.intent); + activeKeyCounts.set(k, (activeKeyCounts.get(k) ?? 0) + 1); + } else { + // Conservative: count both ENTRY and EXIT to avoid duplicate placements when intent is unknown + const kEntry = this.getOrderKey(o.side, priceStr, "ENTRY"); + const kExit = this.getOrderKey(o.side, priceStr, "EXIT"); + activeKeyCounts.set(kEntry, (activeKeyCounts.get(kEntry) ?? 0) + 1); + activeKeyCounts.set(kExit, (activeKeyCounts.get(kExit) ?? 0) + 1); + } + } + + // Any key that is already visible as active should clear local suppression + for (const [k, cnt] of activeKeyCounts.entries()) { + if ((cnt ?? 0) > 0) this.pendingKeyUntil.delete(k); } // Detect disappeared orders by id @@ -546,12 +563,18 @@ export class GridEngine { this.pendingShortLevels.delete(src); this.closeKeyBySourceLevel.delete(src); } + // Clear suppression for this key on fill + const filledKey = this.getOrderKey(meta.side, meta.price, meta.intent); + this.pendingKeyUntil.delete(filledKey); } else if (classified === "canceled") { // if it was EXIT we also drop mapping by source if (meta.intent === "EXIT") { const src = meta.sourceLevel ?? this.findSourceForCloseTarget(meta.level, meta.side); this.closeKeyBySourceLevel.delete(src); } + // Clear suppression for this key on cancel + const canceledKey = this.getOrderKey(meta.side, meta.price, meta.intent); + this.pendingKeyUntil.delete(canceledKey); } // cleanup intent map for this disappeared id to avoid leaks this.orderIntentById.delete(id); @@ -595,6 +618,11 @@ export class GridEngine { if (this.immediateCloseToPlace.length) { for (const item of this.immediateCloseToPlace) { const key = this.getOrderKey(item.side, item.price, "EXIT"); + const until = this.pendingKeyUntil.get(key); + const nowTs = this.now(); + if (until && until > nowTs) { + continue; + } const count = plannedKeyCounts.get(key) ?? 0; if (count < 1 && !desiredKeySet.has(key)) { desired.push({ level: item.targetLevel, side: item.side, price: item.price, amount: this.config.orderSize, intent: "EXIT" }); @@ -622,6 +650,11 @@ export class GridEngine { continue; // wait until close filled } const key = this.getOrderKey("BUY", this.formatPrice(levelPrice), "ENTRY"); + const until = this.pendingKeyUntil.get(key); + const nowTs = this.now(); + if (until && until > nowTs) { + continue; + } if ((plannedKeyCounts.get(key) ?? 0) >= 1) continue; if (!desiredKeySet.has(key)) { desired.push({ level, side: "BUY", price: this.formatPrice(levelPrice), amount: this.config.orderSize, intent: "ENTRY" }); @@ -643,6 +676,11 @@ export class GridEngine { continue; } const key = this.getOrderKey("SELL", this.formatPrice(levelPrice), "ENTRY"); + const until = this.pendingKeyUntil.get(key); + const nowTs = this.now(); + if (until && until > nowTs) { + continue; + } if ((plannedKeyCounts.get(key) ?? 0) >= 1) continue; if (!desiredKeySet.has(key)) { desired.push({ level, side: "SELL", price: this.formatPrice(levelPrice), amount: this.config.orderSize, intent: "ENTRY" }); @@ -657,6 +695,11 @@ export class GridEngine { if (target == null) continue; const priceStr = this.formatPrice(this.gridLevels[target]!); const closeKey = this.getOrderKey("SELL", priceStr, "EXIT"); + const until = this.pendingKeyUntil.get(closeKey); + const nowTs = this.now(); + if (until && until > nowTs) { + continue; + } if ((plannedKeyCounts.get(closeKey) ?? 0) < 1 && !desiredKeySet.has(closeKey)) { desired.push({ level: target, side: "SELL", price: priceStr, amount: this.config.orderSize, intent: "EXIT" }); desiredKeySet.add(closeKey); @@ -671,6 +714,11 @@ export class GridEngine { if (target == null) continue; const priceStr = this.formatPrice(this.gridLevels[target]!); const closeKey = this.getOrderKey("BUY", priceStr, "EXIT"); + const until = this.pendingKeyUntil.get(closeKey); + const nowTs = this.now(); + if (until && until > nowTs) { + continue; + } if ((plannedKeyCounts.get(closeKey) ?? 0) < 1 && !desiredKeySet.has(closeKey)) { desired.push({ level: target, side: "BUY", price: priceStr, amount: this.config.orderSize, intent: "EXIT" }); desiredKeySet.add(closeKey); @@ -723,11 +771,13 @@ export class GridEngine { this.log, false, undefined, - { priceTick: this.config.priceTick, qtyStep: this.config.qtyStep, skipDedupe: true } + { priceTick: this.config.priceTick, qtyStep: this.config.qtyStep } ); if (placed) { plannedKeyCounts.set(key, (plannedKeyCounts.get(key) ?? 0) + 1); activeKeyCounts.set(key, (activeKeyCounts.get(key) ?? 0) + 1); + // start local suppression window for this key + this.pendingKeyUntil.set(key, this.now() + GridEngine.PENDING_TTL_MS); if (placed.orderId != null) { const record: { side: "BUY" | "SELL"; price: string; level: number; intent: "ENTRY" | "EXIT"; sourceLevel?: number } = { side: d.side,