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