#!/usr/bin/env python3 """Market Watch Web Portal - Multiplayer GARP Paper Trading.""" import json import os import sys from datetime import datetime from http.server import HTTPServer, BaseHTTPRequestHandler from socketserver import ThreadingMixIn from urllib.parse import urlparse, parse_qs sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import game_engine PORT = 8889 PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SCANS_DIR = os.path.join(PROJECT_DIR, "data", "scans") class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): daemon_threads = True CSS = """:root{--bg-primary:#0d1117;--bg-secondary:#161b22;--bg-tertiary:#21262d;--text-primary:#f0f6fc;--text-secondary:#8b949e;--border-color:#30363d;--accent-blue:#58a6ff;--accent-purple:#bc8cff;--positive-green:#3fb950;--negative-red:#f85149;--gold:#f0c000;--silver:#c0c0c0;--bronze:#cd7f32} *{margin:0;padding:0;box-sizing:border-box} body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg-primary);color:var(--text-primary);line-height:1.5} a{color:var(--accent-blue);text-decoration:none}a:hover{text-decoration:underline} .navbar{background:var(--bg-secondary);border-bottom:1px solid var(--border-color);padding:1rem 2rem;display:flex;align-items:center;justify-content:space-between} .nav-brand{font-size:1.5rem;font-weight:bold;color:var(--accent-blue)} .nav-links{display:flex;gap:1.5rem} .nav-links a{color:var(--text-secondary);text-decoration:none;padding:.5rem 1rem;border-radius:6px;transition:all .2s} .nav-links a:hover{color:var(--text-primary);background:var(--bg-tertiary)} .nav-links a.active{color:var(--accent-blue);background:var(--bg-tertiary)} .container{max-width:1400px;margin:0 auto;padding:2rem} .card{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:8px;padding:1.5rem;margin-bottom:1.5rem} .card h3{color:var(--text-primary);margin-bottom:1rem;font-size:1.1rem} .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1.5rem;margin-bottom:2rem} .metric-large{font-size:2rem;font-weight:bold;margin-bottom:.3rem} .metric-small{color:var(--text-secondary);font-size:.85rem} .positive{color:var(--positive-green)!important}.negative{color:var(--negative-red)!important} table{width:100%;border-collapse:collapse} th,td{padding:.6rem .8rem;text-align:left;border-bottom:1px solid var(--border-color)} th{color:var(--text-secondary);font-size:.8rem;text-transform:uppercase} td{font-size:.9rem} .rank-1{color:var(--gold);font-weight:bold}.rank-2{color:var(--silver)}.rank-3{color:var(--bronze)} .btn{display:inline-block;padding:.5rem 1.2rem;background:var(--accent-blue);color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:.9rem;text-decoration:none;transition:opacity .2s} .btn:hover{opacity:.85;text-decoration:none} .btn-outline{background:transparent;border:1px solid var(--border-color);color:var(--text-primary)} .btn-outline:hover{border-color:var(--accent-blue)} .btn-green{background:var(--positive-green)}.btn-red{background:var(--negative-red)} input,select{background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-primary);padding:.5rem .8rem;border-radius:6px;font-size:.9rem} .form-row{display:flex;gap:1rem;align-items:end;flex-wrap:wrap;margin-bottom:1rem} .form-group{display:flex;flex-direction:column;gap:.3rem} .form-group label{font-size:.8rem;color:var(--text-secondary);text-transform:uppercase} .badge{display:inline-block;padding:.15rem .5rem;border-radius:4px;font-size:.75rem;font-weight:bold} .badge-ai{background:var(--accent-purple);color:#fff} .badge-human{background:var(--accent-blue);color:#fff} .player-link{color:var(--text-primary);font-weight:500} @media(max-width:768px){.navbar{flex-direction:column;gap:1rem}.cards{grid-template-columns:1fr}.container{padding:1rem}.form-row{flex-direction:column}}""" def nav(active=""): return f"""""" class MarketWatchHandler(BaseHTTPRequestHandler): def do_GET(self): try: parsed = urlparse(self.path) path = parsed.path.rstrip("/") params = parse_qs(parsed.query) if path == "" or path == "/": self.serve_home() elif path == "/create-game": self.serve_create_game() elif path.startswith("/game/") and "/player/" in path: parts = path.split("/") # /game/{gid}/player/{user} self.serve_player(parts[2], parts[4]) elif path.startswith("/game/"): game_id = path.split("/")[2] self.serve_game(game_id) elif path == "/scans": self.serve_scans() # API elif path.startswith("/api/games") and len(path.split("/")) == 3: self.send_json(game_engine.list_games(active_only=False)) elif path.startswith("/api/games/") and path.endswith("/leaderboard"): gid = path.split("/")[3] self.send_json(game_engine.get_leaderboard(gid)) elif "/portfolio" in path: parts = path.split("/") self.send_json(game_engine.get_portfolio(parts[3], parts[5])) else: self.send_error(404) except Exception as e: self.send_response(500) self.send_header("Content-type", "text/html") self.end_headers() self.wfile.write(f"

500

{e}
".encode()) def do_POST(self): try: content_len = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(content_len).decode() if content_len else "" parsed = urlparse(self.path) path = parsed.path.rstrip("/") if path == "/api/games": data = parse_form(body) name = data.get("name", "Untitled Game") cash = float(data.get("starting_cash", 100000)) end_date = data.get("end_date") or None gid = game_engine.create_game(name, cash, end_date) self.redirect(f"/game/{gid}") elif path.endswith("/join"): data = parse_form(body) parts = path.split("/") gid = parts[3] username = data.get("username", "").strip().lower() if username: game_engine.join_game(gid, username) self.redirect(f"/game/{gid}") elif path.endswith("/trade"): data = parse_form(body) parts = path.split("/") gid, username = parts[3], parts[5] action = data.get("action", "").upper() ticker = data.get("ticker", "").upper().strip() shares = int(data.get("shares", 0)) if ticker and shares > 0: import yfinance as yf price = yf.Ticker(ticker).info.get("currentPrice") or yf.Ticker(ticker).info.get("regularMarketPrice", 0) if price and price > 0: if action == "BUY": game_engine.buy(gid, username, ticker, shares, price, reason="Manual trade") elif action == "SELL": game_engine.sell(gid, username, ticker, shares, price, reason="Manual trade") self.redirect(f"/game/{gid}/player/{username}") else: self.send_error(404) except Exception as e: self.send_response(500) self.send_header("Content-type", "text/html") self.end_headers() self.wfile.write(f"

500

{e}
".encode()) def serve_home(self): games = game_engine.list_games(active_only=False) rows = "" for g in games: players = len(g.get("players", [])) status_badge = 'Active' if g["status"] == "active" else 'Ended' rows += f""" {g['name']} {players} ${g['starting_cash']:,.0f} {g['start_date']} {g.get('end_date', 'โ€”') or 'โ€”'} {status_badge} """ html = f""" Market Watch {nav('home')}

๐ŸŽฎ Active Games

+ New Game
{rows if rows else ''}
GamePlayersStarting CashStartedEndsStatus
No games yet โ€” create one!
""" self.send_html(html) def serve_create_game(self): html = f""" Create Game - Market Watch {nav()}

๐ŸŽฎ Create New Game

""" self.send_html(html) def serve_game(self, game_id): game = game_engine.get_game(game_id) if not game: return self.send_error(404) board = game_engine.get_leaderboard(game_id) rank_rows = "" for i, entry in enumerate(board): rank_class = f"rank-{i+1}" if i < 3 else "" medal = ["๐Ÿฅ‡", "๐Ÿฅˆ", "๐Ÿฅ‰"][i] if i < 3 else f"#{i+1}" pnl_class = "positive" if entry["pnl_pct"] >= 0 else "negative" badge = ' AI' if entry["username"] == "case" else "" rank_rows += f""" {medal} {entry['username']}{badge} ${entry['total_value']:,.2f} {entry['pnl_pct']:+.2f}% ${entry['total_pnl']:+,.2f} {entry['num_positions']} {entry['num_trades']} """ html = f""" {game['name']} - Market Watch {nav()}

๐Ÿ† {game['name']}

{game['status'].upper()}

Started {game['start_date']} ยท ${game['starting_cash']:,.0f} starting cash ยท {len(game['players'])} players

Leaderboard

{rank_rows if rank_rows else ''}
RankPlayerPortfolioReturnP&LPositionsTrades
No players yet

Join This Game

""" self.send_html(html) def serve_player(self, game_id, username): game = game_engine.get_game(game_id) p = game_engine.get_portfolio(game_id, username) if not game or not p: return self.send_error(404) trades = game_engine.get_trades(game_id, username) snapshots = game_engine.get_snapshots(game_id, username) pnl_class = "positive" if p["total_pnl"] >= 0 else "negative" is_ai = username == "case" badge = 'AI Player' if is_ai else 'Human' # Positions table pos_rows = "" for ticker, pos in sorted(p["positions"].items()): pc = "positive" if pos["unrealized_pnl"] >= 0 else "negative" pos_rows += f""" {ticker}{pos['shares']} ${pos['avg_cost']:.2f}${pos['current_price']:.2f} ${pos['market_value']:,.2f}${pos['unrealized_pnl']:+,.2f} ${pos.get('trailing_stop',0):.2f} """ if not pos_rows: pos_rows = 'No positions' # Trade log trade_rows = "" for t in reversed(trades[-30:]): action_class = "positive" if t["action"] == "BUY" else "negative" pnl_cell = "" if t["action"] == "SELL": rpnl = t.get("realized_pnl", 0) rpnl_class = "positive" if rpnl >= 0 else "negative" pnl_cell = f'${rpnl:+,.2f}' trade_rows += f""" {t['action']}{t['ticker']}{t['shares']} ${t['price']:.2f}{pnl_cell} {t.get('reason','')[:40]}{t['timestamp'][:16]} """ chart_labels = json.dumps([s["date"] for s in snapshots]) chart_values = json.dumps([s["total_value"] for s in snapshots]) starting = game.get("starting_cash", 100000) # Trade form (only for humans) trade_form = "" if is_ai else f"""

๐Ÿ“ Place Trade

Trades execute at current market price via Yahoo Finance

""" html = f""" {username} - {game['name']} {nav()}
โ† {game['name']}

{username} {badge}

Portfolio Value

${p['total_value']:,.2f}
Started at ${starting:,.0f}

Cash

${p['cash']:,.2f}
{p['cash']/max(p['total_value'],1)*100:.1f}% available

Return

{p['pnl_pct']:+.2f}%
${p['total_pnl']:+,.2f}

Positions

{p['num_positions']}
{len(trades)} total trades

Performance

{trade_form}

Positions

{pos_rows}
TickerSharesAvg CostPriceValueP&LStop

Trade Log

{trade_rows if trade_rows else ''}
ActionTickerSharesPriceP&LReasonTime
No trades yet
""" self.send_html(html) def serve_scans(self): rows = "" if os.path.exists(SCANS_DIR): for sf in sorted(os.listdir(SCANS_DIR), reverse=True)[:30]: if not sf.endswith(".json"): continue data = {} with open(os.path.join(SCANS_DIR, sf)) as f: data = json.load(f) n = data.get("candidates_found", len(data.get("candidates", []))) top = ", ".join(c.get("ticker","?") for c in data.get("candidates", [])[:8]) rows += f'{sf.replace(".json","")}{data.get("total_scanned",0)}{n}{top or "โ€”"}' html = f""" Scans - Market Watch {nav('scans')}

๐Ÿ“ก GARP Scan History

{rows if rows else ''}
DateScannedCandidatesTop Picks
No scans yet
""" self.send_html(html) def redirect(self, url): self.send_response(303) self.send_header("Location", url) self.end_headers() def send_html(self, content): self.send_response(200) self.send_header("Content-type", "text/html") self.end_headers() self.wfile.write(content.encode()) def send_json(self, data): self.send_response(200) self.send_header("Content-type", "application/json") self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps(data, default=str).encode()) def log_message(self, format, *args): pass def parse_form(body): """Parse URL-encoded form data.""" result = {} for pair in body.split("&"): if "=" in pair: k, v = pair.split("=", 1) from urllib.parse import unquote_plus result[unquote_plus(k)] = unquote_plus(v) return result def main(): game_engine.ensure_default_game() print(f"๐Ÿ“Š Market Watch Portal starting on localhost:{PORT}") server = ThreadedHTTPServer(("0.0.0.0", PORT), MarketWatchHandler) try: server.serve_forever() except KeyboardInterrupt: print("\nPortal stopped") if __name__ == "__main__": main()