#!/usr/bin/env python3 """Deep dive analysis on BAC, CFG, FITB, INCY + expanded GARP scan.""" import yfinance as yf import json import datetime import numpy as np from pathlib import Path TARGETS = ["BAC", "CFG", "FITB", "INCY"] def safe_get(d, *keys, default=None): for k in keys: if isinstance(d, dict): d = d.get(k, default) else: return default return d def analyze_stock(ticker_str): print(f"\n{'='*60}\nAnalyzing {ticker_str}...") t = yf.Ticker(ticker_str) info = t.info or {} result = {"ticker": ticker_str, "name": info.get("longName", ticker_str)} # 1. Price action (1yr) hist = t.history(period="1y") if not hist.empty: prices = hist["Close"] result["price_action"] = { "current": round(float(prices.iloc[-1]), 2), "1yr_ago": round(float(prices.iloc[0]), 2), "1yr_return_pct": round(float((prices.iloc[-1] / prices.iloc[0] - 1) * 100), 2), "52wk_high": round(float(prices.max()), 2), "52wk_low": round(float(prices.min()), 2), "pct_from_52wk_high": round(float((prices.iloc[-1] / prices.max() - 1) * 100), 2), "50d_ma": round(float(prices.tail(50).mean()), 2), "200d_ma": round(float(prices.tail(200).mean()), 2) if len(prices) >= 200 else None, } cur = float(prices.iloc[-1]) ma50 = result["price_action"]["50d_ma"] ma200 = result["price_action"]["200d_ma"] result["price_action"]["above_50d_ma"] = cur > ma50 result["price_action"]["above_200d_ma"] = cur > ma200 if ma200 else None # 2. Insider activity try: insiders = t.insider_transactions if insiders is not None and not insiders.empty: recent = insiders.head(10) txns = [] for _, row in recent.iterrows(): txns.append({ "date": str(row.get("Start Date", "")), "insider": str(row.get("Insider", "")), "transaction": str(row.get("Transaction", "")), "shares": str(row.get("Shares", "")), "value": str(row.get("Value", "")), }) result["insider_transactions"] = txns else: result["insider_transactions"] = [] except: result["insider_transactions"] = [] # 3. Institutional ownership try: inst = t.institutional_holders if inst is not None and not inst.empty: top5 = [] for _, row in inst.head(5).iterrows(): top5.append({ "holder": str(row.get("Holder", "")), "shares": int(row.get("Shares", 0)) if row.get("Shares") else 0, "pct_held": str(row.get("pctHeld", "")), }) result["top_institutional_holders"] = top5 result["institutional_pct"] = info.get("heldPercentInstitutions") except: result["top_institutional_holders"] = [] # 4. Earnings surprises try: earn = t.earnings_dates if earn is not None and not earn.empty: surprises = [] for idx, row in earn.head(8).iterrows(): s = { "date": str(idx), "eps_estimate": row.get("EPS Estimate"), "eps_actual": row.get("Reported EPS"), "surprise_pct": row.get("Surprise(%)"), } # clean NaN s = {k: (None if isinstance(v, float) and np.isnan(v) else v) for k, v in s.items()} surprises.append(s) result["earnings"] = surprises else: result["earnings"] = [] except: result["earnings"] = [] # 5. Analyst consensus result["analyst"] = { "target_mean": info.get("targetMeanPrice"), "target_low": info.get("targetLowPrice"), "target_high": info.get("targetHighPrice"), "recommendation": info.get("recommendationKey"), "num_analysts": info.get("numberOfAnalystOpinions"), } if result["price_action"].get("current") and info.get("targetMeanPrice"): upside = (info["targetMeanPrice"] / result["price_action"]["current"] - 1) * 100 result["analyst"]["upside_pct"] = round(upside, 2) # 6. Key fundamentals snapshot result["fundamentals"] = { "trailing_pe": info.get("trailingPE"), "forward_pe": info.get("forwardPE"), "peg_ratio": info.get("pegRatio"), "market_cap": info.get("marketCap"), "revenue_growth": info.get("revenueGrowth"), "earnings_growth": info.get("earningsGrowth"), "roe": info.get("returnOnEquity"), "debt_to_equity": info.get("debtToEquity"), "dividend_yield": info.get("dividendYield"), "beta": info.get("beta"), } # 7. Technical - RSI approximation if not hist.empty and len(hist) >= 14: delta = hist["Close"].diff() gain = delta.where(delta > 0, 0).rolling(14).mean() loss = (-delta.where(delta < 0, 0)).rolling(14).mean() rs = gain / loss rsi = 100 - (100 / (1 + rs)) result["technical"] = { "rsi_14": round(float(rsi.iloc[-1]), 2) if not np.isnan(rsi.iloc[-1]) else None, "trend": "bullish" if cur > ma50 and (ma200 is None or cur > ma200) else "bearish" if cur < ma50 and (ma200 and cur < ma200) else "mixed", } print(f" Done: {result['name']} @ ${result['price_action']['current']}") return result # ============================================================ # EXPANDED GARP SCAN # ============================================================ def get_broad_ticker_list(): """Get a broad list of US tickers to screen.""" import urllib.request # Use Wikipedia's S&P 500 + S&P 400 midcap for broader coverage # Plus some known Russell 1000 members not in S&P 500 # Start with S&P 500 sp500 = [] try: import pandas as pd tables = pd.read_html("https://en.wikipedia.org/wiki/List_of_S%26P_500_companies") sp500 = tables[0]["Symbol"].str.replace(".", "-", regex=False).tolist() except: pass # S&P 400 midcap sp400 = [] try: import pandas as pd tables = pd.read_html("https://en.wikipedia.org/wiki/List_of_S%26P_400_companies") sp400 = tables[0]["Symbol"].str.replace(".", "-", regex=False).tolist() except: pass all_tickers = list(set(sp500 + sp400)) print(f"Total tickers to screen: {len(all_tickers)}") return all_tickers def garp_screen(tickers, exclude=None): """Apply GARP filters to ticker list.""" exclude = set(exclude or []) passed = [] for i, tick in enumerate(tickers): if tick in exclude: continue if i % 50 == 0: print(f" Screening {i}/{len(tickers)}...") try: t = yf.Ticker(tick) info = t.info or {} mc = info.get("marketCap", 0) or 0 if mc < 5e9: continue tpe = info.get("trailingPE") if tpe is None or tpe >= 25 or tpe <= 0: continue fpe = info.get("forwardPE") if fpe is None or fpe >= 15 or fpe <= 0: continue rg = info.get("revenueGrowth") if rg is None or rg < 0.10: continue eg = info.get("earningsGrowth") if eg is None or eg < 0.15: continue roe = info.get("returnOnEquity") if roe is None or roe < 0.05: continue # Optional filters peg = info.get("pegRatio") if peg is not None and peg > 1.2: continue dte = info.get("debtToEquity") if dte is not None and dte > 35: continue passed.append({ "ticker": tick, "name": info.get("longName", tick), "market_cap": mc, "trailing_pe": round(tpe, 2), "forward_pe": round(fpe, 2), "peg": round(peg, 2) if peg else None, "revenue_growth": round(rg * 100, 1), "earnings_growth": round(eg * 100, 1), "roe": round(roe * 100, 1), "debt_to_equity": round(dte, 1) if dte else None, }) print(f" ✅ PASS: {tick} (PE:{tpe:.1f} FPE:{fpe:.1f} RG:{rg*100:.0f}% EG:{eg*100:.0f}%)") except Exception as e: continue return passed # ============================================================ # MAIN # ============================================================ if __name__ == "__main__": output_dir = Path("/home/wdjones/.openclaw/workspace/data") output_dir.mkdir(exist_ok=True) # Deep dive on 4 stocks print("=" * 60) print("DEEP DIVE ANALYSIS") print("=" * 60) analyses = {} for tick in TARGETS: analyses[tick] = analyze_stock(tick) # Save deep dive with open(output_dir / "stock-analysis-deep-dive.json", "w") as f: json.dump(analyses, f, indent=2, default=str) print(f"\nSaved deep dive to stock-analysis-deep-dive.json") # Expanded GARP scan print("\n" + "=" * 60) print("EXPANDED GARP SCAN (S&P 500 + S&P 400 MidCap)") print("=" * 60) tickers = get_broad_ticker_list() new_passes = garp_screen(tickers, exclude=set(TARGETS)) with open(output_dir / "garp-expanded-scan.json", "w") as f: json.dump(new_passes, f, indent=2, default=str) print(f"\nExpanded scan found {len(new_passes)} additional stocks") print("Saved to garp-expanded-scan.json") # Print summary print("\n" + "=" * 60) print("SUMMARY") print("=" * 60) for tick, data in analyses.items(): pa = data.get("price_action", {}) an = data.get("analyst", {}) fu = data.get("fundamentals", {}) te = data.get("technical", {}) print(f"\n{tick} - {data['name']}") print(f" Price: ${pa.get('current')} | 1yr Return: {pa.get('1yr_return_pct')}%") print(f" From 52wk High: {pa.get('pct_from_52wk_high')}%") print(f" PE: {fu.get('trailing_pe')} | Fwd PE: {fu.get('forward_pe')} | PEG: {fu.get('peg_ratio')}") print(f" Target: ${an.get('target_mean')} ({an.get('upside_pct')}% upside) | Rec: {an.get('recommendation')}") print(f" RSI: {te.get('rsi_14')} | Trend: {te.get('trend')}") print(f" Insiders: {len(data.get('insider_transactions', []))} recent txns") if new_passes: print(f"\nNew GARP Candidates ({len(new_passes)}):") for s in sorted(new_passes, key=lambda x: x.get("forward_pe", 99)): print(f" {s['ticker']:6s} PE:{s['trailing_pe']:5.1f} FPE:{s['forward_pe']:5.1f} RG:{s['revenue_growth']:5.1f}% EG:{s['earnings_growth']:5.1f}% ROE:{s['roe']:5.1f}%")