@adaptic/utils 0.0.947 → 0.0.948

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