@adaptic/utils 0.0.947 → 0.0.949

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.mjs CHANGED
@@ -5835,75 +5835,90 @@ async function closePosition$1(auth, symbolOrAssetId, params) {
5835
5835
  // Crypto positions cannot use limit orders with SIP quotes or time_in_force="day".
5836
5836
  // Use direct DELETE (market order) for crypto regardless of useLimitOrder flag.
5837
5837
  if (useLimitOrder && !isCryptoSymbol(symbolOrAssetId)) {
5838
- const { position } = await fetchPosition(auth, symbolOrAssetId);
5839
- if (!position) {
5840
- throw new Error(`Position not found for ${symbolOrAssetId}`);
5841
- }
5842
- // Use the passed auth for quote fetching so multi-account setups
5843
- // use the correct credentials per account
5844
- const quotesResponse = await getLatestQuotes$1(auth, {
5845
- symbols: [symbolOrAssetId],
5846
- });
5847
- const quote = quotesResponse.quotes[symbolOrAssetId];
5848
- if (!quote) {
5849
- throw new Error(`No quote available for ${symbolOrAssetId}`);
5850
- }
5851
- let qty = Math.abs(parseFloat(position.qty));
5852
- if (params?.qty !== undefined) {
5853
- qty = params.qty;
5854
- }
5855
- else if (params?.percentage !== undefined) {
5856
- qty = Math.abs(parseFloat(position.qty)) * (params.percentage / 100);
5838
+ // Attempt limit order closure; if quotes are unavailable (after-hours, IEX gaps),
5839
+ // fall back to market order (DELETE) so the position still gets closed.
5840
+ try {
5841
+ const { position } = await fetchPosition(auth, symbolOrAssetId);
5842
+ if (!position) {
5843
+ throw new Error(`Position not found for ${symbolOrAssetId}`);
5844
+ }
5845
+ // Use the passed auth for quote fetching so multi-account setups
5846
+ // use the correct credentials per account
5847
+ const quotesResponse = await getLatestQuotes$1(auth, {
5848
+ symbols: [symbolOrAssetId],
5849
+ });
5850
+ const quote = quotesResponse.quotes[symbolOrAssetId];
5851
+ if (!quote) {
5852
+ throw new Error(`No quote available for ${symbolOrAssetId}`);
5853
+ }
5854
+ let qty = Math.abs(parseFloat(position.qty));
5855
+ if (params?.qty !== undefined) {
5856
+ qty = params.qty;
5857
+ }
5858
+ else if (params?.percentage !== undefined) {
5859
+ qty = Math.abs(parseFloat(position.qty)) * (params.percentage / 100);
5860
+ }
5861
+ const side = position.side === "long" ? "sell" : "buy";
5862
+ const positionIntent = side === "sell" ? "sell_to_close" : "buy_to_close";
5863
+ const currentPrice = side === "sell" ? quote.bp : quote.ap;
5864
+ if (!currentPrice) {
5865
+ throw new Error(`No valid price available for ${symbolOrAssetId}`);
5866
+ }
5867
+ const limitSlippage = slippagePercent1 / 100;
5868
+ const limitPrice = side === "sell"
5869
+ ? roundPriceForAlpaca$5(currentPrice * (1 - limitSlippage))
5870
+ : roundPriceForAlpaca$5(currentPrice * (1 + limitSlippage));
5871
+ getLogger().info(`Creating limit order to close ${symbolOrAssetId} position: ${side} ${qty} shares at ${limitPrice.toFixed(2)}`, {
5872
+ account: auth.adapticAccountId || "direct",
5873
+ symbol: symbolOrAssetId,
5874
+ });
5875
+ return await createLimitOrder(auth, {
5876
+ symbol: symbolOrAssetId,
5877
+ qty,
5878
+ side,
5879
+ limitPrice,
5880
+ position_intent: positionIntent,
5881
+ extended_hours: extendedHours,
5882
+ });
5857
5883
  }
5858
- const side = position.side === "long" ? "sell" : "buy";
5859
- const positionIntent = side === "sell" ? "sell_to_close" : "buy_to_close";
5860
- const currentPrice = side === "sell" ? quote.bp : quote.ap;
5861
- if (!currentPrice) {
5862
- throw new Error(`No valid price available for ${symbolOrAssetId}`);
5884
+ catch (limitOrderError) {
5885
+ // Quote unavailable or invalid price fall back to market order (DELETE)
5886
+ // so the position still gets closed rather than leaving it open
5887
+ const errMsg = limitOrderError instanceof Error ? limitOrderError.message : String(limitOrderError);
5888
+ getLogger().warn(`Limit order closure failed for ${symbolOrAssetId} (${errMsg}), falling back to market order`, {
5889
+ account: auth.adapticAccountId || "direct",
5890
+ symbol: symbolOrAssetId,
5891
+ type: "warn",
5892
+ });
5893
+ // Fall through to the DELETE (market order) path below
5863
5894
  }
5864
- const limitSlippage = slippagePercent1 / 100;
5865
- const limitPrice = side === "sell"
5866
- ? roundPriceForAlpaca$5(currentPrice * (1 - limitSlippage))
5867
- : roundPriceForAlpaca$5(currentPrice * (1 + limitSlippage));
5868
- getLogger().info(`Creating limit order to close ${symbolOrAssetId} position: ${side} ${qty} shares at ${limitPrice.toFixed(2)}`, {
5869
- account: auth.adapticAccountId || "direct",
5870
- symbol: symbolOrAssetId,
5871
- });
5872
- return await createLimitOrder(auth, {
5873
- symbol: symbolOrAssetId,
5874
- qty,
5875
- side,
5876
- limitPrice,
5877
- position_intent: positionIntent,
5878
- extended_hours: extendedHours,
5879
- });
5880
5895
  }
5881
- else {
5882
- if (isCryptoSymbol(symbolOrAssetId)) {
5883
- getLogger().info(`Closing crypto position ${symbolOrAssetId} via market order (DELETE endpoint)`, { account: auth.adapticAccountId || "direct", symbol: symbolOrAssetId });
5884
- }
5885
- const queryParams = new URLSearchParams();
5886
- if (params?.qty !== undefined) {
5887
- queryParams.append("qty", params.qty.toString());
5888
- }
5889
- if (params?.percentage !== undefined) {
5890
- queryParams.append("percentage", params.percentage.toString());
5891
- }
5892
- const queryString = queryParams.toString();
5893
- const url = `${apiBaseUrl}/positions/${encodeURIComponent(symbolOrAssetId)}${queryString ? `?${queryString}` : ""}`;
5894
- const response = await fetch(url, {
5895
- method: "DELETE",
5896
- headers: {
5897
- "APCA-API-KEY-ID": APIKey,
5898
- "APCA-API-SECRET-KEY": APISecret,
5899
- },
5900
- });
5901
- if (!response.ok) {
5902
- const errorText = await response.text();
5903
- throw new Error(`Failed to close position: ${response.status} ${response.statusText} ${errorText}`);
5904
- }
5905
- return (await response.json());
5896
+ // Market order (DELETE) path — used when limit orders are not requested,
5897
+ // for crypto symbols, or as a fallback when limit order quotes are unavailable
5898
+ if (isCryptoSymbol(symbolOrAssetId)) {
5899
+ getLogger().info(`Closing crypto position ${symbolOrAssetId} via market order (DELETE endpoint)`, { account: auth.adapticAccountId || "direct", symbol: symbolOrAssetId });
5900
+ }
5901
+ const queryParams = new URLSearchParams();
5902
+ if (params?.qty !== undefined) {
5903
+ queryParams.append("qty", params.qty.toString());
5906
5904
  }
5905
+ if (params?.percentage !== undefined) {
5906
+ queryParams.append("percentage", params.percentage.toString());
5907
+ }
5908
+ const queryString = queryParams.toString();
5909
+ const url = `${apiBaseUrl}/positions/${encodeURIComponent(symbolOrAssetId)}${queryString ? `?${queryString}` : ""}`;
5910
+ const response = await fetch(url, {
5911
+ method: "DELETE",
5912
+ headers: {
5913
+ "APCA-API-KEY-ID": APIKey,
5914
+ "APCA-API-SECRET-KEY": APISecret,
5915
+ },
5916
+ });
5917
+ if (!response.ok) {
5918
+ const errorText = await response.text();
5919
+ throw new Error(`Failed to close position: ${response.status} ${response.statusText} ${errorText}`);
5920
+ }
5921
+ return (await response.json());
5907
5922
  }
5908
5923
  catch (error) {
5909
5924
  getLogger().error("Error in closePosition:", error);