aboutsummaryrefslogtreecommitdiff
path: root/screener.js
diff options
context:
space:
mode:
Diffstat (limited to 'screener.js')
-rw-r--r--screener.js320
1 files changed, 320 insertions, 0 deletions
diff --git a/screener.js b/screener.js
new file mode 100644
index 0000000..12567c2
--- /dev/null
+++ b/screener.js
@@ -0,0 +1,320 @@
+const { parse } = require('node:path');
+
+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;"><a href="/symbol/').slice(1).forEach(item => {
+ // get the ticker from the item (before the next ")"
+ const ticker = item.split('"')[0];
+ const name = item.split('">')[1].split('</a>')[0];
+
+ // get weight of item using the %
+ let weight = item.search(/<td>([\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('</td>', 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&region=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 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', 'RSI (14)', 'MACD (Histogram Value)', '1W', '1M', '3M', '6M'];
+
+ const csv_final = [csv_headers];
+ const formattedData = histories.forEach(history => {
+
+ // Tickern, name weight, sector from html pull
+ const { symbol, name, weight } = history;
+ const sector = 'TODO';
+
+ // 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]; // 7 days ago
+ const oneWeekChange = ((currentPrice - oneWeekAgoPrice) / oneWeekAgoPrice) * 100;
+
+ const oneMonthAgoPrice = prices[prices.length - 21]; // 21 days ago (trading days in a month)
+ const oneMonthChange = ((currentPrice - oneMonthAgoPrice) / oneMonthAgoPrice) * 100;
+
+ const threeMonthsAgoPrice = prices[prices.length - 63]; // 63 days ago (trading days in 3 months)
+ 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,
+ 'RSI (14)': rsi,
+ 'MACD (Histogram Value)': macd,
+ '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(',')).join('\n');
+ if (fs.existsSync('sp500_formatted_data.csv')) {
+ fs.unlinkSync('sp500_formatted_data.csv');
+ }
+ fs.writeFileSync('sp500_formatted_data.csv', csvContent);
+ console.log('Formatted data saved to sp500_formatted_data.csv');
+ return csv_final;
+};
+// testGetHistories();
+
+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);
+ }
+}
+
+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);
+}
+
+main();
+