#!/usr/bin/env python3 """Market Watch Web Portal — modern dark-themed dashboard.""" import json import os import sys import traceback 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 PORTAL_DIR = os.path.dirname(os.path.abspath(__file__)) class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): daemon_threads = True def _fetch_live_prices(tickers): """Fetch live prices via yfinance. Returns {ticker: price}.""" try: import yfinance as yf data = yf.download(tickers, period="1d", progress=False) prices = {} if len(tickers) == 1: t = tickers[0] if "Close" in data.columns and len(data) > 0: prices[t] = float(data["Close"].iloc[-1]) else: if "Close" in data.columns: for t in tickers: try: val = data["Close"][t].iloc[-1] if val == val: # not NaN prices[t] = float(val) except Exception: pass return prices except Exception: return {} class Handler(BaseHTTPRequestHandler): def do_GET(self): try: path = urlparse(self.path).path.rstrip("/") or "/" if path == "/": return self._serve_file("index.html", "text/html") # API endpoints if path == "/api/games": games = game_engine.list_games(active_only=False) # Enrich with summary for g in games: board = game_engine.get_leaderboard(g["game_id"]) g["leaderboard"] = board trades_all = [] for p in g.get("players", []): trades_all.extend(game_engine.get_trades(g["game_id"], p)) g["total_trades"] = len(trades_all) sells = [t for t in trades_all if t.get("action") == "SELL"] wins = [t for t in sells if t.get("realized_pnl", 0) > 0] g["win_rate"] = round(len(wins)/len(sells)*100, 1) if sells else None return self._json(games) # /api/game/{id} parts = path.split("/") if len(parts) >= 4 and parts[1] == "api" and parts[2] == "game": gid = parts[3] if len(parts) == 4: game = game_engine.get_game(gid) if not game: return self._json({"error": "not found"}, 404) game["leaderboard"] = game_engine.get_leaderboard(gid) # Add snapshots for each player game["snapshots"] = {} for p in game.get("players", []): game["snapshots"][p] = game_engine.get_snapshots(gid, p) return self._json(game) if len(parts) == 5 and parts[4] == "trades": game = game_engine.get_game(gid) if not game: return self._json({"error": "not found"}, 404) all_trades = [] for p in game.get("players", []): for t in game_engine.get_trades(gid, p): t["player"] = p all_trades.append(t) all_trades.sort(key=lambda x: x.get("timestamp", ""), reverse=True) return self._json(all_trades) if len(parts) == 5 and parts[4] == "portfolio": game = game_engine.get_game(gid) if not game: return self._json({"error": "not found"}, 404) portfolios = {} all_tickers = [] for p in game.get("players", []): pf = game_engine.get_portfolio(gid, p) if pf: portfolios[p] = pf all_tickers.extend(pf["positions"].keys()) # Fetch live prices all_tickers = list(set(all_tickers)) if all_tickers: live = _fetch_live_prices(all_tickers) for p, pf in portfolios.items(): total_value = pf["cash"] for ticker, pos in pf["positions"].items(): if ticker in live: pos["live_price"] = live[ticker] pos["current_price"] = live[ticker] pos["unrealized_pnl"] = round((live[ticker] - pos["avg_cost"]) * pos["shares"], 2) pos["market_value"] = round(live[ticker] * pos["shares"], 2) total_value += pos["market_value"] pf["total_value"] = round(total_value, 2) starting = game.get("starting_cash", 100000) pf["total_pnl"] = round(total_value - starting, 2) pf["pnl_pct"] = round((total_value - starting) / starting * 100, 2) return self._json(portfolios) self._error(404) except Exception as e: self._json({"error": str(e), "trace": traceback.format_exc()}, 500) def _serve_file(self, filename, content_type): filepath = os.path.join(PORTAL_DIR, filename) with open(filepath, "rb") as f: data = f.read() self.send_response(200) self.send_header("Content-Type", content_type) self.end_headers() self.wfile.write(data) def _json(self, data, code=200): body = json.dumps(data, default=str).encode() self.send_response(code) self.send_header("Content-Type", "application/json") self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(body) def _error(self, code): self.send_response(code) self.send_header("Content-Type", "text/plain") self.end_headers() self.wfile.write(f"{code}".encode()) def log_message(self, fmt, *args): pass def main(): game_engine.ensure_default_game() print(f"📊 Market Watch Portal → http://localhost:{PORT}") server = ThreadedHTTPServer(("0.0.0.0", PORT), Handler) try: server.serve_forever() except KeyboardInterrupt: print("\nStopped") if __name__ == "__main__": main()