Files
workspace/projects/market-watch/trader_enhanced.py

297 lines
11 KiB
Python

#!/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()