Spaces:
Paused
Paused
| """HTTP API over the trading floor, for a separate frontend to consume. | |
| The Gradio dashboard in demo/ reads accounts.db in-process. This serves the same | |
| data as JSON so a decoupled web frontend can render it. Everything here is | |
| read-only; the trading floor writes the database out of band. | |
| Run it from the 6_mcp directory so it shares the engine's accounts.db: | |
| uv run uvicorn backend.api:app --port 8000 | |
| """ | |
| from pathlib import Path | |
| from fastapi import FastAPI, HTTPException | |
| from fastapi.staticfiles import StaticFiles | |
| from backend import market | |
| from backend.accounts import Account | |
| from backend.database import read_log | |
| from backend.trading_floor import names, lastnames, short_model_names | |
| # Mirrors the log colours in demo/ so the frontend reproduces the same panel. | |
| LOG_COLORS = { | |
| "trace": "#87CEEB", | |
| "agent": "#00dddd", | |
| "function": "#00dd00", | |
| "generation": "#dddd00", | |
| "response": "#aa00dd", | |
| "account": "#dd0000", | |
| } | |
| DEFAULT_LOG_COLOR = "#87CEEB" | |
| roster = [ | |
| {"name": name, "lastname": lastname, "model_name": model_name} | |
| for name, lastname, model_name in zip(names, lastnames, short_model_names) | |
| ] | |
| roster_by_name = {trader["name"].lower(): trader for trader in roster} | |
| app = FastAPI(title="Trading Floor") | |
| def average_cost(account: Account, symbol: str) -> float: | |
| """Average price paid across this symbol's buys, for per-holding profit.""" | |
| spend = sum(t.price * t.quantity for t in account.transactions if t.symbol == symbol and t.quantity > 0) | |
| bought = sum(t.quantity for t in account.transactions if t.symbol == symbol and t.quantity > 0) | |
| return spend / bought if bought else 0.0 | |
| def holdings_detail(account: Account) -> list[dict]: | |
| """Current holdings enriched with price, market value and unrealised profit.""" | |
| details = [] | |
| for symbol, quantity in account.holdings.items(): | |
| price = market.get_share_price(symbol) | |
| cost = average_cost(account, symbol) | |
| details.append( | |
| { | |
| "symbol": symbol, | |
| "quantity": quantity, | |
| "price": price, | |
| "avg_cost": cost, | |
| "market_value": price * quantity, | |
| "unrealized_pnl": (price - cost) * quantity, | |
| } | |
| ) | |
| return details | |
| def require_trader(name: str) -> dict: | |
| trader = roster_by_name.get(name.lower()) | |
| if not trader: | |
| raise HTTPException(status_code=404, detail=f"Unknown trader {name}") | |
| return trader | |
| def get_traders() -> list[dict]: | |
| """The four traders on the floor.""" | |
| return roster | |
| def get_market() -> dict: | |
| """Which price source is live, and whether the market is open.""" | |
| source = "massive" if market.massive_api_key else "simulator" | |
| return {"source": source, "is_market_open": market.is_market_open()} | |
| def get_trader(name: str) -> dict: | |
| """A trader's full state: value, profit, holdings, transactions and history.""" | |
| trader = require_trader(name) | |
| account = Account.get(name) | |
| holdings = holdings_detail(account) | |
| portfolio_value = account.balance + sum(h["market_value"] for h in holdings) | |
| return { | |
| "name": trader["name"], | |
| "lastname": trader["lastname"], | |
| "model_name": trader["model_name"], | |
| "balance": account.balance, | |
| "strategy": account.strategy, | |
| "portfolio_value": portfolio_value, | |
| "pnl": account.calculate_profit_loss(portfolio_value), | |
| "holdings": holdings, | |
| "transactions": account.list_transactions(), | |
| "time_series": [{"datetime": ts, "value": value} for ts, value in account.portfolio_value_time_series], | |
| } | |
| def get_trader_logs(name: str, last_n: int = 13) -> list[dict]: | |
| """Recent trace and account log lines, oldest first, with their panel colour.""" | |
| require_trader(name) | |
| rows = list(read_log(name, last_n)) | |
| return [ | |
| {"datetime": ts, "type": kind, "message": message, "color": LOG_COLORS.get(kind, DEFAULT_LOG_COLOR)} | |
| for ts, kind, message in rows | |
| ] | |
| static_dir = Path(__file__).resolve().parent.parent / "frontend" / "dist" | |
| if static_dir.is_dir(): | |
| app.mount("/", StaticFiles(directory=str(static_dir), html=True), name="frontend") | |