390 lines
17 KiB
Python
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() |