@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.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$
|
|
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$
|
|
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, {
|
|
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}`, {
|
|
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}`, {
|
|
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
|
|
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)`, {
|
|
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}`, {
|
|
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)}`, {
|
|
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}`, {
|
|
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
|
-
|
|
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")
|