gcx/backend/services/trading-service/internal/domain/entity/orderbook.go

220 lines
5.8 KiB
Go

package entity
import (
"sort"
"sync"
"github.com/genex/trading-service/internal/domain/vo"
)
// PriceLevel represents a group of orders at the same price level.
type PriceLevel struct {
Price vo.Price `json:"price"`
Orders []*Order `json:"orders"`
}
// TotalQuantity returns the sum of remaining quantities at this price level.
func (pl *PriceLevel) TotalQuantity() int {
total := 0
for _, o := range pl.Orders {
total += o.RemainingQty.Int()
}
return total
}
// OrderCount returns the number of orders at this price level.
func (pl *PriceLevel) OrderCount() int {
return len(pl.Orders)
}
// OrderBook is a domain entity representing the order book for a single coupon.
// It maintains bid and ask price levels sorted for price-time priority matching.
type OrderBook struct {
CouponID string `json:"couponId"`
Bids []PriceLevel `json:"bids"` // sorted descending (highest bid first)
Asks []PriceLevel `json:"asks"` // sorted ascending (lowest ask first)
mu sync.RWMutex
}
// NewOrderBook creates an empty order book for a given coupon.
func NewOrderBook(couponID string) *OrderBook {
return &OrderBook{CouponID: couponID}
}
// AddOrder inserts an order into the appropriate side of the order book.
func (ob *OrderBook) AddOrder(order *Order) {
ob.mu.Lock()
defer ob.mu.Unlock()
if order.Side == vo.Buy {
ob.addToPriceLevels(&ob.Bids, order, true)
} else {
ob.addToPriceLevels(&ob.Asks, order, false)
}
}
// RemoveOrder removes an order from the book by ID and side. Returns true if found.
func (ob *OrderBook) RemoveOrder(orderID string, side vo.OrderSide) bool {
ob.mu.Lock()
defer ob.mu.Unlock()
levels := &ob.Bids
if side == vo.Sell {
levels = &ob.Asks
}
for i, level := range *levels {
for j, o := range level.Orders {
if o.ID == orderID {
level.Orders = append(level.Orders[:j], level.Orders[j+1:]...)
if len(level.Orders) == 0 {
*levels = append((*levels)[:i], (*levels)[i+1:]...)
} else {
(*levels)[i] = level
}
return true
}
}
}
return false
}
// BestBid returns the highest bid price level, or nil if no bids exist.
func (ob *OrderBook) BestBid() *PriceLevel {
ob.mu.RLock()
defer ob.mu.RUnlock()
if len(ob.Bids) == 0 {
return nil
}
return &ob.Bids[0]
}
// BestAsk returns the lowest ask price level, or nil if no asks exist.
func (ob *OrderBook) BestAsk() *PriceLevel {
ob.mu.RLock()
defer ob.mu.RUnlock()
if len(ob.Asks) == 0 {
return nil
}
return &ob.Asks[0]
}
// Snapshot returns a copy of up to `depth` price levels from each side.
func (ob *OrderBook) Snapshot(depth int) (bids []PriceLevel, asks []PriceLevel) {
ob.mu.RLock()
defer ob.mu.RUnlock()
bidDepth := minInt(depth, len(ob.Bids))
askDepth := minInt(depth, len(ob.Asks))
bids = make([]PriceLevel, bidDepth)
copy(bids, ob.Bids[:bidDepth])
asks = make([]PriceLevel, askDepth)
copy(asks, ob.Asks[:askDepth])
return
}
// MatchBuyOrder attempts to match a buy order against the ask side.
// Returns the list of trades created and modifies the order book in place.
// The caller must hold no lock; this method acquires the write lock internally.
func (ob *OrderBook) MatchBuyOrder(buyOrder *Order, createTrade func(buy, sell *Order, price vo.Price, qty int) *Trade) []*Trade {
ob.mu.Lock()
defer ob.mu.Unlock()
var trades []*Trade
for len(ob.Asks) > 0 && buyOrder.RemainingQty.IsPositive() {
bestAsk := &ob.Asks[0]
// For limit orders, stop if the ask price exceeds our limit
if buyOrder.Type == vo.Limit && bestAsk.Price.GreaterThan(buyOrder.Price) {
break
}
for len(bestAsk.Orders) > 0 && buyOrder.RemainingQty.IsPositive() {
sellOrder := bestAsk.Orders[0]
matchQty := buyOrder.RemainingQty.Min(sellOrder.RemainingQty)
matchPrice := sellOrder.Price
trade := createTrade(buyOrder, sellOrder, matchPrice, matchQty.Int())
trades = append(trades, trade)
buyOrder.Fill(matchQty)
sellOrder.Fill(matchQty)
if sellOrder.RemainingQty.IsZero() {
bestAsk.Orders = bestAsk.Orders[1:]
}
}
if len(bestAsk.Orders) == 0 {
ob.Asks = ob.Asks[1:]
}
}
return trades
}
// MatchSellOrder attempts to match a sell order against the bid side.
// Returns the list of trades created and modifies the order book in place.
func (ob *OrderBook) MatchSellOrder(sellOrder *Order, createTrade func(buy, sell *Order, price vo.Price, qty int) *Trade) []*Trade {
ob.mu.Lock()
defer ob.mu.Unlock()
var trades []*Trade
for len(ob.Bids) > 0 && sellOrder.RemainingQty.IsPositive() {
bestBid := &ob.Bids[0]
// For limit orders, stop if the bid price is below our limit
if sellOrder.Type == vo.Limit && bestBid.Price.LessThan(sellOrder.Price) {
break
}
for len(bestBid.Orders) > 0 && sellOrder.RemainingQty.IsPositive() {
buyOrder := bestBid.Orders[0]
matchQty := sellOrder.RemainingQty.Min(buyOrder.RemainingQty)
matchPrice := buyOrder.Price
trade := createTrade(buyOrder, sellOrder, matchPrice, matchQty.Int())
trades = append(trades, trade)
sellOrder.Fill(matchQty)
buyOrder.Fill(matchQty)
if buyOrder.RemainingQty.IsZero() {
bestBid.Orders = bestBid.Orders[1:]
}
}
if len(bestBid.Orders) == 0 {
ob.Bids = ob.Bids[1:]
}
}
return trades
}
// addToPriceLevels inserts an order into the correct price level,
// creating a new level if needed. Maintains sort order.
func (ob *OrderBook) addToPriceLevels(levels *[]PriceLevel, order *Order, descending bool) {
for i, level := range *levels {
if level.Price.Equal(order.Price) {
(*levels)[i].Orders = append((*levels)[i].Orders, order)
return
}
}
*levels = append(*levels, PriceLevel{Price: order.Price, Orders: []*Order{order}})
sort.Slice(*levels, func(i, j int) bool {
if descending {
return (*levels)[i].Price.GreaterThan((*levels)[j].Price)
}
return (*levels)[i].Price.LessThan((*levels)[j].Price)
})
}
func minInt(a, b int) int {
if a < b {
return a
}
return b
}