#!/usr/bin/env python3 """Enhanced GARP trading engine with automatic sector diversification and position management""" import sys sys.path.append('/home/wdjones/.openclaw/workspace/projects/market-watch') import game_engine import scanner import trader import yfinance as yf import json import os from datetime import datetime from collections import defaultdict # Enhanced constants MIN_POSITION_SIZE = 1000 # Minimum position size MAX_SECTOR_PCT = 30 # Maximum sector allocation MIN_CASH_PCT = 15 # Minimum cash reserve OPTIMAL_POSITIONS = 12 # Target number of positions SECTOR_BALANCE_THRESHOLD = 5 # Rebalance if any sector is 5%+ over target # Target sector allocation (more conservative than aggressive growth) TARGET_SECTORS = { "Financial Services": 25, "Technology": 20, "Healthcare": 15, "Consumer Cyclical": 10, "Communication Services": 8, "Industrials": 8, "Consumer Defensive": 7, "Energy": 7 } def cleanup_small_positions(game_id, username, dry_run=False): """Automatically clean up positions under minimum size""" p = game_engine.get_portfolio(game_id, username) if not p: return [] cleanup_actions = [] for ticker, pos in p["positions"].items(): if pos["market_value"] < MIN_POSITION_SIZE: reason = f"Position cleanup: ${pos['market_value']:.0f} < ${MIN_POSITION_SIZE:,} minimum" if not dry_run: result = game_engine.sell(game_id, username, ticker, pos["shares"], pos["current_price"], reason) cleanup_actions.append({ "action": "SELL", "ticker": ticker, "shares": pos["shares"], "value": pos["market_value"], "reason": reason, "result": result }) print(f" CLEANUP: Sold {ticker} - {reason}") return cleanup_actions def check_sector_balance(game_id, username): """Check if sector rebalancing is needed beyond basic violations""" sectors = game_engine.get_sector_allocation(game_id, username) p = game_engine.get_portfolio(game_id, username) imbalances = [] for sector, current_pct in sectors.items(): target_pct = TARGET_SECTORS.get(sector, 5) # Default 5% for unknown sectors if current_pct > target_pct + SECTOR_BALANCE_THRESHOLD: excess = current_pct - target_pct excess_value = (excess / 100) * p["total_value"] imbalances.append({ "sector": sector, "current_pct": current_pct, "target_pct": target_pct, "excess_pct": excess, "excess_value": excess_value }) return imbalances def enhanced_buy_logic(game_id, username, candidates=None): """Enhanced buy logic with sector diversification priority""" p = game_engine.get_portfolio(game_id, username) buys = [] if p["num_positions"] >= trader.MAX_POSITIONS: print(f" Max positions reached ({trader.MAX_POSITIONS}), skipping buys") return buys if candidates is None: latest_scan = scanner.load_latest_scan() if not latest_scan: print(" No scan data available") return buys candidates = latest_scan.get("candidates", []) # Get current sector allocations current_sectors = game_engine.get_sector_allocation(game_id, username) existing_tickers = set(p["positions"].keys()) # Calculate sector priorities (higher score for underallocated sectors) sector_priorities = {} for sector, target_pct in TARGET_SECTORS.items(): current_pct = current_sectors.get(sector, 0) shortage = max(0, target_pct - current_pct) sector_priorities[sector] = shortage # Enhance candidates with sector priority scoring enhanced_candidates = [] for c in candidates: if c["ticker"] in existing_tickers: continue # Skip if RSI too high or too close to 52-week high rsi = c.get("rsi") if rsi and rsi > trader.RSI_BUY_LIMIT: continue pct_from_high = c.get("pct_from_52wk_high", 0) if pct_from_high < trader.NEAR_HIGH_PCT: continue # Calculate enhanced score with sector priority sector = c.get("sector", "Unknown") base_score = c.get("score", 0) # Sector priority bonus (negative to improve ranking) sector_bonus = -sector_priorities.get(sector, 0) * 0.8 # Sector cap check current_sector_pct = current_sectors.get(sector, 0) if current_sector_pct >= MAX_SECTOR_PCT: continue # Skip if sector already at cap enhanced_score = base_score + sector_bonus enhanced_candidates.append({ **c, "enhanced_score": enhanced_score, "sector_priority": sector_priorities.get(sector, 0), "current_sector_pct": current_sector_pct }) # Sort by enhanced score (lower is better) enhanced_candidates.sort(key=lambda x: x["enhanced_score"]) # Execute buys with diversification preference sectors_bought = set() cash_available = p["cash"] min_cash_required = p["total_value"] * MIN_CASH_PCT / 100 for c in enhanced_candidates[:20]: # Consider top 20 candidates if len(buys) >= 3: # Limit buys per session break ticker = c["ticker"] sector = c.get("sector", "Unknown") price = c["price"] # Position sizing target_position_value = min( p["total_value"] * trader.MAX_POSITION_PCT, # Max 10% per position (cash_available - min_cash_required) * 0.8 # Don't use all available cash ) if target_position_value < MIN_POSITION_SIZE: continue shares = max(1, int(target_position_value / price)) cost = shares * price # Final checks if cost > (cash_available - min_cash_required): continue # Prefer sectors we haven't bought yet in this session diversity_bonus = 0 if sector in sectors_bought else 1 reason = (f"Enhanced GARP: PE={c['trailing_pe']:.1f}, FwdPE={c['forward_pe']:.1f}, " f"RevGr={c['revenue_growth']:.1f}%, EPSGr={c['earnings_growth']:.1f}%, " f"Sector={sector} (priority={c['sector_priority']:.1f}%)") result = game_engine.buy(game_id, username, ticker, shares, price, reason=reason) if result["success"]: buys.append({ "ticker": ticker, "sector": sector, "shares": shares, "price": price, "cost": cost, "reason": reason, "result": result }) sectors_bought.add(sector) cash_available -= cost print(f" BUY {ticker} ({sector}): {shares} shares @ ${price:.2f} = ${cost:,.0f} [Priority: {c['sector_priority']:.1f}%]") else: print(f" SKIP {ticker}: {result.get('error', 'Unknown error')}") return buys def enhanced_trading_session(game_id, username): """Enhanced trading session with comprehensive portfolio management""" print(f"\n=== ENHANCED TRADING SESSION [{username}@{game_id}] ===") # 1. Update all prices print("\n1. Updating prices...") updated = trader.update_all_prices(game_id, username) for ticker, price in updated: print(f" {ticker}: ${price:.2f}") # 2. Portfolio cleanup print("\n2. Position cleanup...") cleanup_actions = cleanup_small_positions(game_id, username, dry_run=False) if not cleanup_actions: print(" No cleanup needed") # 3. Check sector balance print("\n3. Sector balance check...") imbalances = check_sector_balance(game_id, username) if imbalances: print(" Sector imbalances detected:") for imbalance in imbalances: print(f" {imbalance['sector']}: {imbalance['current_pct']:.1f}% vs {imbalance['target_pct']:.1f}% target ({imbalance['excess_pct']:+.1f}%)") else: print(" Sectors balanced within targets") # 4. Standard violation checks and rebalancing print("\n4. Violation checks...") rebalance_result = game_engine.rebalance_portfolio(game_id, username, dry_run=True) if rebalance_result["violations"]: print(" Executing automatic rebalance...") exec_result = game_engine.rebalance_portfolio(game_id, username, dry_run=False) print(f" Rebalanced {len(exec_result.get('actions', []))} positions") else: print(" No violations found") # 5. Sell signals print("\n5. Checking sell signals...") sells = trader.check_sell_signals(game_id, username) if not sells: print(" No sell signals") # 6. Enhanced buy signals print("\n6. Enhanced buy analysis...") buys = enhanced_buy_logic(game_id, username) if not buys: print(" No suitable buy candidates") # 7. Portfolio summary print("\n7. Updated Portfolio Summary:") p = game_engine.get_portfolio(game_id, username) 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']}") if sectors: print(f" Sector Allocation:") for sector, pct in sorted(sectors.items(), key=lambda x: x[1], reverse=True): target = TARGET_SECTORS.get(sector, 5) status = "" if pct > target + SECTOR_BALANCE_THRESHOLD: status = f" (OVER by {pct-target:.1f}%)" elif pct < target - SECTOR_BALANCE_THRESHOLD: status = f" (UNDER by {target-pct:.1f}%)" print(f" {sector}: {pct:.1f}% (target: {target}%){status}") return { "price_updates": len(updated), "cleanup": len(cleanup_actions), "sells": len(sells), "buys": len(buys), "sector_imbalances": len(imbalances), "violations": len(rebalance_result.get("violations", [])) } def main(): gid = game_engine.get_default_game_id() if gid: result = enhanced_trading_session(gid, "case") print(f"\n=== SESSION SUMMARY ===") print(f"Price updates: {result['price_updates']}") print(f"Positions cleaned: {result['cleanup']}") print(f"Sells executed: {result['sells']}") print(f"Buys executed: {result['buys']}") print(f"Sector imbalances: {result['sector_imbalances']}") print(f"Violations fixed: {result['violations']}") else: print("No default game found") if __name__ == "__main__": main()