Spaces:
Sleeping
Sleeping
feat: Establish initial Munger Engine framework with functional requirements, S&P 500 data fetching, and core stock analysis scripts.
f89d1ea | import YahooFinance from 'yahoo-finance2'; | |
| import dotenv from 'dotenv'; | |
| import path from 'path'; | |
| import fs from 'fs'; | |
| dotenv.config({ path: path.resolve(__dirname, '../.env') }); | |
| const yahooFinance = new (YahooFinance as any)(); | |
| // Load Watchlist from JSON | |
| let WATCHLIST: string[] = []; | |
| try { | |
| const jsonPath = path.resolve(__dirname, 'sp500_symbols.json'); | |
| if (fs.existsSync(jsonPath)) { | |
| WATCHLIST = JSON.parse(fs.readFileSync(jsonPath, 'utf-8')); | |
| console.log(`Loaded ${WATCHLIST.length} symbols from S&P 500 list.`); | |
| } else { | |
| console.warn("Warning: sp500_symbols.json not found. Using fallback list."); | |
| // Fallback | |
| WATCHLIST = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'META', 'BRK-B', 'V', 'JNJ', 'WMT', 'PG']; | |
| } | |
| } catch (e) { | |
| console.error("Error loading watchlist:", e); | |
| } | |
| function getLastSunday(): Date { | |
| const d = new Date(); | |
| const day = d.getDay(); | |
| const diff = d.getDate() - day; | |
| const sunday = new Date(d.setDate(diff)); | |
| sunday.setHours(23, 59, 59, 999); | |
| return sunday; | |
| } | |
| interface StrategyResult { | |
| symbol: string; | |
| price: number; | |
| wma200: number; | |
| roe: number; | |
| debtToEquity: number; | |
| isQuality: boolean; | |
| lastCandleColor: 'GREEN' | 'RED'; | |
| touchedWMA: boolean; | |
| signal: 'BUY_TRIGGER' | 'WATCHLIST' | 'IGNORE' | 'WAIT'; | |
| details: string; | |
| } | |
| async function deepDive(symbol: string) { | |
| console.log(`\n🔍 DEEP DIVE ANALYSIS: ${symbol}`); | |
| console.log('================================================'); | |
| try { | |
| const result = await yahooFinance.quoteSummary(symbol, { | |
| modules: [ | |
| 'financialData', | |
| 'defaultKeyStatistics', | |
| 'recommendationTrend', | |
| 'earnings', | |
| 'summaryDetail' | |
| ] | |
| }); | |
| const fin = result.financialData; | |
| const stats = result.defaultKeyStatistics; | |
| const summary = result.summaryDetail; | |
| const recs = result.recommendationTrend?.trend?.[0]; | |
| console.log('💰 VALUATION'); | |
| console.log(` • Current Price: $${fin?.currentPrice}`); | |
| console.log(` • Target Mean: $${fin?.targetMeanPrice}`); | |
| console.log(` • Forward PE: ${summary?.forwardPE?.toFixed(2)}`); | |
| console.log(` • PEG Ratio: ${stats?.pegRatio}`); | |
| console.log(` • Price/Book: ${stats?.priceToBook?.toFixed(2)}`); | |
| console.log('\n🏰 ECONOMIC MOAT (Profitability)'); | |
| console.log(` • Gross Margin: ${(fin?.grossMargins * 100).toFixed(2)}%`); | |
| console.log(` • Oper. Margin: ${(fin?.operatingMargins * 100).toFixed(2)}%`); | |
| console.log(` • ROE: ${(fin?.returnOnEquity * 100).toFixed(2)}%`); | |
| console.log(` • ROA: ${(fin?.returnOnAssets * 100).toFixed(2)}%`); | |
| console.log('\n🛡️ FINANCIAL HEALTH'); | |
| console.log(` • Total Cash: $${(fin?.totalCash / 1e9).toFixed(2)}B`); | |
| console.log(` • Total Debt: $${(fin?.totalDebt / 1e9).toFixed(2)}B`); | |
| console.log(` • Debt/Equity: ${fin?.debtToEquity}%`); | |
| console.log(` • Current Ratio: ${fin?.currentRatio}`); | |
| console.log(` • Free Cash Flow: $${(fin?.freeCashflow / 1e9).toFixed(2)}B`); | |
| console.log('\n🧠 SENTIMENT & GROWTH'); | |
| console.log(` • Analyst Rec: ${fin?.recommendationKey?.toUpperCase()} (Buy: ${recs?.buy}, Hold: ${recs?.hold})`); | |
| console.log(` • Short Float: ${(stats?.shortPercentOfFloat * 100).toFixed(2)}%`); | |
| console.log('================================================\n'); | |
| } catch (e: any) { | |
| console.error("Deep dive failed:", e.message); | |
| } | |
| } | |
| async function analyzeStock(symbol: string, refDate: Date): Promise<StrategyResult | null> { | |
| try { | |
| const queryOptions = { period1: '2019-01-01', period2: refDate, interval: '1wk' as const }; | |
| const history = await yahooFinance.chart(symbol, queryOptions); | |
| const quotes = history.quotes.filter((q: any) => q.close && q.open); | |
| if (quotes.length < 205) return null; | |
| const latestIndex = quotes.length - 1; | |
| const currentPrice = quotes[latestIndex].close as number; | |
| // Simple MA 200 Calculation | |
| const calculateMA200 = (endIndex: number) => { | |
| if (endIndex < 199) return 0; | |
| const slice = quotes.slice(endIndex - 199, endIndex + 1).map((q: any) => q.close as number); | |
| return slice.reduce((a: number, b: number) => a + b, 0) / slice.length; | |
| }; | |
| const MA200 = calculateMA200(latestIndex); | |
| const summary = await yahooFinance.quoteSummary(symbol, { modules: ['financialData'] }); | |
| const roe = summary.financialData?.returnOnEquity || 0; | |
| const debtToEquity = summary.financialData?.debtToEquity || 999; | |
| // Quality Check | |
| const isQuality = roe > 0.15 && debtToEquity < 60; | |
| if (!isQuality) return { | |
| symbol, price: currentPrice, wma200: MA200, roe, debtToEquity, isQuality, | |
| lastCandleColor: 'RED', touchedWMA: false, signal: 'IGNORE', details: 'Low Quality' | |
| }; | |
| // Strategy Logic | |
| let touchedWMA = false; | |
| for (let i = 0; i < 4; i++) { | |
| const idx = latestIndex - i; | |
| const candle = quotes[idx]; | |
| const ma = calculateMA200(idx); | |
| if (candle.low <= ma) touchedWMA = true; | |
| } | |
| const lastCandle = quotes[latestIndex]; | |
| const isGreen = lastCandle.close > lastCandle.open; | |
| let signal: 'BUY_TRIGGER' | 'WATCHLIST' | 'IGNORE' | 'WAIT' = 'WATCHLIST'; | |
| if (touchedWMA) { | |
| if (isGreen) signal = 'BUY_TRIGGER'; | |
| else signal = 'WAIT'; | |
| } else { | |
| const dist = (currentPrice - MA200) / MA200; | |
| if (dist > 0 && dist < 0.05) signal = 'WATCHLIST'; | |
| else if (dist < 0) signal = 'WAIT'; | |
| else signal = 'IGNORE'; | |
| } | |
| return { | |
| symbol, price: currentPrice, wma200: MA200, roe, debtToEquity, isQuality, | |
| lastCandleColor: isGreen ? 'GREEN' : 'RED', touchedWMA, signal, | |
| details: touchedWMA ? (isGreen ? 'Rebound Confirm' : 'Testing Support') : `Dist: ${(((currentPrice - MA200) / MA200) * 100).toFixed(1)}%` | |
| }; | |
| } catch (error: any) { | |
| return null; | |
| } | |
| } | |
| async function main() { | |
| const lastSunday = getLastSunday(); | |
| console.log(`=== MUNGER SCREENER (S&P 500 FULL) ===`); | |
| console.log(`Ref Date: ${lastSunday.toISOString().split('T')[0]}`); | |
| console.log(`Analyzing ${WATCHLIST.length} companies...`); | |
| const results: StrategyResult[] = []; | |
| const buys: string[] = []; | |
| // Chunking to show progress nicely | |
| const chunkSize = 5; // Smaller chunks for large list stability | |
| for (let i = 0; i < WATCHLIST.length; i += chunkSize) { | |
| const chunk = WATCHLIST.slice(i, i + chunkSize); | |
| const promises = chunk.map(symbol => analyzeStock(symbol, lastSunday)); | |
| const chunkResults = await Promise.all(promises); | |
| chunkResults.forEach((res) => { | |
| if (res) { | |
| results.push(res); | |
| process.stdout.write(res.isQuality ? (res.signal === 'BUY_TRIGGER' ? '!' : '+') : '.'); | |
| if (res.signal === 'BUY_TRIGGER') { | |
| buys.push(res.symbol); | |
| } | |
| } else { | |
| process.stdout.write('x'); | |
| } | |
| }); | |
| // Delay to respect API limits | |
| await new Promise(r => setTimeout(r, 200)); | |
| } | |
| console.log('\n\n--- Analysis Complete ---'); | |
| console.log(`Found ${buys.length} Buy Triggers.`); | |
| if (buys.length > 0) { | |
| const uniqueBuys = [...new Set(buys)]; | |
| for (const symbol of uniqueBuys) { | |
| await deepDive(symbol); | |
| } | |
| } else { | |
| console.log("No stocks triggered Deep Dive criteria this week."); | |
| } | |
| } | |
| main(); | |