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