From 097148c50146f3066afc844a4a758026c6df24b9 Mon Sep 17 00:00:00 2001 From: direkturcrypto Date: Wed, 25 Feb 2026 03:38:38 +0700 Subject: [PATCH] fix: cancel all open orders before sell to release CLOB locked balance Root cause: auto-sell GTC orders lock all shares in the CLOB's internal ledger. When executeSell runs, it tried to cancel only position.sellOrderId but this can fail silently, leaving tokens locked. The CLOB then rejects the new sell with "not enough balance or allowance" because the balance is committed to the existing GTC order. Fix: - Fetch all open orders for the specific tokenId via client.getOpenOrders() - Cancel all of them with Promise.allSettled (non-fatal per order) - Wait 600ms after cancellation so the CLOB updates its locked-balance ledger before we place the new sell - Fallback: if getOpenOrders fails, still attempt to cancel by position.sellOrderId (previous behaviour) then wait 600ms - Use correct cancelOrder({ orderID }) object form throughout Co-Authored-By: Claude Sonnet 4.6 --- src/services/executor.js | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/services/executor.js b/src/services/executor.js index e176a88..6398b55 100644 --- a/src/services/executor.js +++ b/src/services/executor.js @@ -306,15 +306,29 @@ export async function executeSell(trade) { return; } - // Cancel existing auto-sell order if any - if (position.sellOrderId) { - try { - const client = getClient(); - await client.cancelOrder(position.sellOrderId); - logger.info(`Cancelled auto-sell order: ${position.sellOrderId}`); - } catch (err) { - logger.warn(`Failed to cancel auto-sell: ${err.message}`); + // Cancel ALL open orders for this token so the CLOB frees up locked balance. + // Only cancelling by sellOrderId is not enough — the cancel can fail silently + // and locked tokens cause "not enough balance" on the subsequent sell. + const client = getClient(); + try { + const openOrders = await client.getOpenOrders({ asset_id: tokenId }); + if (Array.isArray(openOrders) && openOrders.length > 0) { + logger.info(`Cancelling ${openOrders.length} open order(s) for token before sell`); + await Promise.allSettled( + openOrders.map((o) => client.cancelOrder({ orderID: o.id ?? o.order_id })), + ); + // Brief pause so the CLOB can update the locked-balance ledger + await new Promise((r) => setTimeout(r, 600)); } + } catch (err) { + // Fallback: try to cancel just the tracked auto-sell order ID + if (position.sellOrderId) { + try { + await client.cancelOrder({ orderID: position.sellOrderId }); + await new Promise((r) => setTimeout(r, 600)); + } catch { /* ignore */ } + } + logger.warn(`Could not fetch open orders to cancel: ${err.message}`); } updatePosition(effectiveConditionId, { status: 'selling' });