diff --git a/README.md b/README.md index 80a7bed..e5f554c 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,10 @@ - Windows:在开始菜单搜索 “PowerShell” 或 “Windows Terminal” 并打开。 - 使用 `cd` 切换到项目目录,例如: ```bash - cd ~/Desktop/ritmex-bot # macOS / Linux - cd C:\Users\用户名\Desktop\ritmex-bot # Windows + # macOS / Linux + cd ~/Desktop/ritmex-bot + # Windows + cd C:\Users\用户名\Desktop\ritmex-bot ``` 3. **安装 [Bun](https://bun.com) ≥ 1.2** - macOS / Linux: diff --git a/src/core/maker-engine.ts b/src/core/maker-engine.ts index 0fb1218..8f7ee8b 100644 --- a/src/core/maker-engine.ts +++ b/src/core/maker-engine.ts @@ -54,6 +54,7 @@ export class MakerEngine { private readonly locks: OrderLockMap = {}; private readonly timers: OrderTimerMap = {}; private readonly pending: OrderPendingMap = {}; + private readonly pendingCancelOrders = new Set(); private readonly tradeLog: ReturnType; private readonly listeners = new Map>(); @@ -121,6 +122,12 @@ export class MakerEngine { this.openOrders = Array.isArray(orders) ? orders.filter((order) => order.type !== "MARKET" && order.symbol === this.config.symbol) : []; + const currentIds = new Set(this.openOrders.map((order) => order.orderId)); + for (const id of Array.from(this.pendingCancelOrders)) { + if (!currentIds.has(id)) { + this.pendingCancelOrders.delete(id); + } + } this.emitUpdate(); }); @@ -214,6 +221,9 @@ export class MakerEngine { continue; } const reduceOnly = order.reduceOnly === true; + if (this.pendingCancelOrders.has(order.orderId)) { + continue; + } const matchedIndex = targets.findIndex((target, index) => { if (!unmatched.has(index)) return false; if (target.side !== order.side) return false; @@ -228,14 +238,17 @@ export class MakerEngine { } for (const order of toCancel) { + this.pendingCancelOrders.add(order.orderId); try { await this.exchange.cancelOrder({ symbol: this.config.symbol, orderId: order.orderId }); this.tradeLog.push("order", `撤销不匹配订单 ${order.side} @ ${order.price} reduceOnly=${order.reduceOnly}`); } catch (error) { if (isUnknownOrderError(error)) { this.tradeLog.push("order", "撤销时发现订单已被成交/取消,忽略"); + this.pendingCancelOrders.delete(order.orderId); } else { this.tradeLog.push("error", `撤销订单失败: ${String(error)}`); + this.pendingCancelOrders.delete(order.orderId); } } } @@ -303,13 +316,17 @@ export class MakerEngine { private async flushOrders(): Promise { if (!this.openOrders.length) return; for (const order of this.openOrders) { + if (this.pendingCancelOrders.has(order.orderId)) continue; + this.pendingCancelOrders.add(order.orderId); try { await this.exchange.cancelOrder({ symbol: this.config.symbol, orderId: order.orderId }); } catch (error) { if (isUnknownOrderError(error)) { this.tradeLog.push("order", "订单已不存在,撤销跳过"); + this.pendingCancelOrders.delete(order.orderId); } else { this.tradeLog.push("error", `撤销订单失败: ${String(error)}`); + this.pendingCancelOrders.delete(order.orderId); } } } diff --git a/src/core/trend-engine.ts b/src/core/trend-engine.ts index 5086df2..66182b4 100644 --- a/src/core/trend-engine.ts +++ b/src/core/trend-engine.ts @@ -78,6 +78,8 @@ export class TrendEngine { private sessionQuoteVolume = 0; private prevPositionAmt = 0; private initializedPosition = false; + private cancelAllRequested = false; + private readonly pendingCancelOrders = new Set(); private readonly listeners = new Map>(); @@ -131,6 +133,15 @@ export class TrendEngine { this.openOrders = Array.isArray(orders) ? orders.filter((order) => order.type !== "MARKET" && order.symbol === this.config.symbol) : []; + const currentIds = new Set(this.openOrders.map((order) => order.orderId)); + for (const id of Array.from(this.pendingCancelOrders)) { + if (!currentIds.has(id)) { + this.pendingCancelOrders.delete(id); + } + } + if (this.openOrders.length === 0 || this.pendingCancelOrders.size === 0) { + this.cancelAllRequested = false; + } this.emitUpdate(); }); this.exchange.watchDepth(this.config.symbol, (depth) => { @@ -210,14 +221,17 @@ export class TrendEngine { this.lastPrice = currentPrice; return; } - if (this.openOrders.length > 0) { + if (this.openOrders.length > 0 && !this.cancelAllRequested) { try { await this.exchange.cancelAllOrders({ symbol: this.config.symbol }); + this.cancelAllRequested = true; } catch (err) { if (isUnknownOrderError(err)) { this.tradeLog.push("order", "撤单时部分订单已不存在,忽略"); + this.cancelAllRequested = true; } else { this.tradeLog.push("error", `撤销挂单失败: ${String(err)}`); + this.cancelAllRequested = false; } } } @@ -315,6 +329,7 @@ export class TrendEngine { const orderIdList = this.openOrders.map((order) => order.orderId); try { await this.exchange.cancelOrders({ symbol: this.config.symbol, orderIdList }); + orderIdList.forEach((id) => this.pendingCancelOrders.add(id)); } catch (err) { if (isUnknownOrderError(err)) { this.tradeLog.push("order", "止损前撤单发现订单已不存在"); diff --git a/src/ui/MakerApp.tsx b/src/ui/MakerApp.tsx index 8732574..4c1a88d 100644 --- a/src/ui/MakerApp.tsx +++ b/src/ui/MakerApp.tsx @@ -137,9 +137,6 @@ export function MakerApp({ onExit }: MakerAppProps) { 浮动盈亏: {formatNumber(snapshot.pnl, 4)} USDT | 账户未实现盈亏: {formatNumber(snapshot.accountUnrealized, 4)} USDT - - 累计成交量: {formatNumber(snapshot.sessionVolume, 2)} USDT - ) : ( 当前无持仓 @@ -152,6 +149,9 @@ export function MakerApp({ onExit }: MakerAppProps) { ) : ( 暂无目标挂单 )} + + 累计成交量: {formatNumber(snapshot.sessionVolume, 2)} USDT +