Phase 3: Advanced Analytics Foundation - Models, Schemas, and API

This commit is contained in:
Claude
2025-11-16 05:54:07 +00:00
parent 73a26ea9b7
commit e3f1a8079c
4 changed files with 708 additions and 1 deletions
+455
View File
@@ -0,0 +1,455 @@
"""
Phase 3: Advanced Analytics API Endpoints
Performance tracking, pattern analysis, and reporting
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import func
from datetime import datetime, date, timedelta
from typing import List, Optional
from app.db.database import get_db
from app.models.models import (
PerformanceSnapshot, TradePattern, LessonLearned, MonthlyReview, Trade
)
from app.schemas.schemas import (
PerformanceSnapshotCreate, PerformanceSnapshotResponse,
TradePatternCreate, TradePatternResponse,
LessonLearnedCreate, LessonLearnedResponse,
MonthlyReviewCreate, MonthlyReviewResponse
)
router = APIRouter(prefix="/api/analytics", tags=["Advanced Analytics"])
# ============================================================================
# PERFORMANCE SNAPSHOTS
# ============================================================================
@router.post("/snapshots", response_model=PerformanceSnapshotResponse, status_code=201)
async def create_performance_snapshot(
snapshot: PerformanceSnapshotCreate,
db: Session = Depends(get_db)
):
"""Create a performance snapshot"""
db_snapshot = PerformanceSnapshot(**snapshot.dict())
db.add(db_snapshot)
db.commit()
db.refresh(db_snapshot)
return db_snapshot
@router.get("/snapshots", response_model=List[PerformanceSnapshotResponse])
async def list_performance_snapshots(
start_date: Optional[str] = Query(None),
end_date: Optional[str] = Query(None),
limit: int = Query(30, ge=1, le=365),
db: Session = Depends(get_db)
):
"""List performance snapshots with optional date range"""
query = db.query(PerformanceSnapshot)
if start_date:
start = datetime.fromisoformat(start_date).date()
query = query.filter(PerformanceSnapshot.snapshot_date >= start)
if end_date:
end = datetime.fromisoformat(end_date).date()
query = query.filter(PerformanceSnapshot.snapshot_date <= end)
return query.order_by(PerformanceSnapshot.snapshot_date.desc()).limit(limit).all()
@router.get("/snapshots/stats/monthly")
async def get_monthly_stats(
year: int = Query(...),
month: int = Query(..., ge=1, le=12),
db: Session = Depends(get_db)
):
"""Get monthly aggregate statistics"""
snapshots = db.query(PerformanceSnapshot).filter(
func.extract('year', PerformanceSnapshot.snapshot_date) == year,
func.extract('month', PerformanceSnapshot.snapshot_date) == month
).all()
if not snapshots:
return {
"year": year,
"month": month,
"trading_days": 0,
"total_pnl": 0.0,
"avg_daily_pnl": 0.0,
"best_day_pnl": 0.0,
"worst_day_pnl": 0.0,
"win_rate": 0.0,
"total_trades": 0
}
total_pnl = sum(s.daily_pnl for s in snapshots)
total_trades = sum(s.total_trades for s in snapshots)
winning_days = sum(1 for s in snapshots if s.daily_pnl > 0)
trading_days = len(snapshots)
return {
"year": year,
"month": month,
"trading_days": trading_days,
"total_pnl": total_pnl,
"avg_daily_pnl": total_pnl / trading_days if trading_days > 0 else 0,
"best_day_pnl": max((s.daily_pnl for s in snapshots), default=0),
"worst_day_pnl": min((s.daily_pnl for s in snapshots), default=0),
"win_rate": (winning_days / trading_days * 100) if trading_days > 0 else 0,
"total_trades": total_trades,
"winning_days": winning_days,
"losing_days": trading_days - winning_days
}
@router.get("/snapshots/stats/yearly")
async def get_yearly_stats(
year: int = Query(...),
db: Session = Depends(get_db)
):
"""Get yearly aggregate statistics"""
snapshots = db.query(PerformanceSnapshot).filter(
func.extract('year', PerformanceSnapshot.snapshot_date) == year
).all()
if not snapshots:
return {"year": year, "message": "No data for this year"}
total_pnl = sum(s.daily_pnl for s in snapshots)
total_trades = sum(s.total_trades for s in snapshots)
winning_days = sum(1 for s in snapshots if s.daily_pnl > 0)
trading_days = len(snapshots)
return {
"year": year,
"trading_days": trading_days,
"total_pnl": total_pnl,
"avg_daily_pnl": total_pnl / trading_days if trading_days > 0 else 0,
"best_day": max((s.daily_pnl for s in snapshots), default=0),
"worst_day": min((s.daily_pnl for s in snapshots), default=0),
"win_rate": (winning_days / trading_days * 100) if trading_days > 0 else 0,
"total_trades": total_trades,
"best_month": None, # Can be calculated from monthly stats
"worst_month": None
}
# ============================================================================
# TRADE PATTERNS
# ============================================================================
@router.post("/patterns", response_model=TradePatternResponse, status_code=201)
async def create_pattern(
pattern: TradePatternCreate,
db: Session = Depends(get_db)
):
"""Identify and create a new trade pattern"""
db_pattern = TradePattern(**pattern.dict())
db.add(db_pattern)
db.commit()
db.refresh(db_pattern)
return db_pattern
@router.get("/patterns", response_model=List[TradePatternResponse])
async def list_patterns(
min_confidence: float = Query(0, ge=0, le=100),
min_sample_count: int = Query(3, ge=1),
db: Session = Depends(get_db)
):
"""List identified trade patterns"""
patterns = db.query(TradePattern).filter(
TradePattern.confidence_score >= min_confidence,
TradePattern.sample_count >= min_sample_count
).order_by(TradePattern.confidence_score.desc()).all()
return patterns
@router.get("/patterns/{pattern_id}", response_model=TradePatternResponse)
async def get_pattern(
pattern_id: int,
db: Session = Depends(get_db)
):
"""Get specific pattern details"""
pattern = db.query(TradePattern).filter(TradePattern.id == pattern_id).first()
if not pattern:
raise HTTPException(status_code=404, detail="Pattern not found")
return pattern
@router.get("/patterns/stats/best")
async def get_best_patterns(
limit: int = Query(5, ge=1, le=20),
db: Session = Depends(get_db)
):
"""Get your top performing patterns"""
patterns = db.query(TradePattern).order_by(
TradePattern.confidence_score.desc()
).limit(limit).all()
return [
{
"pattern": p.pattern_name,
"win_rate": p.win_rate,
"confidence": p.confidence_score,
"sample_size": p.sample_count,
"total_profit": p.total_profit,
"best_timeframe": p.best_timeframe,
"best_time": p.best_time_of_day
}
for p in patterns
]
# ============================================================================
# LESSONS LEARNED
# ============================================================================
@router.post("/lessons", response_model=LessonLearnedResponse, status_code=201)
async def create_lesson(
lesson: LessonLearnedCreate,
db: Session = Depends(get_db)
):
"""Log a lesson learned"""
db_lesson = LessonLearned(**lesson.dict())
db.add(db_lesson)
db.commit()
db.refresh(db_lesson)
return db_lesson
@router.get("/lessons", response_model=List[LessonLearnedResponse])
async def list_lessons(
category: Optional[str] = Query(None),
importance: Optional[str] = Query(None),
tag: Optional[str] = Query(None),
limit: int = Query(20, ge=1, le=100),
db: Session = Depends(get_db)
):
"""List lessons learned with optional filters"""
query = db.query(LessonLearned).filter(LessonLearned.status == "active")
if category:
query = query.filter(LessonLearned.category == category)
if importance:
query = query.filter(LessonLearned.importance == importance)
lessons = query.order_by(LessonLearned.date_learned.desc()).limit(limit).all()
# Filter by tag if specified
if tag:
lessons = [l for l in lessons if tag in l.tags]
return lessons
@router.get("/lessons/categories")
async def get_lesson_categories(db: Session = Depends(get_db)):
"""Get available lesson categories"""
categories = db.query(LessonLearned.category).distinct().all()
return {
"categories": [c[0] for c in categories if c[0]],
"available": ["entry", "exit", "risk", "psychology", "market"]
}
@router.get("/lessons/recurring-mistakes")
async def get_recurring_mistakes(
limit: int = Query(10, ge=1, le=20),
db: Session = Depends(get_db)
):
"""Identify recurring mistakes from lessons"""
negative_lessons = db.query(LessonLearned).filter(
LessonLearned.impact == "negative"
).order_by(LessonLearned.date_learned.desc()).all()
# Count tag occurrences
tag_counts = {}
for lesson in negative_lessons:
for tag in lesson.tags:
tag_counts[tag] = tag_counts.get(tag, 0) + 1
# Sort by frequency
recurring = sorted(tag_counts.items(), key=lambda x: x[1], reverse=True)
return {
"recurring_mistakes": recurring[:limit],
"total_negative_lessons": len(negative_lessons),
"recommendation": "Focus on preventing these recurring mistakes"
}
# ============================================================================
# MONTHLY REVIEWS
# ============================================================================
@router.post("/reviews/monthly", response_model=MonthlyReviewResponse, status_code=201)
async def create_monthly_review(
review: MonthlyReviewCreate,
db: Session = Depends(get_db)
):
"""Create a monthly performance review"""
# Check if review already exists
existing = db.query(MonthlyReview).filter(
MonthlyReview.year == review.year,
MonthlyReview.month == review.month
).first()
if existing:
raise HTTPException(
status_code=400,
detail=f"Monthly review for {review.year}-{review.month} already exists"
)
db_review = MonthlyReview(**review.dict())
db.add(db_review)
db.commit()
db.refresh(db_review)
return db_review
@router.get("/reviews/monthly", response_model=List[MonthlyReviewResponse])
async def list_monthly_reviews(
year: Optional[int] = Query(None),
limit: int = Query(12, ge=1, le=60),
db: Session = Depends(get_db)
):
"""List monthly reviews"""
query = db.query(MonthlyReview)
if year:
query = query.filter(MonthlyReview.year == year)
return query.order_by(
MonthlyReview.year.desc(),
MonthlyReview.month.desc()
).limit(limit).all()
@router.get("/reviews/quarterly")
async def get_quarterly_review(
year: int = Query(...),
quarter: int = Query(..., ge=1, le=4),
db: Session = Depends(get_db)
):
"""Get quarterly performance review"""
months = {
1: [1, 2, 3],
2: [4, 5, 6],
3: [7, 8, 9],
4: [10, 11, 12]
}
month_list = months[quarter]
reviews = db.query(MonthlyReview).filter(
MonthlyReview.year == year,
MonthlyReview.month.in_(month_list)
).all()
if not reviews:
return {"quarter": quarter, "year": year, "message": "No data"}
total_pnl = sum(r.total_pnl for r in reviews)
total_trades = sum(r.total_trades for r in reviews)
avg_win_rate = sum(r.win_rate for r in reviews) / len(reviews) if reviews else 0
return {
"quarter": quarter,
"year": year,
"months_covered": month_list,
"total_pnl": total_pnl,
"total_trades": total_trades,
"avg_win_rate": avg_win_rate,
"best_month": max((r.total_pnl for r in reviews), default=0),
"worst_month": min((r.total_pnl for r in reviews), default=0),
"monthly_reviews": [
{
"month": r.month,
"pnl": r.total_pnl,
"win_rate": r.win_rate,
"trades": r.total_trades
}
for r in reviews
]
}
# ============================================================================
# COMPREHENSIVE ANALYTICS DASHBOARD
# ============================================================================
@router.get("/dashboard")
async def get_analytics_dashboard(
period: str = Query("month", regex="^(week|month|quarter|year)$"),
db: Session = Depends(get_db)
):
"""Get comprehensive analytics dashboard"""
today = date.today()
# Determine date range
if period == "week":
start_date = today - timedelta(days=7)
elif period == "month":
start_date = today - timedelta(days=30)
elif period == "quarter":
start_date = today - timedelta(days=90)
else: # year
start_date = today - timedelta(days=365)
# Get snapshots for period
snapshots = db.query(PerformanceSnapshot).filter(
PerformanceSnapshot.snapshot_date >= start_date
).all()
# Get patterns
patterns = db.query(TradePattern).order_by(
TradePattern.confidence_score.desc()
).limit(5).all()
# Get recent lessons
lessons = db.query(LessonLearned).filter(
LessonLearned.status == "active"
).order_by(LessonLearned.date_learned.desc()).limit(5).all()
# Calculate metrics
total_pnl = sum(s.daily_pnl for s in snapshots)
total_trades = sum(s.total_trades for s in snapshots)
winning_days = sum(1 for s in snapshots if s.daily_pnl > 0)
avg_win_rate = sum(s.win_rate for s in snapshots) / len(snapshots) if snapshots else 0
return {
"period": period,
"snapshot_count": len(snapshots),
"performance": {
"total_pnl": total_pnl,
"avg_daily_pnl": total_pnl / len(snapshots) if snapshots else 0,
"total_trades": total_trades,
"winning_days": winning_days,
"losing_days": len(snapshots) - winning_days,
"avg_win_rate": avg_win_rate,
"best_day": max((s.daily_pnl for s in snapshots), default=0),
"worst_day": min((s.daily_pnl for s in snapshots), default=0)
},
"top_patterns": [
{
"name": p.pattern_name,
"confidence": p.confidence_score,
"win_rate": p.win_rate,
"samples": p.sample_count
}
for p in patterns
],
"recent_lessons": [
{
"category": l.category,
"lesson": l.lesson_text[:100],
"importance": l.importance,
"date": l.date_learned.isoformat()
}
for l in lessons
]
}
+2 -1
View File
@@ -7,7 +7,7 @@ from app.streaming.live_store import periodic_flush, periodic_maintenance
import asyncio
# Newly added routers
from app.api import account, performance, status, settings_api, prompts, daily_helper
from app.api import account, performance, status, settings_api, prompts, daily_helper, analytics
app = FastAPI(
title=settings.APP_NAME,
@@ -42,6 +42,7 @@ app.include_router(status.router, prefix="/api")
app.include_router(settings_api.router, prefix="/api")
app.include_router(prompts.router, prefix="/api")
app.include_router(daily_helper.router)
app.include_router(analytics.router)
@app.on_event("startup")
+97
View File
@@ -169,3 +169,100 @@ class HabitTracker(Base):
total_completions = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Phase 3: Advanced Analytics
class PerformanceSnapshot(Base):
"""Daily performance snapshot for historical tracking"""
__tablename__ = "performance_snapshots"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(String, nullable=True)
snapshot_date = Column(Date, default=func.current_date())
daily_pnl = Column(Float, default=0.0)
daily_pnl_percent = Column(Float, default=0.0)
total_trades = Column(Integer, default=0)
winning_trades = Column(Integer, default=0)
losing_trades = Column(Integer, default=0)
win_rate = Column(Float, default=0.0)
best_trade = Column(Float, nullable=True)
worst_trade = Column(Float, nullable=True)
avg_win = Column(Float, nullable=True)
avg_loss = Column(Float, nullable=True)
sharpe_ratio = Column(Float, nullable=True)
profit_factor = Column(Float, nullable=True)
max_drawdown = Column(Float, nullable=True)
cumulative_pnl = Column(Float, default=0.0)
portfolio_value = Column(Float, nullable=True)
equity_curve = Column(JSON, default=[]) # Time series
streak_type = Column(String, nullable=True) # win_streak, loss_streak
streak_count = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), server_default=func.now())
class TradePattern(Base):
"""Identified profitable trade patterns"""
__tablename__ = "trade_patterns"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(String, nullable=True)
pattern_name = Column(String) # e.g., "Morning breakout", "Reversal near support"
description = Column(Text, nullable=True)
win_rate = Column(Float) # Percentage
avg_win = Column(Float)
avg_loss = Column(Float)
sample_count = Column(Integer) # Number of matching trades
best_timeframe = Column(String, nullable=True) # 1m, 5m, 15m, 1h, 1d
best_time_of_day = Column(String, nullable=True) # e.g., "09:30-10:30"
confidence_score = Column(Float) # 0-100
indicators_used = Column(JSON, default=[]) # List of indicators
market_conditions = Column(String, nullable=True) # bullish, bearish, neutral
total_profit = Column(Float, default=0.0)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
class LessonLearned(Base):
"""Track lessons and insights from trading"""
__tablename__ = "lessons_learned"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(String, nullable=True)
date_learned = Column(DateTime(timezone=True), server_default=func.now())
category = Column(String) # entry, exit, risk, psychology, market
lesson_text = Column(Text)
related_trades = Column(JSON, default=[]) # Trade IDs
impact = Column(String) # positive, negative, neutral
tags = Column(JSON, default=[]) # Searchable tags
importance = Column(String) # critical, important, helpful
status = Column(String, default="active") # active, archived
created_at = Column(DateTime(timezone=True), server_default=func.now())
class MonthlyReview(Base):
"""Monthly trading performance review"""
__tablename__ = "monthly_reviews"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(String, nullable=True)
year = Column(Integer)
month = Column(Integer)
total_trades = Column(Integer, default=0)
total_pnl = Column(Float, default=0.0)
total_pnl_percent = Column(Float, default=0.0)
best_day = Column(Date, nullable=True)
worst_day = Column(Date, nullable=True)
best_trade = Column(Float, nullable=True)
worst_trade = Column(Float, nullable=True)
win_rate = Column(Float, default=0.0)
avg_daily_pnl = Column(Float, nullable=True)
sharpe_ratio = Column(Float, nullable=True)
max_drawdown = Column(Float, nullable=True)
trading_days = Column(Integer, default=0)
best_pattern = Column(String, nullable=True)
summary = Column(Text, nullable=True)
improvements = Column(JSON, default=[])
goals_met = Column(JSON, default=[])
goals_missed = Column(JSON, default=[])
created_at = Column(DateTime(timezone=True), server_default=func.now())
+154
View File
@@ -384,3 +384,157 @@ class HabitTrackerResponse(BaseModel):
class HabitCompletionRequest(BaseModel):
habit_id: int
completion_date: Optional[str] = None # ISO date string, defaults to today
# Phase 3: Advanced Analytics Schemas
class PerformanceSnapshotCreate(BaseModel):
snapshot_date: Optional[str] = None # ISO date, defaults to today
daily_pnl: float
daily_pnl_percent: float
total_trades: int
winning_trades: int
losing_trades: int
win_rate: float
best_trade: Optional[float] = None
worst_trade: Optional[float] = None
avg_win: Optional[float] = None
avg_loss: Optional[float] = None
sharpe_ratio: Optional[float] = None
profit_factor: Optional[float] = None
max_drawdown: Optional[float] = None
cumulative_pnl: float
portfolio_value: Optional[float] = None
class PerformanceSnapshotResponse(BaseModel):
id: int
snapshot_date: str
daily_pnl: float
daily_pnl_percent: float
total_trades: int
winning_trades: int
losing_trades: int
win_rate: float
best_trade: Optional[float]
worst_trade: Optional[float]
avg_win: Optional[float]
avg_loss: Optional[float]
sharpe_ratio: Optional[float]
profit_factor: Optional[float]
max_drawdown: Optional[float]
cumulative_pnl: float
portfolio_value: Optional[float]
created_at: datetime
class Config:
from_attributes = True
class TradePatternCreate(BaseModel):
pattern_name: str
description: Optional[str] = None
win_rate: float
avg_win: float
avg_loss: float
sample_count: int
best_timeframe: Optional[str] = None
best_time_of_day: Optional[str] = None
confidence_score: float
indicators_used: List[str] = []
market_conditions: Optional[str] = None
class TradePatternResponse(BaseModel):
id: int
pattern_name: str
description: Optional[str]
win_rate: float
avg_win: float
avg_loss: float
sample_count: int
best_timeframe: Optional[str]
best_time_of_day: Optional[str]
confidence_score: float
indicators_used: List[str]
market_conditions: Optional[str]
total_profit: float
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class LessonLearnedCreate(BaseModel):
category: str # entry, exit, risk, psychology, market
lesson_text: str
related_trades: List[int] = []
impact: str = "neutral" # positive, negative, neutral
tags: List[str] = []
importance: str = "helpful" # critical, important, helpful
class LessonLearnedResponse(BaseModel):
id: int
date_learned: datetime
category: str
lesson_text: str
related_trades: List[int]
impact: str
tags: List[str]
importance: str
status: str
created_at: datetime
class Config:
from_attributes = True
class MonthlyReviewCreate(BaseModel):
year: int
month: int
total_trades: int
total_pnl: float
total_pnl_percent: float
best_day: Optional[str] = None # ISO date
worst_day: Optional[str] = None
best_trade: Optional[float] = None
worst_trade: Optional[float] = None
win_rate: float
avg_daily_pnl: Optional[float] = None
sharpe_ratio: Optional[float] = None
max_drawdown: Optional[float] = None
trading_days: int
best_pattern: Optional[str] = None
summary: Optional[str] = None
improvements: List[str] = []
goals_met: List[str] = []
goals_missed: List[str] = []
class MonthlyReviewResponse(BaseModel):
id: int
year: int
month: int
total_trades: int
total_pnl: float
total_pnl_percent: float
best_day: Optional[str]
worst_day: Optional[str]
best_trade: Optional[float]
worst_trade: Optional[float]
win_rate: float
avg_daily_pnl: Optional[float]
sharpe_ratio: Optional[float]
max_drawdown: Optional[float]
trading_days: int
best_pattern: Optional[str]
summary: Optional[str]
improvements: List[str]
goals_met: List[str]
goals_missed: List[str]
created_at: datetime
class Config:
from_attributes = True