package service import ( "fmt" "sync" "sync/atomic" "github.com/genex/trading-service/internal/domain/entity" "github.com/genex/trading-service/internal/domain/event" "github.com/genex/trading-service/internal/domain/vo" ) // MatchResult holds the outcome of a matching attempt. type MatchResult struct { Trades []*entity.Trade UpdatedOrder *entity.Order } // MatchingService is the application-level service responsible for the matching engine. // It orchestrates order books and produces trades, delegating persistence to repositories // and event publishing to the event publisher. type MatchingService struct { orderbooks map[string]*entity.OrderBook mu sync.RWMutex tradeSeq int64 publisher event.EventPublisher } // NewMatchingService creates a new MatchingService. func NewMatchingService(publisher event.EventPublisher) *MatchingService { if publisher == nil { publisher = &event.NoopEventPublisher{} } return &MatchingService{ orderbooks: make(map[string]*entity.OrderBook), publisher: publisher, } } // getOrCreateOrderBook retrieves or lazily initializes an order book for a coupon. func (s *MatchingService) getOrCreateOrderBook(couponID string) *entity.OrderBook { s.mu.Lock() defer s.mu.Unlock() ob, exists := s.orderbooks[couponID] if !exists { ob = entity.NewOrderBook(couponID) s.orderbooks[couponID] = ob } return ob } // PlaceOrder submits an order to the matching engine, attempts matching, // and places any unmatched remainder on the book. func (s *MatchingService) PlaceOrder(order *entity.Order) *MatchResult { ob := s.getOrCreateOrderBook(order.CouponID) result := &MatchResult{UpdatedOrder: order} // Create a trade factory closure that the orderbook entity can use createTrade := func(buy, sell *entity.Order, price vo.Price, qty int) *entity.Trade { seq := atomic.AddInt64(&s.tradeSeq, 1) tradeID := fmt.Sprintf("trade-%d", seq) trade, _ := entity.NewTrade(tradeID, buy, sell, price, qty) // Publish trade executed event _ = s.publisher.Publish(event.NewTradeExecutedEvent( trade.ID, trade.CouponID, trade.BuyOrderID, trade.SellOrderID, trade.BuyerID, trade.SellerID, trade.Price.Float64(), trade.Quantity, )) return trade } // Execute matching via the domain entity if order.Side == vo.Buy { result.Trades = ob.MatchBuyOrder(order, createTrade) } else { result.Trades = ob.MatchSellOrder(order, createTrade) } // If the order still has remaining quantity, add to the book (limit orders only) if order.RemainingQty.IsPositive() && order.Status != entity.OrderCancelled { if order.Type == vo.Limit { ob.AddOrder(order) if order.FilledQty.IsPositive() { order.Status = entity.OrderPartial } } } // Publish order placed event _ = s.publisher.Publish(event.NewOrderPlacedEvent( order.ID, order.UserID, order.CouponID, order.Side, order.Type, order.Price.Float64(), order.Quantity.Int(), )) // Publish match events for each trade for _, t := range result.Trades { _ = s.publisher.Publish(event.NewOrderMatchedEvent( order.ID, order.CouponID, t.Quantity, t.Price.Float64(), order.IsFilled(), )) } return result } // CancelOrder removes an order from the order book. func (s *MatchingService) CancelOrder(couponID, orderID string, side vo.OrderSide) bool { ob := s.getOrCreateOrderBook(couponID) removed := ob.RemoveOrder(orderID, side) if removed { _ = s.publisher.Publish(event.NewOrderCancelledEvent(orderID, "", couponID)) } return removed } // GetOrderBookSnapshot returns a depth-limited snapshot of an order book. func (s *MatchingService) GetOrderBookSnapshot(couponID string, depth int) (bids []entity.PriceLevel, asks []entity.PriceLevel) { ob := s.getOrCreateOrderBook(couponID) return ob.Snapshot(depth) } // GetAllOrderBooks returns a snapshot map of all active order books (for admin use). func (s *MatchingService) GetAllOrderBooks() map[string]*entity.OrderBook { s.mu.RLock() defer s.mu.RUnlock() result := make(map[string]*entity.OrderBook, len(s.orderbooks)) for k, v := range s.orderbooks { result[k] = v } return result } // GetTradeCount returns the total number of trades executed. func (s *MatchingService) GetTradeCount() int64 { return atomic.LoadInt64(&s.tradeSeq) }