From ccb23ecf44fe0b2d328135060e8cfb62ad4e9d03 Mon Sep 17 00:00:00 2001 From: discountry Date: Tue, 7 Oct 2025 02:54:01 +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=E4=B9=B0=E5=8D=96?= =?UTF-8?q?=E6=B0=B4=E5=B9=B3=E7=B4=A2=E5=BC=95=E4=BB=A5=E7=A8=B3=E5=AE=9A?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E4=BE=A7=E5=88=86=E9=85=8D=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E8=AE=A2=E5=8D=95=E8=AE=A1=E7=AE=97=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/strategy/grid-engine.ts | 34 +++++++++++++++++++++++++++++----- tests/grid-engine.test.ts | 22 ++++++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/strategy/grid-engine.ts b/src/strategy/grid-engine.ts index 2b1eaf0..dc86042 100644 --- a/src/strategy/grid-engine.ts +++ b/src/strategy/grid-engine.ts @@ -81,6 +81,8 @@ export class GridEngine { private readonly levelExposure = new Map(); private readonly lastOrderBook = new Map(); private readonly pendingCancelKeys = new Set(); + private readonly buyLevelIndices: number[]; + private readonly sellLevelIndices: number[]; private accountSnapshot: AsterAccountSnapshot | null = null; private depthSnapshot: AsterDepth | null = null; @@ -119,6 +121,23 @@ export class GridEngine { this.now = options.now ?? Date.now; this.configValid = this.validateConfig(); this.gridLevels = this.computeGridLevels(); + const pivotIndex = Math.floor(Math.max(this.gridLevels.length - 1, 0) / 2); + this.buyLevelIndices = []; + this.sellLevelIndices = []; + for (let i = 0; i < this.gridLevels.length; i += 1) { + if (i <= pivotIndex) { + this.buyLevelIndices.push(i); + } + if (i >= pivotIndex + 1) { + this.sellLevelIndices.push(i); + } + } + if (!this.sellLevelIndices.length && this.gridLevels.length > 1) { + const highest = this.gridLevels.length - 1; + if (!this.sellLevelIndices.includes(highest)) { + this.sellLevelIndices.push(highest); + } + } this.running = this.configValid; if (!this.configValid) { this.stopReason = "配置无效,已暂停网格"; @@ -470,12 +489,12 @@ export class GridEngine { let availableToBuy = Math.max(-this.position.positionAmt, 0); const halfTick = this.config.priceTick / 2; - const belowPrice = this.gridLevels - .map((levelPrice, level) => ({ level, levelPrice })) + const belowPrice = this.buyLevelIndices + .map((level) => ({ level, levelPrice: this.gridLevels[level]! })) .filter(({ levelPrice }) => levelPrice < price - halfTick) .sort((a, b) => b.levelPrice - a.levelPrice); - const abovePrice = this.gridLevels - .map((levelPrice, level) => ({ level, levelPrice })) + const abovePrice = this.sellLevelIndices + .map((level) => ({ level, levelPrice: this.gridLevels[level]! })) .filter(({ levelPrice }) => levelPrice > price + halfTick) .sort((a, b) => a.levelPrice - b.levelPrice); @@ -505,6 +524,10 @@ export class GridEngine { for (const { level, levelPrice } of abovePrice) { const amount = this.config.orderSize; const reduceOnly = this.config.direction === "long"; + const heldLong = this.levelExposure.get(level) ?? 0; + if (!reduceOnly && heldLong > EPSILON) { + continue; + } if (!reduceOnly) { if (remainingShortHeadroom < amount - EPSILON) break; remainingShortHeadroom -= amount; @@ -554,7 +577,8 @@ export class GridEngine { const gridLines: GridLineSnapshot[] = this.gridLevels.map((price, level) => { const desired = this.desiredOrders.find((order) => order.level === level); - const side = desired?.side ?? (price < (lastPrice ?? price) ? "BUY" : "SELL"); + const defaultSide = this.buyLevelIndices.includes(level) ? "BUY" : "SELL"; + const side = desired?.side ?? defaultSide; const key = desired ? this.getOrderKey(desired.side, desired.price) : null; const hasOrder = key ? openOrderKeys.has(key) : false; const active = Boolean(desired && key && desiredKeys.has(key)); diff --git a/tests/grid-engine.test.ts b/tests/grid-engine.test.ts index 4449e86..2e89fe5 100644 --- a/tests/grid-engine.test.ts +++ b/tests/grid-engine.test.ts @@ -219,6 +219,28 @@ describe("GridEngine", () => { engine.stop(); }); + it("keeps level side assignments stable regardless of price", () => { + const adapter = new StubAdapter(); + const engine = new GridEngine(baseConfig, adapter, { now: () => 0 }); + + adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0)); + adapter.emitOrders([]); + + const desiredHigh = (engine as any).computeDesiredOrders(2.45) as Array<{ level: number; side: string }>; + expect(desiredHigh.every((order) => { + const isBuyLevel = order.level <= Math.floor((baseConfig.gridLevels - 1) / 2); + return isBuyLevel ? order.side === "BUY" : order.side === "SELL"; + })).toBe(true); + + const desiredLow = (engine as any).computeDesiredOrders(1.55) as Array<{ level: number; side: string }>; + expect(desiredLow.every((order) => { + const isBuyLevel = order.level <= Math.floor((baseConfig.gridLevels - 1) / 2); + return isBuyLevel ? order.side === "BUY" : order.side === "SELL"; + })).toBe(true); + + engine.stop(); + }); + it("halts the grid and closes positions when stop loss triggers", async () => { const adapter = new StubAdapter(); const engine = new GridEngine(baseConfig, adapter, { now: () => 0 });