from fastapi import APIRouter, HTTPException, Depends from sqlalchemy.orm import Session, selectinload from typing import Dict, Optional, Any, List from datetime import datetime, timezone from app.db.database import get_db from app.models.models import Simulation, Trade, Position, TradeAction, TradeMetadata from app.services.risk import validate_order from pydantic import BaseModel router = APIRouter(prefix="/trading", tags=["Trading"]) # Pydantic models for request/response class TradeRequest(BaseModel): action: str quantity: float price: float symbol: str = "XAU/USD" notes: Optional[str] = None stop_loss: Optional[float] = None take_profit: Optional[float] = None source: Optional[str] = None platform: Optional[str] = None risk_percent: Optional[float] = None entry_time: Optional[str] = None class PortfolioState(BaseModel): cash: float initial_capital: float position: Optional[Dict[str, Any]] = None trades: List[Dict[str, Any]] equity_history: List[Dict[str, Any]] total_pnl: float total_pnl_percent: float def get_or_create_simulation(db: Session, user_id: str = "default") -> Simulation: """Get existing simulation or create a new one""" simulation = db.query(Simulation).filter(Simulation.user_id == user_id).first() if not simulation: simulation = Simulation( user_id=user_id, symbol="XAU/USD", initial_capital=100000.0, current_capital=100000.0, total_pnl=0.0, total_pnl_percent=0.0 ) db.add(simulation) db.commit() db.refresh(simulation) return simulation def _compute_equity_at_price(simulation: Simulation, price: float, db: Session) -> float: """Calculate equity based on current position and price""" position = db.query(Position).filter( Position.simulation_id == simulation.id ).first() qty = position.quantity if position else 0.0 return float(simulation.current_capital + qty * price) def get_portfolio_state_from_db(simulation: Simulation, db: Session) -> PortfolioState: """Convert DB simulation to portfolio state""" # Get current position position = db.query(Position).filter( Position.simulation_id == simulation.id ).first() position_dict = None if position: position_dict = { "symbol": position.symbol, "quantity": position.quantity, "avg_price": position.avg_price, "current_price": position.current_price, "unrealized_pnl": position.unrealized_pnl, "unrealized_pnl_percent": position.unrealized_pnl_percent } # Get all trades trades = db.query(Trade).options(selectinload(Trade.details)).filter( Trade.simulation_id == simulation.id ).order_by(Trade.timestamp).all() trades_list = [] for trade in trades: details = trade.details trades_list.append({ "id": trade.id, "action": trade.action.value, "quantity": trade.quantity, "price": trade.price, "total": trade.total, "pnl": trade.pnl, "timestamp": int(trade.timestamp.timestamp()) if trade.timestamp else None, "stop_loss": details.stop_loss if details else None, "take_profit": details.take_profit if details else None, "notes": details.notes if details else None, "source": details.source if details else None, "platform": details.platform if details else None, "risk_percent": details.risk_percent if details else None, "entry_time": details.entry_time.isoformat() if details and details.entry_time else None, }) # Build equity history from trades equity_history = [] running_equity = simulation.initial_capital for trade in trades: if trade.action == TradeAction.SELL and trade.pnl: running_equity += trade.pnl equity_history.append({ "time": int(trade.timestamp.timestamp()) if trade.timestamp else 0, "equity": running_equity }) return PortfolioState( cash=simulation.current_capital, initial_capital=simulation.initial_capital, position=position_dict, trades=trades_list, equity_history=equity_history, total_pnl=simulation.total_pnl, total_pnl_percent=simulation.total_pnl_percent ) @router.post("/execute") async def execute_trade( trade_request: TradeRequest, db: Session = Depends(get_db), user_id: str = "default" ): """ Execute a trade and persist to database. - Validates risk rules - Updates cash/position in DB - Records trade with timestamp - Returns updated portfolio state """ try: action = trade_request.action.upper() quantity = trade_request.quantity price = trade_request.price if action not in ["BUY", "SELL"]: raise HTTPException(status_code=400, detail="Action must be BUY or SELL") # Get or create simulation simulation = get_or_create_simulation(db, user_id) # Build state dict for risk validation position = db.query(Position).filter( Position.simulation_id == simulation.id ).first() state_dict = { "cash": simulation.current_capital, "position": { "quantity": position.quantity, "avg_price": position.avg_price } if position else None } # Risk validation try: validate_order(state_dict, action, quantity, price) except ValueError as ve: raise HTTPException(status_code=400, detail=str(ve)) total = quantity * price pnl = None if action == "BUY": if total > simulation.current_capital: raise HTTPException(status_code=400, detail="Insufficient funds") simulation.current_capital -= total if not position: # Create new position position = Position( simulation_id=simulation.id, symbol=trade_request.symbol, quantity=quantity, avg_price=price, current_price=price, unrealized_pnl=0.0, unrealized_pnl_percent=0.0 ) db.add(position) else: # Update existing position (average up) new_qty = position.quantity + quantity new_avg = (position.avg_price * position.quantity + price * quantity) / new_qty position.quantity = new_qty position.avg_price = new_avg position.current_price = price elif action == "SELL": if not position or quantity > position.quantity: raise HTTPException(status_code=400, detail="Insufficient position") simulation.current_capital += total pnl = (price - position.avg_price) * quantity # Update simulation totals simulation.total_pnl += pnl if simulation.initial_capital > 0: simulation.total_pnl_percent = (simulation.total_pnl / simulation.initial_capital) * 100 position.quantity -= quantity if position.quantity == 0: # Close position db.delete(position) position = None else: position.current_price = price # Create trade record trade_timestamp = datetime.now(timezone.utc) trade = Trade( simulation_id=simulation.id, action=TradeAction[action], quantity=quantity, price=price, total=total, pnl=pnl, timestamp=trade_timestamp ) db.add(trade) db.flush() entry_time_dt = None if trade_request.entry_time: try: entry_time_dt = datetime.fromisoformat(trade_request.entry_time) if entry_time_dt.tzinfo is None: entry_time_dt = entry_time_dt.replace(tzinfo=timezone.utc) except ValueError: entry_time_dt = trade_timestamp metadata_fields = ( trade_request.source, trade_request.platform, trade_request.notes, trade_request.stop_loss, trade_request.take_profit, trade_request.risk_percent, entry_time_dt, ) if any(field is not None for field in metadata_fields): trade_metadata = TradeMetadata( trade_id=trade.id, source=trade_request.source, platform=trade_request.platform, notes=trade_request.notes, stop_loss=trade_request.stop_loss, take_profit=trade_request.take_profit, risk_percent=trade_request.risk_percent, entry_time=entry_time_dt, ) db.add(trade_metadata) # Commit all changes db.commit() db.refresh(simulation) # Return updated portfolio state portfolio_state = get_portfolio_state_from_db(simulation, db) return { "trade": { "id": trade.id, "action": action, "quantity": quantity, "price": price, "total": total, "pnl": pnl, "timestamp": int(trade.timestamp.timestamp()) if trade.timestamp else None, "stop_loss": trade_request.stop_loss, "take_profit": trade_request.take_profit, "notes": trade_request.notes, "source": trade_request.source, "platform": trade_request.platform, "risk_percent": trade_request.risk_percent, "entry_time": entry_time_dt.isoformat() if entry_time_dt else None, }, "portfolio": portfolio_state.dict() } except HTTPException: raise except Exception as e: db.rollback() raise HTTPException(status_code=500, detail=f"Trade execution failed: {str(e)}") @router.get("/portfolio") async def get_portfolio( db: Session = Depends(get_db), user_id: str = "default" ): """Get current portfolio state from database""" try: simulation = get_or_create_simulation(db, user_id) portfolio_state = get_portfolio_state_from_db(simulation, db) return portfolio_state.dict() except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get portfolio: {str(e)}") @router.post("/reset") async def reset_simulation( db: Session = Depends(get_db), user_id: str = "default" ): """Reset simulation to initial state""" try: simulation = db.query(Simulation).filter(Simulation.user_id == user_id).first() if simulation: # Delete all trades and positions (cascade will handle this) db.delete(simulation) db.commit() # Create new simulation new_simulation = Simulation( user_id=user_id, symbol="XAU/USD", initial_capital=100000.0, current_capital=100000.0, total_pnl=0.0, total_pnl_percent=0.0 ) db.add(new_simulation) db.commit() db.refresh(new_simulation) portfolio_state = get_portfolio_state_from_db(new_simulation, db) return { "message": "Simulation reset successfully", "portfolio": portfolio_state.dict() } except Exception as e: db.rollback() raise HTTPException(status_code=500, detail=f"Reset failed: {str(e)}") @router.get("/history") async def get_trade_history( db: Session = Depends(get_db), user_id: str = "default", limit: int = 100 ): """Get trade history from database""" try: simulation = get_or_create_simulation(db, user_id) trades = db.query(Trade).options(selectinload(Trade.details)).filter( Trade.simulation_id == simulation.id ).order_by(Trade.timestamp.desc()).limit(limit).all() return [ { "id": trade.id, "action": trade.action.value, "quantity": trade.quantity, "price": trade.price, "total": trade.total, "pnl": trade.pnl, "timestamp": int(trade.timestamp.timestamp()) if trade.timestamp else None, "stop_loss": trade.details.stop_loss if trade.details else None, "take_profit": trade.details.take_profit if trade.details else None, "notes": trade.details.notes if trade.details else None, "source": trade.details.source if trade.details else None, "platform": trade.details.platform if trade.details else None, "risk_percent": trade.details.risk_percent if trade.details else None, "entry_time": trade.details.entry_time.isoformat() if trade.details and trade.details.entry_time else None, } for trade in trades ] except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get history: {str(e)}") @router.get("/stats") async def get_trading_stats( db: Session = Depends(get_db), user_id: str = "default" ): """Get trading statistics""" try: simulation = get_or_create_simulation(db, user_id) trades = db.query(Trade).filter( Trade.simulation_id == simulation.id ).all() total_trades = len(trades) winning_trades = sum(1 for t in trades if t.pnl and t.pnl > 0) losing_trades = sum(1 for t in trades if t.pnl and t.pnl < 0) total_profit = sum(t.pnl for t in trades if t.pnl and t.pnl > 0) total_loss = sum(abs(t.pnl) for t in trades if t.pnl and t.pnl < 0) win_rate = (winning_trades / total_trades * 100) if total_trades > 0 else 0 profit_factor = (total_profit / total_loss) if total_loss > 0 else 0 return { "total_trades": total_trades, "winning_trades": winning_trades, "losing_trades": losing_trades, "win_rate": round(win_rate, 2), "total_pnl": simulation.total_pnl, "total_pnl_percent": simulation.total_pnl_percent, "total_profit": total_profit, "total_loss": total_loss, "profit_factor": round(profit_factor, 2), "current_capital": simulation.current_capital, "initial_capital": simulation.initial_capital } except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get stats: {str(e)}")