munger-engine / scripts /find_forever_stocks.ts
dromero-nttd's picture
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();