Files
robinhood/backend/app/api/performance.py

101 lines
3.2 KiB
Python

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,
}