101 lines
3.2 KiB
Python
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,
|
|
} |