Full sync - all projects, memory, configs
This commit is contained in:
390
projects/market-watch/enhanced_garp_screener.py
Normal file
390
projects/market-watch/enhanced_garp_screener.py
Normal file
@ -0,0 +1,390 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user