diff --git a/src/exchanges/lighter/gateway.ts b/src/exchanges/lighter/gateway.ts index 4b8189c..f6b90ad 100644 --- a/src/exchanges/lighter/gateway.ts +++ b/src/exchanges/lighter/gateway.ts @@ -133,7 +133,7 @@ const RESOLUTION_MS: Record = { "1d": 86_400_000, }; -const TERMINAL_ORDER_STATUSES = new Set(["filled", "canceled", "cancelled", "expired"]); +const TERMINAL_ORDER_STATUSES = new Set(["filled", "canceled", "cancelled", "expired", "canceled-post-only"]); export interface LighterGatewayOptions { symbol: string; // display symbol used by strategy logging diff --git a/src/exchanges/lighter/types.ts b/src/exchanges/lighter/types.ts index 7a57197..9a3e13e 100644 --- a/src/exchanges/lighter/types.ts +++ b/src/exchanges/lighter/types.ts @@ -29,8 +29,8 @@ export interface LighterOrder { time_in_force?: string; trigger_price?: string; reduce_only?: boolean; - status?: string; - trigger_status?: string; + status?: string | number; + trigger_status?: string | number; trigger_time?: number; updated_at?: number; created_at?: number; diff --git a/src/strategy/offset-maker-engine.ts b/src/strategy/offset-maker-engine.ts index 91168b7..fc72140 100644 --- a/src/strategy/offset-maker-engine.ts +++ b/src/strategy/offset-maker-engine.ts @@ -546,7 +546,10 @@ export class OffsetMakerEngine { this.lastEntryOrderBySide[target.side] = { price: target.price, ts: Date.now() }; } } catch (error) { - this.tradeLog.push("error", `挂单失败(${target.side} ${target.price}): ${String(error)}`); + const dustClosed = await this.tryDustMarketClose(target, error); + if (!dustClosed) { + this.tradeLog.push("error", `挂单失败(${target.side} ${target.price}): ${String(error)}`); + } } } } @@ -713,4 +716,52 @@ export class OffsetMakerEngine { private getReferencePrice(): number | null { return getMidOrLast(this.depthSnapshot, this.tickerSnapshot); } + + private isInvalidAmountError(error: unknown): boolean { + const message = + typeof error === "string" + ? error + : error instanceof Error + ? error.message + : JSON.stringify(error); + if (!message) return false; + if (message.includes("\"code\":21706")) return true; + return message.toLowerCase().includes("invalid order base or quote amount"); + } + + private async tryDustMarketClose(target: DesiredOrder, error: unknown): Promise { + if (!target.reduceOnly) return false; + if (!this.isInvalidAmountError(error)) return false; + const position = getPosition(this.accountSnapshot, this.config.symbol); + const absQty = Math.abs(target.amount); + if (absQty < EPS) return false; + const { topBid, topAsk } = getTopPrices(this.depthSnapshot); + try { + await marketClose( + this.exchange, + this.config.symbol, + this.openOrders, + this.locks, + this.timers, + this.pending, + target.side, + absQty, + (type, detail) => this.tradeLog.push(type, detail), + { + markPrice: position.markPrice, + expectedPrice: + target.side === "SELL" + ? (topBid != null ? Number(topBid) : null) + : (topAsk != null ? Number(topAsk) : null), + maxPct: this.config.maxCloseSlippagePct, + }, + { qtyStep: this.qtyStep } + ); + this.tradeLog.push("order", `小额仓位使用市价平仓 ${target.side} 数量 ${absQty.toFixed(6)}`); + return true; + } catch (closeError) { + this.tradeLog.push("error", `小额市价平仓失败: ${String(closeError)}`); + return false; + } + } }