const { parse } = require('node:path'); const { default: test } = require('node:test'); fs = require('node:fs'); // this gets the HTML page of the SP500 list from slickcharts.com const getSPHTML = async () => { const response = await fetch('https://www.slickcharts.com/sp500'); if (!response.ok) { throw new Error('Network response was not ok'); } const text = await response.text(); return text; } // this parses the HTML of the SP500 list to tickers const parseHTMLToTickers = (html) => { // get the tickers by slicing on all `/symbol/` occurrences // (with the special format before it to only get one occurance) const tickers = []; html.split('nowrap;">')[1].split('')[0]; // get weight of item using the % let weight = item.search(/([\d.]+)%<\/td>/); if (weight === -1) { console.warn(`Ticker ${ticker} does not have a valid weight, skipping.`); return; } weight = parseFloat(item.slice(weight + 4, item.indexOf('', weight))); if (ticker && name && weight) { tickers.push({ name, symbol: ticker, weight }); } }); // update the tickers file with the new tickers if (fs.existsSync('sp500_tickers.json')) { fs.unlinkSync('sp500_tickers.json'); } fs.writeFileSync('sp500_tickers.json', JSON.stringify(tickers, null, 2)); console.log(`Saved ${tickers.length} tickers to sp500_tickers.json`); return tickers; } const getTickersFromFile = () => { if (!fs.existsSync('sp500_tickers.json')) { console.error('sp500_tickers.json file does not exist. Please run the script to fetch tickers first.'); return []; } const data = fs.readFileSync('sp500_tickers.json', 'utf8'); return JSON.parse(data); } // get the JSON history data from the symbol const getSymbolHistory = async (symbol) => { // clean the symbol (reaplce . with dash) symbol = symbol.replace(/\./g, '-'); const parameters = 'interval=1d&includePrePost=true&events=div%7Csplit%7Cearn&lang=en-US®ion=US&range=6mo'; const response = await fetch(`https://query1.finance.yahoo.com/v8/finance/chart/${symbol}?${parameters}`); if (!response.ok) { console.error(`Network response was not ok for symbol: ${symbol}. Status: ${response.status}`); return {}; } const data = await response.json(); return data; } const getSectorMap = () => { // pull from the sector map tsv file if (!fs.existsSync('sector-map.tsv')) { console.error('sector-map.tsv file does not exist. Please run the script to fetch sectors first.'); return 'Unknown'; } const sectorMap = fs.readFileSync('sector-map.tsv', 'utf8'); const lines = sectorMap.split('\n'); const sectorMapObj = {}; lines.forEach((line, index) => { if (index === 0) return; // skip the header line // split the line by comma and get the name, ticker, sector, and subSector const [name, ticker, sector, subSector] = line.split('\t'); sectorMapObj[ticker.trim()] = [sector.trim(), subSector.trim(), name.trim()]; }); return sectorMapObj; } const getHistoriesForEachTicker = async (tickers) => { // use Promise.all to fetch all histories concurrently const histories = await Promise.all(tickers.map(ticker => getSymbolHistory(ticker.symbol))); // zip the histories with the tickers const zippedHistories = histories.map((history, index) => ({ ...tickers[index], history })); return zippedHistories; } // test getting the histories for all the symbols const testGetHistories = async () => { try { // const html = await getSPHTML(); // const tickers = parseHTMLToTickers(html); // console.log('SP500 Tickers:', tickers); // get tickers from file const tickers = getTickersFromFile(); if (tickers.length === 0) { console.error('No tickers found. Please ensure sp500_tickers.json exists and is populated.'); return; } console.log(`Found ${tickers.length} tickers in sp500_tickers.json`); // get histories for each symbol const histories = await getHistoriesForEachTicker(tickers); console.log('Histories fetched for all symbols:', histories.length); // save histories to a file if (fs.existsSync('sp500_histories.json')) { fs.unlinkSync('sp500_histories.json'); } fs.writeFileSync('sp500_histories.json', JSON.stringify(histories, null, 2)); console.log(`Saved ${histories.length} histories to sp500_histories.json`); // print first 5 and last 5 histories console.log('First 5 histories:'); histories.slice(0, 5).forEach(h => console.log(h.symbol, h.history)); // console.log('Last 5 histories:', histories.slice(-5)); } catch (error) { console.error('Error fetching SP500 histories:', error); } }; const formatDataFromHistories = (histories) => { // format the data from the histories to a more readable format const csv_headers = ['Ticker', 'Name', '% Weight', 'Sector', 'SubSector', 'RSI (14)', 'MACD (Histogram Value)', '1W', '1M', '3M', '6M']; const csv_final = [csv_headers]; // get the sector map const sectorMap = getSectorMap(); histories.forEach(history => { // Tickern, name weight, sector from html pull const { symbol, webName, weight } = history; const sector = sectorMap[symbol] ? sectorMap[symbol][0] : 'Unknown'; const subSector = sectorMap[symbol] ? sectorMap[symbol][1] : 'Unknown'; const name = sectorMap[symbol] ? sectorMap[symbol][2] : webName || 'Unknown'; // Get RSI, MACD from helper const timestamps = history.history.chart.result[0].timestamp; const prices = history.history.chart.result[0].indicators.quote[0].close; const rsi = calculateRSI(prices); const macd = calculateMACD(prices); // print first 5 timestamps and prices for debugging console.log('First 5 timestamps:', timestamps.slice(0, 5).map(ts => new Date(ts * 1000).toLocaleDateString())); console.log('First 5 prices:', prices.slice(0, 5)); const currentPrice = prices[prices.length - 1]; // Directly calculate the percentage changes for 1W, 1M, 3M, and 6M\ const oneWeekAgoPrice = prices[prices.length - 6]; // 5 days of trading const oneWeekChange = ((currentPrice - oneWeekAgoPrice) / oneWeekAgoPrice) * 100; const oneMonthAgoPrice = prices[prices.length - 21]; // 20 days of trading (4 weeks) const oneMonthChange = ((currentPrice - oneMonthAgoPrice) / oneMonthAgoPrice) * 100; const threeMonthsAgoPrice = prices[parseInt(prices.length / 2) - 1]; // 3 months is half the length of the prices array const threeMonthChange = ((currentPrice - threeMonthsAgoPrice) / threeMonthsAgoPrice) * 100; const sixMonthsAgoPrice = prices[0]; // last 6 months is the first price in the array const sixMonthChange = ((currentPrice - sixMonthsAgoPrice) / sixMonthsAgoPrice) * 100; const mappedValues = { Ticker: symbol, Name: name, '% Weight': weight, 'Sector': sector, 'Subsector': subSector, 'RSI (14)': rsi.toFixed(3), 'MACD (Histogram Value)': macd.toFixed(3), '1W': oneWeekChange.toFixed(3) + '%', '1M': oneMonthChange.toFixed(3) + '%', '3M': threeMonthChange.toFixed(3) + '%', '6M': sixMonthChange.toFixed(3) + '%' }; // pushed the mapped values to the formatted data csv_final.push(Object.values(mappedValues)); }); // write the formatted data to a CSV file const csvContent = csv_final.map(e => e.join('\t')).join('\n'); if (fs.existsSync('sp500_formatted_data.tsv')) { fs.unlinkSync('sp500_formatted_data.tsv'); } fs.writeFileSync('sp500_formatted_data.tsv', csvContent); console.log('Formatted data saved to sp500_formatted_data.tsv'); return csv_final; }; // testGetHistories(); const calculateMACD = (prices, shortPeriod = 12, longPeriod = 26, signalPeriod = 9) => { // Helper function to calculate the Exponential Moving Average (EMA) const exponentialMovingAverage = (data, period) => { const k = 2 / (period + 1); let ema = [data[0]]; // Start with the first price as the initial EMA for (let i = 1; i < data.length; i++) { const currentEma = (data[i] * k) + (ema[i - 1] * (1 - k)); ema.push(currentEma); } return ema; } // Calculate the short and long periods const ema12 = exponentialMovingAverage(prices, shortPeriod); const ema26 = exponentialMovingAverage(prices, longPeriod); // Calcualte the MACD line const macdLine = ema12.map((value, index) => value - ema26[index]) // Calculate the signal line const signalLine = exponentialMovingAverage(macdLine, signalPeriod); // Calculate the MACD histogram const macdHistogram = macdLine.map((value, index) => value - signalLine[index]); // Return the last value of the MACD histogram return macdHistogram[macdHistogram.length - 1]; } const calculateRSI = (prices, period = 14) => { // calculate the first RSI within our period let gains = []; let losses = []; for (let i = 0; i < period; i++) { const change = prices[i + 1] - prices[i]; if (change > 0) { gains.push(change); losses.push(0); } else { losses.push(Math.abs(change)); gains.push(0); } } const averageGain = gains.reduce((a, b) => a + b, 0) / period; const averageLoss = losses.reduce((a, b) => a + b, 0) / period; if (averageLoss === 0) { console.log('No losses in the period, RSI is 100'); return 100; // RSI is 100 if there are no losses } const firstRSI = 100 - (100 / (1 + (averageGain / averageLoss))); if (isNaN(firstRSI)) { console.error('Calculated RSI is NaN, returning 0'); return 0; // Return 0 if RSI calculation fails } const RSIs = [firstRSI]; console.log(`Initial RSI for the first ${period} data points: ${firstRSI}`); // Calculate the RSI for the rest of the data points let previousAverageGain = averageGain; let previousAverageLoss = averageLoss; for (let i = period; i < prices.length - 1; i++) { const change = prices[i + 1] - prices[i]; let gain = 0; let loss = 0; if (change > 0) { gain = change; } else { loss = Math.abs(change); } // Calculate the new average gain and loss previousAverageGain = (previousAverageGain * (period - 1) + gain) / period; previousAverageLoss = (previousAverageLoss * (period - 1) + loss) / period; if (previousAverageLoss === 0) { console.log('No losses in the period, RSI is 100'); return 100; // RSI is 100 if there are no losses } // add this RSI to the list const rsi = 100 - (100 / (1 + (previousAverageGain / previousAverageLoss))); RSIs.push(rsi); } // Return the last calculated RSI return RSIs[RSIs.length - 1]; } // test rsi calculation on one stock const testRSI = async () => { const symbol = 'NVDA'; const history = await getSymbolHistory(symbol); if (!history.chart || !history.chart.result || history.chart.result.length === 0) { console.error(`No data found for symbol: ${symbol}`); return; } const prices = history.chart.result[0].indicators.quote[0].close; const timestamps = history.chart.result[0].timestamp; // print the last 10 prices and dates const rsi = calculateRSI(prices); console.log(`RSI for ${symbol}:`, rsi); } const testGetSector = async () => { // pull for each ticker in the sp500_tickers.json file const tickers = getTickersFromFile(); if (tickers.length === 0) { console.error('No tickers found. Please ensure sp500_tickers.json exists and is populated.'); return; } // get the sector map const sectorMap = getSectorMap(); tickers.forEach(async (ticker) => { const sector = sectorMap[ticker.symbol] ? sectorMap[ticker.symbol][0] : 'Unknown'; console.log(`Ticker: ${ticker.symbol}, Sector: ${sector}`); }); } const main = async () => { try { // gt the test histories from the file // const histories = fs.readFileSync('sp500_histories.json', 'utf8'); // const parsedHistories = JSON.parse(histories); // console.log(`Loaded ${parsedHistories.length} histories from sp500_histories.json`); // get tickers from file const tickers = getTickersFromFile(); if (tickers.length === 0) { console.error('No tickers found. Please ensure sp500_tickers.json exists and is populated.'); return; } console.log(`Found ${tickers.length} tickers in sp500_tickers.json`); // get histories for each symbol const parsedHistories = await getHistoriesForEachTicker(tickers); console.log(`Fetched histories for ${parsedHistories.length} symbols.`); // format the data from the histories const formattedData = formatDataFromHistories(parsedHistories); console.log('Formatted data:', formattedData.slice(0, 5)); // Print first 5 entries for brevity } catch (error) { console.error('Error in main function:', error); } } main();