import os import time from fastapi import APIRouter, Request from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates from ib_async import IB import dependencies router = APIRouter(prefix="/portfolio") templates = Jinja2Templates(directory="templates") def _extract_summary_values(summary) -> dict: keys = {"NetLiquidation", "TotalCashValue", "UnrealizedPnL", "RealizedPnL"} result = {} for item in summary: if item.tag in keys: try: result[item.tag] = float(item.value) except (ValueError, TypeError): result[item.tag] = item.value return result def _extract_cached_account_values(ib) -> dict: getter = getattr(ib, "accountValues", None) if getter is None: return {} return _extract_summary_values(getter() or []) async def _read_summary_values(ib) -> dict: result = _extract_cached_account_values(ib) if result: return result summary = await ib.reqAccountSummaryAsync() result = _extract_summary_values(summary) if result: return result return _extract_cached_account_values(ib) async def _fetch_summary_with_fallback() -> dict: ib = dependencies.get_ib() result = await _read_summary_values(ib) if result: return result # A fresh client recovers account summary when the shared session returns an empty snapshot. fallback_ib = IB() host = os.getenv("IBKR_HOST", "127.0.0.1") port = int(os.getenv("IBKR_PORT", "7497")) client_id = int(time.time()) % 100000 + 5000 try: await fallback_ib.connectAsync(host, port, clientId=client_id) return await _read_summary_values(fallback_ib) finally: fallback_ib.disconnect() @router.get("", response_class=HTMLResponse) async def portfolio_page(request: Request): return templates.TemplateResponse(request, "portfolio.html", {}) @router.get("/data") async def portfolio_data(): try: ib = dependencies.get_ib() positions = ib.positions() return [ { "symbol": pos.contract.symbol, "position": pos.position, "avg_cost": pos.avgCost, } for pos in positions ] except Exception as e: return {"error": str(e)} @router.get("/pnl") async def portfolio_pnl(): try: return await _fetch_summary_with_fallback() except Exception as e: return {"error": str(e)}