Full sync - all projects, memory, configs

This commit is contained in:
2026-03-21 20:27:59 -05:00
parent 2447677d4a
commit b33de10902
395 changed files with 1635300 additions and 459211 deletions

View File

@ -6,9 +6,15 @@ import os
import uuid
from datetime import datetime, date
import yfinance as yf
DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
GAMES_DIR = os.path.join(DATA_DIR, "games")
# Risk management constants
MAX_SECTOR_PCT = 0.30 # Max 30% in any single sector
MIN_CASH_PCT = 0.15 # Keep minimum 15% cash
def _load_json(path, default=None):
if os.path.exists(path):
@ -47,6 +53,43 @@ def _game_config_path(game_id):
return os.path.join(_game_dir(game_id), "game.json")
def get_stock_sector(ticker):
"""Get sector information for a stock ticker. Returns sector name or None."""
try:
stock = yf.Ticker(ticker)
info = stock.info
return info.get("sector")
except Exception as e:
print(f" Warning: Could not get sector for {ticker}: {e}")
return None
def get_sector_allocation(game_id, username):
"""Get current sector allocation as percentage of total portfolio value."""
pf = _load_json(_portfolio_path(game_id, username))
if not pf or not pf["positions"]:
return {}
p = get_portfolio(game_id, username)
if not p:
return {}
sector_values = {}
total_invested = p["total_value"] - p["cash"]
for ticker, pos in p["positions"].items():
sector = get_stock_sector(ticker)
if sector:
sector_values[sector] = sector_values.get(sector, 0) + pos["market_value"]
# Convert to percentages of total portfolio
sector_pcts = {}
for sector, value in sector_values.items():
sector_pcts[sector] = (value / p["total_value"]) * 100 if p["total_value"] > 0 else 0
return sector_pcts
# ── Game Management ──
def create_game(name, starting_cash=100_000.0, end_date=None, creator="system"):
@ -115,7 +158,7 @@ def join_game(game_id, username):
# ── Trading ──
def buy(game_id, username, ticker, shares, price, reason="Manual"):
"""Buy shares for a player in a game."""
"""Buy shares for a player in a game with sector diversification and cash reserve checks."""
pf = _load_json(_portfolio_path(game_id, username))
if not pf:
return {"success": False, "error": "Player portfolio not found"}
@ -124,6 +167,30 @@ def buy(game_id, username, ticker, shares, price, reason="Manual"):
if cost > pf["cash"]:
return {"success": False, "error": f"Insufficient cash. Need ${cost:,.2f}, have ${pf['cash']:,.2f}"}
# Get current portfolio to check constraints
p = get_portfolio(game_id, username)
if not p:
return {"success": False, "error": "Could not get current portfolio"}
# Check minimum cash reserve (15%)
min_cash_required = p["total_value"] * MIN_CASH_PCT
cash_after_purchase = pf["cash"] - cost
if cash_after_purchase < min_cash_required:
return {"success": False, "error": f"Cash reserve violation. Need ${min_cash_required:,.2f} minimum cash, would have ${cash_after_purchase:,.2f}"}
# Check sector diversification (30% max per sector)
stock_sector = get_stock_sector(ticker)
if stock_sector:
current_sectors = get_sector_allocation(game_id, username)
current_sector_pct = current_sectors.get(stock_sector, 0)
# Calculate what sector percentage would be after this purchase
new_sector_value = current_sectors.get(stock_sector, 0) * p["total_value"] / 100 + cost
new_sector_pct = (new_sector_value / p["total_value"]) * 100
if new_sector_pct > MAX_SECTOR_PCT * 100:
return {"success": False, "error": f"Sector cap violation. {stock_sector} would be {new_sector_pct:.1f}%, max allowed is {MAX_SECTOR_PCT*100:.1f}%"}
pf["cash"] -= cost
if ticker in pf["positions"]:
@ -144,6 +211,7 @@ def buy(game_id, username, ticker, shares, price, reason="Manual"):
"entry_date": datetime.now().isoformat(),
"entry_reason": reason,
"trailing_stop": price * 0.90,
"sector": stock_sector, # Store sector info
}
_save_json(_portfolio_path(game_id, username), pf)
@ -285,6 +353,108 @@ def get_snapshots(game_id, username):
return _load_json(_snapshots_path(game_id, username), [])
def rebalance_portfolio(game_id, username, dry_run=True):
"""Rebalance portfolio to enforce sector caps and cash reserves.
Args:
game_id: Game ID
username: Username
dry_run: If True, return recommendations without executing trades
Returns:
Dict with rebalance actions and results
"""
p = get_portfolio(game_id, username)
if not p:
return {"success": False, "error": "Portfolio not found"}
actions = []
violations = []
# Check cash reserve
min_cash_required = p["total_value"] * MIN_CASH_PCT
cash_deficit = max(0, min_cash_required - p["cash"])
if cash_deficit > 0:
violations.append(f"Cash reserve deficit: ${cash_deficit:,.2f} (need ${min_cash_required:,.2f}, have ${p['cash']:,.2f})")
# Check sector allocations
sector_allocations = get_sector_allocation(game_id, username)
sector_violations = {}
for sector, pct in sector_allocations.items():
if pct > MAX_SECTOR_PCT * 100:
excess_pct = pct - (MAX_SECTOR_PCT * 100)
excess_value = (excess_pct / 100) * p["total_value"]
sector_violations[sector] = {
"current_pct": pct,
"excess_pct": excess_pct,
"excess_value": excess_value,
}
violations.append(f"{sector} sector: {pct:.1f}% (excess: {excess_pct:.1f}%, ${excess_value:,.2f})")
# Calculate total excess to sell
total_excess = cash_deficit + sum(v["excess_value"] for v in sector_violations.values())
if total_excess > 0:
# Find positions to sell (prioritize largest positions in violated sectors)
sell_candidates = []
for ticker, pos in p["positions"].items():
sector = pos.get("sector") or get_stock_sector(ticker)
if sector in sector_violations:
sell_candidates.append({
"ticker": ticker,
"sector": sector,
"market_value": pos["market_value"],
"shares": pos["shares"],
"current_price": pos["current_price"],
"priority": sector_violations[sector]["excess_pct"],
})
# Sort by priority (highest excess first) and market value
sell_candidates.sort(key=lambda x: (x["priority"], -x["market_value"]), reverse=True)
# Calculate sells needed
remaining_to_sell = total_excess
for candidate in sell_candidates:
if remaining_to_sell <= 0:
break
# Sell partial or full position
sell_value = min(remaining_to_sell, candidate["market_value"])
shares_to_sell = min(candidate["shares"], int(sell_value / candidate["current_price"]))
if shares_to_sell > 0:
action = {
"action": "SELL",
"ticker": candidate["ticker"],
"shares": shares_to_sell,
"price": candidate["current_price"],
"value": shares_to_sell * candidate["current_price"],
"reason": f"Rebalance: {candidate['sector']} sector cap violation",
}
actions.append(action)
remaining_to_sell -= action["value"]
# Execute if not dry run
if not dry_run:
result = sell(game_id, username, candidate["ticker"],
shares_to_sell, candidate["current_price"],
action["reason"])
action["result"] = result
return {
"success": True,
"dry_run": dry_run,
"violations": violations,
"actions": actions,
"total_excess": total_excess,
"sector_allocations": sector_allocations,
"cash_deficit": cash_deficit,
}
def get_leaderboard(game_id):
"""Get game leaderboard sorted by % return."""
game = get_game(game_id)