package entity import ( "sort" "sync" "github.com/genex/trading-service/internal/domain/vo" ) // PriceLevel represents a group of orders at the same price level. type PriceLevel struct { Price vo.Price `json:"price"` Orders []*Order `json:"orders"` } // TotalQuantity returns the sum of remaining quantities at this price level. func (pl *PriceLevel) TotalQuantity() int { total := 0 for _, o := range pl.Orders { total += o.RemainingQty.Int() } return total } // OrderCount returns the number of orders at this price level. func (pl *PriceLevel) OrderCount() int { return len(pl.Orders) } // OrderBook is a domain entity representing the order book for a single coupon. // It maintains bid and ask price levels sorted for price-time priority matching. type OrderBook struct { CouponID string `json:"couponId"` Bids []PriceLevel `json:"bids"` // sorted descending (highest bid first) Asks []PriceLevel `json:"asks"` // sorted ascending (lowest ask first) mu sync.RWMutex } // NewOrderBook creates an empty order book for a given coupon. func NewOrderBook(couponID string) *OrderBook { return &OrderBook{CouponID: couponID} } // AddOrder inserts an order into the appropriate side of the order book. func (ob *OrderBook) AddOrder(order *Order) { ob.mu.Lock() defer ob.mu.Unlock() if order.Side == vo.Buy { ob.addToPriceLevels(&ob.Bids, order, true) } else { ob.addToPriceLevels(&ob.Asks, order, false) } } // RemoveOrder removes an order from the book by ID and side. Returns true if found. func (ob *OrderBook) RemoveOrder(orderID string, side vo.OrderSide) bool { ob.mu.Lock() defer ob.mu.Unlock() levels := &ob.Bids if side == vo.Sell { levels = &ob.Asks } for i, level := range *levels { for j, o := range level.Orders { if o.ID == orderID { level.Orders = append(level.Orders[:j], level.Orders[j+1:]...) if len(level.Orders) == 0 { *levels = append((*levels)[:i], (*levels)[i+1:]...) } else { (*levels)[i] = level } return true } } } return false } // BestBid returns the highest bid price level, or nil if no bids exist. func (ob *OrderBook) BestBid() *PriceLevel { ob.mu.RLock() defer ob.mu.RUnlock() if len(ob.Bids) == 0 { return nil } return &ob.Bids[0] } // BestAsk returns the lowest ask price level, or nil if no asks exist. func (ob *OrderBook) BestAsk() *PriceLevel { ob.mu.RLock() defer ob.mu.RUnlock() if len(ob.Asks) == 0 { return nil } return &ob.Asks[0] } // Snapshot returns a copy of up to `depth` price levels from each side. func (ob *OrderBook) Snapshot(depth int) (bids []PriceLevel, asks []PriceLevel) { ob.mu.RLock() defer ob.mu.RUnlock() bidDepth := minInt(depth, len(ob.Bids)) askDepth := minInt(depth, len(ob.Asks)) bids = make([]PriceLevel, bidDepth) copy(bids, ob.Bids[:bidDepth]) asks = make([]PriceLevel, askDepth) copy(asks, ob.Asks[:askDepth]) return } // MatchBuyOrder attempts to match a buy order against the ask side. // Returns the list of trades created and modifies the order book in place. // The caller must hold no lock; this method acquires the write lock internally. func (ob *OrderBook) MatchBuyOrder(buyOrder *Order, createTrade func(buy, sell *Order, price vo.Price, qty int) *Trade) []*Trade { ob.mu.Lock() defer ob.mu.Unlock() var trades []*Trade for len(ob.Asks) > 0 && buyOrder.RemainingQty.IsPositive() { bestAsk := &ob.Asks[0] // For limit orders, stop if the ask price exceeds our limit if buyOrder.Type == vo.Limit && bestAsk.Price.GreaterThan(buyOrder.Price) { break } for len(bestAsk.Orders) > 0 && buyOrder.RemainingQty.IsPositive() { sellOrder := bestAsk.Orders[0] matchQty := buyOrder.RemainingQty.Min(sellOrder.RemainingQty) matchPrice := sellOrder.Price trade := createTrade(buyOrder, sellOrder, matchPrice, matchQty.Int()) trades = append(trades, trade) buyOrder.Fill(matchQty) sellOrder.Fill(matchQty) if sellOrder.RemainingQty.IsZero() { bestAsk.Orders = bestAsk.Orders[1:] } } if len(bestAsk.Orders) == 0 { ob.Asks = ob.Asks[1:] } } return trades } // MatchSellOrder attempts to match a sell order against the bid side. // Returns the list of trades created and modifies the order book in place. func (ob *OrderBook) MatchSellOrder(sellOrder *Order, createTrade func(buy, sell *Order, price vo.Price, qty int) *Trade) []*Trade { ob.mu.Lock() defer ob.mu.Unlock() var trades []*Trade for len(ob.Bids) > 0 && sellOrder.RemainingQty.IsPositive() { bestBid := &ob.Bids[0] // For limit orders, stop if the bid price is below our limit if sellOrder.Type == vo.Limit && bestBid.Price.LessThan(sellOrder.Price) { break } for len(bestBid.Orders) > 0 && sellOrder.RemainingQty.IsPositive() { buyOrder := bestBid.Orders[0] matchQty := sellOrder.RemainingQty.Min(buyOrder.RemainingQty) matchPrice := buyOrder.Price trade := createTrade(buyOrder, sellOrder, matchPrice, matchQty.Int()) trades = append(trades, trade) sellOrder.Fill(matchQty) buyOrder.Fill(matchQty) if buyOrder.RemainingQty.IsZero() { bestBid.Orders = bestBid.Orders[1:] } } if len(bestBid.Orders) == 0 { ob.Bids = ob.Bids[1:] } } return trades } // addToPriceLevels inserts an order into the correct price level, // creating a new level if needed. Maintains sort order. func (ob *OrderBook) addToPriceLevels(levels *[]PriceLevel, order *Order, descending bool) { for i, level := range *levels { if level.Price.Equal(order.Price) { (*levels)[i].Orders = append((*levels)[i].Orders, order) return } } *levels = append(*levels, PriceLevel{Price: order.Price, Orders: []*Order{order}}) sort.Slice(*levels, func(i, j int) bool { if descending { return (*levels)[i].Price.GreaterThan((*levels)[j].Price) } return (*levels)[i].Price.LessThan((*levels)[j].Price) }) } func minInt(a, b int) int { if a < b { return a } return b }