from fastapi import APIRouter, HTTPException from typing import Dict from datetime import datetime, timezone from app.services.risk import validate_order router = APIRouter(prefix="/trading", tags=["Trading"]) # In-memory simulation state (for MVP - will use DB in future) simulation_state = { "cash": 100000.0, "initial_capital": 100000.0, "position": None, "trades": [], "equity_history": [], # list of {time: epoch_sec, equity: float} } def _compute_equity_at_price(price: float) -> float: pos = simulation_state.get("position") qty = pos["quantity"] if pos else 0.0 return float(simulation_state.get("cash", 0.0) + qty * price) @router.post("/execute") async def execute_trade(trade: Dict): """ Execute a trade in the simulation. - Validates simple risk rules (position cap, anti-stacking) - Updates cash/position - Records trade with timestamp - Appends equity snapshot after execution """ try: action = trade.get("action") quantity = float(trade.get("quantity")) if trade.get("quantity") is not None else None price = float(trade.get("price")) if trade.get("price") is not None else None if not all([action, quantity is not None, price is not None]): raise HTTPException(status_code=400, detail="Missing required fields") # Risk validation prior to execution try: validate_order(simulation_state, action, quantity, price) except ValueError as ve: raise HTTPException(status_code=400, detail=str(ve)) total = quantity * price if action == "BUY": if total > simulation_state["cash"]: raise HTTPException(status_code=400, detail="Insufficient funds") simulation_state["cash"] -= total if simulation_state["position"] is None: simulation_state["position"] = { "symbol": "XAU/USD", "quantity": quantity, "avg_price": price, } else: # Update average price for additional buy pos = simulation_state["position"] new_qty = pos["quantity"] + quantity new_avg = ( pos["avg_price"] * pos["quantity"] + price * quantity ) / new_qty pos["quantity"] = new_qty pos["avg_price"] = new_avg elif action == "SELL": if ( simulation_state["position"] is None or quantity > simulation_state["position"]["quantity"] ): raise HTTPException(status_code=400, detail="Insufficient position") simulation_state["cash"] += total pnl = (price - simulation_state["position"]["avg_price"]) * quantity simulation_state["position"]["quantity"] -= quantity if simulation_state["position"]["quantity"] == 0: simulation_state["position"] = None trade["pnl"] = pnl else: raise HTTPException(status_code=400, detail="Unsupported action") now_ts = int(datetime.now(timezone.utc).timestamp()) trade["id"] = len(simulation_state["trades"]) + 1 trade["timestamp"] = now_ts simulation_state["trades"].append(trade) # Append equity snapshot post trade using trade price equity = _compute_equity_at_price(price) simulation_state["equity_history"].append({"time": now_ts, "equity": equity}) return trade except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/portfolio") async def get_portfolio(): """Get current portfolio state""" return simulation_state @router.post("/reset") async def reset_simulation(): """Reset simulation to initial state""" global simulation_state simulation_state = { "cash": 100000.0, "initial_capital": 100000.0, "position": None, "trades": [], "equity_history": [], } return {"message": "Simulation reset successfully"} @router.get("/history") async def get_trade_history(): """Get trade history""" return simulation_state["trades"]