@discomedia/utils 1.0.32 → 1.0.34

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
@@ -16315,6 +16315,7 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
16315
16315
  dataURL;
16316
16316
  apiURL;
16317
16317
  v1beta1url;
16318
+ v1beta3url;
16318
16319
  stockStreamUrl = 'wss://stream.data.alpaca.markets/v2/sip'; // production values
16319
16320
  optionStreamUrl = 'wss://stream.data.alpaca.markets/v1beta3/options'; // production values
16320
16321
  stockWs = null;
@@ -16357,6 +16358,7 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
16357
16358
  ? 'https://paper-api.alpaca.markets/v2'
16358
16359
  : 'https://api.alpaca.markets/v2'; // used by some, e.g. getAssets
16359
16360
  this.v1beta1url = 'https://data.alpaca.markets/v1beta1'; // used for options endpoints
16361
+ this.v1beta3url = 'https://data.alpaca.markets/v1beta3'; // used for crypto endpoints
16360
16362
  this.setMode('production'); // sets stockStreamUrl and optionStreamUrl
16361
16363
  this.headers = {
16362
16364
  'APCA-API-KEY-ID': process.env.ALPACA_API_KEY,
@@ -16494,7 +16496,9 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
16494
16496
  }
16495
16497
  }
16496
16498
  async makeRequest(endpoint, method = 'GET', params, baseUrlName = 'data') {
16497
- const baseUrl = baseUrlName === 'data' ? this.dataURL : baseUrlName === 'api' ? this.apiURL : this.v1beta1url;
16499
+ const baseUrl = baseUrlName === 'data' ? this.dataURL :
16500
+ baseUrlName === 'api' ? this.apiURL :
16501
+ baseUrlName === 'v1beta1' ? this.v1beta1url : this.v1beta3url;
16498
16502
  const url = new URL(`${baseUrl}${endpoint}`);
16499
16503
  try {
16500
16504
  if (params) {
@@ -17202,6 +17206,253 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
17202
17206
  }
17203
17207
  return newsArticles;
17204
17208
  }
17209
+ // ===== CRYPTO MARKET DATA METHODS =====
17210
+ /**
17211
+ * Get historical OHLCV bars for crypto symbols
17212
+ * Automatically handles pagination to fetch all available data
17213
+ * @param params Parameters for crypto historical bars request
17214
+ * @returns Historical bars data with all pages combined
17215
+ */
17216
+ async getCryptoHistoricalBars(params) {
17217
+ const symbols = params.symbols;
17218
+ const symbolsStr = symbols.join(',');
17219
+ let allBars = {};
17220
+ let pageToken = null;
17221
+ let hasMorePages = true;
17222
+ let totalBarsCount = 0;
17223
+ let pageCount = 0;
17224
+ // Initialize bar arrays for each symbol
17225
+ symbols.forEach((symbol) => {
17226
+ allBars[symbol] = [];
17227
+ });
17228
+ log(`Starting crypto historical bars fetch for ${symbols.length} symbols (${params.timeframe}, ${params.start || 'no start'} to ${params.end || 'no end'})`, { type: 'info' });
17229
+ while (hasMorePages) {
17230
+ pageCount++;
17231
+ const requestParams = {
17232
+ ...params,
17233
+ symbols: symbolsStr,
17234
+ ...(pageToken && { page_token: pageToken }),
17235
+ };
17236
+ const response = await this.makeRequest('/crypto/us/bars', 'GET', requestParams, 'v1beta3');
17237
+ if (!response.bars) {
17238
+ log(`No crypto bars data found in response for ${symbols.length} symbols`, { type: 'warn' });
17239
+ break;
17240
+ }
17241
+ // Combine bars for each symbol
17242
+ let pageBarsCount = 0;
17243
+ Object.entries(response.bars).forEach(([symbol, bars]) => {
17244
+ if (bars && bars.length > 0) {
17245
+ allBars[symbol] = [...allBars[symbol], ...bars];
17246
+ pageBarsCount += bars.length;
17247
+ }
17248
+ });
17249
+ totalBarsCount += pageBarsCount;
17250
+ pageToken = response.next_page_token || null;
17251
+ hasMorePages = !!pageToken;
17252
+ log(`Page ${pageCount}: Fetched ${pageBarsCount.toLocaleString()} crypto bars (total: ${totalBarsCount.toLocaleString()}) for ${symbols.length} symbols${hasMorePages ? ', more pages available' : ', complete'}`);
17253
+ // Prevent infinite loops
17254
+ if (pageCount > 1000) {
17255
+ log(`Stopping crypto bars pagination after ${pageCount} pages to prevent infinite loop`, { type: 'warn' });
17256
+ break;
17257
+ }
17258
+ }
17259
+ log(`Crypto historical bars fetch complete: ${totalBarsCount.toLocaleString()} total bars across ${pageCount} pages`, { type: 'info' });
17260
+ return {
17261
+ bars: allBars,
17262
+ next_page_token: null, // Always null since we fetch all pages
17263
+ };
17264
+ }
17265
+ /**
17266
+ * Get the most recent minute bar for requested crypto symbols
17267
+ * @param symbols Array of crypto symbols to query
17268
+ * @returns Latest bar data for each symbol
17269
+ */
17270
+ async getCryptoLatestBars(symbols) {
17271
+ return this.makeRequest('/crypto/us/latest/bars', 'GET', { symbols: symbols.join(',') }, 'v1beta3');
17272
+ }
17273
+ /**
17274
+ * Get historical quotes for crypto symbols
17275
+ * Automatically handles pagination to fetch all available data
17276
+ * @param params Parameters for crypto historical quotes request
17277
+ * @returns Historical quotes data with all pages combined
17278
+ */
17279
+ async getCryptoHistoricalQuotes(params) {
17280
+ const symbols = params.symbols;
17281
+ const symbolsStr = symbols.join(',');
17282
+ let allQuotes = {};
17283
+ let pageToken = null;
17284
+ let hasMorePages = true;
17285
+ let totalQuotesCount = 0;
17286
+ let pageCount = 0;
17287
+ // Initialize quotes arrays for each symbol
17288
+ symbols.forEach((symbol) => {
17289
+ allQuotes[symbol] = [];
17290
+ });
17291
+ log(`Starting crypto historical quotes fetch for ${symbols.length} symbols (${params.start || 'no start'} to ${params.end || 'no end'})`, { type: 'info' });
17292
+ while (hasMorePages) {
17293
+ pageCount++;
17294
+ const requestParams = {
17295
+ ...params,
17296
+ symbols: symbolsStr,
17297
+ ...(pageToken && { page_token: pageToken }),
17298
+ };
17299
+ const response = await this.makeRequest('/crypto/us/quotes', 'GET', requestParams, 'v1beta3');
17300
+ if (!response.quotes) {
17301
+ log(`No crypto quotes data found in response for ${symbols.length} symbols`, { type: 'warn' });
17302
+ break;
17303
+ }
17304
+ // Combine quotes for each symbol
17305
+ let pageQuotesCount = 0;
17306
+ Object.entries(response.quotes).forEach(([symbol, quotes]) => {
17307
+ if (quotes && quotes.length > 0) {
17308
+ allQuotes[symbol] = [...allQuotes[symbol], ...quotes];
17309
+ pageQuotesCount += quotes.length;
17310
+ }
17311
+ });
17312
+ totalQuotesCount += pageQuotesCount;
17313
+ pageToken = response.next_page_token || null;
17314
+ hasMorePages = !!pageToken;
17315
+ log(`Page ${pageCount}: Fetched ${pageQuotesCount.toLocaleString()} crypto quotes (total: ${totalQuotesCount.toLocaleString()}) for ${symbols.length} symbols${hasMorePages ? ', more pages available' : ', complete'}`);
17316
+ // Prevent infinite loops
17317
+ if (pageCount > 1000) {
17318
+ log(`Stopping crypto quotes pagination after ${pageCount} pages to prevent infinite loop`, { type: 'warn' });
17319
+ break;
17320
+ }
17321
+ }
17322
+ log(`Crypto historical quotes fetch complete: ${totalQuotesCount.toLocaleString()} total quotes across ${pageCount} pages`, { type: 'info' });
17323
+ return {
17324
+ quotes: allQuotes,
17325
+ next_page_token: null, // Always null since we fetch all pages
17326
+ };
17327
+ }
17328
+ /**
17329
+ * Get the most recent quotes for requested crypto symbols
17330
+ * @param symbols Array of crypto symbols to query
17331
+ * @returns Latest quote data for each symbol
17332
+ */
17333
+ async getCryptoLatestQuotes(symbols) {
17334
+ if (!symbols || symbols.length === 0) {
17335
+ log('No symbols provided to getCryptoLatestQuotes, returning empty response', { type: 'warn' });
17336
+ return { quotes: {} };
17337
+ }
17338
+ return this.makeRequest('/crypto/us/latest/quotes', 'GET', { symbols: symbols.join(',') }, 'v1beta3');
17339
+ }
17340
+ /**
17341
+ * Get historical trades for crypto symbols
17342
+ * Automatically handles pagination to fetch all available data
17343
+ * @param params Parameters for crypto historical trades request
17344
+ * @returns Historical trades data with all pages combined
17345
+ */
17346
+ async getCryptoHistoricalTrades(params) {
17347
+ const symbols = params.symbols;
17348
+ const symbolsStr = symbols.join(',');
17349
+ let allTrades = {};
17350
+ let pageToken = null;
17351
+ let hasMorePages = true;
17352
+ let totalTradesCount = 0;
17353
+ let pageCount = 0;
17354
+ // Initialize trades arrays for each symbol
17355
+ symbols.forEach((symbol) => {
17356
+ allTrades[symbol] = [];
17357
+ });
17358
+ log(`Starting crypto historical trades fetch for ${symbols.length} symbols (${params.start || 'no start'} to ${params.end || 'no end'})`, { type: 'info' });
17359
+ while (hasMorePages) {
17360
+ pageCount++;
17361
+ const requestParams = {
17362
+ ...params,
17363
+ symbols: symbolsStr,
17364
+ ...(pageToken && { page_token: pageToken }),
17365
+ };
17366
+ const response = await this.makeRequest('/crypto/us/trades', 'GET', requestParams, 'v1beta3');
17367
+ if (!response.trades) {
17368
+ log(`No crypto trades data found in response for ${symbols.length} symbols`, { type: 'warn' });
17369
+ break;
17370
+ }
17371
+ // Combine trades for each symbol
17372
+ let pageTradesCount = 0;
17373
+ Object.entries(response.trades).forEach(([symbol, trades]) => {
17374
+ if (trades && trades.length > 0) {
17375
+ allTrades[symbol] = [...allTrades[symbol], ...trades];
17376
+ pageTradesCount += trades.length;
17377
+ }
17378
+ });
17379
+ totalTradesCount += pageTradesCount;
17380
+ pageToken = response.next_page_token || null;
17381
+ hasMorePages = !!pageToken;
17382
+ log(`Page ${pageCount}: Fetched ${pageTradesCount.toLocaleString()} crypto trades (total: ${totalTradesCount.toLocaleString()}) for ${symbols.length} symbols${hasMorePages ? ', more pages available' : ', complete'}`);
17383
+ // Prevent infinite loops
17384
+ if (pageCount > 1000) {
17385
+ log(`Stopping crypto trades pagination after ${pageCount} pages to prevent infinite loop`, { type: 'warn' });
17386
+ break;
17387
+ }
17388
+ }
17389
+ log(`Crypto historical trades fetch complete: ${totalTradesCount.toLocaleString()} total trades across ${pageCount} pages`, { type: 'info' });
17390
+ return {
17391
+ trades: allTrades,
17392
+ next_page_token: null, // Always null since we fetch all pages
17393
+ };
17394
+ }
17395
+ /**
17396
+ * Get the most recent trades for requested crypto symbols
17397
+ * @param symbols Array of crypto symbols to query
17398
+ * @returns Latest trade data for each symbol
17399
+ */
17400
+ async getCryptoLatestTrades(symbols) {
17401
+ if (!symbols || symbols.length === 0) {
17402
+ log('No symbols provided to getCryptoLatestTrades, returning empty response', { type: 'warn' });
17403
+ return { trades: {} };
17404
+ }
17405
+ return this.makeRequest('/crypto/us/latest/trades', 'GET', { symbols: symbols.join(',') }, 'v1beta3');
17406
+ }
17407
+ /**
17408
+ * Get snapshots for crypto symbols
17409
+ * Returns the latest trade, latest quote, latest minute bar, latest daily bar, and previous daily bar data
17410
+ * @param symbols Array of crypto symbols to query
17411
+ * @returns Snapshot data for each symbol
17412
+ */
17413
+ async getCryptoSnapshots(symbols) {
17414
+ if (!symbols || symbols.length === 0) {
17415
+ log('No symbols provided to getCryptoSnapshots, returning empty response', { type: 'warn' });
17416
+ return { snapshots: {} };
17417
+ }
17418
+ return this.makeRequest('/crypto/us/snapshots', 'GET', { symbols: symbols.join(',') }, 'v1beta3');
17419
+ }
17420
+ /**
17421
+ * Get the latest orderbook for requested crypto symbols
17422
+ * @param symbols Array of crypto symbols to query
17423
+ * @returns Latest orderbook data for each symbol
17424
+ */
17425
+ async getCryptoLatestOrderbooks(symbols) {
17426
+ if (!symbols || symbols.length === 0) {
17427
+ log('No symbols provided to getCryptoLatestOrderbooks, returning empty response', { type: 'warn' });
17428
+ return { orderbooks: {} };
17429
+ }
17430
+ return this.makeRequest('/crypto/us/latest/orderbooks', 'GET', { symbols: symbols.join(',') }, 'v1beta3');
17431
+ }
17432
+ /**
17433
+ * Analyzes an array of crypto bars and returns a summary string
17434
+ * @param bars Array of crypto bars to analyze
17435
+ * @returns A string summarizing the crypto price data
17436
+ */
17437
+ static analyzeCryptoBars(bars) {
17438
+ if (!bars || bars.length === 0) {
17439
+ return 'No crypto price data available';
17440
+ }
17441
+ const firstBar = bars[0];
17442
+ const lastBar = bars[bars.length - 1];
17443
+ const priceChange = lastBar.c - firstBar.o;
17444
+ const percentChange = (priceChange / firstBar.o) * 100;
17445
+ const volumeChange = lastBar.v - firstBar.v;
17446
+ const percentVolumeChange = firstBar.v > 0 ? (volumeChange / firstBar.v) * 100 : 0;
17447
+ const high = Math.max(...bars.map((bar) => bar.h));
17448
+ const low = Math.min(...bars.map((bar) => bar.l));
17449
+ const totalVolume = bars.reduce((sum, bar) => sum + bar.v, 0);
17450
+ const avgVolume = totalVolume / bars.length;
17451
+ return (`Crypto Price: $${firstBar.o.toFixed(6)} -> $${lastBar.c.toFixed(6)} (${percentChange.toFixed(2)}%), ` +
17452
+ `Volume: ${firstBar.v.toLocaleString()} -> ${lastBar.v.toLocaleString()} (${percentVolumeChange.toFixed(2)}%), ` +
17453
+ `High: $${high.toFixed(6)}, Low: $${low.toFixed(6)}, ` +
17454
+ `Avg Volume: ${avgVolume.toLocaleString()}`);
17455
+ }
17205
17456
  }
17206
17457
  // Export the singleton instance
17207
17458
  const marketDataAPI = AlpacaMarketDataAPI.getInstance();
@@ -17962,9 +18213,18 @@ class AlpacaTradingAPI {
17962
18213
  // Get the most recent hourly data point
17963
18214
  const mostRecentHourly = recentHourlyData[recentHourlyData.length - 1];
17964
18215
  const mostRecentIndex = mostRecentHourly.index;
17965
- // Calculate the timestamp for the new daily entry (most recent day + 1 day worth of seconds)
17966
- const oneDayInSeconds = 24 * 60 * 60;
17967
- const newDailyTimestamp = mostRecentHourly.timestamp + oneDayInSeconds;
18216
+ // Calculate the timestamp for the new daily entry.
18217
+ // Alpaca's daily history timestamps are at 00:00:00Z for the calendar day
18218
+ // following the NY trading date. Derive the trading date in NY time from the
18219
+ // most recent intraday timestamp, then set the new daily timestamp to
18220
+ // midnight UTC of the next calendar day.
18221
+ const mostRecentMs = mostRecentHourly.timestamp * 1000; // hourly timestamps are seconds
18222
+ const tradingDateStr = getTradingDate(new Date(mostRecentMs)); // e.g., '2025-09-05' (NY trading date)
18223
+ const [yearStr, monthStr, dayStr] = tradingDateStr.split('-');
18224
+ const year = Number(yearStr);
18225
+ const month = Number(monthStr); // 1-based
18226
+ const day = Number(dayStr);
18227
+ const newDailyTimestamp = Math.floor(Date.UTC(year, month - 1, day + 1, 0, 0, 0, 0) / 1000);
17968
18228
  // Create a new daily history entry with the most recent hourly values
17969
18229
  const updatedDailyHistory = {
17970
18230
  ...dailyHistory,