164 lines
6.1 KiB
Python
164 lines
6.1 KiB
Python
from fastapi import APIRouter, Depends, HTTPException
|
|
from typing import List, Any, Optional
|
|
from pathlib import Path
|
|
import json
|
|
from agent_os.backend.dependencies import get_current_user, get_db_client
|
|
from tradingagents.portfolio.supabase_client import SupabaseClient
|
|
from tradingagents.portfolio.exceptions import PortfolioNotFoundError
|
|
from tradingagents.report_paths import get_market_dir
|
|
import datetime
|
|
|
|
router = APIRouter(prefix="/api/portfolios", tags=["portfolios"])
|
|
|
|
def _resolve_portfolio_id(portfolio_id: str, db: SupabaseClient) -> str:
|
|
"""Resolves the 'main_portfolio' alias to the first available portfolio ID."""
|
|
if portfolio_id == "main_portfolio":
|
|
portfolios = db.list_portfolios()
|
|
if portfolios:
|
|
return portfolios[0].portfolio_id
|
|
else:
|
|
raise PortfolioNotFoundError("No portfolios found to resolve 'main_portfolio' alias.")
|
|
return portfolio_id
|
|
|
|
@router.get("/")
|
|
async def list_portfolios(
|
|
user: dict = Depends(get_current_user),
|
|
db: SupabaseClient = Depends(get_db_client)
|
|
):
|
|
portfolios = db.list_portfolios()
|
|
return [p.to_dict() for p in portfolios]
|
|
|
|
@router.get("/{portfolio_id}")
|
|
async def get_portfolio(
|
|
portfolio_id: str,
|
|
user: dict = Depends(get_current_user),
|
|
db: SupabaseClient = Depends(get_db_client)
|
|
):
|
|
try:
|
|
portfolio_id = _resolve_portfolio_id(portfolio_id, db)
|
|
portfolio = db.get_portfolio(portfolio_id)
|
|
return portfolio.to_dict()
|
|
except PortfolioNotFoundError:
|
|
raise HTTPException(status_code=404, detail="Portfolio not found")
|
|
|
|
@router.get("/{portfolio_id}/summary")
|
|
async def get_portfolio_summary(
|
|
portfolio_id: str,
|
|
date: Optional[str] = None,
|
|
user: dict = Depends(get_current_user),
|
|
db: SupabaseClient = Depends(get_db_client)
|
|
):
|
|
"""Returns the 'Top 3 Metrics' for the dashboard header."""
|
|
if not date:
|
|
date = datetime.datetime.now().strftime("%Y-%m-%d")
|
|
|
|
try:
|
|
portfolio_id = _resolve_portfolio_id(portfolio_id, db)
|
|
# 1. Sharpe & Drawdown from latest snapshot
|
|
snapshot = db.get_latest_snapshot(portfolio_id)
|
|
sharpe = 0.0
|
|
drawdown = 0.0
|
|
|
|
if snapshot and snapshot.metadata:
|
|
# Try to get calculated risk metrics from snapshot metadata
|
|
risk = snapshot.metadata.get("risk_metrics", {})
|
|
sharpe = risk.get("sharpe", 0.0)
|
|
drawdown = risk.get("max_drawdown", 0.0)
|
|
|
|
# 2. Market Regime from latest scan summary
|
|
regime = "NEUTRAL"
|
|
beta = 1.0
|
|
|
|
scan_path = get_market_dir(date) / "scan_summary.json"
|
|
if scan_path.exists():
|
|
try:
|
|
scan_data = json.loads(scan_path.read_text())
|
|
ctx = scan_data.get("macro_context", {})
|
|
regime = ctx.get("economic_cycle", "NEUTRAL").upper()
|
|
# Beta is often calculated per-portfolio or per-holding
|
|
# For now, we use a placeholder or pull from metadata
|
|
except:
|
|
pass
|
|
|
|
return {
|
|
"sharpe_ratio": sharpe or 2.42, # Fallback to demo values if 0
|
|
"market_regime": regime,
|
|
"beta": beta,
|
|
"drawdown": drawdown or -2.4,
|
|
"var_1d": 4200.0, # Placeholder
|
|
"efficiency_label": "High Efficiency" if sharpe > 2.0 else "Normal"
|
|
}
|
|
except Exception as e:
|
|
# Fallback for demo
|
|
return {
|
|
"sharpe_ratio": 2.42,
|
|
"market_regime": "BULL",
|
|
"beta": 1.15,
|
|
"drawdown": -2.4,
|
|
"var_1d": 4200.0,
|
|
"efficiency_label": "High Efficiency"
|
|
}
|
|
|
|
@router.get("/{portfolio_id}/latest")
|
|
async def get_latest_portfolio_state(
|
|
portfolio_id: str,
|
|
user: dict = Depends(get_current_user),
|
|
db: SupabaseClient = Depends(get_db_client)
|
|
):
|
|
try:
|
|
portfolio_id = _resolve_portfolio_id(portfolio_id, db)
|
|
portfolio = db.get_portfolio(portfolio_id)
|
|
snapshot = db.get_latest_snapshot(portfolio_id)
|
|
holdings = db.list_holdings(portfolio_id)
|
|
trades = db.list_trades(portfolio_id, limit=10)
|
|
|
|
# Map portfolio fields to the shape the frontend expects
|
|
p = portfolio.to_dict()
|
|
portfolio_out = {
|
|
"id": p.get("portfolio_id", ""),
|
|
"name": p.get("name", ""),
|
|
"cash_balance": p.get("cash", 0.0),
|
|
**{k: v for k, v in p.items() if k not in ("portfolio_id", "name", "cash")},
|
|
}
|
|
|
|
# Map holdings: shares→quantity, include computed fields
|
|
holdings_out = []
|
|
for h in holdings:
|
|
d = h.to_dict()
|
|
market_value = (h.current_value or 0.0) if h.current_value is not None else 0.0
|
|
unrealized_pnl = (h.unrealized_pnl or 0.0) if h.unrealized_pnl is not None else 0.0
|
|
holdings_out.append({
|
|
"ticker": d.get("ticker", ""),
|
|
"quantity": d.get("shares", 0),
|
|
"avg_cost": d.get("avg_cost", 0.0),
|
|
"current_price": h.current_price if h.current_price is not None else 0.0,
|
|
"market_value": market_value,
|
|
"unrealized_pnl": unrealized_pnl,
|
|
"sector": d.get("sector"),
|
|
})
|
|
|
|
# Map trades: shares→quantity, trade_date→executed_at
|
|
trades_out = []
|
|
for t in trades:
|
|
d = t.to_dict()
|
|
trades_out.append({
|
|
"id": d.get("trade_id", ""),
|
|
"ticker": d.get("ticker", ""),
|
|
"action": d.get("action", ""),
|
|
"quantity": d.get("shares", 0),
|
|
"price": d.get("price", 0.0),
|
|
"executed_at": d.get("trade_date", ""),
|
|
"rationale": d.get("rationale"),
|
|
"stop_loss": d.get("stop_loss"),
|
|
"take_profit": d.get("take_profit"),
|
|
})
|
|
|
|
return {
|
|
"portfolio": portfolio_out,
|
|
"snapshot": snapshot.to_dict() if snapshot else None,
|
|
"holdings": holdings_out,
|
|
"recent_trades": trades_out,
|
|
}
|
|
except PortfolioNotFoundError:
|
|
raise HTTPException(status_code=404, detail="Portfolio not found")
|