From 20c73eba2193c1f3b5a91dd7b858bbd31e2f56ae Mon Sep 17 00:00:00 2001 From: discountry Date: Wed, 8 Oct 2025 00:14:12 +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=E7=AD=89=E5=BE=85?= =?UTF-8?q?=E5=88=86=E7=B1=BB=E6=9C=BA=E5=88=B6=E4=BB=A5=E5=A4=84=E7=90=86?= =?UTF-8?q?=E6=B6=88=E5=A4=B1=E8=AE=A2=E5=8D=95=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E7=8A=B6=E6=80=81=E7=A1=AE=E8=AE=A4=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/strategy/grid-engine.ts | 53 +++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/src/strategy/grid-engine.ts b/src/strategy/grid-engine.ts index 01b70d6..fefe4bf 100644 --- a/src/strategy/grid-engine.ts +++ b/src/strategy/grid-engine.ts @@ -98,6 +98,9 @@ export class GridEngine { // temporarily block re-opening at that level to avoid immediate re-placement. private readonly levelReopenBlockUntil = new Map(); private static readonly DISAPPEAR_REOPEN_COOLDOWN_MS = 2000; + // Track levels awaiting classification after disappearance when neither fill nor cancel is confirmed + private readonly awaitingClassification = new Map(); + private static readonly AWAIT_CLASSIFICATION_TIMEOUT_MS = 2500; private sidesLocked = false; private startupCleaned = false; private initialCloseHandled = false; @@ -502,39 +505,35 @@ export class GridEngine { if (meta) { const prevIds = this.lastOrderIdsByKey.get(prevKey); let classified: "filled" | "canceled" | "unknown" = "unknown"; - if (prevIds && prevIds.size) { + // We generally cannot see final states for disappeared orders in openOrders; rely on position if available + const absNow = Math.abs(this.position.positionAmt); + if (absNow > this.lastAbsPositionAmt + EPSILON) { + classified = "filled"; + } else if (prevIds && prevIds.size) { + // If any explicit canceled-like status is observable (rare in open snapshot), allow classification for (const id of prevIds) { const rec = allOrdersById.get(id); if (!rec) continue; const status = String(rec.status || "").toUpperCase(); const executed = Number(rec.executedQty); - if (status === "FILLED" || (executed > EPSILON && ["CANCELED", "CANCELLED", "EXPIRED", "REJECTED"].includes(status) === false)) { - classified = "filled"; - break; - } if (["CANCELED", "CANCELLED", "EXPIRED", "REJECTED"].includes(status) && executed <= EPSILON) { classified = "canceled"; - // do not break; keep searching for any filled among duplicates; but canceled is acceptable if no filled found } } } - if (classified === "unknown") { - // fallback to position delta confirmation - const absNow = Math.abs(this.position.positionAmt); - if (absNow > this.lastAbsPositionAmt + EPSILON) { - classified = "filled"; - } else { - classified = "canceled"; - } - } if (classified === "filled") { if (meta.side === "BUY") this.pendingLongLevels.add(meta.level); else this.pendingShortLevels.add(meta.level); } else if (classified === "canceled") { // no action; allow immediate re-open (do not block) + this.awaitingClassification.delete(meta.level); + this.levelReopenBlockUntil.delete(meta.level); + } else { + // unknown: defer decision until position/account updates or timeout + const until = this.now() + GridEngine.AWAIT_CLASSIFICATION_TIMEOUT_MS; + this.awaitingClassification.set(meta.level, { until, absAtStart: this.lastAbsPositionAmt }); + this.blockLevelReopen(meta.level); } - // ensure any previous block is cleared if exists - this.levelReopenBlockUntil.delete(meta.level); } } this.lastOpenOrderKeys = currentKeys; @@ -542,6 +541,26 @@ export class GridEngine { this.lastKeyMeta = keyToMeta; this.lastOrderIdsByKey = currentOrderIdsByKey; + // Resolve any awaiting classifications based on updated account snapshot or timeout + if (this.awaitingClassification.size) { + const nowTs = this.now(); + for (const [level, info] of Array.from(this.awaitingClassification.entries())) { + const absNow = Math.abs(this.position.positionAmt); + if (absNow > info.absAtStart + EPSILON) { + // treat as filled + const side = this.levelMeta[level]?.side === "BUY" ? "BUY" : "SELL"; + if (side === "BUY") this.pendingLongLevels.add(level); + else this.pendingShortLevels.add(level); + this.awaitingClassification.delete(level); + this.levelReopenBlockUntil.delete(level); + } else if (nowTs >= info.until) { + // timeout -> treat as canceled + this.awaitingClassification.delete(level); + this.levelReopenBlockUntil.delete(level); + } + } + } + // Desired open orders according to locked sides const desired: DesiredGridOrder[] = []; const halfTick = this.config.priceTick / 2;