gcx/backend/services/trading-service/internal/matching/engine.go

209 lines
5.2 KiB
Go

package matching
import (
"fmt"
"sync"
"time"
"github.com/genex/trading-service/internal/domain/entity"
"github.com/genex/trading-service/internal/orderbook"
)
type MatchResult struct {
Trades []*entity.Trade
UpdatedOrder *entity.Order
}
type Engine struct {
orderbooks map[string]*orderbook.OrderBook
mu sync.RWMutex
tradeSeq int64
}
func NewEngine() *Engine {
return &Engine{
orderbooks: make(map[string]*orderbook.OrderBook),
}
}
func (e *Engine) getOrCreateOrderBook(couponID string) *orderbook.OrderBook {
e.mu.Lock()
defer e.mu.Unlock()
ob, exists := e.orderbooks[couponID]
if !exists {
ob = orderbook.NewOrderBook(couponID)
e.orderbooks[couponID] = ob
}
return ob
}
func (e *Engine) PlaceOrder(order *entity.Order) *MatchResult {
ob := e.getOrCreateOrderBook(order.CouponID)
result := &MatchResult{UpdatedOrder: order}
if order.Type == entity.Market {
e.matchMarketOrder(ob, order, result)
} else {
e.matchLimitOrder(ob, order, result)
}
// If order still has remaining quantity, add to book
if order.RemainingQty > 0 && order.Status != entity.OrderCancelled {
if order.Type == entity.Limit {
ob.AddOrder(order)
if order.FilledQty > 0 {
order.Status = entity.OrderPartial
}
}
}
return result
}
func (e *Engine) CancelOrder(couponID, orderID string, side entity.OrderSide) bool {
ob := e.getOrCreateOrderBook(couponID)
return ob.RemoveOrder(orderID, side)
}
func (e *Engine) GetOrderBookSnapshot(couponID string, depth int) (bids []orderbook.PriceLevel, asks []orderbook.PriceLevel) {
ob := e.getOrCreateOrderBook(couponID)
return ob.Snapshot(depth)
}
func (e *Engine) matchLimitOrder(ob *orderbook.OrderBook, order *entity.Order, result *MatchResult) {
if order.Side == entity.Buy {
e.matchBuyOrder(ob, order, result)
} else {
e.matchSellOrder(ob, order, result)
}
}
func (e *Engine) matchMarketOrder(ob *orderbook.OrderBook, order *entity.Order, result *MatchResult) {
if order.Side == entity.Buy {
e.matchBuyOrder(ob, order, result)
} else {
e.matchSellOrder(ob, order, result)
}
}
func (e *Engine) matchBuyOrder(ob *orderbook.OrderBook, buyOrder *entity.Order, result *MatchResult) {
for len(ob.Asks) > 0 && buyOrder.RemainingQty > 0 {
bestAsk := &ob.Asks[0]
if buyOrder.Type == entity.Limit && bestAsk.Price > buyOrder.Price {
break
}
for len(bestAsk.Orders) > 0 && buyOrder.RemainingQty > 0 {
sellOrder := bestAsk.Orders[0]
matchQty := min(buyOrder.RemainingQty, sellOrder.RemainingQty)
matchPrice := sellOrder.Price
trade := e.createTrade(buyOrder, sellOrder, matchPrice, matchQty)
result.Trades = append(result.Trades, trade)
buyOrder.FilledQty += matchQty
buyOrder.RemainingQty -= matchQty
sellOrder.FilledQty += matchQty
sellOrder.RemainingQty -= matchQty
if sellOrder.RemainingQty == 0 {
sellOrder.Status = entity.OrderFilled
bestAsk.Orders = bestAsk.Orders[1:]
} else {
sellOrder.Status = entity.OrderPartial
}
}
if len(bestAsk.Orders) == 0 {
ob.Asks = ob.Asks[1:]
}
}
if buyOrder.RemainingQty == 0 {
buyOrder.Status = entity.OrderFilled
}
}
func (e *Engine) matchSellOrder(ob *orderbook.OrderBook, sellOrder *entity.Order, result *MatchResult) {
for len(ob.Bids) > 0 && sellOrder.RemainingQty > 0 {
bestBid := &ob.Bids[0]
if sellOrder.Type == entity.Limit && bestBid.Price < sellOrder.Price {
break
}
for len(bestBid.Orders) > 0 && sellOrder.RemainingQty > 0 {
buyOrder := bestBid.Orders[0]
matchQty := min(sellOrder.RemainingQty, buyOrder.RemainingQty)
matchPrice := buyOrder.Price
trade := e.createTrade(buyOrder, sellOrder, matchPrice, matchQty)
result.Trades = append(result.Trades, trade)
sellOrder.FilledQty += matchQty
sellOrder.RemainingQty -= matchQty
buyOrder.FilledQty += matchQty
buyOrder.RemainingQty -= matchQty
if buyOrder.RemainingQty == 0 {
buyOrder.Status = entity.OrderFilled
bestBid.Orders = bestBid.Orders[1:]
} else {
buyOrder.Status = entity.OrderPartial
}
}
if len(bestBid.Orders) == 0 {
ob.Bids = ob.Bids[1:]
}
}
if sellOrder.RemainingQty == 0 {
sellOrder.Status = entity.OrderFilled
}
}
func (e *Engine) createTrade(buyOrder, sellOrder *entity.Order, price float64, qty int) *entity.Trade {
e.tradeSeq++
takerFee := price * float64(qty) * 0.005 // 0.5% taker fee
makerFee := price * float64(qty) * 0.001 // 0.1% maker fee
return &entity.Trade{
ID: fmt.Sprintf("trade-%d", e.tradeSeq),
CouponID: buyOrder.CouponID,
BuyOrderID: buyOrder.ID,
SellOrderID: sellOrder.ID,
BuyerID: buyOrder.UserID,
SellerID: sellOrder.UserID,
Price: price,
Quantity: qty,
BuyerFee: takerFee,
SellerFee: makerFee,
CreatedAt: time.Now(),
}
}
// GetAllOrderBooks returns a snapshot of all active orderbooks for admin use.
func (e *Engine) GetAllOrderBooks() map[string]*orderbook.OrderBook {
e.mu.RLock()
defer e.mu.RUnlock()
result := make(map[string]*orderbook.OrderBook, len(e.orderbooks))
for k, v := range e.orderbooks {
result[k] = v
}
return result
}
// GetTradeCount returns the total number of trades executed.
func (e *Engine) GetTradeCount() int64 {
e.mu.RLock()
defer e.mu.RUnlock()
return e.tradeSeq
}
func min(a, b int) int {
if a < b {
return a
}
return b
}