Files
robinhood/backend/app/api/trading.py
T

134 lines
4.2 KiB
Python

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"]