from __future__ import annotations from fastapi import APIRouter, Query from typing import Any, Dict, List, Optional import math from app.api.trading import simulation_state from app.streaming.live_store import live_store router = APIRouter(tags=["Performance"]) # paths mounted at /api def _equity_from_live(symbol: str = "XAU/USD", timeframe: str = "1m", limit: int = 300) -> List[Dict[str, Any]]: bars = live_store.get_history(symbol, timeframe) if not bars: return [] if limit > 0: bars = bars[-limit:] cash = float(simulation_state.get("cash", 0.0)) qty = float(simulation_state.get("position", {}).get("quantity", 0.0) if simulation_state.get("position") else 0.0) out: List[Dict[str, Any]] = [] for b in bars: out.append({ "time": int(b["time"]), "equity": cash + qty * float(b["close"]), }) return out @router.get("/equity-history") async def equity_history(symbol: str = Query("XAU/USD"), timeframe: str = Query("1m"), limit: int = Query(300, ge=1, le=5000)) -> List[Dict[str, Any]]: # Prefer recorded equity history if available hist = simulation_state.get("equity_history") or [] if hist: if limit > 0: hist = hist[-limit:] return hist # Fallback: derive from current cash and open qty over historical closes return _equity_from_live(symbol, timeframe, limit) def _max_drawdown(eqs: List[float]) -> float: max_peak = -math.inf max_dd = 0.0 for v in eqs: if v > max_peak: max_peak = v dd = (max_peak - v) / max_peak if max_peak > 0 else 0.0 if dd > max_dd: max_dd = dd return max_dd @router.get("/performance") async def performance(symbol: str = Query("XAU/USD"), timeframe: str = Query("1m"), limit: int = Query(300, ge=10, le=5000)) -> Dict[str, Any]: series = await equity_history(symbol=symbol, timeframe=timeframe, limit=limit) if not series or len(series) < 2: return {"available": False} eq = [float(x["equity"]) for x in series] rets = [] for i in range(1, len(eq)): prev = eq[i-1] curr = eq[i] if prev > 0: rets.append(curr/prev - 1.0) if not rets: return {"available": False} avg = sum(rets) / len(rets) var = sum((r - avg)**2 for r in rets) / (len(rets) - 1) if len(rets) > 1 else 0.0 std = math.sqrt(var) downside = [r for r in rets if r < 0] if downside: d_avg = sum(downside) / len(downside) d_var = sum((r - d_avg)**2 for r in downside) / (len(downside) - 1) if len(downside) > 1 else 0.0 d_std = math.sqrt(d_var) else: d_std = 0.0 periods_per_year = { "1m": 365*24*60, "5m": 365*24*12, "1h": 365*24, "4h": 365*6, "1d": 365, }.get(timeframe, 365) sharpe = (avg/std*math.sqrt(periods_per_year)) if std > 0 else None sortino = (avg/d_std*math.sqrt(periods_per_year)) if d_std > 0 else None total_return = (eq[-1]/eq[0] - 1.0) if eq[0] > 0 else None mdd = _max_drawdown(eq) return { "available": True, "count": len(eq), "total_return": total_return, "sharpe": sharpe, "sortino": sortino, "max_drawdown": mdd, }