@adaptic/utils 0.0.958 → 0.0.960

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
@@ -5557,7 +5557,7 @@ const CRYPTO_QUOTE_CURRENCIES$1 = ["USD", "USDT", "USDC", "BTC"];
5557
5557
  * "BTC/USD"). Equity tickers never end with these suffixes because Alpaca
5558
5558
  * equity symbols are plain tickers without a quote currency.
5559
5559
  */
5560
- function isCryptoSymbol$1(symbol) {
5560
+ function isCryptoSymbol$2(symbol) {
5561
5561
  if (symbol.includes("/"))
5562
5562
  return true;
5563
5563
  const upper = symbol.toUpperCase();
@@ -5611,7 +5611,7 @@ async function getLatestQuotes$1(auth, params) {
5611
5611
  const equitySymbols = [];
5612
5612
  const cryptoSymbols = [];
5613
5613
  for (const sym of symbols) {
5614
- if (isCryptoSymbol$1(sym)) {
5614
+ if (isCryptoSymbol$2(sym)) {
5615
5615
  cryptoSymbols.push(sym);
5616
5616
  }
5617
5617
  else {
@@ -5819,7 +5819,7 @@ const CRYPTO_QUOTE_CURRENCIES = ["USD", "USDT", "USDC", "BTC"];
5819
5819
  * "BTC/USD"). Equity tickers never end with these suffixes because Alpaca
5820
5820
  * equity symbols are plain tickers without a quote currency.
5821
5821
  */
5822
- function isCryptoSymbol(symbol) {
5822
+ function isCryptoSymbol$1(symbol) {
5823
5823
  if (symbol.includes("/"))
5824
5824
  return true;
5825
5825
  const upper = symbol.toUpperCase();
@@ -5871,7 +5871,7 @@ async function fetchPosition(auth, symbolOrAssetId) {
5871
5871
  const { APIKey, APISecret, type } = await validateAuth(auth);
5872
5872
  const apiBaseUrl = getTradingApiUrl(type);
5873
5873
  // Normalize crypto symbols for Alpaca API compatibility
5874
- const normalizedSymbol = isCryptoSymbol(symbolOrAssetId)
5874
+ const normalizedSymbol = isCryptoSymbol$1(symbolOrAssetId)
5875
5875
  ? symbolOrAssetId.replace(/[-/]/g, "")
5876
5876
  : symbolOrAssetId;
5877
5877
  const response = await fetch(`${apiBaseUrl}/positions/${normalizedSymbol}`, {
@@ -5908,7 +5908,7 @@ async function fetchPosition(auth, symbolOrAssetId) {
5908
5908
  * @param auth - The authentication details for Alpaca
5909
5909
  * @param symbolOrAssetId - The symbol or asset ID of the position to close
5910
5910
  * @param params - Optional parameters for closing the position
5911
- * @returns The order created to close the position
5911
+ * @returns The order created to close the position, or null if position doesn't exist (404)
5912
5912
  */
5913
5913
  async function closePosition$1(auth, symbolOrAssetId, params) {
5914
5914
  try {
@@ -5917,7 +5917,7 @@ async function closePosition$1(auth, symbolOrAssetId, params) {
5917
5917
  // Normalize crypto symbols for Alpaca API compatibility.
5918
5918
  // Alpaca positions endpoint rejects hyphenated format (e.g., "SOL-USD")
5919
5919
  // but accepts concatenated form (e.g., "SOLUSD").
5920
- const normalizedSymbol = isCryptoSymbol(symbolOrAssetId)
5920
+ const normalizedSymbol = isCryptoSymbol$1(symbolOrAssetId)
5921
5921
  ? symbolOrAssetId.replace(/[-/]/g, "")
5922
5922
  : symbolOrAssetId;
5923
5923
  const useLimitOrder = params?.useLimitOrder ?? false;
@@ -5933,25 +5933,34 @@ async function closePosition$1(auth, symbolOrAssetId, params) {
5933
5933
  // For crypto, Alpaca stores orders under "SOL/USD" (slash format) but the
5934
5934
  // symbols filter may not match across formats reliably. Fetch all open
5935
5935
  // orders without symbol filter and match client-side via normalization.
5936
- const openOrders = isCryptoSymbol(symbolOrAssetId)
5936
+ const openOrders = isCryptoSymbol$1(symbolOrAssetId)
5937
5937
  ? await getOrders$1(auth, { status: "open" })
5938
- : await getOrders$1(auth, { status: "open", symbols: [normalizedSymbol] });
5938
+ : await getOrders$1(auth, {
5939
+ status: "open",
5940
+ symbols: [normalizedSymbol],
5941
+ });
5939
5942
  let cancelledCount = 0;
5940
5943
  for (const order of openOrders) {
5941
5944
  const orderSymbolNorm = order.symbol.replace(/[-/]/g, "");
5942
5945
  if (orderSymbolNorm === normalizedSymbol) {
5943
- getLogger().info(`Cancelling order ${order.id} (${order.symbol}) for ${normalizedSymbol}`, { account: auth.adapticAccountId || "direct", symbol: normalizedSymbol });
5946
+ getLogger().info(`Cancelling order ${order.id} (${order.symbol}) for ${normalizedSymbol}`, {
5947
+ account: auth.adapticAccountId || "direct",
5948
+ symbol: normalizedSymbol,
5949
+ });
5944
5950
  await cancelOrder$1(auth, order.id);
5945
5951
  cancelledCount++;
5946
5952
  }
5947
5953
  }
5948
5954
  if (cancelledCount > 0) {
5949
- getLogger().info(`Cancelled ${cancelledCount} open orders for ${normalizedSymbol}`, { account: auth.adapticAccountId || "direct", symbol: normalizedSymbol });
5955
+ getLogger().info(`Cancelled ${cancelledCount} open orders for ${normalizedSymbol}`, {
5956
+ account: auth.adapticAccountId || "direct",
5957
+ symbol: normalizedSymbol,
5958
+ });
5950
5959
  }
5951
5960
  }
5952
5961
  // Crypto positions cannot use limit orders with SIP quotes or time_in_force="day".
5953
5962
  // Use direct DELETE (market order) for crypto regardless of useLimitOrder flag.
5954
- if (useLimitOrder && !isCryptoSymbol(symbolOrAssetId)) {
5963
+ if (useLimitOrder && !isCryptoSymbol$1(symbolOrAssetId)) {
5955
5964
  // Attempt limit order closure; if quotes are unavailable (after-hours, IEX gaps),
5956
5965
  // fall back to market order (DELETE) so the position still gets closed.
5957
5966
  try {
@@ -6001,7 +6010,9 @@ async function closePosition$1(auth, symbolOrAssetId, params) {
6001
6010
  catch (limitOrderError) {
6002
6011
  // Quote unavailable or invalid price — fall back to market order (DELETE)
6003
6012
  // so the position still gets closed rather than leaving it open
6004
- const errMsg = limitOrderError instanceof Error ? limitOrderError.message : String(limitOrderError);
6013
+ const errMsg = limitOrderError instanceof Error
6014
+ ? limitOrderError.message
6015
+ : String(limitOrderError);
6005
6016
  getLogger().warn(`Limit order closure failed for ${symbolOrAssetId} (${errMsg}), falling back to market order`, {
6006
6017
  account: auth.adapticAccountId || "direct",
6007
6018
  symbol: symbolOrAssetId,
@@ -6012,8 +6023,11 @@ async function closePosition$1(auth, symbolOrAssetId, params) {
6012
6023
  }
6013
6024
  // Market order (DELETE) path — used when limit orders are not requested,
6014
6025
  // for crypto symbols, or as a fallback when limit order quotes are unavailable
6015
- if (isCryptoSymbol(symbolOrAssetId)) {
6016
- getLogger().info(`Closing crypto position ${normalizedSymbol} via market order (DELETE endpoint)`, { account: auth.adapticAccountId || "direct", symbol: normalizedSymbol });
6026
+ if (isCryptoSymbol$1(symbolOrAssetId)) {
6027
+ getLogger().info(`Closing crypto position ${normalizedSymbol} via market order (DELETE endpoint)`, {
6028
+ account: auth.adapticAccountId || "direct",
6029
+ symbol: normalizedSymbol,
6030
+ });
6017
6031
  }
6018
6032
  const queryParams = new URLSearchParams();
6019
6033
  if (params?.qty !== undefined) {
@@ -6033,6 +6047,15 @@ async function closePosition$1(auth, symbolOrAssetId, params) {
6033
6047
  });
6034
6048
  if (!response.ok) {
6035
6049
  const errorText = await response.text();
6050
+ // Handle 404 (position not found) gracefully - position may have already been closed
6051
+ // or never existed. Return null instead of throwing to allow callers to handle this.
6052
+ if (response.status === 404) {
6053
+ getLogger().info(`Position ${normalizedSymbol} not found in Alpaca (404) - may already be closed`, {
6054
+ account: auth.adapticAccountId || "direct",
6055
+ symbol: normalizedSymbol,
6056
+ });
6057
+ return null;
6058
+ }
6036
6059
  throw new Error(`Failed to close position: ${response.status} ${response.statusText} ${errorText}`);
6037
6060
  }
6038
6061
  return (await response.json());
@@ -6080,8 +6103,8 @@ async function closeAllPositions$1(auth, params = { cancel_orders: true, useLimi
6080
6103
  return [];
6081
6104
  }
6082
6105
  // Separate crypto and equity positions — crypto cannot use SIP quotes or time_in_force="day"
6083
- const equityPositions = allPositions.filter((p) => !isCryptoSymbol(p.symbol));
6084
- const cryptoPositions = allPositions.filter((p) => isCryptoSymbol(p.symbol));
6106
+ const equityPositions = allPositions.filter((p) => !isCryptoSymbol$1(p.symbol));
6107
+ const cryptoPositions = allPositions.filter((p) => isCryptoSymbol$1(p.symbol));
6085
6108
  getLogger().info(`Found ${allPositions.length} positions to close (${equityPositions.length} equity, ${cryptoPositions.length} crypto)`, { account: auth.adapticAccountId || "direct" });
6086
6109
  // Close crypto positions via direct DELETE (market order) — no SIP quotes needed
6087
6110
  for (const position of cryptoPositions) {
@@ -6104,11 +6127,17 @@ async function closeAllPositions$1(auth, params = { cancel_orders: true, useLimi
6104
6127
  }
6105
6128
  else {
6106
6129
  const errorText = await response.text();
6107
- getLogger().warn(`Failed to close crypto position ${position.symbol}: ${response.status} ${errorText}`, { account: auth.adapticAccountId || "direct", symbol: position.symbol });
6130
+ getLogger().warn(`Failed to close crypto position ${position.symbol}: ${response.status} ${errorText}`, {
6131
+ account: auth.adapticAccountId || "direct",
6132
+ symbol: position.symbol,
6133
+ });
6108
6134
  }
6109
6135
  }
6110
6136
  catch (cryptoError) {
6111
- getLogger().warn(`Error closing crypto position ${position.symbol}: ${cryptoError instanceof Error ? cryptoError.message : String(cryptoError)}`, { account: auth.adapticAccountId || "direct", symbol: position.symbol });
6137
+ getLogger().warn(`Error closing crypto position ${position.symbol}: ${cryptoError instanceof Error ? cryptoError.message : String(cryptoError)}`, {
6138
+ account: auth.adapticAccountId || "direct",
6139
+ symbol: position.symbol,
6140
+ });
6112
6141
  }
6113
6142
  }
6114
6143
  // Close equity positions via limit orders with SIP quotes
@@ -6201,8 +6230,8 @@ async function closeAllPositionsAfterHours$1(auth, params = { cancel_orders: tru
6201
6230
  return;
6202
6231
  }
6203
6232
  // Separate crypto and equity positions
6204
- const equityPositions = allPositions.filter((p) => !isCryptoSymbol(p.symbol));
6205
- const cryptoPositions = allPositions.filter((p) => isCryptoSymbol(p.symbol));
6233
+ const equityPositions = allPositions.filter((p) => !isCryptoSymbol$1(p.symbol));
6234
+ const cryptoPositions = allPositions.filter((p) => isCryptoSymbol$1(p.symbol));
6206
6235
  getLogger().info(`Found ${allPositions.length} positions to close after hours (${equityPositions.length} equity, ${cryptoPositions.length} crypto)`, { account: auth.adapticAccountId || "direct" });
6207
6236
  if (cancel_orders) {
6208
6237
  await cancelAllOrders$1(auth);
@@ -6231,7 +6260,10 @@ async function closeAllPositionsAfterHours$1(auth, params = { cancel_orders: tru
6231
6260
  }
6232
6261
  else {
6233
6262
  const errorText = await response.text();
6234
- getLogger().warn(`Failed to close crypto position ${position.symbol}: ${response.status} ${errorText}`, { account: auth.adapticAccountId || "direct", symbol: position.symbol });
6263
+ getLogger().warn(`Failed to close crypto position ${position.symbol}: ${response.status} ${errorText}`, {
6264
+ account: auth.adapticAccountId || "direct",
6265
+ symbol: position.symbol,
6266
+ });
6235
6267
  }
6236
6268
  }
6237
6269
  catch (cryptoError) {
@@ -7873,6 +7905,84 @@ const massiveLimit = pLimit(MASSIVE_CONCURRENCY_LIMIT);
7873
7905
  * request settles, so subsequent calls after resolution make a fresh request.
7874
7906
  */
7875
7907
  const fetchLastTradeInflight = new Map();
7908
+ /**
7909
+ * Check if a symbol is a crypto pair based on common patterns.
7910
+ * Crypto symbols typically end in USD, USDT, USDC, or contain a hyphen with USD.
7911
+ * Examples: BTCUSD, BTC-USD, BTC/USD, LINKUSD, SOL-USD
7912
+ *
7913
+ * @param symbol - The ticker symbol to check
7914
+ * @returns True if the symbol appears to be a crypto pair
7915
+ */
7916
+ function isCryptoSymbol(symbol) {
7917
+ // Pattern: ends with USD/USDT/USDC and has 3-4 letter base (e.g., BTCUSD, LINKUSD)
7918
+ if (/^[A-Z]{2,5}(USD[TC]?)$/i.test(symbol)) {
7919
+ return true;
7920
+ }
7921
+ // Pattern: contains hyphen or slash with USD (e.g., BTC-USD, BTC/USD)
7922
+ if (/^[A-Z]{2,5}[-/]USD[TC]?$/i.test(symbol)) {
7923
+ return true;
7924
+ }
7925
+ // Pattern: already has X: prefix (e.g., X:BTC-USD)
7926
+ if (symbol.startsWith("X:")) {
7927
+ return true;
7928
+ }
7929
+ return false;
7930
+ }
7931
+ /**
7932
+ * Normalize a symbol for the Massive.com API.
7933
+ * Crypto symbols must be prefixed with "X:" and use hyphen format (e.g., X:BTC-USD).
7934
+ * Stock symbols are passed through unchanged.
7935
+ *
7936
+ * @param symbol - The raw ticker symbol
7937
+ * @returns The symbol formatted for Massive.com API
7938
+ */
7939
+ function normalizeMassiveSymbol(symbol) {
7940
+ // If already has X: prefix, ensure hyphen format
7941
+ if (symbol.startsWith("X:")) {
7942
+ return symbol;
7943
+ }
7944
+ // Check if it's a crypto symbol
7945
+ if (!isCryptoSymbol(symbol)) {
7946
+ return symbol; // Stock symbol - return unchanged
7947
+ }
7948
+ // Normalize crypto symbol to X:BASE-QUOTE format
7949
+ // Handle formats: BTCUSD, BTC-USD, BTC/USD -> X:BTC-USD
7950
+ let base;
7951
+ let quote;
7952
+ if (symbol.includes("-")) {
7953
+ // Format: BTC-USD
7954
+ const parts = symbol.split("-");
7955
+ base = parts[0];
7956
+ quote = parts.slice(1).join("-");
7957
+ }
7958
+ else if (symbol.includes("/")) {
7959
+ // Format: BTC/USD
7960
+ const parts = symbol.split("/");
7961
+ base = parts[0];
7962
+ quote = parts.slice(1).join("/");
7963
+ }
7964
+ else {
7965
+ // Format: BTCUSD - need to extract base and quote
7966
+ // Common quote currencies: USD, USDT, USDC
7967
+ if (symbol.endsWith("USDT")) {
7968
+ base = symbol.slice(0, -4);
7969
+ quote = "USDT";
7970
+ }
7971
+ else if (symbol.endsWith("USDC")) {
7972
+ base = symbol.slice(0, -4);
7973
+ quote = "USDC";
7974
+ }
7975
+ else if (symbol.endsWith("USD")) {
7976
+ base = symbol.slice(0, -3);
7977
+ quote = "USD";
7978
+ }
7979
+ else {
7980
+ // Unknown format, return as-is
7981
+ return symbol;
7982
+ }
7983
+ }
7984
+ return `X:${base.toUpperCase()}-${quote.toUpperCase()}`;
7985
+ }
7876
7986
  // Use to update general information about stocks
7877
7987
  /**
7878
7988
  * Fetches general information about a stock ticker.
@@ -7994,7 +8104,9 @@ const fetchLastTradeImpl = async (symbol, options) => {
7994
8104
  }
7995
8105
  const apiKey = options?.apiKey || MASSIVE_API_KEY;
7996
8106
  validateMassiveApiKey$1(apiKey);
7997
- const baseUrl = `https://api.massive.com/v3/trades/${encodeURIComponent(symbol)}`;
8107
+ // Normalize crypto symbols to Massive.com API format (e.g., LINK-USD -> X:LINK-USD)
8108
+ const normalizedSymbol = normalizeMassiveSymbol(symbol);
8109
+ const baseUrl = `https://api.massive.com/v3/trades/${encodeURIComponent(normalizedSymbol)}`;
7998
8110
  const params = new URLSearchParams({
7999
8111
  apiKey,
8000
8112
  limit: "1",
@@ -8030,9 +8142,10 @@ const fetchLastTradeImpl = async (symbol, options) => {
8030
8142
  }
8031
8143
  catch (error) {
8032
8144
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
8033
- const contextualMessage = `Error fetching last trade for ${symbol}`;
8145
+ const contextualMessage = `Error fetching last trade for ${symbol}${normalizedSymbol !== symbol ? ` (normalized: ${normalizedSymbol})` : ""}`;
8034
8146
  getLogger().error(`${contextualMessage}: ${errorMessage}`, {
8035
8147
  symbol,
8148
+ normalizedSymbol,
8036
8149
  errorType: error instanceof Error && error.message.includes("AUTH_ERROR")
8037
8150
  ? "AUTH_ERROR"
8038
8151
  : error instanceof Error && error.message.includes("RATE_LIMIT")