Files
workspace/projects/market-watch/enhanced_garp_screener.py

390 lines
17 KiB
Python

#!/usr/bin/env python3
"""Enhanced GARP screener with comprehensive sector diversification and rebalancing"""
import sys
sys.path.append('/home/wdjones/.openclaw/workspace/projects/market-watch')
import scanner
import game_engine
from collections import defaultdict
import yfinance as yf
# Sector diversification targets
TARGET_SECTORS = {
"Technology": 20,
"Healthcare": 15,
"Financial Services": 25,
"Consumer Cyclical": 10,
"Communication Services": 10,
"Industrials": 10,
"Consumer Defensive": 5,
"Energy": 5
}
MAX_SECTOR_PCT = 30
MIN_CASH_PCT = 15
def analyze_portfolio_risks(game_id, username):
"""Analyze current portfolio for sector and cash risks"""
p = game_engine.get_portfolio(game_id, username)
if not p:
return None
current_sectors = game_engine.get_sector_allocation(game_id, username)
cash_pct = (p["cash"] / p["total_value"]) * 100
risks = {
"sector_violations": [],
"cash_risk": None,
"recommendations": []
}
# Check sector caps
for sector, pct in current_sectors.items():
if pct > MAX_SECTOR_PCT:
excess_pct = pct - MAX_SECTOR_PCT
excess_value = (excess_pct / 100) * p["total_value"]
risks["sector_violations"].append({
"sector": sector,
"current_pct": pct,
"excess_pct": excess_pct,
"excess_value": excess_value
})
# Check cash reserves
if cash_pct < MIN_CASH_PCT:
cash_deficit = (MIN_CASH_PCT - cash_pct) / 100 * p["total_value"]
risks["cash_risk"] = {
"current_pct": cash_pct,
"deficit_pct": MIN_CASH_PCT - cash_pct,
"deficit_value": cash_deficit
}
elif cash_pct > 70: # Too much cash sitting idle
risks["cash_risk"] = {
"current_pct": cash_pct,
"excess_pct": cash_pct - 15,
"deployable_value": ((cash_pct - 15) / 100) * p["total_value"]
}
return risks
def get_rebalancing_recommendations(game_id, username):
"""Get specific rebalancing recommendations using game_engine functions"""
risks = analyze_portfolio_risks(game_id, username)
if not risks:
return []
recommendations = []
# Use game_engine's rebalance function for sell recommendations
if risks["sector_violations"] or risks["cash_risk"]:
rebalance_result = game_engine.rebalance_portfolio(game_id, username, dry_run=True)
if rebalance_result["success"] and rebalance_result["actions"]:
recommendations.extend([{
"type": "SELL",
"ticker": action["ticker"],
"shares": action["shares"],
"reason": action["reason"],
"estimated_proceeds": action["value"]
} for action in rebalance_result["actions"]])
# Add buy recommendations for underallocated sectors
current_sectors = game_engine.get_sector_allocation(game_id, username)
p = game_engine.get_portfolio(game_id, username)
# Calculate deployable cash (current cash minus minimum reserve)
min_cash_reserve = p["total_value"] * (MIN_CASH_PCT / 100)
deployable_cash = max(0, p["cash"] - min_cash_reserve)
if deployable_cash > 1000: # Only recommend if we have meaningful cash to deploy
sector_priorities = get_sector_priority_scoring(game_id, username)
underallocated = [(sector, shortage) for sector, shortage in sector_priorities.items() if shortage > 2]
underallocated.sort(key=lambda x: x[1], reverse=True)
if underallocated:
recommendations.append({
"type": "DIVERSIFY",
"deployable_cash": deployable_cash,
"priority_sectors": underallocated[:3],
"reason": "Deploy excess cash to underallocated sectors"
})
return recommendations
def get_sector_priority_scoring(game_id, username):
"""Calculate sector priority scoring based on current allocation vs targets"""
current_sectors = game_engine.get_sector_allocation(game_id, username)
priority_scores = {}
for sector, target_pct in TARGET_SECTORS.items():
current_pct = current_sectors.get(sector, 0)
# Higher score for underallocated sectors
shortage = max(0, target_pct - current_pct)
priority_scores[sector] = shortage
return priority_scores
def enhanced_garp_scan_with_diversification(game_id=None, username="case"):
"""Run GARP scan with sector diversification prioritization and sector data"""
print("=== Enhanced GARP Scan with Sector Diversification ===")
# Get current scan results
latest_scan = scanner.load_latest_scan()
if not latest_scan:
print("No recent scan found, running new scan...")
candidates = scanner.run_scan()
latest_scan = scanner.load_latest_scan()
else:
candidates = latest_scan.get("candidates", [])
if not candidates:
print("No GARP candidates found")
return []
print(f"Found {len(candidates)} GARP candidates")
# Enhance candidates with sector information
for candidate in candidates:
if "sector" not in candidate:
candidate["sector"] = game_engine.get_stock_sector(candidate["ticker"]) or "Unknown"
# Group by sector for analysis
by_sector = defaultdict(list)
for candidate in candidates:
sector = candidate.get("sector", "Unknown")
by_sector[sector].append(candidate)
print(f"\n📊 Sector Distribution in GARP Candidates:")
for sector in sorted(by_sector.keys()):
count = len(by_sector[sector])
print(f" {sector}: {count} candidates")
# Portfolio context if available
if game_id:
print(f"\n💼 Current Portfolio Analysis:")
p = game_engine.get_portfolio(game_id, username)
current_sectors = game_engine.get_sector_allocation(game_id, username)
cash_pct = (p["cash"] / p["total_value"]) * 100
print(f" Total Value: ${p['total_value']:,.2f}")
print(f" Cash: ${p['cash']:,.2f} ({cash_pct:.1f}%)")
print(f" Positions: {p['num_positions']}")
print(f"\n📈 Current Sector Allocation vs Targets:")
all_sectors = set(TARGET_SECTORS.keys()) | set(current_sectors.keys())
for sector in sorted(all_sectors):
current_pct = current_sectors.get(sector, 0)
target_pct = TARGET_SECTORS.get(sector, 0)
status = ""
if current_pct > MAX_SECTOR_PCT:
status = " ⚠️ OVER CAP"
elif current_pct > target_pct * 1.5:
status = " 🔸 OVER TARGET"
elif current_pct < target_pct * 0.5 and target_pct > 5:
status = " 🔹 UNDER TARGET"
print(f" {sector:25s}: {current_pct:5.1f}% (target: {target_pct:2d}%){status}")
# Analyze risks
risks = analyze_portfolio_risks(game_id, username)
if risks["sector_violations"]:
print(f"\n⚠️ SECTOR VIOLATIONS:")
for violation in risks["sector_violations"]:
print(f" {violation['sector']}: {violation['current_pct']:.1f}% " +
f"(excess: {violation['excess_pct']:.1f}%, ${violation['excess_value']:,.2f})")
if risks["cash_risk"]:
cash_risk = risks["cash_risk"]
if "deficit_pct" in cash_risk:
print(f"\n💸 CASH RESERVE RISK: {cash_risk['current_pct']:.1f}% " +
f"(need ${cash_risk['deficit_value']:,.2f} more)")
elif "excess_pct" in cash_risk:
print(f"\n💰 EXCESS CASH: {cash_risk['current_pct']:.1f}% " +
f"(${cash_risk['deployable_value']:,.2f} available for deployment)")
# Get sector priorities for candidate scoring
sector_priorities = get_sector_priority_scoring(game_id, username)
# Enhance candidates with sector priority scoring
for candidate in candidates:
sector = candidate.get("sector", "Unknown")
# Base score (lower is better)
base_score = candidate.get("score", 0)
# Sector priority bonus (negative to improve score for high priority sectors)
sector_bonus = 0
if game_id and sector in sector_priorities:
sector_bonus = -sector_priorities[sector] * 0.5
candidate["enhanced_score"] = base_score + sector_bonus
candidate["sector_priority"] = sector_priorities.get(sector, 0) if game_id else 0
# Sort by enhanced score
enhanced_candidates = sorted(candidates, key=lambda x: x["enhanced_score"])
print(f"\n🎯 Top Diversified GARP Candidates:")
print(f"{'#':>2} {'Ticker':>6} {'Sector':>20} {'Price':>8} {'PE':>6} {'FwdPE':>6} {'RevGr':>6} {'EPSGr':>6} {'RSI':>5} {'Priority'}")
print("-" * 95)
for i, c in enumerate(enhanced_candidates[:15], 1):
priority_note = f"{c['sector_priority']:4.1f}" if c['sector_priority'] > 0 else " -"
rsi = f"{c.get('rsi', 0):4.0f}" if c.get('rsi') else " N/A"
print(f"{i:2d} {c['ticker']:>6s} {c['sector'][:20]:>20s} ${c['price']:7.2f} " +
f"{c['trailing_pe']:5.1f} {c['forward_pe']:5.1f} {c['revenue_growth']:5.1f}% " +
f"{c['earnings_growth']:5.1f}% {rsi} {priority_note}")
return enhanced_candidates
def suggest_specific_trades(game_id, username, max_suggestions=5):
"""Generate specific buy/sell recommendations with dollar amounts"""
print(f"\n🎯 SPECIFIC TRADE RECOMMENDATIONS:")
# Get rebalancing recommendations first
rebalance_recs = get_rebalancing_recommendations(game_id, username)
if rebalance_recs:
# Show sell recommendations
sells = [r for r in rebalance_recs if r["type"] == "SELL"]
if sells:
print(f"\n🔴 SELL RECOMMENDATIONS (Risk Reduction):")
for i, rec in enumerate(sells, 1):
print(f" {i}. SELL {rec['shares']} shares of {rec['ticker']}")
print(f" Proceeds: ${rec['estimated_proceeds']:,.2f}")
print(f" Reason: {rec['reason']}")
# Show diversification recommendations
diversify_recs = [r for r in rebalance_recs if r["type"] == "DIVERSIFY"]
if diversify_recs:
rec = diversify_recs[0]
print(f"\n🟢 BUY RECOMMENDATIONS (Diversification):")
print(f"Available Cash for Deployment: ${rec['deployable_cash']:,.2f}")
print(f"Priority Sectors (shortage from target):")
# Get GARP candidates for priority sectors
candidates = enhanced_garp_scan_with_diversification(game_id, username)
p = game_engine.get_portfolio(game_id, username)
existing_tickers = set(p["positions"].keys())
cash_per_sector = rec['deployable_cash'] / min(3, len(rec['priority_sectors']))
for sector, shortage in rec['priority_sectors']:
print(f"\n 📍 {sector} (need {shortage:.1f}% more):")
sector_candidates = [c for c in candidates[:20]
if c.get('sector') == sector
and c['ticker'] not in existing_tickers]
if sector_candidates:
best_candidate = sector_candidates[0]
shares_to_buy = int(cash_per_sector / best_candidate['price'])
investment = shares_to_buy * best_candidate['price']
print(f" → BUY {shares_to_buy} shares of {best_candidate['ticker']}")
print(f" Price: ${best_candidate['price']:.2f} | Investment: ${investment:,.2f}")
print(f" PE: {best_candidate['trailing_pe']:.1f} | FwdPE: {best_candidate['forward_pe']:.1f}")
print(f" RevGr: {best_candidate['revenue_growth']:.1f}% | EPSGr: {best_candidate['earnings_growth']:.1f}%")
else:
print(f" ⚠️ No suitable GARP candidates found in {sector}")
else:
print("✅ Portfolio is well-balanced. No immediate rebalancing needed.")
# Still show opportunities for excess cash deployment
p = game_engine.get_portfolio(game_id, username)
cash_pct = (p["cash"] / p["total_value"]) * 100
if cash_pct > 25: # Significant cash position
print(f"\n💰 Cash Deployment Opportunities:")
print(f"Current cash: ${p['cash']:,.2f} ({cash_pct:.1f}% of portfolio)")
min_reserve = p["total_value"] * (MIN_CASH_PCT / 100)
deployable = p["cash"] - min_reserve
if deployable > 1000:
print(f"Deployable (above {MIN_CASH_PCT}% reserve): ${deployable:,.2f}")
# Show top GARP picks regardless of sector
candidates = enhanced_garp_scan_with_diversification(game_id, username)
existing_tickers = set(p["positions"].keys())
available_candidates = [c for c in candidates[:10]
if c['ticker'] not in existing_tickers]
if available_candidates:
print(f"\nTop New GARP Opportunities:")
for i, c in enumerate(available_candidates[:3], 1):
shares = int(deployable * 0.33 / c['price']) # 1/3 of deployable cash
investment = shares * c['price']
print(f" {i}. {c['ticker']} ({c['sector']})")
print(f" Buy {shares} shares @ ${c['price']:.2f} = ${investment:,.2f}")
print(f" PE: {c['trailing_pe']:.1f} | FwdPE: {c['forward_pe']:.1f}")
def suggest_sector_buy_candidates(game_id, username, max_suggestions=5):
"""Legacy function - maintained for compatibility"""
return suggest_specific_trades(game_id, username, max_suggestions)
def main():
"""Main execution with comprehensive portfolio analysis"""
gid = game_engine.get_default_game_id()
if gid:
# Run enhanced scan with portfolio context
candidates = enhanced_garp_scan_with_diversification(gid, "case")
# Generate specific trade recommendations
suggest_specific_trades(gid, "case")
print(f"\n" + "="*80)
print(f"💡 PORTFOLIO HEALTH SUMMARY:")
# Quick portfolio health check
risks = analyze_portfolio_risks(gid, "case")
health_score = 100
if risks["sector_violations"]:
health_score -= len(risks["sector_violations"]) * 20
print(f"❌ Sector violations detected (-{len(risks['sector_violations']) * 20} points)")
else:
print(f"✅ All sectors within limits (+0 points)")
if risks["cash_risk"] and "deficit_pct" in risks["cash_risk"]:
health_score -= 15
print(f"❌ Insufficient cash reserves (-15 points)")
elif risks["cash_risk"] and "excess_pct" in risks["cash_risk"]:
if risks["cash_risk"]["excess_pct"] > 50:
health_score -= 10
print(f"⚠️ Significant cash drag (-10 points)")
else:
print(f"✅ Cash reserves adequate (+0 points)")
p = game_engine.get_portfolio(gid, "case")
current_sectors = game_engine.get_sector_allocation(gid, "case")
diversification_score = len(current_sectors) # Simple metric
if diversification_score >= 4:
print(f"✅ Good diversification across {diversification_score} sectors")
elif diversification_score >= 2:
health_score -= 10
print(f"⚠️ Limited diversification ({diversification_score} sectors) (-10 points)")
else:
health_score -= 20
print(f"❌ Poor diversification ({diversification_score} sectors) (-20 points)")
health_score = max(0, health_score)
print(f"\n📊 OVERALL PORTFOLIO HEALTH: {health_score}/100")
if health_score >= 90:
print("🟢 Excellent portfolio management!")
elif health_score >= 70:
print("🟡 Good portfolio, minor optimizations possible")
elif health_score >= 50:
print("🟠 Portfolio needs attention")
else:
print("🔴 Portfolio requires immediate rebalancing")
else:
print("❌ No active game found. Cannot provide portfolio analysis.")
# Still run basic GARP scan
candidates = enhanced_garp_scan_with_diversification()
if __name__ == "__main__":
main()