""" Smart Trade Hub API - Unified trade entry system Consolidates Simulator, Manual Logger, and Broker Bridge into one intelligent interface """ from fastapi import APIRouter, HTTPException, Depends, Query from sqlalchemy.orm import Session from typing import Any, Dict, List, Optional, Literal from datetime import datetime, timezone from pydantic import BaseModel, Field from app.db.database import get_db from app.services.simulation_state import load_simulation_state from app.api.trading_persistent import ( TradeRequest as PersistentTradeRequest, execute_trade as persistent_execute_trade, ) from app.services.risk import validate_order from app.services.ai_context_builder import ai_context_builder from app.services.price_anchor import price_anchor_service router = APIRouter(prefix="/api/smart-trade-hub", tags=["Smart Trade Hub"]) class TradeSource(str): """Enumeration of trade sources""" SIMULATOR = "simulator" MANUAL = "manual" BROKER = "broker" VOICE = "voice" OCR = "ocr" class SmartTradeRequest(BaseModel): """Unified trade entry request with auto-detection""" action: Literal["BUY", "SELL", "CLOSE"] symbol: str = Field(default="XAU/USD", description="Trading symbol") quantity: Optional[float] = Field(None, description="Trade quantity (auto-filled if None)") price: Optional[float] = Field(None, description="Entry price (uses current market if None)") # Optional guards (auto-calculated if None) stop_loss: Optional[float] = None take_profit: Optional[float] = None risk_percent: Optional[float] = None # Source detection and metadata source: Optional[str] = Field(None, description="Trade source: simulator/manual/broker/voice/ocr") platform: Optional[str] = Field(None, description="Trading platform (e.g., MT5, TradingView)") notes: Optional[str] = Field(None, description="Trade notes or voice transcription") entry_time: Optional[str] = Field(None, description="Custom entry time (ISO format)") # OCR/Voice metadata image_data: Optional[str] = Field(None, description="Base64 encoded screenshot for OCR") voice_data: Optional[str] = Field(None, description="Voice memo data") # Pre-fill hints use_last_trade_defaults: bool = Field(True, description="Auto-fill from last trade") apply_smart_guards: bool = Field(True, description="Apply AI-suggested guards") class SmartTradeResponse(BaseModel): """Response with executed trade and suggestions""" trade_id: int action: str symbol: str quantity: float price: float stop_loss: Optional[float] take_profit: Optional[float] risk_percent: Optional[float] # Execution details source: str executed_at: str total_cost: float # Smart suggestions applied guards_applied: bool guards_suggested: Optional[Dict] = None prefill_used: bool # Position state after trade remaining_cash: float total_equity: float position_size: Optional[float] unrealized_pnl: Optional[float] class SmartPreFillResponse(BaseModel): """Pre-fill suggestions for trade entry""" symbol: str suggested_quantity: float current_price: float suggested_guards: Dict last_trade_context: Optional[Dict] market_context: Dict confidence: float class SmartGuardSuggestion(BaseModel): """AI-suggested risk guards""" stop_loss_price: float stop_loss_percent: float take_profit_price: float take_profit_percent: float risk_percent: float position_size: float risk_reward_ratio: float reasoning: str confidence: float def _get_current_market_price(symbol: str) -> float: """Get current market price from price anchor service""" try: anchor_price = price_anchor_service.get_anchor_price_sync(symbol.upper().replace("/", "")) if anchor_price and anchor_price > 0: return anchor_price except: pass # Fallback to a reasonable default for XAU/USD return 2034.0 def _compute_equity(state: Dict[str, Any], price_hint: Optional[float] = None) -> float: """Compute total equity using cash and current position.""" cash = float(state.get("cash", 0.0) or 0.0) position = state.get("position") or {} if position: current_price = price_hint or position.get("current_price") or position.get("avg_price") or 0.0 quantity = position.get("quantity", 0.0) or 0.0 cash += float(quantity) * float(current_price) return cash def _get_last_trade_defaults(state: Dict[str, Any]) -> Optional[Dict]: """Get defaults from the last trade""" trades = state.get("trades", []) if not trades: return None last_trade = trades[-1] return { "quantity": last_trade.get("quantity"), "symbol": last_trade.get("symbol", "XAU/USD"), "platform": last_trade.get("platform"), "stop_loss": last_trade.get("stop_loss"), "take_profit": last_trade.get("take_profit"), "risk_percent": last_trade.get("risk_percent"), } def _calculate_smart_guards( symbol: str, action: str, price: float, quantity: float, equity: float ) -> SmartGuardSuggestion: """ Calculate optimal stop loss and take profit using ATR and risk management principles """ try: # Get market metrics including ATR ctx = ai_context_builder.build_request( symbol.upper().replace("/", ""), "1h", # Use hourly for guard calculation 100 ) metrics = ai_context_builder.build_metrics( symbol.upper().replace("/", ""), "1h", ctx.price_data ) # Extract ATR value atr = metrics.atr_14 if hasattr(metrics, 'atr_14') else (price * 0.015) # Default to 1.5% # Calculate stop loss (1.5x ATR from entry) sl_distance = atr * 1.5 sl_percent = (sl_distance / price) * 100 # Calculate take profit (2x stop loss for 1:2 risk/reward minimum) tp_distance = sl_distance * 2.0 tp_percent = (tp_distance / price) * 100 if action == "BUY": sl_price = price - sl_distance tp_price = price + tp_distance else: # SELL sl_price = price + sl_distance tp_price = price - tp_distance # Calculate position risk as % of equity risk_amount = quantity * sl_distance risk_percent = (risk_amount / equity) * 100 # Ensure risk doesn't exceed 2% of equity (conservative default) if risk_percent > 2.0: # Adjust quantity to maintain 2% risk adjusted_quantity = (equity * 0.02) / sl_distance risk_percent = 2.0 else: adjusted_quantity = quantity return SmartGuardSuggestion( stop_loss_price=round(sl_price, 2), stop_loss_percent=round(sl_percent, 2), take_profit_price=round(tp_price, 2), take_profit_percent=round(tp_percent, 2), risk_percent=round(risk_percent, 2), position_size=round(adjusted_quantity, 2), risk_reward_ratio=2.0, reasoning=f"ATR-based guards: {atr:.2f} | 1.5x ATR stop | 1:2 R:R ratio | Max 2% risk", confidence=0.85 ) except Exception as e: # Fallback to simple percentage-based guards sl_percent = 2.0 tp_percent = 4.0 if action == "BUY": sl_price = price * (1 - sl_percent / 100) tp_price = price * (1 + tp_percent / 100) else: sl_price = price * (1 + sl_percent / 100) tp_price = price * (1 - tp_percent / 100) risk_amount = quantity * price * (sl_percent / 100) risk_percent = (risk_amount / equity) * 100 return SmartGuardSuggestion( stop_loss_price=round(sl_price, 2), stop_loss_percent=round(sl_percent, 2), take_profit_price=round(tp_price, 2), take_profit_percent=round(tp_percent, 2), risk_percent=round(risk_percent, 2), position_size=quantity, risk_reward_ratio=2.0, reasoning="Fallback guards: 2% stop loss | 4% take profit | 1:2 ratio", confidence=0.60 ) @router.post("/prefill", response_model=SmartPreFillResponse) async def get_smart_prefill( symbol: str = Query("XAU/USD"), action: Optional[str] = Query(None), db: Session = Depends(get_db), user_id: str = "default", ) -> SmartPreFillResponse: """Get smart pre-fill suggestions based on last trade and current market context.""" try: current_price = _get_current_market_price(symbol) state = load_simulation_state(db, user_id) last_trade = _get_last_trade_defaults(state) suggested_quantity = 1.0 if last_trade and last_trade.get("quantity"): suggested_quantity = last_trade["quantity"] equity = _compute_equity(state, price_hint=current_price) trade_action = (action or "BUY").upper() guards = _calculate_smart_guards( symbol, trade_action, current_price, suggested_quantity, equity, ) market_context = { "current_price": current_price, "equity": equity, "cash": state.get("cash", 0.0), "position": state.get("position"), } return SmartPreFillResponse( symbol=symbol, suggested_quantity=suggested_quantity, current_price=current_price, suggested_guards={ "stop_loss": guards.stop_loss_price, "take_profit": guards.take_profit_price, "risk_percent": guards.risk_percent, "reasoning": guards.reasoning, "confidence": guards.confidence, }, last_trade_context=last_trade, market_context=market_context, confidence=0.80, ) except Exception as e: raise HTTPException( status_code=500, detail=f"Failed to generate pre-fill suggestions: {str(e)}", ) @router.post("/execute", response_model=SmartTradeResponse) async def execute_smart_trade( request: SmartTradeRequest, db: Session = Depends(get_db), user_id: str = "default", ) -> SmartTradeResponse: """Execute a trade through the unified smart trade hub using the persistent state.""" try: source = request.source or TradeSource.MANUAL if request.image_data: source = TradeSource.OCR elif request.voice_data: source = TradeSource.VOICE price = request.price or _get_current_market_price(request.symbol) state = load_simulation_state(db, user_id) last_trade = _get_last_trade_defaults(state) if request.use_last_trade_defaults else None quantity = request.quantity if quantity is None: if last_trade and last_trade.get("quantity"): quantity = last_trade["quantity"] else: quantity = 1.0 equity = _compute_equity(state, price_hint=price) guards_applied = False guards_suggested: Optional[Dict[str, Any]] = None guards: Optional[SmartGuardSuggestion] = None if request.apply_smart_guards: guards = _calculate_smart_guards( request.symbol, request.action, price, quantity, equity, ) if request.stop_loss is None: request.stop_loss = guards.stop_loss_price guards_applied = True if request.take_profit is None: request.take_profit = guards.take_profit_price guards_applied = True if request.risk_percent is None: request.risk_percent = guards.risk_percent guards_applied = True if guards.position_size != quantity: quantity = guards.position_size guards_applied = True guards_suggested = guards.model_dump() if request.action == "CLOSE": position = state.get("position") if not position: raise HTTPException(status_code=400, detail="No position to close") request.action = "SELL" quantity = position.get("quantity", 0.0) or 0.0 try: validate_order(state, request.action, quantity, price) except ValueError as ve: raise HTTPException(status_code=400, detail=str(ve)) persistent_request = PersistentTradeRequest( action=request.action, quantity=quantity, price=price, symbol=request.symbol, notes=request.notes, stop_loss=request.stop_loss, take_profit=request.take_profit, source=source, platform=request.platform, risk_percent=request.risk_percent, entry_time=request.entry_time, ) result = await persistent_execute_trade(persistent_request, db=db, user_id=user_id) trade_info = result["trade"] portfolio = result["portfolio"] total_cost = trade_info.get("total", quantity * price) executed_ts = trade_info.get("timestamp") executed_at = ( datetime.fromtimestamp(executed_ts, tz=timezone.utc).isoformat() if executed_ts else datetime.now(timezone.utc).isoformat() ) position_after = portfolio.get("position") or {} position_size = position_after.get("quantity") unrealized_pnl = position_after.get("unrealized_pnl") total_equity = _compute_equity(portfolio, price_hint=price) return SmartTradeResponse( trade_id=trade_info["id"], action=trade_info["action"], symbol=request.symbol, quantity=trade_info["quantity"], price=trade_info["price"], stop_loss=trade_info.get("stop_loss"), take_profit=trade_info.get("take_profit"), risk_percent=trade_info.get("risk_percent"), source=source, executed_at=executed_at, total_cost=total_cost, guards_applied=guards_applied, guards_suggested=guards_suggested, prefill_used=request.use_last_trade_defaults, remaining_cash=portfolio.get("cash", 0.0), total_equity=total_equity, position_size=position_size, unrealized_pnl=unrealized_pnl, ) except HTTPException: raise except Exception as e: raise HTTPException( status_code=500, detail=f"Failed to execute smart trade: {str(e)}", ) @router.get("/suggestions", response_model=SmartGuardSuggestion) async def get_guard_suggestions( symbol: str = Query("XAU/USD"), action: str = Query("BUY"), quantity: float = Query(1.0), price: Optional[float] = Query(None), db: Session = Depends(get_db), user_id: str = "default", ) -> SmartGuardSuggestion: """Get AI-suggested stop loss and take profit guards using persistent state.""" try: if price is None: price = _get_current_market_price(symbol) state = load_simulation_state(db, user_id) equity = _compute_equity(state, price_hint=price) return _calculate_smart_guards(symbol, action, price, quantity, equity) except Exception as e: raise HTTPException( status_code=500, detail=f"Failed to calculate guard suggestions: {str(e)}", ) @router.get("/history") async def get_trade_history( limit: int = Query(50, ge=1, le=500), source: Optional[str] = Query(None), db: Session = Depends(get_db), user_id: str = "default", ) -> Dict: """Get trade history with optional source filtering from persisted trades.""" try: state = load_simulation_state(db, user_id) trades = state.get("trades", []) if source: trades = [t for t in trades if t.get("source") == source] trades = trades[-limit:] sources = { (t.get("source") or "unknown") for t in state.get("trades", []) } return { "trades": trades, "total": len(trades), "sources": sorted(sources), } except Exception as e: raise HTTPException( status_code=500, detail=f"Failed to retrieve trade history: {str(e)}", )