analytics.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. import logging
  2. from fastapi import APIRouter, Depends, HTTPException, Query
  3. from typing import List, Optional, Dict, Any
  4. from pydantic import BaseModel
  5. from datetime import datetime
  6. from src.stats.trading_stats import TradingStats
  7. from ..dependencies import get_stats
  8. logger = logging.getLogger(__name__)
  9. router = APIRouter(prefix="/analytics", tags=["analytics"])
  10. # Pydantic models for response data
  11. class OverallStats(BaseModel):
  12. balance: float
  13. initial_balance: float
  14. total_pnl: float
  15. total_return_pct: float
  16. win_rate: float
  17. profit_factor: float
  18. total_trades: int
  19. total_wins: int
  20. total_losses: int
  21. expectancy: float
  22. avg_trade_pnl: float
  23. avg_win_pnl: float
  24. avg_loss_pnl: float
  25. largest_win: float
  26. largest_loss: float
  27. largest_win_token: str
  28. largest_loss_token: str
  29. max_drawdown: float
  30. max_drawdown_pct: float
  31. best_token: str
  32. best_token_pnl: float
  33. worst_token: str
  34. worst_token_pnl: float
  35. total_volume: float
  36. class TokenStats(BaseModel):
  37. token: str
  38. total_pnl: float
  39. win_rate: float
  40. profit_factor: float
  41. total_trades: int
  42. winning_trades: int
  43. losing_trades: int
  44. roe_percentage: float
  45. entry_volume: float
  46. exit_volume: float
  47. avg_duration: str
  48. largest_win: float
  49. largest_loss: float
  50. class PeriodStats(BaseModel):
  51. period: str
  52. period_formatted: str
  53. has_trades: bool
  54. pnl: float
  55. pnl_pct: float
  56. roe: float
  57. trades: int
  58. volume: float
  59. class PerformanceMetrics(BaseModel):
  60. sharpe_ratio: Optional[float]
  61. max_consecutive_wins: int
  62. max_consecutive_losses: int
  63. avg_trade_duration: str
  64. best_roe_trade: Optional[Dict[str, Any]]
  65. worst_roe_trade: Optional[Dict[str, Any]]
  66. volatility: float
  67. risk_metrics: Dict[str, Any]
  68. @router.get("/stats", response_model=OverallStats)
  69. async def get_overall_stats(
  70. stats: TradingStats = Depends(get_stats)
  71. ):
  72. """Get overall trading statistics - equivalent to /stats command."""
  73. try:
  74. # Get performance stats
  75. perf_stats = stats.get_performance_stats()
  76. basic_stats = stats.get_basic_stats()
  77. return OverallStats(
  78. balance=basic_stats.get('current_balance', 0.0),
  79. initial_balance=perf_stats.get('initial_balance', 0.0),
  80. total_pnl=perf_stats.get('total_pnl', 0.0),
  81. total_return_pct=(perf_stats.get('total_pnl', 0.0) / max(perf_stats.get('initial_balance', 1.0), 1.0) * 100),
  82. win_rate=perf_stats.get('win_rate', 0.0),
  83. profit_factor=perf_stats.get('profit_factor', 0.0),
  84. total_trades=perf_stats.get('total_trades', 0),
  85. total_wins=perf_stats.get('total_wins', 0),
  86. total_losses=perf_stats.get('total_losses', 0),
  87. expectancy=perf_stats.get('expectancy', 0.0),
  88. avg_trade_pnl=perf_stats.get('avg_trade_pnl', 0.0),
  89. avg_win_pnl=perf_stats.get('avg_win_pnl', 0.0),
  90. avg_loss_pnl=perf_stats.get('avg_loss_pnl', 0.0),
  91. largest_win=perf_stats.get('largest_win', 0.0),
  92. largest_loss=perf_stats.get('largest_loss', 0.0),
  93. largest_win_token=perf_stats.get('largest_win_token', 'N/A'),
  94. largest_loss_token=perf_stats.get('largest_loss_token', 'N/A'),
  95. max_drawdown=perf_stats.get('max_drawdown', 0.0),
  96. max_drawdown_pct=perf_stats.get('max_drawdown_pct', 0.0),
  97. best_token=perf_stats.get('best_token', 'N/A'),
  98. best_token_pnl=perf_stats.get('best_token_pnl', 0.0),
  99. worst_token=perf_stats.get('worst_token', 'N/A'),
  100. worst_token_pnl=perf_stats.get('worst_token_pnl', 0.0),
  101. total_volume=perf_stats.get('total_entry_volume', 0.0)
  102. )
  103. except Exception as e:
  104. logger.error(f"Error getting overall stats: {e}")
  105. raise HTTPException(status_code=500, detail=f"Error getting overall stats: {str(e)}")
  106. @router.get("/stats/{token}", response_model=TokenStats)
  107. async def get_token_stats(
  108. token: str,
  109. stats: TradingStats = Depends(get_stats)
  110. ):
  111. """Get detailed statistics for a specific token - equivalent to /stats {token} command."""
  112. try:
  113. token_data = stats.get_token_detailed_stats(token.upper())
  114. if not token_data or token_data.get('summary_total_trades', 0) == 0:
  115. raise HTTPException(
  116. status_code=404,
  117. detail=f"No trading data found for {token.upper()}"
  118. )
  119. perf_summary = token_data.get('performance_summary', {})
  120. return TokenStats(
  121. token=token.upper(),
  122. total_pnl=perf_summary.get('total_pnl', 0.0),
  123. win_rate=perf_summary.get('win_rate', 0.0),
  124. profit_factor=perf_summary.get('profit_factor', 0.0),
  125. total_trades=perf_summary.get('completed_trades', 0),
  126. winning_trades=perf_summary.get('total_wins', 0),
  127. losing_trades=perf_summary.get('total_losses', 0),
  128. roe_percentage=(perf_summary.get('total_pnl', 0.0) / max(perf_summary.get('completed_entry_volume', 1.0), 1.0) * 100),
  129. entry_volume=perf_summary.get('completed_entry_volume', 0.0),
  130. exit_volume=perf_summary.get('completed_exit_volume', 0.0),
  131. avg_duration=perf_summary.get('avg_trade_duration', 'N/A'),
  132. largest_win=perf_summary.get('largest_win', 0.0),
  133. largest_loss=perf_summary.get('largest_loss', 0.0)
  134. )
  135. except HTTPException:
  136. raise
  137. except Exception as e:
  138. logger.error(f"Error getting token stats for {token}: {e}")
  139. raise HTTPException(status_code=500, detail=f"Error getting token stats: {str(e)}")
  140. @router.get("/performance", response_model=List[TokenStats])
  141. async def get_performance_ranking(
  142. limit: int = Query(20, ge=1, le=50),
  143. stats: TradingStats = Depends(get_stats)
  144. ):
  145. """Get performance ranking of all tokens - equivalent to /performance command."""
  146. try:
  147. token_performance = stats.get_token_performance(limit=limit)
  148. result = []
  149. for token_data in token_performance:
  150. result.append(TokenStats(
  151. token=token_data['token'],
  152. total_pnl=token_data.get('total_realized_pnl', 0.0),
  153. win_rate=token_data.get('win_rate', 0.0),
  154. profit_factor=token_data.get('profit_factor', 0.0),
  155. total_trades=token_data.get('total_completed_cycles', 0),
  156. winning_trades=token_data.get('winning_cycles', 0),
  157. losing_trades=token_data.get('losing_cycles', 0),
  158. roe_percentage=token_data.get('roe_percentage', 0.0),
  159. entry_volume=token_data.get('total_entry_volume', 0.0),
  160. exit_volume=token_data.get('total_exit_volume', 0.0),
  161. avg_duration=token_data.get('average_trade_duration_formatted', 'N/A'),
  162. largest_win=token_data.get('largest_winning_cycle_pnl', 0.0),
  163. largest_loss=token_data.get('largest_losing_cycle_pnl', 0.0)
  164. ))
  165. return result
  166. except Exception as e:
  167. logger.error(f"Error getting performance ranking: {e}")
  168. raise HTTPException(status_code=500, detail=f"Error getting performance ranking: {str(e)}")
  169. @router.get("/daily", response_model=List[PeriodStats])
  170. async def get_daily_stats(
  171. limit: int = Query(10, ge=1, le=30),
  172. stats: TradingStats = Depends(get_stats)
  173. ):
  174. """Get daily performance stats - equivalent to /daily command."""
  175. try:
  176. daily_stats = stats.get_daily_stats(limit=limit)
  177. result = []
  178. for day_data in daily_stats:
  179. result.append(PeriodStats(
  180. period=day_data['date'],
  181. period_formatted=day_data['date_formatted'],
  182. has_trades=day_data['has_trades'],
  183. pnl=day_data['pnl'],
  184. pnl_pct=day_data['pnl_pct'],
  185. roe=day_data['roe'],
  186. trades=day_data['trades'],
  187. volume=day_data.get('volume', 0.0)
  188. ))
  189. return result
  190. except Exception as e:
  191. logger.error(f"Error getting daily stats: {e}")
  192. raise HTTPException(status_code=500, detail=f"Error getting daily stats: {str(e)}")
  193. @router.get("/weekly", response_model=List[PeriodStats])
  194. async def get_weekly_stats(
  195. limit: int = Query(10, ge=1, le=20),
  196. stats: TradingStats = Depends(get_stats)
  197. ):
  198. """Get weekly performance stats - equivalent to /weekly command."""
  199. try:
  200. weekly_stats = stats.get_weekly_stats(limit=limit)
  201. result = []
  202. for week_data in weekly_stats:
  203. result.append(PeriodStats(
  204. period=week_data['week'],
  205. period_formatted=week_data['week_formatted'],
  206. has_trades=week_data['has_trades'],
  207. pnl=week_data['pnl'],
  208. pnl_pct=week_data['pnl_pct'],
  209. roe=week_data.get('roe', 0.0),
  210. trades=week_data['trades'],
  211. volume=week_data['volume']
  212. ))
  213. return result
  214. except Exception as e:
  215. logger.error(f"Error getting weekly stats: {e}")
  216. raise HTTPException(status_code=500, detail=f"Error getting weekly stats: {str(e)}")
  217. @router.get("/monthly", response_model=List[PeriodStats])
  218. async def get_monthly_stats(
  219. limit: int = Query(12, ge=1, le=24),
  220. stats: TradingStats = Depends(get_stats)
  221. ):
  222. """Get monthly performance stats - equivalent to /monthly command."""
  223. try:
  224. monthly_stats = stats.get_monthly_stats(limit=limit)
  225. result = []
  226. for month_data in monthly_stats:
  227. result.append(PeriodStats(
  228. period=month_data['month'],
  229. period_formatted=month_data['month_formatted'],
  230. has_trades=month_data['has_trades'],
  231. pnl=month_data['pnl'],
  232. pnl_pct=month_data['pnl_pct'],
  233. roe=month_data.get('roe', 0.0),
  234. trades=month_data['trades'],
  235. volume=month_data['volume']
  236. ))
  237. return result
  238. except Exception as e:
  239. logger.error(f"Error getting monthly stats: {e}")
  240. raise HTTPException(status_code=500, detail=f"Error getting monthly stats: {str(e)}")
  241. @router.get("/metrics", response_model=PerformanceMetrics)
  242. async def get_performance_metrics(
  243. stats: TradingStats = Depends(get_stats)
  244. ):
  245. """Get advanced performance metrics including risk analysis."""
  246. try:
  247. perf_stats = stats.get_performance_stats()
  248. risk_metrics = stats.performance_calculator.get_risk_metrics()
  249. # Calculate additional metrics
  250. best_roe = perf_stats.get('best_roe_trade', {})
  251. worst_roe = perf_stats.get('worst_roe_trade', {})
  252. return PerformanceMetrics(
  253. sharpe_ratio=risk_metrics.get('sharpe_ratio'),
  254. max_consecutive_wins=0, # TODO: Implement
  255. max_consecutive_losses=0, # TODO: Implement
  256. avg_trade_duration="N/A", # TODO: Calculate from stats
  257. best_roe_trade=best_roe if best_roe else None,
  258. worst_roe_trade=worst_roe if worst_roe else None,
  259. volatility=0.0, # TODO: Calculate
  260. risk_metrics=risk_metrics
  261. )
  262. except Exception as e:
  263. logger.error(f"Error getting performance metrics: {e}")
  264. raise HTTPException(status_code=500, detail=f"Error getting performance metrics: {str(e)}")
  265. @router.get("/balance-history")
  266. async def get_balance_history(
  267. days: int = Query(30, ge=1, le=365),
  268. stats: TradingStats = Depends(get_stats)
  269. ):
  270. """Get balance history for charting."""
  271. try:
  272. balance_data, balance_stats = stats.performance_calculator.get_balance_history(days=days)
  273. return {
  274. "balance_history": balance_data,
  275. "stats": balance_stats,
  276. "days": days
  277. }
  278. except Exception as e:
  279. logger.error(f"Error getting balance history: {e}")
  280. raise HTTPException(status_code=500, detail=f"Error getting balance history: {str(e)}")
  281. @router.get("/summary")
  282. async def get_analytics_summary(
  283. stats: TradingStats = Depends(get_stats)
  284. ):
  285. """Get analytics summary for overview page."""
  286. try:
  287. # Get key metrics
  288. perf_stats = stats.get_performance_stats()
  289. daily_stats = stats.get_daily_stats(limit=7)
  290. top_tokens = stats.get_token_performance(limit=5)
  291. # Calculate recent trend
  292. recent_pnl = sum(day['pnl'] for day in daily_stats if day['has_trades'])
  293. recent_trades = sum(day['trades'] for day in daily_stats if day['has_trades'])
  294. return {
  295. "performance_overview": {
  296. "total_pnl": perf_stats.get('total_pnl', 0.0),
  297. "win_rate": perf_stats.get('win_rate', 0.0),
  298. "profit_factor": perf_stats.get('profit_factor', 0.0),
  299. "total_trades": perf_stats.get('total_trades', 0),
  300. "max_drawdown_pct": perf_stats.get('max_drawdown_pct', 0.0)
  301. },
  302. "recent_performance": {
  303. "pnl_7d": recent_pnl,
  304. "trades_7d": recent_trades,
  305. "daily_avg": recent_pnl / 7 if recent_pnl else 0.0
  306. },
  307. "top_tokens": top_tokens[:3], # Top 3 performers
  308. "last_updated": datetime.now().isoformat()
  309. }
  310. except Exception as e:
  311. logger.error(f"Error getting analytics summary: {e}")
  312. raise HTTPException(status_code=500, detail=f"Error getting analytics summary: {str(e)}")