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