@adaptic/utils 0.0.995 → 0.0.996

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -6947,8 +6947,38 @@ async function closePosition$1(auth, symbolOrAssetId, params) {
6947
6947
  account: auth.adapticAccountId || "direct",
6948
6948
  symbol: normalizedSymbol,
6949
6949
  });
6950
- await cancelOrder$1(auth, order.id);
6951
- cancelledCount++;
6950
+ try {
6951
+ await cancelOrder$1(auth, order.id);
6952
+ cancelledCount++;
6953
+ }
6954
+ catch (cancelError) {
6955
+ // Alpaca rejects DELETE on an order that is already in
6956
+ // pending_cancel / pending_replace with HTTP 422 code
6957
+ // 42210000 ("order pending cancel"). The order is already
6958
+ // dying — which is exactly what this sweep wants — so
6959
+ // aborting the entire position close here is wrong: it
6960
+ // left REDUCE/EXIT risk actions permanently blocked when a
6961
+ // protective stop wedged in pending_cancel at the broker
6962
+ // (observed live 2026-06-10: an 8-day-old pending_cancel
6963
+ // trailing stop on a naked short made every exit attempt
6964
+ // throw). Treat already-cancelling as success-in-progress
6965
+ // and let the subsequent close attempt get the broker's
6966
+ // authoritative answer on held quantity. Any other cancel
6967
+ // failure still aborts the close as before.
6968
+ const message = cancelError instanceof Error
6969
+ ? cancelError.message
6970
+ : String(cancelError);
6971
+ const alreadyCancelling = message.includes("42210000") ||
6972
+ message.includes("order pending cancel");
6973
+ if (!alreadyCancelling) {
6974
+ throw cancelError;
6975
+ }
6976
+ getLogger().warn(`Order ${order.id} (${order.symbol}) is already pending cancel at the broker — proceeding with position close without it`, {
6977
+ account: auth.adapticAccountId || "direct",
6978
+ symbol: normalizedSymbol,
6979
+ type: "warn",
6980
+ });
6981
+ }
6952
6982
  }
6953
6983
  }
6954
6984
  if (cancelledCount > 0) {
@@ -6964,12 +6994,17 @@ async function closePosition$1(auth, symbolOrAssetId, params) {
6964
6994
  const verifyDelayMs = 500;
6965
6995
  for (let attempt = 1; attempt <= maxVerifyAttempts; attempt++) {
6966
6996
  await new Promise((r) => setTimeout(r, verifyDelayMs));
6967
- const remainingOrders = isCryptoSymbol$1(symbolOrAssetId)
6997
+ const allRemainingOrders = isCryptoSymbol$1(symbolOrAssetId)
6968
6998
  ? (await getOrders$1(auth, { status: "open" })).filter((o) => o.symbol.replace(/[-/]/g, "") === normalizedSymbol)
6969
6999
  : await getOrders$1(auth, {
6970
7000
  status: "open",
6971
7001
  symbols: [normalizedSymbol],
6972
7002
  });
7003
+ // Orders stuck in pending_cancel / pending_replace are already
7004
+ // terminalising (or wedged — see the 42210000 handling above);
7005
+ // waiting longer will not change them, so they must not make
7006
+ // the verification spin to exhaustion on every close attempt.
7007
+ const remainingOrders = allRemainingOrders.filter((o) => o.status !== "pending_cancel" && o.status !== "pending_replace");
6973
7008
  if (remainingOrders.length === 0) {
6974
7009
  getLogger().info(`Cancel verification passed for ${normalizedSymbol} (attempt ${attempt}/${maxVerifyAttempts})`, {
6975
7010
  account: auth.adapticAccountId || "direct",