""" Trading Journal API Handles daily/weekly plans, manual trade logging, journal entries, and decision logging """ from fastapi import APIRouter, HTTPException, Depends, UploadFile, File from sqlalchemy.orm import Session from sqlalchemy import and_, desc from typing import List, Optional from datetime import date, datetime, timedelta from pydantic import BaseModel import os import shutil import uuid from app.db.database import get_db from app.models.models import ( TradingPlan, ManualTrade, JournalEntry, DecisionLog, WeeklyPlan, TradeAction ) router = APIRouter(prefix="/api/journal", tags=["Trading Journal"]) # Pydantic Schemas class TradingPlanCreate(BaseModel): plan_date: date plan_type: str = "daily" market_bias: str daily_target: Optional[float] = None max_loss: Optional[float] = None entry_zone_min: Optional[float] = None entry_zone_max: Optional[float] = None target_price: Optional[float] = None stop_loss: Optional[float] = None support_levels: List[float] = [] resistance_levels: List[float] = [] trading_notes: Optional[str] = None max_trades: int = 3 ai_generated: bool = False ai_confidence: Optional[float] = None context_metrics: Optional[dict] = None class TradingPlanUpdate(BaseModel): market_bias: Optional[str] = None daily_target: Optional[float] = None max_loss: Optional[float] = None entry_zone_min: Optional[float] = None entry_zone_max: Optional[float] = None target_price: Optional[float] = None stop_loss: Optional[float] = None support_levels: Optional[List[float]] = None resistance_levels: Optional[List[float]] = None trading_notes: Optional[str] = None max_trades: Optional[int] = None actual_trades: Optional[int] = None actual_pnl: Optional[float] = None plan_followed: Optional[bool] = None class ManualTradeCreate(BaseModel): plan_id: Optional[int] = None symbol: str = "XAUUSD" action: str # BUY or SELL entry_price: float exit_price: Optional[float] = None quantity: float broker: Optional[str] = None pnl: Optional[float] = None pnl_percent: Optional[float] = None notes: Optional[str] = None followed_plan: bool = True entry_time: Optional[datetime] = None exit_time: Optional[datetime] = None class ManualTradeUpdate(BaseModel): exit_price: Optional[float] = None pnl: Optional[float] = None pnl_percent: Optional[float] = None notes: Optional[str] = None exit_time: Optional[datetime] = None class JournalEntryCreate(BaseModel): entry_date: date mood: Optional[str] = None energy_level: Optional[int] = None stress_level: Optional[int] = None lessons_learned: Optional[str] = None what_went_well: Optional[str] = None what_to_improve: Optional[str] = None tomorrow_focus: Optional[str] = None mistakes_made: Optional[str] = None market_conditions: Optional[str] = None market_notes: Optional[str] = None class DecisionLogCreate(BaseModel): ai_recommendation: Optional[str] = None ai_confidence: Optional[float] = None ai_reasoning: Optional[str] = None trader_action: Optional[str] = None trade_id: Optional[int] = None outcome: Optional[str] = None outcome_pnl: Optional[float] = None notes: Optional[str] = None class WeeklyPlanCreate(BaseModel): week_start_date: date year: int week_number: int market_outlook: Optional[str] = None key_events: List[dict] = [] major_levels: List[float] = [] weekly_target: Optional[float] = None max_weekly_loss: Optional[float] = None target_trade_count: Optional[int] = None primary_strategy: Optional[str] = None focus_areas: Optional[str] = None risks_to_watch: Optional[str] = None # Trading Plans Endpoints @router.post("/plans", status_code=201) async def create_trading_plan( plan: TradingPlanCreate, db: Session = Depends(get_db) ): """Create a new daily/weekly trading plan""" db_plan = TradingPlan(**plan.dict()) db.add(db_plan) db.commit() db.refresh(db_plan) return db_plan @router.get("/plans/today") async def get_today_plan(db: Session = Depends(get_db)): """Get today's trading plan""" today = date.today() plan = db.query(TradingPlan).filter( and_( TradingPlan.plan_date == today, TradingPlan.plan_type == "daily" ) ).first() if not plan: raise HTTPException(status_code=404, detail="No plan found for today") return plan @router.get("/plans/date/{plan_date}") async def get_plan_by_date( plan_date: date, db: Session = Depends(get_db) ): """Get trading plan for a specific date""" plan = db.query(TradingPlan).filter( TradingPlan.plan_date == plan_date ).first() if not plan: raise HTTPException(status_code=404, detail=f"No plan found for {plan_date}") return plan @router.get("/plans") async def get_plans( limit: int = 30, offset: int = 0, db: Session = Depends(get_db) ): """Get recent trading plans""" plans = db.query(TradingPlan).order_by( desc(TradingPlan.plan_date) ).limit(limit).offset(offset).all() return {"plans": plans, "total": db.query(TradingPlan).count()} @router.put("/plans/{plan_id}") async def update_trading_plan( plan_id: int, plan_update: TradingPlanUpdate, db: Session = Depends(get_db) ): """Update an existing trading plan""" db_plan = db.query(TradingPlan).filter(TradingPlan.id == plan_id).first() if not db_plan: raise HTTPException(status_code=404, detail="Plan not found") update_data = plan_update.dict(exclude_unset=True) for key, value in update_data.items(): setattr(db_plan, key, value) db.commit() db.refresh(db_plan) return db_plan @router.delete("/plans/{plan_id}") async def delete_trading_plan( plan_id: int, db: Session = Depends(get_db) ): """Delete a trading plan""" db_plan = db.query(TradingPlan).filter(TradingPlan.id == plan_id).first() if not db_plan: raise HTTPException(status_code=404, detail="Plan not found") db.delete(db_plan) db.commit() return {"message": "Plan deleted successfully"} # Manual Trades Endpoints @router.post("/trades", status_code=201) async def create_manual_trade( trade: ManualTradeCreate, db: Session = Depends(get_db) ): """Log a manual trade from broker platform""" try: action_enum = TradeAction[trade.action.upper()] except KeyError: raise HTTPException(status_code=400, detail=f"Invalid action: {trade.action}") trade_dict = trade.dict() trade_dict['action'] = action_enum db_trade = ManualTrade(**trade_dict) db.add(db_trade) # Update plan if linked if trade.plan_id: plan = db.query(TradingPlan).filter(TradingPlan.id == trade.plan_id).first() if plan: plan.actual_trades += 1 if trade.pnl is not None: plan.actual_pnl += trade.pnl db.commit() db.refresh(db_trade) return db_trade @router.get("/trades") async def get_manual_trades( limit: int = 50, offset: int = 0, plan_id: Optional[int] = None, db: Session = Depends(get_db) ): """Get manual trades, optionally filtered by plan""" query = db.query(ManualTrade) if plan_id: query = query.filter(ManualTrade.plan_id == plan_id) trades = query.order_by(desc(ManualTrade.created_at)).limit(limit).offset(offset).all() total = query.count() return {"trades": trades, "total": total} @router.get("/trades/{trade_id}") async def get_manual_trade( trade_id: int, db: Session = Depends(get_db) ): """Get a specific manual trade""" trade = db.query(ManualTrade).filter(ManualTrade.id == trade_id).first() if not trade: raise HTTPException(status_code=404, detail="Trade not found") return trade @router.put("/trades/{trade_id}") async def update_manual_trade( trade_id: int, trade_update: ManualTradeUpdate, db: Session = Depends(get_db) ): """Update a manual trade (e.g., closing a position)""" db_trade = db.query(ManualTrade).filter(ManualTrade.id == trade_id).first() if not db_trade: raise HTTPException(status_code=404, detail="Trade not found") update_data = trade_update.dict(exclude_unset=True) # Calculate PnL if exit price provided if 'exit_price' in update_data and db_trade.exit_price is None: exit_price = update_data['exit_price'] if db_trade.action == TradeAction.BUY: pnl = (exit_price - db_trade.entry_price) * db_trade.quantity else: # SELL pnl = (db_trade.entry_price - exit_price) * db_trade.quantity update_data['pnl'] = round(pnl, 2) update_data['pnl_percent'] = round((pnl / (db_trade.entry_price * db_trade.quantity)) * 100, 2) # Update plan PnL if db_trade.plan_id: plan = db.query(TradingPlan).filter(TradingPlan.id == db_trade.plan_id).first() if plan: plan.actual_pnl += pnl for key, value in update_data.items(): setattr(db_trade, key, value) db.commit() db.refresh(db_trade) return db_trade @router.post("/trades/{trade_id}/screenshot") async def upload_trade_screenshot( trade_id: int, file: UploadFile = File(...), db: Session = Depends(get_db) ): """Upload a screenshot for a trade""" db_trade = db.query(ManualTrade).filter(ManualTrade.id == trade_id).first() if not db_trade: raise HTTPException(status_code=404, detail="Trade not found") # Create uploads directory if it doesn't exist upload_dir = "uploads/trade_screenshots" os.makedirs(upload_dir, exist_ok=True) # Generate unique filename file_extension = os.path.splitext(file.filename)[1] unique_filename = f"{trade_id}_{uuid.uuid4()}{file_extension}" file_path = os.path.join(upload_dir, unique_filename) # Save file with open(file_path, "wb") as buffer: shutil.copyfileobj(file.file, buffer) # Update trade record db_trade.screenshot_url = file_path db.commit() return {"filename": unique_filename, "path": file_path} # Journal Entries Endpoints @router.post("/entries", status_code=201) async def create_journal_entry( entry: JournalEntryCreate, db: Session = Depends(get_db) ): """Create a daily journal entry""" # Check if entry for this date already exists existing = db.query(JournalEntry).filter( JournalEntry.entry_date == entry.entry_date ).first() if existing: # Update existing entry update_data = entry.dict(exclude_unset=True) for key, value in update_data.items(): setattr(existing, key, value) db.commit() db.refresh(existing) return existing db_entry = JournalEntry(**entry.dict()) db.add(db_entry) db.commit() db.refresh(db_entry) return db_entry @router.get("/entries/today") async def get_today_journal(db: Session = Depends(get_db)): """Get today's journal entry""" today = date.today() entry = db.query(JournalEntry).filter( JournalEntry.entry_date == today ).first() if not entry: raise HTTPException(status_code=404, detail="No journal entry for today") return entry @router.get("/entries") async def get_journal_entries( limit: int = 30, offset: int = 0, db: Session = Depends(get_db) ): """Get recent journal entries""" entries = db.query(JournalEntry).order_by( desc(JournalEntry.entry_date) ).limit(limit).offset(offset).all() return {"entries": entries, "total": db.query(JournalEntry).count()} # Decision Log Endpoints @router.post("/decisions", status_code=201) async def create_decision_log( decision: DecisionLogCreate, db: Session = Depends(get_db) ): """Log a trading decision""" db_decision = DecisionLog(**decision.dict()) db.add(db_decision) db.commit() db.refresh(db_decision) return db_decision @router.get("/decisions") async def get_decisions( limit: int = 50, offset: int = 0, db: Session = Depends(get_db) ): """Get recent decisions""" decisions = db.query(DecisionLog).order_by( desc(DecisionLog.decision_time) ).limit(limit).offset(offset).all() return {"decisions": decisions, "total": db.query(DecisionLog).count()} @router.get("/decisions/accuracy") async def get_ai_accuracy( days: int = 30, db: Session = Depends(get_db) ): """Calculate AI recommendation accuracy""" cutoff_date = datetime.now() - timedelta(days=days) decisions = db.query(DecisionLog).filter( and_( DecisionLog.decision_time >= cutoff_date, DecisionLog.trader_action == "FOLLOWED", DecisionLog.outcome.isnot(None) ) ).all() if not decisions: return { "total_decisions": 0, "accuracy": 0.0, "win_rate": 0.0, "avg_pnl": 0.0 } wins = sum(1 for d in decisions if d.outcome == "WIN") total_pnl = sum(d.outcome_pnl for d in decisions if d.outcome_pnl is not None) return { "total_decisions": len(decisions), "wins": wins, "losses": len(decisions) - wins, "win_rate": round((wins / len(decisions)) * 100, 2), "avg_pnl": round(total_pnl / len(decisions), 2) if decisions else 0, "total_pnl": round(total_pnl, 2) } # Weekly Plans Endpoints @router.post("/weekly-plans", status_code=201) async def create_weekly_plan( plan: WeeklyPlanCreate, db: Session = Depends(get_db) ): """Create a weekly trading plan""" db_plan = WeeklyPlan(**plan.dict()) db.add(db_plan) db.commit() db.refresh(db_plan) return db_plan @router.get("/weekly-plans/current") async def get_current_week_plan(db: Session = Depends(get_db)): """Get this week's plan""" today = date.today() # Get Monday of current week monday = today - timedelta(days=today.weekday()) plan = db.query(WeeklyPlan).filter( WeeklyPlan.week_start_date == monday ).first() if not plan: raise HTTPException(status_code=404, detail="No plan found for current week") return plan @router.get("/weekly-plans") async def get_weekly_plans( limit: int = 12, db: Session = Depends(get_db) ): """Get recent weekly plans""" plans = db.query(WeeklyPlan).order_by( desc(WeeklyPlan.week_start_date) ).limit(limit).all() return {"plans": plans, "total": db.query(WeeklyPlan).count()}