Phase 3: Advanced Analytics Foundation - Models, Schemas, and API
This commit is contained in:
@@ -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
@@ -7,7 +7,7 @@ from app.streaming.live_store import periodic_flush, periodic_maintenance
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
# Newly added routers
|
# 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(
|
app = FastAPI(
|
||||||
title=settings.APP_NAME,
|
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(settings_api.router, prefix="/api")
|
||||||
app.include_router(prompts.router, prefix="/api")
|
app.include_router(prompts.router, prefix="/api")
|
||||||
app.include_router(daily_helper.router)
|
app.include_router(daily_helper.router)
|
||||||
|
app.include_router(analytics.router)
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
|
|||||||
@@ -169,3 +169,100 @@ class HabitTracker(Base):
|
|||||||
total_completions = Column(Integer, default=0)
|
total_completions = Column(Integer, default=0)
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=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())
|
||||||
|
|||||||
@@ -384,3 +384,157 @@ class HabitTrackerResponse(BaseModel):
|
|||||||
class HabitCompletionRequest(BaseModel):
|
class HabitCompletionRequest(BaseModel):
|
||||||
habit_id: int
|
habit_id: int
|
||||||
completion_date: Optional[str] = None # ISO date string, defaults to today
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user