209 lines
5.2 KiB
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
|
|
}
|