diff --git a/src/core/order-coordinator.ts b/src/core/order-coordinator.ts index 1bb36bf..a0caec4 100644 --- a/src/core/order-coordinator.ts +++ b/src/core/order-coordinator.ts @@ -89,7 +89,14 @@ export async function deduplicateOrders( side: string, log: LogHandler ): Promise { - const sameTypeOrders = openOrders.filter((o) => o.type === type && o.side === side); + // Treat STOP orders on some exchanges (e.g., Lighter) as LIMIT with stopPrice populated. + const sameTypeOrders = openOrders.filter((o) => { + const normalizedType = String(o.type).toUpperCase(); + const isStopLike = Number.isFinite(Number(o.stopPrice)) && Number(o.stopPrice) > 0; + const matchesStop = type === "STOP_MARKET" && isStopLike && o.side === side; + const exactMatch = normalizedType === type && o.side === side; + return exactMatch || matchesStop; + }); if (sameTypeOrders.length <= 1) return; sameTypeOrders.sort((a, b) => { const ta = b.updateTime || b.time || 0; @@ -241,7 +248,6 @@ export async function placeStopLossOrder( } const priceTick = opts?.priceTick ?? 0.1; const qtyStep = opts?.qtyStep ?? 0.001; - const isAster = String((adapter as any).id ?? "").toLowerCase() === "aster"; const params: CreateOrderParams = { symbol, side, @@ -249,14 +255,10 @@ export async function placeStopLossOrder( stopPrice: roundDownToTick(stopPrice, priceTick), closePosition: "true", timeInForce: "GTC", - // Do not round down stop quantity here to avoid underflow to exchange min; gateway will quantize precisely quantity, triggerType: "STOP_LOSS", }; - // For Aster futures, STOP orders must not include reduceOnly; omit it specifically. - if (!isAster) { - params.reduceOnly = "true"; - } + // Avoid forcing price for STOP_MARKET globally; keep this exchange-specific in gateways await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log); lockOperating(locks, timers, pendings, type, log); @@ -299,8 +301,6 @@ export async function placeTrailingStopOrder( symbol, side, type, - // Do not round down trailing-stop quantity to avoid underflowing small positions to zero; - // let the exchange adapter handle precise quantization. quantity, reduceOnly: "true", activationPrice: roundDownToTick(activationPrice, priceTick), @@ -351,8 +351,6 @@ export async function marketClose( side, type, quantity: safeQty, - reduceOnly: "true", - // Hint exchanges (like Paradex) to close the whole position and tolerate omitted size closePosition: "true", }; await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log); diff --git a/src/exchanges/lighter/adapter.ts b/src/exchanges/lighter/adapter.ts index 726432d..e00fe27 100644 --- a/src/exchanges/lighter/adapter.ts +++ b/src/exchanges/lighter/adapter.ts @@ -108,13 +108,14 @@ export class LighterExchangeAdapter implements ExchangeAdapter { async cancelOrder(params: { symbol: string; orderId: number | string }): Promise { await this.ensureInitialized("cancelOrder"); - await this.gateway.cancelOrder({ orderId: params.orderId }); + // Accept both clientOrderId and order_index as strings; forward as-is to preserve precision + await this.gateway.cancelOrder({ orderId: String(params.orderId) }); } async cancelOrders(params: { symbol: string; orderIdList: Array }): Promise { await this.ensureInitialized("cancelOrders"); for (const orderId of params.orderIdList) { - await this.gateway.cancelOrder({ orderId }); + await this.gateway.cancelOrder({ orderId: String(orderId) }); } } diff --git a/src/exchanges/lighter/gateway.ts b/src/exchanges/lighter/gateway.ts index 99a652c..e74967c 100644 --- a/src/exchanges/lighter/gateway.ts +++ b/src/exchanges/lighter/gateway.ts @@ -312,7 +312,14 @@ export class LighterGateway { await this.ensureInitialized(); const marketIndex = params.marketIndex ?? this.marketId; if (marketIndex == null) throw new Error("Market index unknown"); - const indexValue = BigInt(typeof params.orderId === "string" ? Number(params.orderId) : params.orderId); + // Parse order id to BigInt without precision loss; prefer string input + let indexValue: bigint; + if (typeof params.orderId === "string") { + indexValue = BigInt(params.orderId); + } else { + // Fallback for numeric ids (may be unsafe if beyond 2^53-1) + indexValue = BigInt(Math.trunc(params.orderId)); + } const { apiKeyIndex, nonce } = this.nonceManager.next(); try { const signed = await this.signer.signCancelOrder({ @@ -323,6 +330,11 @@ export class LighterGateway { }); const auth = await this.ensureAuthToken(); await this.http.sendTransaction(signed.txType, signed.txInfo, { authToken: auth }); + // Optimistically remove the order locally to avoid stale duplicates until WS confirms + const key = String(params.orderId); + this.orderMap.delete(key); + this.orders = Array.from(this.orderMap.values()); + this.emitOrders(); } catch (error) { this.nonceManager.acknowledgeFailure(apiKeyIndex); throw error; diff --git a/src/exchanges/lighter/mappers.ts b/src/exchanges/lighter/mappers.ts index 663ff11..5e4c21c 100644 --- a/src/exchanges/lighter/mappers.ts +++ b/src/exchanges/lighter/mappers.ts @@ -78,7 +78,8 @@ export function lighterOrderToAster(symbol: string, order: LighterOrder): AsterO ? "SELL" : "BUY"; return { - orderId: order.order_index, + // Use string order id to avoid precision loss; prefer on-chain order_index for cancellation + orderId: String(order.order_index ?? order.client_order_index ?? ""), clientOrderId: String(order.client_order_index ?? order.order_index ?? ""), symbol, side,