139 lines
4.2 KiB
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)
|
|
}
|