Files
workspace/data/stock_deep_dive.py
Case be43231c3f Market Watch: multiplayer GARP paper trading simulator
- Game engine with multiplayer support (create games, join, leaderboard)
- GARP stock screener (S&P 500 + 400 MidCap, 900+ tickers)
- Automated trading logic for AI player (Case)
- Web portal at marketwatch.local:8889 with dark theme
- Systemd timer for Mon-Fri market hours
- Telegram alerts on trades and daily summary
- Stock analysis deep dive data (BAC, CFG, FITB, INCY)
- Expanded scan results (22 GARP candidates)
- Craigslist account setup + credentials
2026-02-08 15:18:41 -06:00

296 lines
11 KiB
Python

#!/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}%")