218 lines
7.5 KiB
Python
218 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Enhanced portfolio management with sector diversification and position cleanup"""
|
|
|
|
import sys
|
|
sys.path.append('/home/wdjones/.openclaw/workspace/projects/market-watch')
|
|
|
|
import game_engine
|
|
import yfinance as yf
|
|
from datetime import datetime
|
|
|
|
MIN_POSITION_SIZE = 1000 # Minimum position size in dollars
|
|
MAX_SECTOR_PCT = 30 # Maximum sector allocation percentage
|
|
MIN_CASH_PCT = 15 # Minimum cash percentage
|
|
|
|
def cleanup_small_positions(game_id, username, min_size=MIN_POSITION_SIZE, dry_run=True):
|
|
"""Sell positions below minimum size threshold"""
|
|
p = game_engine.get_portfolio(game_id, username)
|
|
if not p:
|
|
return {"error": "Portfolio not found"}
|
|
|
|
small_positions = []
|
|
actions = []
|
|
|
|
for ticker, pos in p["positions"].items():
|
|
if pos["market_value"] < min_size:
|
|
small_positions.append({
|
|
"ticker": ticker,
|
|
"value": pos["market_value"],
|
|
"shares": pos["shares"],
|
|
"price": pos["current_price"]
|
|
})
|
|
|
|
if not small_positions:
|
|
return {"message": "No small positions found", "actions": []}
|
|
|
|
print(f"Found {len(small_positions)} positions under ${min_size:,}:")
|
|
total_cleanup_value = 0
|
|
|
|
for pos in small_positions:
|
|
reason = f"Position cleanup: ${pos['value']:.0f} < ${min_size:,} minimum"
|
|
action = {
|
|
"action": "SELL",
|
|
"ticker": pos["ticker"],
|
|
"shares": pos["shares"],
|
|
"price": pos["price"],
|
|
"value": pos["value"],
|
|
"reason": reason
|
|
}
|
|
actions.append(action)
|
|
total_cleanup_value += pos["value"]
|
|
print(f" - SELL {pos['ticker']}: {pos['shares']} @ ${pos['price']:.2f} = ${pos['value']:.0f}")
|
|
|
|
if not dry_run:
|
|
result = game_engine.sell(game_id, username, pos["ticker"],
|
|
pos["shares"], pos["price"], reason)
|
|
action["result"] = result
|
|
|
|
return {
|
|
"message": f"Cleanup would free ${total_cleanup_value:.0f} from {len(small_positions)} small positions",
|
|
"actions": actions,
|
|
"total_value": total_cleanup_value
|
|
}
|
|
|
|
def analyze_sector_opportunities(game_id, username):
|
|
"""Analyze sector allocation and identify opportunities for better diversification"""
|
|
p = game_engine.get_portfolio(game_id, username)
|
|
if not p:
|
|
return {"error": "Portfolio not found"}
|
|
|
|
sectors = game_engine.get_sector_allocation(game_id, username)
|
|
|
|
# Calculate current allocations
|
|
invested_value = p["total_value"] - p["cash"]
|
|
cash_pct = (p["cash"] / p["total_value"]) * 100
|
|
|
|
print(f"\nSector Analysis:")
|
|
print(f"Portfolio Value: ${p['total_value']:,.2f}")
|
|
print(f"Invested: ${invested_value:,.2f}")
|
|
print(f"Cash: ${p['cash']:,.2f} ({cash_pct:.1f}%)")
|
|
print(f"Min Cash Required: ${p['total_value'] * MIN_CASH_PCT / 100:,.2f} ({MIN_CASH_PCT}%)")
|
|
|
|
print(f"\nSector Allocation:")
|
|
recommendations = []
|
|
|
|
for sector, pct in sorted(sectors.items(), key=lambda x: x[1], reverse=True):
|
|
status = ""
|
|
if pct > MAX_SECTOR_PCT:
|
|
excess = pct - MAX_SECTOR_PCT
|
|
excess_value = (excess / 100) * p["total_value"]
|
|
status = f" *** OVERWEIGHT by {excess:.1f}% (${excess_value:,.0f}) ***"
|
|
recommendations.append({
|
|
"type": "REDUCE",
|
|
"sector": sector,
|
|
"current_pct": pct,
|
|
"target_pct": MAX_SECTOR_PCT,
|
|
"excess_value": excess_value
|
|
})
|
|
elif pct < 5:
|
|
status = " (Underweight - potential opportunity)"
|
|
|
|
print(f" {sector}: {pct:.1f}%{status}")
|
|
|
|
# Identify underrepresented sectors
|
|
major_sectors = [
|
|
"Technology", "Healthcare", "Financial Services", "Consumer Cyclical",
|
|
"Communication Services", "Industrials", "Consumer Defensive", "Energy"
|
|
]
|
|
|
|
missing_sectors = [s for s in major_sectors if s not in sectors or sectors[s] < 5]
|
|
if missing_sectors:
|
|
print(f"\nUnderrepresented sectors (< 5%): {', '.join(missing_sectors)}")
|
|
recommendations.extend([
|
|
{"type": "ADD", "sector": sector, "current_pct": sectors.get(sector, 0)}
|
|
for sector in missing_sectors
|
|
])
|
|
|
|
return {
|
|
"sectors": sectors,
|
|
"cash_pct": cash_pct,
|
|
"recommendations": recommendations
|
|
}
|
|
|
|
def suggest_rebalance_actions(game_id, username):
|
|
"""Suggest comprehensive rebalance actions"""
|
|
print("=== ENHANCED PORTFOLIO REBALANCE ANALYSIS ===")
|
|
|
|
# 1. Clean up small positions
|
|
print("\n1. Position Cleanup Analysis:")
|
|
cleanup_result = cleanup_small_positions(game_id, username, dry_run=True)
|
|
if cleanup_result.get("actions"):
|
|
print(cleanup_result["message"])
|
|
else:
|
|
print("No small positions to cleanup")
|
|
|
|
# 2. Analyze sectors
|
|
print("\n2. Sector Diversification Analysis:")
|
|
sector_analysis = analyze_sector_opportunities(game_id, username)
|
|
|
|
# 3. Check existing violations
|
|
print("\n3. Current Violations Check:")
|
|
violation_check = game_engine.rebalance_portfolio(game_id, username, dry_run=True)
|
|
if violation_check["violations"]:
|
|
print("Violations found:")
|
|
for v in violation_check["violations"]:
|
|
print(f" - {v}")
|
|
else:
|
|
print("No current violations")
|
|
|
|
# 4. Generate action plan
|
|
print("\n4. Recommended Action Plan:")
|
|
action_plan = []
|
|
|
|
# Add cleanup actions if needed
|
|
if cleanup_result.get("actions"):
|
|
action_plan.extend(cleanup_result["actions"])
|
|
|
|
# Add violation fixes if needed
|
|
if violation_check.get("actions"):
|
|
action_plan.extend(violation_check["actions"])
|
|
|
|
if action_plan:
|
|
print("Recommended actions:")
|
|
for i, action in enumerate(action_plan, 1):
|
|
print(f" {i}. {action['action']} {action['shares']} {action['ticker']} @ ${action['price']:.2f} - {action['reason']}")
|
|
else:
|
|
print("No immediate actions required")
|
|
|
|
return {
|
|
"cleanup": cleanup_result,
|
|
"sectors": sector_analysis,
|
|
"violations": violation_check,
|
|
"action_plan": action_plan
|
|
}
|
|
|
|
def execute_rebalance(game_id, username):
|
|
"""Execute the rebalance plan"""
|
|
print("=== EXECUTING PORTFOLIO REBALANCE ===")
|
|
|
|
# 1. Execute cleanup
|
|
print("\n1. Executing position cleanup...")
|
|
cleanup_result = cleanup_small_positions(game_id, username, dry_run=False)
|
|
|
|
# 2. Execute violation fixes
|
|
print("\n2. Executing violation fixes...")
|
|
violation_result = game_engine.rebalance_portfolio(game_id, username, dry_run=False)
|
|
|
|
print("\nRebalance completed!")
|
|
return {
|
|
"cleanup": cleanup_result,
|
|
"violations": violation_result
|
|
}
|
|
|
|
def main():
|
|
gid = game_engine.get_default_game_id()
|
|
if not gid:
|
|
print("No game found")
|
|
return
|
|
|
|
# Run analysis
|
|
analysis = suggest_rebalance_actions(gid, "case")
|
|
|
|
# Ask if user wants to execute
|
|
print("\n" + "="*50)
|
|
response = input("Execute rebalance actions? (y/N): ").strip().lower()
|
|
|
|
if response in ['y', 'yes']:
|
|
execute_rebalance(gid, "case")
|
|
|
|
# Show updated status
|
|
print("\n" + "="*50)
|
|
print("UPDATED PORTFOLIO STATUS:")
|
|
import subprocess
|
|
subprocess.run(["python3", "check_portfolio.py"], cwd="/home/wdjones/.openclaw/workspace-glitch")
|
|
else:
|
|
print("Rebalance cancelled")
|
|
|
|
if __name__ == "__main__":
|
|
main() |