diff --git a/internal/api/order_handler.go b/internal/api/order_handler.go index 84752c8..21d588f 100644 --- a/internal/api/order_handler.go +++ b/internal/api/order_handler.go @@ -188,6 +188,48 @@ func (h *OrderHandler) PlaceMarketOrder(c *gin.Context) { } book := h.bookManager.GetOrCreate(req.Symbol) + + // Pre-execution validation for Market Orders: + // Verify that the user has sufficient funds/assets before hitting the engine. + p, err := h.portfolioMgr.LoadOrFetch(c.Request.Context(), userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load portfolio: " + err.Error()}) + return + } + + if side == engine.SideBuy { + bestAsk := book.BestAsk() + if bestAsk <= 0 { + // If no liquidity, we can't estimate cost. + // Rejection is safer for massive orders like 5B. + c.JSON(http.StatusBadRequest, gin.H{"error": "insufficient liquidity in the order book"}) + return + } + estCost := (float64(bestAsk) / 100.0) * float64(req.Quantity) + if p.AvailableCash < estCost { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "insufficient funds", + "available_cash": p.AvailableCash, + "estimated_cost": estCost, + }) + return + } + } else if side == engine.SideSell { + pos, ok := p.Positions[req.Symbol] + if !ok || pos.Quantity < float64(req.Quantity) { + avail := 0.0 + if ok { + avail = pos.Quantity + } + c.JSON(http.StatusBadRequest, gin.H{ + "error": "insufficient assets", + "available_shares": avail, + "required_shares": req.Quantity, + }) + return + } + } + orderID, status, result := book.MarketAutoID(side, int(req.Quantity)) if status != engine.StatusOK { c.JSON(http.StatusInternalServerError, gin.H{"error": "engine error: " + status.Error()})