From 8c13d20cb7e267e1035c28cb4749036a7a446d67 Mon Sep 17 00:00:00 2001 From: discountry Date: Wed, 8 Oct 2025 04:44:48 +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=E5=B9=B3=E4=BB=93?= =?UTF-8?q?=E4=BC=98=E5=85=88=E9=80=BB=E8=BE=91=E4=BB=A5=E7=A1=AE=E4=BF=9D?= =?UTF-8?q?=E5=9C=A8=E6=8C=81=E6=9C=89=E4=BB=93=E4=BD=8D=E6=97=B6=E6=AD=A3?= =?UTF-8?q?=E7=A1=AE=E5=A4=84=E7=90=86=E6=92=A4=E5=8D=95=E4=B8=8E=E6=96=B0?= =?UTF-8?q?=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 | 87 ++++++++++++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 6 deletions(-) diff --git a/src/strategy/grid-engine.ts b/src/strategy/grid-engine.ts index 981351a..1c028ca 100644 --- a/src/strategy/grid-engine.ts +++ b/src/strategy/grid-engine.ts @@ -477,6 +477,82 @@ export class GridEngine { } private async syncGridSimple(price: number): Promise { + // --- 0) If there is an existing net position, enforce "exit-first" before any ENTRY --- + const hasNetLong = this.position.positionAmt > EPSILON; + const hasNetShort = this.position.positionAmt < -EPSILON; + + const hasActiveExit = (side: "BUY" | "SELL"): boolean => { + for (const o of this.openOrders) { + if (!this.isActiveLimitOrder(o)) continue; + if (o.side !== side) continue; + const meta = this.orderIntentById.get(String(o.orderId)); + if (meta && meta.intent === "EXIT") return true; + } + return false; + }; + + const ensureSingleExitForExistingPosition = async (): Promise => { + const qty = this.position.positionAmt; + if (!Number.isFinite(qty) || Math.abs(qty) <= EPSILON) return false; + const entry = this.position.entryPrice; + if (!Number.isFinite(entry)) return false; + const dir: "long" | "short" = qty > 0 ? "long" : "short"; + const nearest = this.findNearestProfitableCloseLevel(dir, Number(entry)); + if (nearest == null) return false; + const exitSide: "BUY" | "SELL" = qty > 0 ? "SELL" : "BUY"; + const priceStr = this.formatPrice(this.gridLevels[nearest]!); + const key = this.getOrderKey(exitSide, priceStr, "EXIT"); + const activeAlready = hasActiveExit(exitSide); + const until = this.pendingKeyUntil.get(key); + if (activeAlready || (until && until > this.now())) return activeAlready; + try { + const placed = await placeOrder( + this.exchange, + this.config.symbol, + this.openOrders, + this.locks, + this.timers, + this.pendings, + exitSide, + priceStr, + Math.abs(qty), + this.log, + false, + undefined, + { priceTick: this.config.priceTick, qtyStep: this.config.qtyStep } + ); + this.pendingKeyUntil.set(key, this.now() + GridEngine.PENDING_TTL_MS); + if (placed?.orderId != null) { + const source = this.findSourceForInitialPosition(exitSide); + if (exitSide === "SELL") this.pendingLongLevels.add(source); + else this.pendingShortLevels.add(source); + this.closeKeyBySourceLevel.set(source, key); + this.orderIntentById.set(String(placed.orderId), { + side: exitSide, + price: priceStr, + level: nearest, + intent: "EXIT", + sourceLevel: source, + }); + this.log("order", `兜底:为已有仓位挂平仓单 ${exitSide} @ ${priceStr}`); + } + return true; + } catch (err) { + this.log("error", `兜底平仓单下单失败: ${extractMessage(err)}`); + return false; + } + }; + + if (hasNetLong || hasNetShort) { + const needExitSide: "BUY" | "SELL" = hasNetLong ? "SELL" : "BUY"; + if (!hasActiveExit(needExitSide)) { + await ensureSingleExitForExistingPosition(); + this.lastUpdated = this.now(); + this.prevActiveIds = new Set(this.openOrders.filter(o => this.isActiveLimitOrder(o)).map(o => String(o.orderId))); + return; + } + } + const activeOrders = this.openOrders.filter((o) => this.isActiveLimitOrder(o)); // Build lookup for all recent orders by id (including non-active) to read final statuses const allOrdersById = new Map(); @@ -639,13 +715,12 @@ export class GridEngine { this.immediateCloseToPlace = []; } - const hasNetLong = this.position.positionAmt > EPSILON; - const hasNetShort = this.position.positionAmt < -EPSILON; + // hasNetLong/hasNetShort already computed above for exit-first // ENTRY opens below price (BUY) for (const level of this.buyLevelIndices) { - if (hasNetShort) { - // Avoid competing with EXIT(BUY) while holding net short; focus on closing first + if (hasNetLong || hasNetShort) { + // During exit-first, do not place any ENTRY orders continue; } const levelPrice = this.gridLevels[level]!; @@ -680,8 +755,8 @@ export class GridEngine { // ENTRY opens above price (SELL) for (const level of this.sellLevelIndices) { - if (hasNetLong) { - // Avoid competing with EXIT(SELL) while holding net long; focus on closing first + if (hasNetLong || hasNetShort) { + // During exit-first, do not place any ENTRY orders continue; } const levelPrice = this.gridLevels[level]!;