From e531cf97db022c43d21a0d205a96c7febf9e752a Mon Sep 17 00:00:00 2001 From: discountry Date: Sat, 27 Sep 2025 02:25:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E8=B6=8B=E5=8A=BF=E5=BC=95?= =?UTF-8?q?=E6=93=8E=EF=BC=8C=E6=B7=BB=E5=8A=A0=E8=B4=A6=E6=88=B7=E6=8C=81?= =?UTF-8?q?=E4=BB=93=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F=E8=B7=9F=E8=B8=AA?= =?UTF-8?q?=E5=92=8C=E7=9B=88=E4=BA=8F=E4=BC=B0=E7=AE=97=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E5=B9=B3=E4=BB=93=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E4=BB=A5=E6=8F=90=E5=8D=87=E4=BA=A4=E6=98=93=E7=AD=96=E7=95=A5?= =?UTF-8?q?=E7=9A=84=E5=87=86=E7=A1=AE=E6=80=A7=E5=92=8C=E7=A8=B3=E5=AE=9A?= =?UTF-8?q?=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/trend-engine.ts | 57 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/src/core/trend-engine.ts b/src/core/trend-engine.ts index 69580e8..dc292d2 100644 --- a/src/core/trend-engine.ts +++ b/src/core/trend-engine.ts @@ -87,6 +87,13 @@ export class TrendEngine { private cancelAllRequested = false; private readonly pendingCancelOrders = new Set(); private readonly rateLimit: RateLimitController; + private lastAccountPosition: PositionSnapshot = { + positionAmt: 0, + entryPrice: 0, + unrealizedProfit: 0, + markPrice: null, + }; + private pendingRealized: { pnl: number; timestamp: number } | null = null; // 控制入场频率:同一分钟内最多入场一次 private lastEntryMinute: number | null = null; @@ -151,6 +158,7 @@ export class TrendEngine { this.accountSnapshot = snapshot; const position = getPosition(snapshot, this.config.symbol); this.updateSessionVolume(position); + this.trackPositionLifecycle(position, this.getReferencePrice()); this.emitUpdate(); } catch (err) { this.tradeLog.push("error", `账户推送处理异常: ${extractMessage(err)}`); @@ -280,12 +288,12 @@ export class TrendEngine { } else { const result = await this.handlePositionManagement(position, price); if (result.closed) { - this.totalTrades += 1; - this.totalProfit += result.pnl; + this.pendingRealized = { pnl: result.pnl, timestamp: Date.now() }; } } this.updateSessionVolume(position); + this.trackPositionLifecycle(position, price); this.lastSma30 = sma30; this.lastPrice = price; this.emitUpdate(); @@ -317,8 +325,7 @@ export class TrendEngine { if (!Number.isFinite(price) || price == null) return; const result = await this.handlePositionManagement(position, Number(price)); if (result.closed) { - this.totalTrades += 1; - this.totalProfit += result.pnl; + this.pendingRealized = { pnl: result.pnl, timestamp: Date.now() }; } } @@ -882,4 +889,46 @@ export class TrendEngine { return getMidOrLast(this.depthSnapshot, this.tickerSnapshot) ?? (this.lastPrice != null && Number.isFinite(this.lastPrice) ? this.lastPrice : null); } + private trackPositionLifecycle(position: PositionSnapshot, referencePrice: number | null): void { + const prev = this.lastAccountPosition; + const prevExposure = Math.abs(prev.positionAmt) > 1e-5; + const currentExposure = Math.abs(position.positionAmt) > 1e-5; + const signChanged = + prevExposure && currentExposure && Math.sign(prev.positionAmt) !== Math.sign(position.positionAmt); + + if (prevExposure && (!currentExposure || signChanged)) { + let realized: number | null = this.pendingRealized?.pnl ?? null; + if (!Number.isFinite(realized)) { + realized = this.estimateRealizedPnl(prev, referencePrice); + } + if (Number.isFinite(realized)) { + this.totalTrades += 1; + this.totalProfit += realized ?? 0; + } + this.pendingRealized = null; + } + + if (!prevExposure && currentExposure) { + this.pendingRealized = null; + } + + this.lastAccountPosition = { + positionAmt: position.positionAmt, + entryPrice: position.entryPrice, + unrealizedProfit: position.unrealizedProfit, + markPrice: position.markPrice, + }; + } + + private estimateRealizedPnl(position: PositionSnapshot, referencePrice: number | null): number { + const fallbackPrice = + referencePrice ?? + this.getReferencePrice() ?? + (this.lastPrice != null && Number.isFinite(this.lastPrice) ? this.lastPrice : position.entryPrice); + if (!Number.isFinite(fallbackPrice)) { + return 0; + } + return computePositionPnl(position, fallbackPrice, fallbackPrice); + } + }