Files
Krikorios 48e60d015f feat: Add Phase 4 advanced metrics and components
- Add advanced metrics dashboard with trade analytics
- Add new trading components (EntryTypeAnalysis, MultiDayPositionTracker, NewsEventTracker, etc.)
- Add strategy mode selector and trend confirmation
- Add risk automation panel and slippage correlation analysis
- Add daily trading plan enhancements with modal components
- Add custom hooks (useApi, useLocalStorage, useAdvancedTradeMetrics)
- Add broker service integration and trading API
- Add test setup and vitest configuration
- Include parquet data files for live market data
- Add comprehensive documentation in docs/ folder
2025-11-27 10:23:58 +02:00

414 lines
14 KiB
Python

"""
Live Performance Dashboard API - Real-time plan monitoring and alerts
"""
from fastapi import APIRouter, HTTPException, Depends, Query
from sqlalchemy.orm import Session
from typing import Any, Dict, List, Optional, Literal
from datetime import datetime, date, timezone
from pydantic import BaseModel, Field
from app.db.database import get_db
from app.models.models import DailyChecklist, UserProfile
from app.services.simulation_state import load_simulation_state
router = APIRouter(prefix="/api/live-dashboard", tags=["Live Dashboard"])
class DailyPlanStatus(BaseModel):
"""Current status of today's trading plan"""
date: str
target: float
actual_pnl: float
progress_percent: float
max_loss: float
current_drawdown: float
max_trades: int
actual_trades: int
trades_remaining: int
status: Literal["on-track", "near-limit", "limit-reached", "target-met"]
alerts: List[str]
class PerformanceWidget(BaseModel):
"""Sticky dashboard widget data"""
daily_plan: DailyPlanStatus
position_summary: Dict
risk_metrics: Dict
alerts: List[Dict]
recommendations: List[str]
class AlertConfig(BaseModel):
"""Alert configuration"""
alert_type: str # trade_limit, loss_limit, target_achieved, break_recommended
enabled: bool
threshold: Optional[float] = None
message: str
# In-memory simulation state (shared with trading.py)
def _get_today_plan_from_storage() -> Optional[Dict]:
"""Get today's trading plan blueprint (defaults until persistence is added)."""
# In production, this would query the database
# For now, we'll use a default plan structure
return {
"date": date.today().isoformat(),
"daily_target": 500.0,
"max_loss": 250.0,
"max_trades": 3,
"bias": "NEUTRAL",
}
def _calculate_daily_pnl(trades: List[Dict[str, Any]], target_date: date | None = None) -> float:
"""Calculate P&L for trades executed on the target date"""
target_date = target_date or date.today()
daily_pnl = 0.0
for trade in trades:
trade_ts = trade.get("timestamp", 0)
trade_date = datetime.fromtimestamp(trade_ts, tz=timezone.utc).date()
if trade_date == target_date:
pnl = trade.get("pnl", 0.0)
if pnl:
daily_pnl += pnl
return daily_pnl
def _count_today_trades(trades: List[Dict[str, Any]], target_date: date | None = None) -> int:
"""Count trades executed on the target date"""
target_date = target_date or date.today()
count = 0
for trade in trades:
trade_ts = trade.get("timestamp", 0)
trade_date = datetime.fromtimestamp(trade_ts, tz=timezone.utc).date()
if trade_date == target_date:
count += 1
return count
def _generate_alerts(plan: Dict, actual_pnl: float, trades_count: int) -> List[str]:
"""Generate smart alerts based on plan vs actual"""
alerts = []
target = plan.get("daily_target", 500.0)
max_loss = plan.get("max_loss", 250.0)
max_trades = plan.get("max_trades", 3)
# Trade limit alerts
trades_remaining = max_trades - trades_count
if trades_remaining == 1:
alerts.append(f"⚠️ Only 1 trade remaining before daily limit")
elif trades_remaining <= 0:
alerts.append(f"🛑 Daily trade limit reached ({max_trades} trades)")
# Loss alerts
if actual_pnl < 0:
loss_percent = (abs(actual_pnl) / max_loss) * 100
if loss_percent >= 100:
alerts.append(f"🚨 Max loss limit reached (${abs(actual_pnl):.2f})")
elif loss_percent >= 80:
alerts.append(f"⚠️ Near max loss limit ({loss_percent:.0f}% of ${max_loss})")
elif loss_percent >= 50:
alerts.append(f"⚡ Drawdown at {loss_percent:.0f}% of max loss")
# Target achievement alerts
if actual_pnl > 0:
progress_percent = (actual_pnl / target) * 100
if progress_percent >= 100:
alerts.append(f"🎉 Daily target achieved! (+${actual_pnl:.2f})")
elif progress_percent >= 80:
alerts.append(f"🎯 ${target - actual_pnl:.2f} away from daily target")
# Trading duration alerts (if 2+ hours and significant losses)
if trades_count >= 2 and actual_pnl < -100:
alerts.append(f"💡 Consider taking a break. ${abs(actual_pnl):.2f} in losses after {trades_count} trades")
return alerts
def _determine_status(
actual_pnl: float,
target: float,
max_loss: float,
trades_count: int,
max_trades: int
) -> Literal["on-track", "near-limit", "limit-reached", "target-met"]:
"""Determine overall plan status"""
# Target met
if actual_pnl >= target:
return "target-met"
# Limits reached
if trades_count >= max_trades:
return "limit-reached"
if actual_pnl <= -max_loss:
return "limit-reached"
# Near limits
loss_percent = (abs(actual_pnl) / max_loss) * 100 if actual_pnl < 0 else 0
trades_percent = (trades_count / max_trades) * 100
if loss_percent >= 80 or trades_percent >= 80:
return "near-limit"
# On track
return "on-track"
@router.get("/status", response_model=DailyPlanStatus)
async def get_dashboard_status(db: Session = Depends(get_db)) -> DailyPlanStatus:
"""
Get current status of today's trading plan with real-time metrics
"""
try:
plan = _get_today_plan_from_storage()
if not plan:
raise HTTPException(status_code=404, detail="No trading plan found for today")
state = load_simulation_state(db)
trades = state.get("trades", [])
actual_pnl = _calculate_daily_pnl(trades)
trades_count = _count_today_trades(trades)
target = plan.get("daily_target", 500.0)
max_loss = plan.get("max_loss", 250.0)
max_trades = plan.get("max_trades", 3)
progress_percent = (actual_pnl / target) * 100 if target > 0 else 0
current_drawdown = abs(actual_pnl) if actual_pnl < 0 else 0
trades_remaining = max(0, max_trades - trades_count)
alerts = _generate_alerts(plan, actual_pnl, trades_count)
status = _determine_status(actual_pnl, target, max_loss, trades_count, max_trades)
return DailyPlanStatus(
date=plan["date"],
target=target,
actual_pnl=actual_pnl,
progress_percent=round(progress_percent, 1),
max_loss=max_loss,
current_drawdown=current_drawdown,
max_trades=max_trades,
actual_trades=trades_count,
trades_remaining=trades_remaining,
status=status,
alerts=alerts,
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to get dashboard status: {str(e)}"
)
@router.get("/widget", response_model=PerformanceWidget)
async def get_performance_widget(db: Session = Depends(get_db)) -> PerformanceWidget:
"""
Get complete performance widget data for sticky dashboard
"""
try:
# Get daily plan status
daily_plan = await get_dashboard_status(db=db)
state = load_simulation_state(db)
position = state.get("position")
cash = float(state.get("cash", 100000.0))
position_value = 0.0
if position:
position_value = float(position.get("quantity", 0.0)) * float(position.get("avg_price", 0.0))
total_equity = cash + position_value
position_summary = {
"has_position": position is not None,
"quantity": float(position.get("quantity", 0.0)) if position else 0,
"avg_price": float(position.get("avg_price", 0.0)) if position else 0,
"cash": cash,
"total_equity": total_equity,
}
# Calculate risk metrics
initial_capital = float(state.get("initial_capital", 100000.0))
safe_equity = total_equity if total_equity != 0 else 1
total_return = ((total_equity - initial_capital) / initial_capital) * 100 if initial_capital else 0
risk_metrics = {
"total_equity": total_equity,
"total_return_percent": round(total_return, 2),
"position_size_percent": round((position_value / safe_equity * 100), 2) if position else 0,
"cash_percent": round((cash / safe_equity * 100), 2),
}
# Generate smart recommendations
recommendations = []
if daily_plan.status == "target-met":
recommendations.append("🎉 Consider closing for the day - target achieved!")
elif daily_plan.status == "limit-reached":
recommendations.append("🛑 Trading halt recommended - daily limits reached")
elif daily_plan.status == "near-limit":
if daily_plan.trades_remaining == 1:
recommendations.append("⚠️ Last trade available - make it count")
if daily_plan.current_drawdown > daily_plan.max_loss * 0.8:
recommendations.append("🔻 Consider defensive position sizing")
else:
if daily_plan.actual_pnl > daily_plan.target * 0.7:
recommendations.append("🎯 Near target - consider taking profits")
# Alert objects with metadata
alert_objects = [
{
"type": "info",
"message": alert,
"timestamp": datetime.now(timezone.utc).isoformat(),
}
for alert in daily_plan.alerts
]
return PerformanceWidget(
daily_plan=daily_plan,
position_summary=position_summary,
risk_metrics=risk_metrics,
alerts=alert_objects,
recommendations=recommendations,
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to get performance widget: {str(e)}"
)
@router.post("/check-limits")
async def check_trading_limits(db: Session = Depends(get_db)) -> Dict:
"""
Check if trading should be halted based on plan limits
Returns: {can_trade: bool, reason: str}
"""
try:
plan = _get_today_plan_from_storage()
if not plan:
return {"can_trade": True, "reason": "No plan configured"}
state = load_simulation_state(db)
trades = state.get("trades", [])
actual_pnl = _calculate_daily_pnl(trades)
trades_count = _count_today_trades(trades)
max_loss = plan.get("max_loss", 250.0)
max_trades = plan.get("max_trades", 3)
target = plan.get("daily_target", 500.0)
if actual_pnl <= -max_loss:
return {
"can_trade": False,
"reason": f"Max loss limit reached (${abs(actual_pnl):.2f})",
"limit_type": "loss",
}
if trades_count >= max_trades:
return {
"can_trade": False,
"reason": f"Max trades limit reached ({trades_count}/{max_trades})",
"limit_type": "trades",
}
if actual_pnl >= target:
return {
"can_trade": True,
"reason": f"Target achieved (+${actual_pnl:.2f}) - consider closing for the day",
"warning": True,
}
return {
"can_trade": True,
"reason": "Within limits",
"remaining_trades": max_trades - trades_count,
"remaining_loss_buffer": max_loss + actual_pnl,
}
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to check trading limits: {str(e)}"
)
@router.get("/session-summary")
async def get_session_summary(db: Session = Depends(get_db)) -> Dict:
"""
Get end-of-day session summary with AI coaching suggestions
"""
try:
plan = _get_today_plan_from_storage()
state = load_simulation_state(db)
trades = state.get("trades", [])
actual_pnl = _calculate_daily_pnl(trades)
trades_count = _count_today_trades(trades)
if not plan:
raise HTTPException(status_code=404, detail="No trading plan found")
target = plan.get("daily_target", 500.0)
max_loss = plan.get("max_loss", 250.0)
target_achieved = actual_pnl >= target
within_limits = actual_pnl > -max_loss and trades_count <= plan.get("max_trades", 3)
today = date.today()
today_trades = [
t for t in trades
if datetime.fromtimestamp(t.get("timestamp", 0), tz=timezone.utc).date() == today
]
winning_trades = sum(1 for t in today_trades if t.get("pnl", 0) > 0)
win_rate = (winning_trades / len(today_trades) * 100) if today_trades else 0
coaching = []
if target_achieved:
coaching.append("✅ Excellent discipline - you met your daily target!")
else:
deficit = target - actual_pnl
coaching.append(f"📊 ${deficit:.2f} short of target. Review your entry setups.")
if win_rate >= 60:
coaching.append(f"🎯 Strong win rate ({win_rate:.0f}%). Keep following your strategy.")
elif win_rate < 40:
coaching.append(f"⚠️ Low win rate ({win_rate:.0f}%). Review your trade selection criteria.")
if not within_limits:
coaching.append("🔻 Limits exceeded. Focus on risk management tomorrow.")
if trades_count > plan.get("max_trades", 3):
coaching.append("⚠️ Over-trading detected. Stick to your max trades limit.")
return {
"date": plan["date"],
"summary": {
"target": target,
"actual_pnl": actual_pnl,
"target_achieved": target_achieved,
"within_limits": within_limits,
"trades_count": trades_count,
"win_rate": round(win_rate, 1),
},
"coaching": coaching,
"next_session_suggestions": [
"Review today's winning trades for patterns",
"Adjust stop loss strategy if needed",
"Focus on high-probability setups only",
],
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to generate session summary: {str(e)}"
)