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