- 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
296 lines
11 KiB
Python
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}%")
|