gcx/backend/services/trading-service/internal/application/service/matching_service.go

139 lines
4.2 KiB
Go

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)
}