Files
workspace/projects/market-watch/portal/server.py

175 lines
6.8 KiB
Python
Executable File

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