@adaptic/utils 0.0.941 → 0.0.943

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
@@ -4956,9 +4956,11 @@ async function createLimitOrder(auth, params = {
4956
4956
  position_intent: "buy_to_open",
4957
4957
  extended_hours: false,
4958
4958
  client_order_id: undefined,
4959
+ time_in_force: undefined,
4959
4960
  }) {
4960
- const { symbol, qty, side, limitPrice, position_intent, extended_hours, client_order_id, } = params;
4961
- getLogger().info(`Creating limit order for ${symbol}: ${side} ${qty} shares at ${limitPrice.toFixed(2)} (${position_intent})`, {
4961
+ const { symbol, qty, side, limitPrice, position_intent, extended_hours, client_order_id, time_in_force, } = params;
4962
+ const effectiveTimeInForce = time_in_force ?? "day";
4963
+ getLogger().info(`Creating limit order for ${symbol}: ${side} ${qty} shares at ${limitPrice.toFixed(2)} (${position_intent}, tif=${effectiveTimeInForce})`, {
4962
4964
  account: auth.adapticAccountId || "direct",
4963
4965
  symbol,
4964
4966
  });
@@ -4969,7 +4971,7 @@ async function createLimitOrder(auth, params = {
4969
4971
  position_intent,
4970
4972
  type: "limit",
4971
4973
  limit_price: limitPrice.toString(),
4972
- time_in_force: "day",
4974
+ time_in_force: effectiveTimeInForce,
4973
4975
  order_class: "simple",
4974
4976
  extended_hours,
4975
4977
  };
@@ -5453,17 +5455,17 @@ const DEFAULT_FEED = "sip";
5453
5455
  * Known quote currencies that signal a crypto pair when found as a suffix.
5454
5456
  * E.g. "BTCUSD" → crypto, "AAPL" → equity.
5455
5457
  */
5456
- const CRYPTO_QUOTE_CURRENCIES = ["USD", "USDT", "USDC", "BTC"];
5458
+ const CRYPTO_QUOTE_CURRENCIES$1 = ["USD", "USDT", "USDC", "BTC"];
5457
5459
  /**
5458
5460
  * Detect whether a symbol looks like a crypto pair (e.g. "BTCUSD", "DOGEUSD",
5459
5461
  * "BTC/USD"). Equity tickers never end with these suffixes because Alpaca
5460
5462
  * equity symbols are plain tickers without a quote currency.
5461
5463
  */
5462
- function isCryptoSymbol(symbol) {
5464
+ function isCryptoSymbol$1(symbol) {
5463
5465
  if (symbol.includes("/"))
5464
5466
  return true;
5465
5467
  const upper = symbol.toUpperCase();
5466
- return CRYPTO_QUOTE_CURRENCIES.some((qc) => {
5468
+ return CRYPTO_QUOTE_CURRENCIES$1.some((qc) => {
5467
5469
  if (!upper.endsWith(qc))
5468
5470
  return false;
5469
5471
  const base = upper.slice(0, -qc.length);
@@ -5479,7 +5481,7 @@ function normalizeCryptoSymbolForApi(symbol) {
5479
5481
  if (symbol.includes("/"))
5480
5482
  return symbol.toUpperCase();
5481
5483
  const upper = symbol.toUpperCase();
5482
- for (const qc of CRYPTO_QUOTE_CURRENCIES) {
5484
+ for (const qc of CRYPTO_QUOTE_CURRENCIES$1) {
5483
5485
  if (upper.endsWith(qc)) {
5484
5486
  const base = upper.slice(0, -qc.length);
5485
5487
  if (base.length >= 2)
@@ -5513,7 +5515,7 @@ async function getLatestQuotes$1(auth, params) {
5513
5515
  const equitySymbols = [];
5514
5516
  const cryptoSymbols = [];
5515
5517
  for (const sym of symbols) {
5516
- if (isCryptoSymbol(sym)) {
5518
+ if (isCryptoSymbol$1(sym)) {
5517
5519
  cryptoSymbols.push(sym);
5518
5520
  }
5519
5521
  else {
@@ -5689,6 +5691,24 @@ async function fetchNews$1(symbols, params) {
5689
5691
  }
5690
5692
  }
5691
5693
 
5694
+ /** Known quote currencies that signal a crypto pair when found as a suffix */
5695
+ const CRYPTO_QUOTE_CURRENCIES = ["USD", "USDT", "USDC", "BTC"];
5696
+ /**
5697
+ * Detect whether a symbol looks like a crypto pair (e.g. "BTCUSD", "DOGEUSD",
5698
+ * "BTC/USD"). Equity tickers never end with these suffixes because Alpaca
5699
+ * equity symbols are plain tickers without a quote currency.
5700
+ */
5701
+ function isCryptoSymbol(symbol) {
5702
+ if (symbol.includes("/"))
5703
+ return true;
5704
+ const upper = symbol.toUpperCase();
5705
+ return CRYPTO_QUOTE_CURRENCIES.some((qc) => {
5706
+ if (!upper.endsWith(qc))
5707
+ return false;
5708
+ const base = upper.slice(0, -qc.length);
5709
+ return base.length >= 2;
5710
+ });
5711
+ }
5692
5712
  /**
5693
5713
  * Fetches all positions for an Alpaca trading account.
5694
5714
  * @param auth - The authentication details for Alpaca
@@ -5789,7 +5809,9 @@ async function closePosition$1(auth, symbolOrAssetId, params) {
5789
5809
  }
5790
5810
  }
5791
5811
  }
5792
- if (useLimitOrder) {
5812
+ // Crypto positions cannot use limit orders with SIP quotes or time_in_force="day".
5813
+ // Use direct DELETE (market order) for crypto regardless of useLimitOrder flag.
5814
+ if (useLimitOrder && !isCryptoSymbol(symbolOrAssetId)) {
5793
5815
  const { position } = await fetchPosition(auth, symbolOrAssetId);
5794
5816
  if (!position) {
5795
5817
  throw new Error(`Position not found for ${symbolOrAssetId}`);
@@ -5839,6 +5861,9 @@ async function closePosition$1(auth, symbolOrAssetId, params) {
5839
5861
  });
5840
5862
  }
5841
5863
  else {
5864
+ if (isCryptoSymbol(symbolOrAssetId)) {
5865
+ getLogger().info(`Closing crypto position ${symbolOrAssetId} via market order (DELETE endpoint)`, { account: auth.adapticAccountId || "direct", symbol: symbolOrAssetId });
5866
+ }
5842
5867
  const queryParams = new URLSearchParams();
5843
5868
  if (params?.qty !== undefined) {
5844
5869
  queryParams.append("qty", params.qty.toString());
@@ -5897,37 +5922,68 @@ async function closeAllPositions$1(auth, params = { cancel_orders: true, useLimi
5897
5922
  // Continue with closure attempt even if cancel failed
5898
5923
  }
5899
5924
  }
5900
- const positions = await fetchAllPositions(auth);
5901
- if (positions.length === 0) {
5925
+ const allPositions = await fetchAllPositions(auth);
5926
+ if (allPositions.length === 0) {
5902
5927
  getLogger().info("No positions to close", {
5903
5928
  account: auth.adapticAccountId || "direct",
5904
5929
  });
5905
5930
  return [];
5906
5931
  }
5907
- getLogger().info(`Found ${positions.length} positions to close`, {
5908
- account: auth.adapticAccountId || "direct",
5909
- });
5932
+ // Separate crypto and equity positions crypto cannot use SIP quotes or time_in_force="day"
5933
+ const equityPositions = allPositions.filter((p) => !isCryptoSymbol(p.symbol));
5934
+ const cryptoPositions = allPositions.filter((p) => isCryptoSymbol(p.symbol));
5935
+ getLogger().info(`Found ${allPositions.length} positions to close (${equityPositions.length} equity, ${cryptoPositions.length} crypto)`, { account: auth.adapticAccountId || "direct" });
5936
+ // Close crypto positions via direct DELETE (market order) — no SIP quotes needed
5937
+ for (const position of cryptoPositions) {
5938
+ try {
5939
+ const { APIKey, APISecret, type } = await validateAuth(auth);
5940
+ const apiBaseUrl = getTradingApiUrl(type);
5941
+ const url = `${apiBaseUrl}/positions/${encodeURIComponent(position.symbol)}`;
5942
+ const response = await fetch(url, {
5943
+ method: "DELETE",
5944
+ headers: {
5945
+ "APCA-API-KEY-ID": APIKey,
5946
+ "APCA-API-SECRET-KEY": APISecret,
5947
+ },
5948
+ });
5949
+ if (response.ok) {
5950
+ getLogger().info(`Closed crypto position ${position.symbol} via market order`, {
5951
+ account: auth.adapticAccountId || "direct",
5952
+ symbol: position.symbol,
5953
+ });
5954
+ }
5955
+ else {
5956
+ const errorText = await response.text();
5957
+ getLogger().warn(`Failed to close crypto position ${position.symbol}: ${response.status} ${errorText}`, { account: auth.adapticAccountId || "direct", symbol: position.symbol });
5958
+ }
5959
+ }
5960
+ catch (cryptoError) {
5961
+ getLogger().warn(`Error closing crypto position ${position.symbol}: ${cryptoError instanceof Error ? cryptoError.message : String(cryptoError)}`, { account: auth.adapticAccountId || "direct", symbol: position.symbol });
5962
+ }
5963
+ }
5964
+ // Close equity positions via limit orders with SIP quotes
5965
+ if (equityPositions.length === 0) {
5966
+ return [];
5967
+ }
5910
5968
  // Use the passed auth for quote fetching (not hardcoded env vars)
5911
5969
  // so multi-account setups use the correct credentials per account
5912
- const symbols = positions.map((position) => position.symbol);
5970
+ const symbols = equityPositions.map((position) => position.symbol);
5913
5971
  const quotesResponse = await getLatestQuotes$1(auth, { symbols });
5914
5972
  const lengthOfQuotes = Object.keys(quotesResponse.quotes).length;
5915
5973
  if (lengthOfQuotes === 0) {
5916
- getLogger().error("No quotes available for positions, received 0 quotes", {
5974
+ getLogger().error("No quotes available for equity positions, received 0 quotes", {
5917
5975
  account: auth.adapticAccountId || "direct",
5918
5976
  type: "error",
5919
5977
  });
5920
5978
  return [];
5921
5979
  }
5922
- if (lengthOfQuotes !== positions.length) {
5923
- getLogger().warn(`Received ${lengthOfQuotes} quotes for ${positions.length} positions (expected ${positions.length}), proceeding with available quotes`, {
5980
+ if (lengthOfQuotes !== equityPositions.length) {
5981
+ getLogger().warn(`Received ${lengthOfQuotes} quotes for ${equityPositions.length} equity positions, proceeding with available quotes`, {
5924
5982
  account: auth.adapticAccountId || "direct",
5925
5983
  type: "warn",
5926
5984
  });
5927
- // Continue with available quotes instead of aborting — some symbols may
5928
- // lack quotes (halted, delisted) but other positions should still close
5929
5985
  }
5930
- for (const position of positions) {
5986
+ for (const position of equityPositions) {
5931
5987
  const quote = quotesResponse.quotes[position.symbol];
5932
5988
  if (!quote) {
5933
5989
  getLogger().warn(`No quote available for ${position.symbol}, skipping limit order`, {
@@ -5987,31 +6043,63 @@ async function closeAllPositionsAfterHours$1(auth, params = { cancel_orders: tru
5987
6043
  account: auth.adapticAccountId || "direct",
5988
6044
  });
5989
6045
  const { cancel_orders, slippagePercent1 = 0.1 } = params;
5990
- const positions = await fetchAllPositions(auth);
5991
- if (positions.length === 0) {
6046
+ const allPositions = await fetchAllPositions(auth);
6047
+ if (allPositions.length === 0) {
5992
6048
  getLogger().info("No positions to close", {
5993
6049
  account: auth.adapticAccountId || "direct",
5994
6050
  });
5995
6051
  return;
5996
6052
  }
5997
- getLogger().info(`Found ${positions.length} positions to close`, {
5998
- account: auth.adapticAccountId || "direct",
5999
- });
6053
+ // Separate crypto and equity positions
6054
+ const equityPositions = allPositions.filter((p) => !isCryptoSymbol(p.symbol));
6055
+ const cryptoPositions = allPositions.filter((p) => isCryptoSymbol(p.symbol));
6056
+ getLogger().info(`Found ${allPositions.length} positions to close after hours (${equityPositions.length} equity, ${cryptoPositions.length} crypto)`, { account: auth.adapticAccountId || "direct" });
6000
6057
  if (cancel_orders) {
6001
6058
  await cancelAllOrders$1(auth);
6002
6059
  getLogger().info("Cancelled all open orders", {
6003
6060
  account: auth.adapticAccountId || "direct",
6004
6061
  });
6005
6062
  }
6063
+ // Close crypto positions via direct DELETE (market order)
6064
+ for (const position of cryptoPositions) {
6065
+ try {
6066
+ const { APIKey, APISecret, type } = await validateAuth(auth);
6067
+ const apiBaseUrl = getTradingApiUrl(type);
6068
+ const url = `${apiBaseUrl}/positions/${encodeURIComponent(position.symbol)}`;
6069
+ const response = await fetch(url, {
6070
+ method: "DELETE",
6071
+ headers: {
6072
+ "APCA-API-KEY-ID": APIKey,
6073
+ "APCA-API-SECRET-KEY": APISecret,
6074
+ },
6075
+ });
6076
+ if (response.ok) {
6077
+ getLogger().info(`Closed crypto position ${position.symbol} via market order`, {
6078
+ account: auth.adapticAccountId || "direct",
6079
+ symbol: position.symbol,
6080
+ });
6081
+ }
6082
+ else {
6083
+ const errorText = await response.text();
6084
+ getLogger().warn(`Failed to close crypto position ${position.symbol}: ${response.status} ${errorText}`, { account: auth.adapticAccountId || "direct", symbol: position.symbol });
6085
+ }
6086
+ }
6087
+ catch (cryptoError) {
6088
+ getLogger().warn(`Error closing crypto position ${position.symbol}: ${cryptoError instanceof Error ? cryptoError.message : String(cryptoError)}`, { account: auth.adapticAccountId || "direct", symbol: position.symbol });
6089
+ }
6090
+ }
6091
+ if (equityPositions.length === 0) {
6092
+ return;
6093
+ }
6006
6094
  const alpacaAuthObj = {
6007
6095
  type: "LIVE",
6008
6096
  alpacaApiKey: process.env.ALPACA_API_KEY,
6009
6097
  alpacaApiSecret: process.env.ALPACA_SECRET_KEY,
6010
6098
  };
6011
6099
  const alpacaAuth = alpacaAuthObj;
6012
- const symbols = positions.map((position) => position.symbol);
6100
+ const symbols = equityPositions.map((position) => position.symbol);
6013
6101
  const quotesResponse = await getLatestQuotes$1(alpacaAuth, { symbols });
6014
- for (const position of positions) {
6102
+ for (const position of equityPositions) {
6015
6103
  const quote = quotesResponse.quotes[position.symbol];
6016
6104
  if (!quote) {
6017
6105
  getLogger().warn(`No quote available for ${position.symbol}, skipping limit order`, {
@@ -6050,7 +6138,7 @@ async function closeAllPositionsAfterHours$1(auth, params = { cancel_orders: tru
6050
6138
  extended_hours: true,
6051
6139
  });
6052
6140
  }
6053
- getLogger().info(`All positions closed: ${positions.map((p) => p.symbol).join(", ")}`, {
6141
+ getLogger().info(`All positions closed: ${allPositions.map((p) => p.symbol).join(", ")}`, {
6054
6142
  account: auth.adapticAccountId || "direct",
6055
6143
  });
6056
6144
  }