@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.
@@ -13864,6 +13864,21 @@ function getLastFullTradingDateImpl(currentDate = new Date()) {
13864
13864
  function getLastFullTradingDate(currentDate = new Date()) {
13865
13865
  return getLastFullTradingDateImpl(currentDate);
13866
13866
  }
13867
+ /**
13868
+ * Returns the trading date for a given time. Note: Just trims the date string; does not validate if the date is a market day.
13869
+ * @param time - a string, number (unix timestamp), or Date object representing the time
13870
+ * @returns the trading date as a string in YYYY-MM-DD format
13871
+ */
13872
+ /**
13873
+ * Returns the trading date for a given time in YYYY-MM-DD format (NY time).
13874
+ * @param time - string, number, or Date
13875
+ * @returns trading date string
13876
+ */
13877
+ function getTradingDate(time) {
13878
+ const date = typeof time === 'number' ? new Date(time) : typeof time === 'string' ? new Date(time) : time;
13879
+ const nyDate = toNYTime(date);
13880
+ return `${nyDate.getUTCFullYear()}-${String(nyDate.getUTCMonth() + 1).padStart(2, '0')}-${String(nyDate.getUTCDate()).padStart(2, '0')}`;
13881
+ }
13867
13882
 
13868
13883
  const log = (message, options = { type: 'info' }) => {
13869
13884
  log$1(message, { ...options, source: 'AlpacaMarketDataAPI' });
@@ -13882,6 +13897,7 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
13882
13897
  dataURL;
13883
13898
  apiURL;
13884
13899
  v1beta1url;
13900
+ v1beta3url;
13885
13901
  stockStreamUrl = 'wss://stream.data.alpaca.markets/v2/sip'; // production values
13886
13902
  optionStreamUrl = 'wss://stream.data.alpaca.markets/v1beta3/options'; // production values
13887
13903
  stockWs = null;
@@ -13924,6 +13940,7 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
13924
13940
  ? 'https://paper-api.alpaca.markets/v2'
13925
13941
  : 'https://api.alpaca.markets/v2'; // used by some, e.g. getAssets
13926
13942
  this.v1beta1url = 'https://data.alpaca.markets/v1beta1'; // used for options endpoints
13943
+ this.v1beta3url = 'https://data.alpaca.markets/v1beta3'; // used for crypto endpoints
13927
13944
  this.setMode('production'); // sets stockStreamUrl and optionStreamUrl
13928
13945
  this.headers = {
13929
13946
  'APCA-API-KEY-ID': process.env.ALPACA_API_KEY,
@@ -14061,7 +14078,9 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
14061
14078
  }
14062
14079
  }
14063
14080
  async makeRequest(endpoint, method = 'GET', params, baseUrlName = 'data') {
14064
- const baseUrl = baseUrlName === 'data' ? this.dataURL : baseUrlName === 'api' ? this.apiURL : this.v1beta1url;
14081
+ const baseUrl = baseUrlName === 'data' ? this.dataURL :
14082
+ baseUrlName === 'api' ? this.apiURL :
14083
+ baseUrlName === 'v1beta1' ? this.v1beta1url : this.v1beta3url;
14065
14084
  const url = new URL(`${baseUrl}${endpoint}`);
14066
14085
  try {
14067
14086
  if (params) {
@@ -14769,6 +14788,253 @@ class AlpacaMarketDataAPI extends require$$0$3.EventEmitter {
14769
14788
  }
14770
14789
  return newsArticles;
14771
14790
  }
14791
+ // ===== CRYPTO MARKET DATA METHODS =====
14792
+ /**
14793
+ * Get historical OHLCV bars for crypto symbols
14794
+ * Automatically handles pagination to fetch all available data
14795
+ * @param params Parameters for crypto historical bars request
14796
+ * @returns Historical bars data with all pages combined
14797
+ */
14798
+ async getCryptoHistoricalBars(params) {
14799
+ const symbols = params.symbols;
14800
+ const symbolsStr = symbols.join(',');
14801
+ let allBars = {};
14802
+ let pageToken = null;
14803
+ let hasMorePages = true;
14804
+ let totalBarsCount = 0;
14805
+ let pageCount = 0;
14806
+ // Initialize bar arrays for each symbol
14807
+ symbols.forEach((symbol) => {
14808
+ allBars[symbol] = [];
14809
+ });
14810
+ log(`Starting crypto historical bars fetch for ${symbols.length} symbols (${params.timeframe}, ${params.start || 'no start'} to ${params.end || 'no end'})`, { type: 'info' });
14811
+ while (hasMorePages) {
14812
+ pageCount++;
14813
+ const requestParams = {
14814
+ ...params,
14815
+ symbols: symbolsStr,
14816
+ ...(pageToken && { page_token: pageToken }),
14817
+ };
14818
+ const response = await this.makeRequest('/crypto/us/bars', 'GET', requestParams, 'v1beta3');
14819
+ if (!response.bars) {
14820
+ log(`No crypto bars data found in response for ${symbols.length} symbols`, { type: 'warn' });
14821
+ break;
14822
+ }
14823
+ // Combine bars for each symbol
14824
+ let pageBarsCount = 0;
14825
+ Object.entries(response.bars).forEach(([symbol, bars]) => {
14826
+ if (bars && bars.length > 0) {
14827
+ allBars[symbol] = [...allBars[symbol], ...bars];
14828
+ pageBarsCount += bars.length;
14829
+ }
14830
+ });
14831
+ totalBarsCount += pageBarsCount;
14832
+ pageToken = response.next_page_token || null;
14833
+ hasMorePages = !!pageToken;
14834
+ log(`Page ${pageCount}: Fetched ${pageBarsCount.toLocaleString()} crypto bars (total: ${totalBarsCount.toLocaleString()}) for ${symbols.length} symbols${hasMorePages ? ', more pages available' : ', complete'}`);
14835
+ // Prevent infinite loops
14836
+ if (pageCount > 1000) {
14837
+ log(`Stopping crypto bars pagination after ${pageCount} pages to prevent infinite loop`, { type: 'warn' });
14838
+ break;
14839
+ }
14840
+ }
14841
+ log(`Crypto historical bars fetch complete: ${totalBarsCount.toLocaleString()} total bars across ${pageCount} pages`, { type: 'info' });
14842
+ return {
14843
+ bars: allBars,
14844
+ next_page_token: null, // Always null since we fetch all pages
14845
+ };
14846
+ }
14847
+ /**
14848
+ * Get the most recent minute bar for requested crypto symbols
14849
+ * @param symbols Array of crypto symbols to query
14850
+ * @returns Latest bar data for each symbol
14851
+ */
14852
+ async getCryptoLatestBars(symbols) {
14853
+ return this.makeRequest('/crypto/us/latest/bars', 'GET', { symbols: symbols.join(',') }, 'v1beta3');
14854
+ }
14855
+ /**
14856
+ * Get historical quotes for crypto symbols
14857
+ * Automatically handles pagination to fetch all available data
14858
+ * @param params Parameters for crypto historical quotes request
14859
+ * @returns Historical quotes data with all pages combined
14860
+ */
14861
+ async getCryptoHistoricalQuotes(params) {
14862
+ const symbols = params.symbols;
14863
+ const symbolsStr = symbols.join(',');
14864
+ let allQuotes = {};
14865
+ let pageToken = null;
14866
+ let hasMorePages = true;
14867
+ let totalQuotesCount = 0;
14868
+ let pageCount = 0;
14869
+ // Initialize quotes arrays for each symbol
14870
+ symbols.forEach((symbol) => {
14871
+ allQuotes[symbol] = [];
14872
+ });
14873
+ log(`Starting crypto historical quotes fetch for ${symbols.length} symbols (${params.start || 'no start'} to ${params.end || 'no end'})`, { type: 'info' });
14874
+ while (hasMorePages) {
14875
+ pageCount++;
14876
+ const requestParams = {
14877
+ ...params,
14878
+ symbols: symbolsStr,
14879
+ ...(pageToken && { page_token: pageToken }),
14880
+ };
14881
+ const response = await this.makeRequest('/crypto/us/quotes', 'GET', requestParams, 'v1beta3');
14882
+ if (!response.quotes) {
14883
+ log(`No crypto quotes data found in response for ${symbols.length} symbols`, { type: 'warn' });
14884
+ break;
14885
+ }
14886
+ // Combine quotes for each symbol
14887
+ let pageQuotesCount = 0;
14888
+ Object.entries(response.quotes).forEach(([symbol, quotes]) => {
14889
+ if (quotes && quotes.length > 0) {
14890
+ allQuotes[symbol] = [...allQuotes[symbol], ...quotes];
14891
+ pageQuotesCount += quotes.length;
14892
+ }
14893
+ });
14894
+ totalQuotesCount += pageQuotesCount;
14895
+ pageToken = response.next_page_token || null;
14896
+ hasMorePages = !!pageToken;
14897
+ log(`Page ${pageCount}: Fetched ${pageQuotesCount.toLocaleString()} crypto quotes (total: ${totalQuotesCount.toLocaleString()}) for ${symbols.length} symbols${hasMorePages ? ', more pages available' : ', complete'}`);
14898
+ // Prevent infinite loops
14899
+ if (pageCount > 1000) {
14900
+ log(`Stopping crypto quotes pagination after ${pageCount} pages to prevent infinite loop`, { type: 'warn' });
14901
+ break;
14902
+ }
14903
+ }
14904
+ log(`Crypto historical quotes fetch complete: ${totalQuotesCount.toLocaleString()} total quotes across ${pageCount} pages`, { type: 'info' });
14905
+ return {
14906
+ quotes: allQuotes,
14907
+ next_page_token: null, // Always null since we fetch all pages
14908
+ };
14909
+ }
14910
+ /**
14911
+ * Get the most recent quotes for requested crypto symbols
14912
+ * @param symbols Array of crypto symbols to query
14913
+ * @returns Latest quote data for each symbol
14914
+ */
14915
+ async getCryptoLatestQuotes(symbols) {
14916
+ if (!symbols || symbols.length === 0) {
14917
+ log('No symbols provided to getCryptoLatestQuotes, returning empty response', { type: 'warn' });
14918
+ return { quotes: {} };
14919
+ }
14920
+ return this.makeRequest('/crypto/us/latest/quotes', 'GET', { symbols: symbols.join(',') }, 'v1beta3');
14921
+ }
14922
+ /**
14923
+ * Get historical trades for crypto symbols
14924
+ * Automatically handles pagination to fetch all available data
14925
+ * @param params Parameters for crypto historical trades request
14926
+ * @returns Historical trades data with all pages combined
14927
+ */
14928
+ async getCryptoHistoricalTrades(params) {
14929
+ const symbols = params.symbols;
14930
+ const symbolsStr = symbols.join(',');
14931
+ let allTrades = {};
14932
+ let pageToken = null;
14933
+ let hasMorePages = true;
14934
+ let totalTradesCount = 0;
14935
+ let pageCount = 0;
14936
+ // Initialize trades arrays for each symbol
14937
+ symbols.forEach((symbol) => {
14938
+ allTrades[symbol] = [];
14939
+ });
14940
+ log(`Starting crypto historical trades fetch for ${symbols.length} symbols (${params.start || 'no start'} to ${params.end || 'no end'})`, { type: 'info' });
14941
+ while (hasMorePages) {
14942
+ pageCount++;
14943
+ const requestParams = {
14944
+ ...params,
14945
+ symbols: symbolsStr,
14946
+ ...(pageToken && { page_token: pageToken }),
14947
+ };
14948
+ const response = await this.makeRequest('/crypto/us/trades', 'GET', requestParams, 'v1beta3');
14949
+ if (!response.trades) {
14950
+ log(`No crypto trades data found in response for ${symbols.length} symbols`, { type: 'warn' });
14951
+ break;
14952
+ }
14953
+ // Combine trades for each symbol
14954
+ let pageTradesCount = 0;
14955
+ Object.entries(response.trades).forEach(([symbol, trades]) => {
14956
+ if (trades && trades.length > 0) {
14957
+ allTrades[symbol] = [...allTrades[symbol], ...trades];
14958
+ pageTradesCount += trades.length;
14959
+ }
14960
+ });
14961
+ totalTradesCount += pageTradesCount;
14962
+ pageToken = response.next_page_token || null;
14963
+ hasMorePages = !!pageToken;
14964
+ log(`Page ${pageCount}: Fetched ${pageTradesCount.toLocaleString()} crypto trades (total: ${totalTradesCount.toLocaleString()}) for ${symbols.length} symbols${hasMorePages ? ', more pages available' : ', complete'}`);
14965
+ // Prevent infinite loops
14966
+ if (pageCount > 1000) {
14967
+ log(`Stopping crypto trades pagination after ${pageCount} pages to prevent infinite loop`, { type: 'warn' });
14968
+ break;
14969
+ }
14970
+ }
14971
+ log(`Crypto historical trades fetch complete: ${totalTradesCount.toLocaleString()} total trades across ${pageCount} pages`, { type: 'info' });
14972
+ return {
14973
+ trades: allTrades,
14974
+ next_page_token: null, // Always null since we fetch all pages
14975
+ };
14976
+ }
14977
+ /**
14978
+ * Get the most recent trades for requested crypto symbols
14979
+ * @param symbols Array of crypto symbols to query
14980
+ * @returns Latest trade data for each symbol
14981
+ */
14982
+ async getCryptoLatestTrades(symbols) {
14983
+ if (!symbols || symbols.length === 0) {
14984
+ log('No symbols provided to getCryptoLatestTrades, returning empty response', { type: 'warn' });
14985
+ return { trades: {} };
14986
+ }
14987
+ return this.makeRequest('/crypto/us/latest/trades', 'GET', { symbols: symbols.join(',') }, 'v1beta3');
14988
+ }
14989
+ /**
14990
+ * Get snapshots for crypto symbols
14991
+ * Returns the latest trade, latest quote, latest minute bar, latest daily bar, and previous daily bar data
14992
+ * @param symbols Array of crypto symbols to query
14993
+ * @returns Snapshot data for each symbol
14994
+ */
14995
+ async getCryptoSnapshots(symbols) {
14996
+ if (!symbols || symbols.length === 0) {
14997
+ log('No symbols provided to getCryptoSnapshots, returning empty response', { type: 'warn' });
14998
+ return { snapshots: {} };
14999
+ }
15000
+ return this.makeRequest('/crypto/us/snapshots', 'GET', { symbols: symbols.join(',') }, 'v1beta3');
15001
+ }
15002
+ /**
15003
+ * Get the latest orderbook for requested crypto symbols
15004
+ * @param symbols Array of crypto symbols to query
15005
+ * @returns Latest orderbook data for each symbol
15006
+ */
15007
+ async getCryptoLatestOrderbooks(symbols) {
15008
+ if (!symbols || symbols.length === 0) {
15009
+ log('No symbols provided to getCryptoLatestOrderbooks, returning empty response', { type: 'warn' });
15010
+ return { orderbooks: {} };
15011
+ }
15012
+ return this.makeRequest('/crypto/us/latest/orderbooks', 'GET', { symbols: symbols.join(',') }, 'v1beta3');
15013
+ }
15014
+ /**
15015
+ * Analyzes an array of crypto bars and returns a summary string
15016
+ * @param bars Array of crypto bars to analyze
15017
+ * @returns A string summarizing the crypto price data
15018
+ */
15019
+ static analyzeCryptoBars(bars) {
15020
+ if (!bars || bars.length === 0) {
15021
+ return 'No crypto price data available';
15022
+ }
15023
+ const firstBar = bars[0];
15024
+ const lastBar = bars[bars.length - 1];
15025
+ const priceChange = lastBar.c - firstBar.o;
15026
+ const percentChange = (priceChange / firstBar.o) * 100;
15027
+ const volumeChange = lastBar.v - firstBar.v;
15028
+ const percentVolumeChange = firstBar.v > 0 ? (volumeChange / firstBar.v) * 100 : 0;
15029
+ const high = Math.max(...bars.map((bar) => bar.h));
15030
+ const low = Math.min(...bars.map((bar) => bar.l));
15031
+ const totalVolume = bars.reduce((sum, bar) => sum + bar.v, 0);
15032
+ const avgVolume = totalVolume / bars.length;
15033
+ return (`Crypto Price: $${firstBar.o.toFixed(6)} -> $${lastBar.c.toFixed(6)} (${percentChange.toFixed(2)}%), ` +
15034
+ `Volume: ${firstBar.v.toLocaleString()} -> ${lastBar.v.toLocaleString()} (${percentVolumeChange.toFixed(2)}%), ` +
15035
+ `High: $${high.toFixed(6)}, Low: $${low.toFixed(6)}, ` +
15036
+ `Avg Volume: ${avgVolume.toLocaleString()}`);
15037
+ }
14772
15038
  }
14773
15039
  // Export the singleton instance
14774
15040
  const marketDataAPI = AlpacaMarketDataAPI.getInstance();
@@ -15529,9 +15795,18 @@ class AlpacaTradingAPI {
15529
15795
  // Get the most recent hourly data point
15530
15796
  const mostRecentHourly = recentHourlyData[recentHourlyData.length - 1];
15531
15797
  const mostRecentIndex = mostRecentHourly.index;
15532
- // Calculate the timestamp for the new daily entry (most recent day + 1 day worth of seconds)
15533
- const oneDayInSeconds = 24 * 60 * 60;
15534
- const newDailyTimestamp = mostRecentHourly.timestamp + oneDayInSeconds;
15798
+ // Calculate the timestamp for the new daily entry.
15799
+ // Alpaca's daily history timestamps are at 00:00:00Z for the calendar day
15800
+ // following the NY trading date. Derive the trading date in NY time from the
15801
+ // most recent intraday timestamp, then set the new daily timestamp to
15802
+ // midnight UTC of the next calendar day.
15803
+ const mostRecentMs = mostRecentHourly.timestamp * 1000; // hourly timestamps are seconds
15804
+ const tradingDateStr = getTradingDate(new Date(mostRecentMs)); // e.g., '2025-09-05' (NY trading date)
15805
+ const [yearStr, monthStr, dayStr] = tradingDateStr.split('-');
15806
+ const year = Number(yearStr);
15807
+ const month = Number(monthStr); // 1-based
15808
+ const day = Number(dayStr);
15809
+ const newDailyTimestamp = Math.floor(Date.UTC(year, month - 1, day + 1, 0, 0, 0, 0) / 1000);
15535
15810
  // Create a new daily history entry with the most recent hourly values
15536
15811
  const updatedDailyHistory = {
15537
15812
  ...dailyHistory,