220 lines
5.8 KiB
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
|
|
}
|