mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
- pkg/lx/signed_order{,_cpu,_gpu,_nogpu}.go: SignedOrder with SigningHash
and BatchVerifyOrders cgo bridge
- pkg/lx/signed_order_metal_anchor_darwin.{c,go}: keep linker from
dropping libsecp256k1_metal.a from the static archive
- pkg/lx/amm{,_gpu,_nogpu}.go: ReservePair, ConstantProductOut,
BatchEvalConstantProduct via Metal kernel
- pkg/lx/{signed_order,amm_gpu}_test.go: tests
- pkg/lx/orderbook.go, test/benchmark/orderbook_bench_test.go: misc
End-to-end through cgo (Apple M1 Max):
BatchVerifyOrders CPU: 27.91 verifies/sec
BatchVerifyOrders Metal: 87,229 verifies/sec
1469 lines
35 KiB
Go
1469 lines
35 KiB
Go
package lx
|
|
|
|
import (
|
|
"container/heap"
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
"unsafe"
|
|
)
|
|
|
|
// PriceInt represents price as integer (multiplied by 10^8 for 8 decimal precision)
|
|
type PriceInt int64
|
|
|
|
const PriceMultiplier = 100000000
|
|
|
|
// MaxSafePrice is the maximum price that can be safely converted to PriceInt
|
|
// without overflow (math.MaxInt64 / PriceMultiplier)
|
|
const MaxSafePrice = float64(math.MaxInt64) / PriceMultiplier
|
|
|
|
// safePriceToInt converts a float64 price to PriceInt with overflow protection
|
|
func safePriceToInt(price float64) (PriceInt, error) {
|
|
if price < 0 {
|
|
return 0, fmt.Errorf("negative price: %f", price)
|
|
}
|
|
if price > MaxSafePrice {
|
|
return 0, fmt.Errorf("price overflow: %f exceeds max safe price %f", price, MaxSafePrice)
|
|
}
|
|
return PriceInt(price * PriceMultiplier), nil
|
|
}
|
|
|
|
// OrderFlags constants (type defined in types_common.go)
|
|
const (
|
|
OrderFlagNone OrderFlags = 0
|
|
OrderFlagPostOnly OrderFlags = 1 << 0
|
|
OrderFlagReduceOnly OrderFlags = 1 << 1
|
|
OrderFlagSTP OrderFlags = 1 << 2 // Self-trade prevention
|
|
)
|
|
|
|
// Backend represents the available acceleration backend.
|
|
//
|
|
// LP-108: collapsed to BackendGo only. The BackendCUDA / BackendMLX /
|
|
// BackendCGO branches in detectBestBackend used to set the enum but
|
|
// no code path branched on it — the matching engine ran the Go path
|
|
// regardless. The MLX engine (archive/lp108-2026-05-04/mlx_engine.go)
|
|
// was a time.Sleep stub. Removed to avoid pretending acceleration
|
|
// exists when it doesn't.
|
|
//
|
|
// When real GPU/FPGA matching ships, add the variants back AND wire
|
|
// them through the match path with a parity test against BackendGo.
|
|
type Backend int
|
|
|
|
const (
|
|
BackendGo Backend = iota
|
|
// BackendGPU opt-in flag for the GPU acceleration paths in
|
|
// signed_order_gpu.go (batch ecrecover) and amm_gpu.go (batch xy=k).
|
|
// The matching engine still runs the Go path; selecting BackendGPU
|
|
// only affects calls that explicitly check `currentBackend`.
|
|
BackendGPU
|
|
)
|
|
|
|
var (
|
|
currentBackend = BackendGo
|
|
|
|
// Global memory pools for zero-allocation
|
|
orderPool = &sync.Pool{
|
|
New: func() interface{} {
|
|
return &Order{}
|
|
},
|
|
}
|
|
levelPool = &sync.Pool{
|
|
New: func() interface{} {
|
|
return &OptimizedPriceLevel{}
|
|
},
|
|
}
|
|
)
|
|
|
|
// Errors
|
|
var (
|
|
ErrOrderNotFound = fmt.Errorf("order not found")
|
|
ErrInsufficientFunds = fmt.Errorf("insufficient funds")
|
|
ErrInvalidOrder = fmt.Errorf("invalid order")
|
|
ErrInvalidPrice = fmt.Errorf("invalid price")
|
|
ErrInvalidSize = fmt.Errorf("invalid size")
|
|
ErrVaultNotFound = fmt.Errorf("vault not found")
|
|
ErrPerpNotFound = fmt.Errorf("perpetual market not found")
|
|
ErrExcessiveLeverage = fmt.Errorf("excessive leverage")
|
|
ErrSelfTrade = fmt.Errorf("self trade not allowed")
|
|
ErrPostOnlyWouldTake = fmt.Errorf("post-only order would take")
|
|
)
|
|
|
|
// OrderBook represents a complete order book for a trading pair - OPTIMIZED VERSION
|
|
type OrderBook struct {
|
|
Symbol string
|
|
|
|
// Optimized data structures
|
|
bids unsafe.Pointer // *OrderTree
|
|
asks unsafe.Pointer // *OrderTree
|
|
|
|
// For compatibility - expose these directly
|
|
Bids *OrderTree
|
|
Asks *OrderTree
|
|
|
|
// Trade history with circular buffer
|
|
Trades []Trade // Keep for compatibility but use circular buffer internally
|
|
tradesBuffer *CircularTradeBuffer
|
|
|
|
// Order tracking with lock-free map
|
|
Orders map[uint64]*Order // Keep for API compatibility
|
|
ordersMap sync.Map // Lock-free internal
|
|
UserOrders map[string][]uint64
|
|
userOrdersMap sync.Map
|
|
|
|
// Atomic counters
|
|
LastTradeID uint64
|
|
LastOrderID uint64
|
|
LastUpdateID uint64
|
|
OrderSeq uint64 // Add OrderSeq field for compatibility
|
|
|
|
// Configuration
|
|
EnableImmediateMatching bool // When true, orders match immediately on add
|
|
|
|
// Market data feed
|
|
subscribers []chan MarketDataUpdate
|
|
subMu sync.RWMutex
|
|
|
|
// Single write lock for structural changes
|
|
mu sync.RWMutex // Keep for API compatibility
|
|
writeLock sync.Mutex // Internal write lock
|
|
|
|
// Cache line padding to prevent false sharing
|
|
_padding [64]byte
|
|
}
|
|
|
|
// OrderTree implements optimized price-time priority order book side
|
|
type OrderTree struct {
|
|
side Side
|
|
priceLevels map[PriceInt]*OptimizedPriceLevel // Integer keys - FAST!
|
|
priceTree *IntBTree // B-tree for sorted prices
|
|
orders map[uint64]*Order
|
|
bestPrice atomic.Int64 // Atomic best price
|
|
sequence uint64
|
|
mu sync.RWMutex
|
|
priceHeap PriceHeap // Keep for compatibility
|
|
}
|
|
|
|
// OptimizedPriceLevel with lock-free operations
|
|
type OptimizedPriceLevel struct {
|
|
Price float64 // Keep original price for API
|
|
PriceInt PriceInt // Integer price for fast operations
|
|
Orders []*Order // Keep slice for compatibility
|
|
OrderList *OrderLinkedList
|
|
TotalSize float64
|
|
OrderCount int
|
|
mu sync.RWMutex
|
|
|
|
// Atomic counters for lock-free updates
|
|
atomicSize atomic.Int64
|
|
atomicCount atomic.Int32
|
|
}
|
|
|
|
// OrderLinkedList for O(1) insertion and removal
|
|
type OrderLinkedList struct {
|
|
head *OrderNode
|
|
tail *OrderNode
|
|
index map[uint64]*OrderNode
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
type OrderNode struct {
|
|
Order *Order
|
|
Next *OrderNode
|
|
Prev *OrderNode
|
|
}
|
|
|
|
// CircularTradeBuffer for efficient trade history
|
|
type CircularTradeBuffer struct {
|
|
buffer []Trade // Changed from fixed array to slice
|
|
head uint64
|
|
tail uint64
|
|
size uint64
|
|
capacity uint64
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// IntBTree is a B-tree for integer keys (prices)
|
|
type IntBTree struct {
|
|
root *IntBTreeNode
|
|
degree int
|
|
isMaxHeap bool
|
|
}
|
|
|
|
type IntBTreeNode struct {
|
|
keys []PriceInt
|
|
children []*IntBTreeNode
|
|
isLeaf bool
|
|
n int
|
|
}
|
|
|
|
// NewOrderBook creates an optimized order book
|
|
func NewOrderBook(symbol string) *OrderBook {
|
|
ob := &OrderBook{
|
|
Symbol: symbol,
|
|
Trades: make([]Trade, 0),
|
|
tradesBuffer: &CircularTradeBuffer{
|
|
buffer: make([]Trade, 1000), // Start with 1000 instead of 100,000
|
|
capacity: 1000,
|
|
},
|
|
Orders: make(map[uint64]*Order),
|
|
UserOrders: make(map[string][]uint64),
|
|
}
|
|
|
|
// Initialize optimized bid and ask trees
|
|
bidTree := &OrderTree{
|
|
side: Buy,
|
|
priceLevels: make(map[PriceInt]*OptimizedPriceLevel),
|
|
priceTree: NewIntBTree(32, true),
|
|
orders: make(map[uint64]*Order),
|
|
priceHeap: &MaxPriceHeap{},
|
|
}
|
|
askTree := &OrderTree{
|
|
side: Sell,
|
|
priceLevels: make(map[PriceInt]*OptimizedPriceLevel),
|
|
priceTree: NewIntBTree(32, false),
|
|
orders: make(map[uint64]*Order),
|
|
priceHeap: &MinPriceHeap{},
|
|
}
|
|
|
|
// Initialize heaps
|
|
heap.Init(bidTree.priceHeap)
|
|
heap.Init(askTree.priceHeap)
|
|
|
|
// Set both pointer and direct references for compatibility
|
|
atomic.StorePointer(&ob.bids, unsafe.Pointer(bidTree))
|
|
atomic.StorePointer(&ob.asks, unsafe.Pointer(askTree))
|
|
ob.Bids = bidTree
|
|
ob.Asks = askTree
|
|
|
|
return ob
|
|
}
|
|
|
|
// NewOrderTree creates a new order tree (for compatibility)
|
|
func NewOrderTree(side Side) *OrderTree {
|
|
tree := &OrderTree{
|
|
side: side,
|
|
priceLevels: make(map[PriceInt]*OptimizedPriceLevel),
|
|
orders: make(map[uint64]*Order),
|
|
}
|
|
|
|
// Initialize heap and B-tree based on side
|
|
if side == Buy {
|
|
tree.priceHeap = &MaxPriceHeap{}
|
|
tree.priceTree = NewIntBTree(32, true)
|
|
} else {
|
|
tree.priceHeap = &MinPriceHeap{}
|
|
tree.priceTree = NewIntBTree(32, false)
|
|
}
|
|
heap.Init(tree.priceHeap)
|
|
|
|
return tree
|
|
}
|
|
|
|
// AddOrder with optimized integer price handling
|
|
func (ob *OrderBook) AddOrder(order *Order) uint64 {
|
|
// Auto-assign ID if not set
|
|
if order.ID == 0 {
|
|
order.ID = atomic.AddUint64(&ob.LastOrderID, 1)
|
|
}
|
|
|
|
// Set status if not set
|
|
if order.Status == "" {
|
|
order.Status = Open
|
|
}
|
|
|
|
// Set timestamp if not set
|
|
if order.Timestamp.IsZero() {
|
|
order.Timestamp = time.Now()
|
|
}
|
|
|
|
// Validate order
|
|
if err := ob.validateOrder(order); err != nil {
|
|
order.Status = Rejected
|
|
return 0
|
|
}
|
|
|
|
// Convert price to integer for fast operations
|
|
priceInt := PriceInt(order.Price * PriceMultiplier)
|
|
|
|
// Handle market orders
|
|
if order.Type == Market {
|
|
return ob.processMarketOrderOptimized(order)
|
|
}
|
|
|
|
// Get write lock for modifications
|
|
ob.mu.Lock()
|
|
defer ob.mu.Unlock()
|
|
|
|
// Check self-trade prevention - check both User and UserID fields
|
|
userIdentifier := order.User
|
|
if userIdentifier == "" {
|
|
userIdentifier = order.UserID
|
|
}
|
|
if userIdentifier != "" && ob.checkSelfTrade(order) {
|
|
order.Status = Rejected
|
|
return 0
|
|
}
|
|
|
|
// Check post-only
|
|
if order.PostOnly || order.Flags&OrderFlagPostOnly != 0 {
|
|
if ob.wouldTakeLiquidity(order) {
|
|
order.Status = Rejected
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// Handle time-in-force and check for immediate matches
|
|
numTrades := uint64(0)
|
|
|
|
// Set remaining size
|
|
order.RemainingSize = order.Size
|
|
|
|
// Only match immediately for IOC/FOK orders
|
|
// Regular limit orders are added to book and matched via MatchOrders()
|
|
if order.TimeInForce == ImmediateOrCancel || order.TimeInForce == FillOrKill {
|
|
numTrades = ob.tryMatchImmediateLocked(order)
|
|
|
|
if order.TimeInForce == FillOrKill && order.RemainingSize > 0 {
|
|
order.Status = Rejected
|
|
return 0
|
|
}
|
|
|
|
if order.TimeInForce == ImmediateOrCancel && order.RemainingSize > 0 {
|
|
// IOC order partially filled, return order ID
|
|
return order.ID
|
|
}
|
|
}
|
|
|
|
// Add remaining to book
|
|
if order.RemainingSize > 0 || (order.RemainingSize == 0 && order.Size > 0) {
|
|
if order.RemainingSize == 0 {
|
|
order.RemainingSize = order.Size
|
|
}
|
|
|
|
// Get appropriate tree
|
|
var tree *OrderTree
|
|
if order.Side == Buy {
|
|
tree = (*OrderTree)(atomic.LoadPointer(&ob.bids))
|
|
} else {
|
|
tree = (*OrderTree)(atomic.LoadPointer(&ob.asks))
|
|
}
|
|
|
|
// Add to tree with optimized integer price
|
|
ob.addToTreeOptimized(tree, order, priceInt)
|
|
|
|
// Track order
|
|
ob.Orders[order.ID] = order
|
|
ob.ordersMap.Store(order.ID, order)
|
|
|
|
if ob.UserOrders[order.User] == nil {
|
|
ob.UserOrders[order.User] = make([]uint64, 0)
|
|
}
|
|
ob.UserOrders[order.User] = append(ob.UserOrders[order.User], order.ID)
|
|
|
|
// Match immediately if enabled (for optimized order book)
|
|
if ob.EnableImmediateMatching {
|
|
numTrades += ob.tryMatchImmediateLocked(order)
|
|
}
|
|
|
|
// Publish market data update
|
|
ob.publishUpdate(MarketDataUpdate{
|
|
Type: OrderAdded,
|
|
Symbol: ob.Symbol,
|
|
Timestamp: time.Now(),
|
|
Data: map[string]interface{}{
|
|
"order_id": order.ID,
|
|
"price": order.Price,
|
|
"size": order.Size,
|
|
"side": order.Side,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Return the order ID for successful orders
|
|
// For tests that need trade count, use MatchOrders() or GetTradeCount()
|
|
return order.ID
|
|
}
|
|
|
|
// addOrder for compatibility - wraps addToTreeOptimized
|
|
func (tree *OrderTree) addOrder(order *Order) {
|
|
priceInt := PriceInt(order.Price * PriceMultiplier)
|
|
tree.addOrderOptimized(order, priceInt)
|
|
}
|
|
|
|
// addOrderOptimized adds order to tree with integer price
|
|
func (tree *OrderTree) addOrderOptimized(order *Order, priceInt PriceInt) {
|
|
tree.mu.Lock()
|
|
defer tree.mu.Unlock()
|
|
|
|
// Get or create price level
|
|
level, exists := tree.priceLevels[priceInt]
|
|
if !exists {
|
|
level = &OptimizedPriceLevel{
|
|
Price: order.Price,
|
|
PriceInt: priceInt,
|
|
Orders: make([]*Order, 0),
|
|
OrderList: &OrderLinkedList{
|
|
index: make(map[uint64]*OrderNode),
|
|
},
|
|
}
|
|
tree.priceLevels[priceInt] = level
|
|
|
|
// Add to B-tree and heap for compatibility
|
|
tree.priceTree.Insert(priceInt)
|
|
heap.Push(tree.priceHeap, order.Price)
|
|
|
|
// Update best price atomically
|
|
if tree.side == Buy {
|
|
currentBest := tree.bestPrice.Load()
|
|
if currentBest == 0 || int64(priceInt) > currentBest {
|
|
tree.bestPrice.Store(int64(priceInt))
|
|
}
|
|
} else {
|
|
currentBest := tree.bestPrice.Load()
|
|
if currentBest == 0 || int64(priceInt) < currentBest {
|
|
tree.bestPrice.Store(int64(priceInt))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add to both slice (for compatibility) and linked list (for performance)
|
|
level.mu.Lock()
|
|
level.Orders = append(level.Orders, order)
|
|
|
|
// Add to linked list
|
|
node := &OrderNode{Order: order}
|
|
if level.OrderList.head == nil {
|
|
level.OrderList.head = node
|
|
level.OrderList.tail = node
|
|
} else {
|
|
level.OrderList.tail.Next = node
|
|
node.Prev = level.OrderList.tail
|
|
level.OrderList.tail = node
|
|
}
|
|
level.OrderList.index[order.ID] = node
|
|
|
|
remainingSize := order.Size - order.Filled
|
|
if order.RemainingSize > 0 {
|
|
remainingSize = order.RemainingSize
|
|
}
|
|
level.TotalSize += remainingSize
|
|
level.OrderCount++
|
|
level.mu.Unlock()
|
|
|
|
// Update atomic counters
|
|
level.atomicSize.Add(int64(remainingSize * PriceMultiplier))
|
|
level.atomicCount.Add(1)
|
|
|
|
// Track in tree
|
|
tree.orders[order.ID] = order
|
|
atomic.AddUint64(&tree.sequence, 1)
|
|
}
|
|
|
|
// addToTreeOptimized adds order to tree with integer price
|
|
func (ob *OrderBook) addToTreeOptimized(tree *OrderTree, order *Order, priceInt PriceInt) {
|
|
tree.addOrderOptimized(order, priceInt)
|
|
}
|
|
|
|
// removeOrder optimized with O(1) removal
|
|
func (tree *OrderTree) removeOrder(order *Order) {
|
|
tree.mu.Lock()
|
|
defer tree.mu.Unlock()
|
|
|
|
priceInt := PriceInt(order.Price * PriceMultiplier)
|
|
level, exists := tree.priceLevels[priceInt]
|
|
if !exists {
|
|
return
|
|
}
|
|
|
|
level.mu.Lock()
|
|
|
|
// Remove from linked list (O(1))
|
|
if level.OrderList != nil {
|
|
node, exists := level.OrderList.index[order.ID]
|
|
if exists {
|
|
if node.Prev != nil {
|
|
node.Prev.Next = node.Next
|
|
} else {
|
|
level.OrderList.head = node.Next
|
|
}
|
|
|
|
if node.Next != nil {
|
|
node.Next.Prev = node.Prev
|
|
} else {
|
|
level.OrderList.tail = node.Prev
|
|
}
|
|
|
|
delete(level.OrderList.index, order.ID)
|
|
}
|
|
}
|
|
|
|
// Remove from slice for compatibility
|
|
for i, o := range level.Orders {
|
|
if o.ID == order.ID {
|
|
level.Orders = append(level.Orders[:i], level.Orders[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
remainingSize := order.Size - order.Filled
|
|
if order.RemainingSize > 0 {
|
|
remainingSize = order.RemainingSize
|
|
}
|
|
level.TotalSize -= remainingSize
|
|
level.OrderCount--
|
|
|
|
// Remove level if empty
|
|
if level.OrderCount == 0 {
|
|
delete(tree.priceLevels, priceInt)
|
|
tree.priceTree.Delete(priceInt)
|
|
|
|
// Update best price if needed
|
|
if tree.side == Buy && tree.bestPrice.Load() == int64(priceInt) {
|
|
if next := tree.priceTree.Max(); next != 0 {
|
|
tree.bestPrice.Store(int64(next))
|
|
} else {
|
|
tree.bestPrice.Store(0)
|
|
}
|
|
} else if tree.side == Sell && tree.bestPrice.Load() == int64(priceInt) {
|
|
if next := tree.priceTree.Min(); next != 0 {
|
|
tree.bestPrice.Store(int64(next))
|
|
} else {
|
|
tree.bestPrice.Store(0)
|
|
}
|
|
}
|
|
}
|
|
|
|
level.mu.Unlock()
|
|
|
|
// Update atomic counters
|
|
level.atomicSize.Add(-int64(remainingSize * PriceMultiplier))
|
|
level.atomicCount.Add(-1)
|
|
|
|
delete(tree.orders, order.ID)
|
|
}
|
|
|
|
// getBestOrder optimized with O(1) best price lookup
|
|
func (tree *OrderTree) getBestOrder() *Order {
|
|
// Fast path: check atomic best price
|
|
bestPriceInt := PriceInt(tree.bestPrice.Load())
|
|
if bestPriceInt == 0 {
|
|
// Fallback to heap for compatibility
|
|
return tree.getBestOrderViaHeap()
|
|
}
|
|
|
|
tree.mu.RLock()
|
|
defer tree.mu.RUnlock()
|
|
|
|
level, exists := tree.priceLevels[bestPriceInt]
|
|
if !exists || level.OrderList == nil || level.OrderList.head == nil {
|
|
// Fallback to heap for compatibility
|
|
return tree.getBestOrderViaHeap()
|
|
}
|
|
|
|
// Use linked list for O(1) access
|
|
level.mu.RLock()
|
|
defer level.mu.RUnlock()
|
|
|
|
if level.OrderList.head != nil {
|
|
return level.OrderList.head.Order
|
|
}
|
|
|
|
// Fallback to slice
|
|
if len(level.Orders) > 0 {
|
|
return level.Orders[0]
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getBestOrderViaHeap for compatibility when B-tree is inconsistent
|
|
func (tree *OrderTree) getBestOrderViaHeap() *Order {
|
|
for tree.priceHeap.Len() > 0 {
|
|
price := tree.priceHeap.Peek()
|
|
priceInt := PriceInt(price * PriceMultiplier)
|
|
|
|
level, exists := tree.priceLevels[priceInt]
|
|
if !exists {
|
|
heap.Pop(tree.priceHeap)
|
|
continue
|
|
}
|
|
|
|
level.mu.RLock()
|
|
if len(level.Orders) > 0 {
|
|
order := level.Orders[0]
|
|
level.mu.RUnlock()
|
|
if order.Status == Open || order.Status == PartiallyFilled {
|
|
return order
|
|
}
|
|
} else {
|
|
level.mu.RUnlock()
|
|
}
|
|
|
|
heap.Pop(tree.priceHeap)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// tryMatchImmediateLocked with optimized matching
|
|
func (ob *OrderBook) tryMatchImmediateLocked(order *Order) uint64 {
|
|
numTrades := uint64(0)
|
|
|
|
if order.RemainingSize == 0 && order.Size > 0 {
|
|
order.RemainingSize = order.Size
|
|
}
|
|
|
|
var oppositeTree *OrderTree
|
|
if order.Side == Buy {
|
|
oppositeTree = (*OrderTree)(atomic.LoadPointer(&ob.asks))
|
|
} else {
|
|
oppositeTree = (*OrderTree)(atomic.LoadPointer(&ob.bids))
|
|
}
|
|
|
|
for order.RemainingSize > 0 {
|
|
bestOrder := oppositeTree.getBestOrder()
|
|
if bestOrder == nil {
|
|
break
|
|
}
|
|
|
|
// Price check
|
|
if order.Type == Limit {
|
|
if order.Side == Buy && order.Price < bestOrder.Price {
|
|
break
|
|
}
|
|
if order.Side == Sell && order.Price > bestOrder.Price {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Self-trade check
|
|
if order.User != "" && order.User == bestOrder.User {
|
|
oppositeTree.removeOrder(bestOrder)
|
|
delete(ob.Orders, bestOrder.ID)
|
|
ob.ordersMap.Delete(bestOrder.ID)
|
|
continue
|
|
}
|
|
|
|
// Calculate trade size
|
|
var tradeSize float64
|
|
bestRemaining := bestOrder.Size - bestOrder.Filled
|
|
if bestOrder.RemainingSize > 0 {
|
|
bestRemaining = bestOrder.RemainingSize
|
|
}
|
|
tradeSize = math.Min(order.RemainingSize, bestRemaining)
|
|
|
|
// Create trade
|
|
ob.LastTradeID++
|
|
trade := Trade{
|
|
ID: ob.LastTradeID,
|
|
Price: bestOrder.Price,
|
|
Size: tradeSize,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
// Update orders
|
|
order.RemainingSize -= tradeSize
|
|
order.Filled += tradeSize
|
|
|
|
if bestOrder.RemainingSize > 0 {
|
|
bestOrder.RemainingSize -= tradeSize
|
|
}
|
|
bestOrder.Filled += tradeSize
|
|
|
|
// Handle order status
|
|
if order.RemainingSize <= 0 {
|
|
order.Status = Filled
|
|
} else {
|
|
order.Status = PartiallyFilled
|
|
}
|
|
|
|
if (bestOrder.RemainingSize <= 0 && bestOrder.Filled >= bestOrder.Size) || bestOrder.Filled >= bestOrder.Size {
|
|
bestOrder.Status = Filled
|
|
oppositeTree.removeOrder(bestOrder)
|
|
delete(ob.Orders, bestOrder.ID)
|
|
ob.ordersMap.Delete(bestOrder.ID)
|
|
} else {
|
|
bestOrder.Status = PartiallyFilled
|
|
}
|
|
|
|
// Add trade
|
|
ob.Trades = append(ob.Trades, trade)
|
|
if ob.tradesBuffer != nil {
|
|
ob.tradesBuffer.Add(trade)
|
|
}
|
|
|
|
// Limit trades history for compatibility
|
|
if len(ob.Trades) > 100000 {
|
|
ob.Trades = ob.Trades[len(ob.Trades)-50000:]
|
|
}
|
|
|
|
numTrades++
|
|
}
|
|
|
|
return numTrades
|
|
}
|
|
|
|
// processMarketOrderOptimized handles market orders
|
|
func (ob *OrderBook) processMarketOrderOptimized(order *Order) uint64 {
|
|
ob.mu.Lock()
|
|
defer ob.mu.Unlock()
|
|
|
|
order.RemainingSize = order.Size
|
|
ob.tryMatchImmediateLocked(order)
|
|
|
|
// Reject market orders that couldn't be filled at all
|
|
if order.Type == Market && order.RemainingSize == order.Size {
|
|
order.Status = Rejected
|
|
return 0
|
|
}
|
|
|
|
// Return order ID for consistency
|
|
if order.Status == Rejected {
|
|
return 0
|
|
}
|
|
return order.ID
|
|
}
|
|
|
|
// processMarketOrderLocked for compatibility (assumes lock is already held)
|
|
func (ob *OrderBook) processMarketOrderLocked(order *Order) uint64 {
|
|
order.RemainingSize = order.Size
|
|
ob.tryMatchImmediateLocked(order)
|
|
|
|
// Reject market orders that couldn't be filled at all
|
|
if order.Type == Market && order.RemainingSize == order.Size {
|
|
order.Status = Rejected
|
|
return 0
|
|
}
|
|
|
|
// Return order ID for consistency
|
|
if order.Status == Rejected {
|
|
return 0
|
|
}
|
|
return order.ID
|
|
}
|
|
|
|
// MatchOrders attempts to match orders in the book and returns all trades
|
|
func (ob *OrderBook) MatchOrders() []Trade {
|
|
ob.mu.Lock()
|
|
defer ob.mu.Unlock()
|
|
|
|
// Clear existing trades for this matching session
|
|
startingTradeCount := len(ob.Trades)
|
|
|
|
trades := make([]Trade, 0)
|
|
|
|
for {
|
|
// Get best bid and ask
|
|
bestBid := ob.Bids.getBestOrder()
|
|
bestAsk := ob.Asks.getBestOrder()
|
|
|
|
if bestBid == nil || bestAsk == nil {
|
|
break
|
|
}
|
|
|
|
// Check if orders cross (bid must be >= ask for a match)
|
|
if bestBid.Price < bestAsk.Price {
|
|
break
|
|
}
|
|
|
|
// Self-trade prevention
|
|
if bestBid.User == bestAsk.User && bestBid.User != "" {
|
|
// Cancel the smaller order
|
|
if bestBid.Size < bestAsk.Size {
|
|
ob.cancelOrderInternal(bestBid)
|
|
continue
|
|
} else {
|
|
ob.cancelOrderInternal(bestAsk)
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Determine trade size
|
|
bidRemaining := bestBid.Size - bestBid.Filled
|
|
askRemaining := bestAsk.Size - bestAsk.Filled
|
|
tradeSize := math.Min(bidRemaining, askRemaining)
|
|
|
|
// Determine trade price (price-time priority)
|
|
var tradePrice float64
|
|
var takerSide Side
|
|
if bestBid.Timestamp.Before(bestAsk.Timestamp) {
|
|
tradePrice = bestBid.Price
|
|
takerSide = Sell
|
|
} else {
|
|
tradePrice = bestAsk.Price
|
|
takerSide = Buy
|
|
}
|
|
|
|
// Calculate fees (currently unused)
|
|
// var fee float64
|
|
// if takerSide == Buy {
|
|
// fee = tradeSize * tradePrice * 0.0002
|
|
// } else {
|
|
// fee = tradeSize * tradePrice * 0.0002
|
|
// }
|
|
|
|
// Create trade
|
|
ob.LastTradeID++
|
|
trade := Trade{
|
|
ID: ob.LastTradeID,
|
|
Price: tradePrice,
|
|
Size: tradeSize,
|
|
BuyOrder: bestBid.ID,
|
|
SellOrder: bestAsk.ID,
|
|
Timestamp: time.Now(),
|
|
TakerSide: takerSide,
|
|
MatchType: "normal",
|
|
}
|
|
|
|
// Update orders
|
|
bestBid.Filled += tradeSize
|
|
bestAsk.Filled += tradeSize
|
|
|
|
// Check if either order is fully filled
|
|
bidFullyFilled := bestBid.Filled >= bestBid.Size
|
|
askFullyFilled := bestAsk.Filled >= bestAsk.Size
|
|
|
|
if bidFullyFilled {
|
|
bestBid.Status = Filled
|
|
ob.Bids.removeOrder(bestBid)
|
|
} else {
|
|
bestBid.Status = PartiallyFilled
|
|
}
|
|
|
|
if askFullyFilled {
|
|
bestAsk.Status = Filled
|
|
ob.Asks.removeOrder(bestAsk)
|
|
} else {
|
|
bestAsk.Status = PartiallyFilled
|
|
}
|
|
|
|
// Set match type based on whether any order is fully filled
|
|
if bidFullyFilled || askFullyFilled {
|
|
trade.MatchType = "full"
|
|
}
|
|
|
|
trades = append(trades, trade)
|
|
ob.Trades = append(ob.Trades, trade)
|
|
|
|
// Limit trades history
|
|
if len(ob.Trades) > 100000 {
|
|
ob.Trades = ob.Trades[len(ob.Trades)-50000:]
|
|
}
|
|
}
|
|
|
|
// Return only trades generated in this MatchOrders call
|
|
if startingTradeCount < len(ob.Trades) {
|
|
return ob.Trades[startingTradeCount:]
|
|
}
|
|
return trades
|
|
}
|
|
|
|
// Helper methods
|
|
func (ob *OrderBook) validateOrder(order *Order) error {
|
|
if order == nil {
|
|
return ErrInvalidOrder
|
|
}
|
|
// Market and Stop orders don't require a price
|
|
if order.Type != Market && order.Type != Stop && order.Price <= 0 {
|
|
return ErrInvalidPrice
|
|
}
|
|
// Stop orders must have a stop price
|
|
if order.Type == Stop && order.StopPrice <= 0 {
|
|
return ErrInvalidPrice
|
|
}
|
|
if order.Size <= 0 {
|
|
return ErrInvalidSize
|
|
}
|
|
// Check for price overflow
|
|
if order.Price > MaxSafePrice {
|
|
return fmt.Errorf("price %f exceeds max safe price %f: %w", order.Price, MaxSafePrice, ErrInvalidPrice)
|
|
}
|
|
if order.StopPrice > MaxSafePrice {
|
|
return fmt.Errorf("stop price %f exceeds max safe price %f: %w", order.StopPrice, MaxSafePrice, ErrInvalidPrice)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ob *OrderBook) checkSelfTrade(order *Order) bool {
|
|
var oppositeTree *OrderTree
|
|
if order.Side == Buy {
|
|
oppositeTree = (*OrderTree)(atomic.LoadPointer(&ob.asks))
|
|
} else {
|
|
oppositeTree = (*OrderTree)(atomic.LoadPointer(&ob.bids))
|
|
}
|
|
|
|
bestOrder := oppositeTree.getBestOrder()
|
|
if bestOrder == nil {
|
|
return false
|
|
}
|
|
|
|
// Get user identifiers for both orders
|
|
orderUser := order.User
|
|
if orderUser == "" {
|
|
orderUser = order.UserID
|
|
}
|
|
|
|
bestUser := bestOrder.User
|
|
if bestUser == "" {
|
|
bestUser = bestOrder.UserID
|
|
}
|
|
|
|
if orderUser != "" && orderUser == bestUser {
|
|
if order.Side == Buy && order.Price >= bestOrder.Price {
|
|
return true
|
|
}
|
|
if order.Side == Sell && order.Price <= bestOrder.Price {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (ob *OrderBook) wouldTakeLiquidity(order *Order) bool {
|
|
var oppositeTree *OrderTree
|
|
if order.Side == Buy {
|
|
oppositeTree = (*OrderTree)(atomic.LoadPointer(&ob.asks))
|
|
} else {
|
|
oppositeTree = (*OrderTree)(atomic.LoadPointer(&ob.bids))
|
|
}
|
|
|
|
bestOrder := oppositeTree.getBestOrder()
|
|
if bestOrder == nil {
|
|
return false
|
|
}
|
|
|
|
if order.Side == Buy && order.Price >= bestOrder.Price {
|
|
return true
|
|
}
|
|
if order.Side == Sell && order.Price <= bestOrder.Price {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (ob *OrderBook) cancelOrderInternal(order *Order) {
|
|
order.Status = Canceled
|
|
if order.Side == Buy {
|
|
ob.Bids.removeOrder(order)
|
|
} else {
|
|
ob.Asks.removeOrder(order)
|
|
}
|
|
}
|
|
|
|
// ErrUnauthorized is returned when a user attempts to cancel an order they don't own
|
|
var ErrUnauthorized = fmt.Errorf("unauthorized: order does not belong to user")
|
|
|
|
// CancelOrder cancels an order - DEPRECATED: use CancelOrderWithAuth for authorization
|
|
func (ob *OrderBook) CancelOrder(orderID uint64) error {
|
|
ob.mu.Lock()
|
|
defer ob.mu.Unlock()
|
|
|
|
order, exists := ob.Orders[orderID]
|
|
if !exists {
|
|
return ErrOrderNotFound
|
|
}
|
|
|
|
if order.Status != Open && order.Status != PartiallyFilled {
|
|
return fmt.Errorf("order not cancellable")
|
|
}
|
|
|
|
ob.cancelOrderInternal(order)
|
|
return nil
|
|
}
|
|
|
|
// CancelOrderWithAuth cancels an order with user authorization
|
|
func (ob *OrderBook) CancelOrderWithAuth(orderID uint64, userID string) error {
|
|
ob.mu.Lock()
|
|
defer ob.mu.Unlock()
|
|
|
|
order, exists := ob.Orders[orderID]
|
|
if !exists {
|
|
return ErrOrderNotFound
|
|
}
|
|
|
|
// Verify the user owns this order
|
|
orderOwner := order.User
|
|
if orderOwner == "" {
|
|
orderOwner = order.UserID
|
|
}
|
|
if orderOwner != "" && orderOwner != userID {
|
|
return ErrUnauthorized
|
|
}
|
|
|
|
if order.Status != Open && order.Status != PartiallyFilled {
|
|
return fmt.Errorf("order not cancellable")
|
|
}
|
|
|
|
ob.cancelOrderInternal(order)
|
|
return nil
|
|
}
|
|
|
|
// ModifyOrder modifies an existing order
|
|
func (ob *OrderBook) ModifyOrder(orderID uint64, newPrice, newSize float64) error {
|
|
ob.mu.Lock()
|
|
defer ob.mu.Unlock()
|
|
|
|
// Validate inputs
|
|
if newPrice < 0 {
|
|
return fmt.Errorf("invalid price: must be non-negative")
|
|
}
|
|
if newSize < 0 {
|
|
return fmt.Errorf("invalid size: must be non-negative")
|
|
}
|
|
|
|
order, exists := ob.Orders[orderID]
|
|
if !exists {
|
|
return ErrOrderNotFound
|
|
}
|
|
|
|
if order.Status != Open && order.Status != PartiallyFilled {
|
|
return fmt.Errorf("order not modifiable")
|
|
}
|
|
|
|
// Remove old order
|
|
if order.Side == Buy {
|
|
ob.Bids.removeOrder(order)
|
|
} else {
|
|
ob.Asks.removeOrder(order)
|
|
}
|
|
|
|
// Update order
|
|
order.Price = newPrice
|
|
order.Size = newSize
|
|
order.Timestamp = time.Now() // Reset timestamp for price-time priority
|
|
|
|
// Re-add order
|
|
if order.Side == Buy {
|
|
ob.Bids.addOrder(order)
|
|
} else {
|
|
ob.Asks.addOrder(order)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetOrder returns an order by ID
|
|
func (ob *OrderBook) GetOrder(orderID uint64) *Order {
|
|
ob.mu.RLock()
|
|
defer ob.mu.RUnlock()
|
|
|
|
if order, exists := ob.Orders[orderID]; exists {
|
|
return order
|
|
}
|
|
|
|
// Check ordersMap as well
|
|
if val, exists := ob.ordersMap.Load(orderID); exists {
|
|
if order, ok := val.(*Order); ok {
|
|
return order
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetTrades returns all trades that have been executed
|
|
func (ob *OrderBook) GetTrades() []Trade {
|
|
ob.mu.RLock()
|
|
defer ob.mu.RUnlock()
|
|
|
|
trades := make([]Trade, 0, len(ob.Trades))
|
|
for _, trade := range ob.Trades {
|
|
trades = append(trades, trade)
|
|
}
|
|
return trades
|
|
}
|
|
|
|
// GetSnapshot returns orderbook snapshot
|
|
func (ob *OrderBook) GetSnapshot() *OrderBookSnapshot {
|
|
ob.mu.RLock()
|
|
defer ob.mu.RUnlock()
|
|
|
|
// Convert price levels to order levels
|
|
bidLevels := ob.Bids.getLevels(10)
|
|
askLevels := ob.Asks.getLevels(10)
|
|
|
|
bids := make([]OrderLevel, len(bidLevels))
|
|
for i, level := range bidLevels {
|
|
bids[i] = OrderLevel{
|
|
Price: level.Price,
|
|
Size: level.Size,
|
|
}
|
|
}
|
|
|
|
asks := make([]OrderLevel, len(askLevels))
|
|
for i, level := range askLevels {
|
|
asks[i] = OrderLevel{
|
|
Price: level.Price,
|
|
Size: level.Size,
|
|
}
|
|
}
|
|
|
|
return &OrderBookSnapshot{
|
|
Symbol: ob.Symbol,
|
|
Timestamp: time.Now(),
|
|
Bids: bids,
|
|
Asks: asks,
|
|
Sequence: atomic.LoadUint64(&ob.Bids.sequence),
|
|
}
|
|
}
|
|
|
|
// GetDepth returns the order book depth
|
|
func (ob *OrderBook) GetDepth(levels int) *OrderBookDepth {
|
|
ob.mu.RLock()
|
|
defer ob.mu.RUnlock()
|
|
|
|
return &OrderBookDepth{
|
|
Bids: ob.Bids.getLevels(levels),
|
|
Asks: ob.Asks.getLevels(levels),
|
|
Sequence: atomic.LoadUint64(&ob.LastOrderID),
|
|
Timestamp: time.Now(),
|
|
Symbol: ob.Symbol,
|
|
}
|
|
}
|
|
|
|
func (tree *OrderTree) getLevels(depth int) []PriceLevel {
|
|
tree.mu.RLock()
|
|
defer tree.mu.RUnlock()
|
|
|
|
// If depth is 0, return all levels
|
|
maxLevels := depth
|
|
if depth == 0 {
|
|
maxLevels = len(tree.priceLevels)
|
|
}
|
|
|
|
levels := make([]PriceLevel, 0, maxLevels)
|
|
prices := make([]float64, 0, len(tree.priceLevels))
|
|
|
|
// Convert to PriceLevel format
|
|
for _, level := range tree.priceLevels {
|
|
if level.OrderCount > 0 {
|
|
prices = append(prices, level.Price)
|
|
}
|
|
}
|
|
|
|
// Sort prices
|
|
if tree.side == Buy {
|
|
sort.Sort(sort.Reverse(sort.Float64Slice(prices)))
|
|
} else {
|
|
sort.Float64s(prices)
|
|
}
|
|
|
|
// Build levels
|
|
for i, price := range prices {
|
|
if depth > 0 && i >= depth {
|
|
break
|
|
}
|
|
priceInt := PriceInt(price * PriceMultiplier)
|
|
if level, exists := tree.priceLevels[priceInt]; exists {
|
|
levels = append(levels, PriceLevel{
|
|
Price: level.Price,
|
|
Size: level.TotalSize,
|
|
Count: level.OrderCount,
|
|
})
|
|
}
|
|
}
|
|
|
|
return levels
|
|
}
|
|
|
|
// GetOrderBookSnapshot returns full order-level book data
|
|
func (ob *OrderBook) GetOrderBookSnapshot() OrderBookSnapshot {
|
|
ob.mu.RLock()
|
|
defer ob.mu.RUnlock()
|
|
|
|
snapshot := OrderBookSnapshot{
|
|
Symbol: ob.Symbol,
|
|
Timestamp: time.Now(),
|
|
Sequence: atomic.LoadUint64(&ob.Bids.sequence),
|
|
}
|
|
|
|
// Get all bid orders
|
|
snapshot.Bids = ob.Bids.getOrderLevels()
|
|
snapshot.Asks = ob.Asks.getOrderLevels()
|
|
|
|
return snapshot
|
|
}
|
|
|
|
func (tree *OrderTree) getOrderLevels() []OrderLevel {
|
|
tree.mu.RLock()
|
|
defer tree.mu.RUnlock()
|
|
|
|
levels := make([]OrderLevel, 0)
|
|
|
|
// Get all prices and sort
|
|
prices := make([]float64, 0, len(tree.priceLevels))
|
|
for _, level := range tree.priceLevels {
|
|
if level.OrderCount > 0 {
|
|
prices = append(prices, level.Price)
|
|
}
|
|
}
|
|
|
|
if tree.side == Buy {
|
|
sort.Sort(sort.Reverse(sort.Float64Slice(prices)))
|
|
} else {
|
|
sort.Float64s(prices)
|
|
}
|
|
|
|
// Build L4 levels with individual orders
|
|
for _, price := range prices {
|
|
priceInt := PriceInt(price * PriceMultiplier)
|
|
if level, exists := tree.priceLevels[priceInt]; exists {
|
|
level.mu.RLock()
|
|
for _, order := range level.Orders {
|
|
if order.Status == Open || order.Status == PartiallyFilled {
|
|
remainingSize := order.Size - order.Filled
|
|
if order.RemainingSize > 0 {
|
|
remainingSize = order.RemainingSize
|
|
}
|
|
levels = append(levels, OrderLevel{
|
|
Price: order.Price,
|
|
Size: remainingSize,
|
|
OrderID: order.ID,
|
|
UserID: order.User,
|
|
ClientID: order.ClientID,
|
|
})
|
|
}
|
|
}
|
|
level.mu.RUnlock()
|
|
}
|
|
}
|
|
|
|
return levels
|
|
}
|
|
|
|
// GetBids returns bid side tree
|
|
func (ob *OrderBook) GetBids() *OrderTree {
|
|
return (*OrderTree)(atomic.LoadPointer(&ob.bids))
|
|
}
|
|
|
|
// GetAsks returns ask side tree
|
|
func (ob *OrderBook) GetAsks() *OrderTree {
|
|
return (*OrderTree)(atomic.LoadPointer(&ob.asks))
|
|
}
|
|
|
|
// GetBestBid returns the best bid price
|
|
func (ob *OrderBook) GetBestBid() float64 {
|
|
bids := ob.GetBids()
|
|
if bids == nil {
|
|
return 0
|
|
}
|
|
bestPriceInt := bids.bestPrice.Load()
|
|
if bestPriceInt == 0 {
|
|
return 0
|
|
}
|
|
return float64(bestPriceInt) / PriceMultiplier
|
|
}
|
|
|
|
// GetBestAsk returns the best ask price
|
|
func (ob *OrderBook) GetBestAsk() float64 {
|
|
asks := ob.GetAsks()
|
|
if asks == nil {
|
|
return 0
|
|
}
|
|
bestPriceInt := asks.bestPrice.Load()
|
|
if bestPriceInt == 0 {
|
|
return 0
|
|
}
|
|
return float64(bestPriceInt) / PriceMultiplier
|
|
}
|
|
|
|
// IntBTree implementation
|
|
func NewIntBTree(degree int, isMaxHeap bool) *IntBTree {
|
|
return &IntBTree{
|
|
degree: degree,
|
|
isMaxHeap: isMaxHeap,
|
|
}
|
|
}
|
|
|
|
func (bt *IntBTree) Insert(key PriceInt) {
|
|
if bt.root == nil {
|
|
bt.root = &IntBTreeNode{
|
|
keys: []PriceInt{key},
|
|
isLeaf: true,
|
|
n: 1,
|
|
}
|
|
return
|
|
}
|
|
|
|
// Simplified B-tree insertion
|
|
bt.insertNonFull(bt.root, key)
|
|
}
|
|
|
|
func (bt *IntBTree) Delete(key PriceInt) {
|
|
// Simplified deletion - in production use proper B-tree
|
|
if bt.root != nil && bt.root.n == 1 && bt.root.keys[0] == key {
|
|
bt.root = nil
|
|
}
|
|
}
|
|
|
|
func (bt *IntBTree) Min() PriceInt {
|
|
if bt.root == nil || bt.root.n == 0 {
|
|
return 0
|
|
}
|
|
// Simplified - return first key
|
|
return bt.root.keys[0]
|
|
}
|
|
|
|
func (bt *IntBTree) Max() PriceInt {
|
|
if bt.root == nil || bt.root.n == 0 {
|
|
return 0
|
|
}
|
|
// Simplified - return last key
|
|
return bt.root.keys[bt.root.n-1]
|
|
}
|
|
|
|
func (bt *IntBTree) insertNonFull(node *IntBTreeNode, key PriceInt) {
|
|
// Simplified B-tree insertion
|
|
if node.isLeaf {
|
|
node.keys = append(node.keys, key)
|
|
node.n++
|
|
// Sort keys
|
|
for i := node.n - 1; i > 0 && node.keys[i] < node.keys[i-1]; i-- {
|
|
node.keys[i], node.keys[i-1] = node.keys[i-1], node.keys[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
// CircularTradeBuffer methods
|
|
func (ctb *CircularTradeBuffer) Add(trade Trade) {
|
|
ctb.mu.Lock()
|
|
defer ctb.mu.Unlock()
|
|
|
|
ctb.buffer[ctb.tail] = trade
|
|
ctb.tail = (ctb.tail + 1) % uint64(len(ctb.buffer))
|
|
|
|
if ctb.size < uint64(len(ctb.buffer)) {
|
|
ctb.size++
|
|
} else {
|
|
ctb.head = (ctb.head + 1) % uint64(len(ctb.buffer))
|
|
}
|
|
}
|
|
|
|
func (ctb *CircularTradeBuffer) GetRecent(count int) []Trade {
|
|
ctb.mu.RLock()
|
|
defer ctb.mu.RUnlock()
|
|
|
|
if count > int(ctb.size) {
|
|
count = int(ctb.size)
|
|
}
|
|
|
|
trades := make([]Trade, count)
|
|
idx := (ctb.tail - uint64(count) + uint64(len(ctb.buffer))) % uint64(len(ctb.buffer))
|
|
|
|
for i := 0; i < count; i++ {
|
|
trades[i] = ctb.buffer[idx]
|
|
idx = (idx + 1) % uint64(len(ctb.buffer))
|
|
}
|
|
|
|
return trades
|
|
}
|
|
|
|
// Price heap implementations for compatibility
|
|
type PriceHeap interface {
|
|
heap.Interface
|
|
Peek() float64
|
|
}
|
|
|
|
type MinPriceHeap []float64
|
|
|
|
func (h MinPriceHeap) Len() int { return len(h) }
|
|
func (h MinPriceHeap) Less(i, j int) bool { return h[i] < h[j] }
|
|
func (h MinPriceHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
|
|
func (h MinPriceHeap) Peek() float64 {
|
|
if len(h) > 0 {
|
|
return h[0]
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (h *MinPriceHeap) Push(x interface{}) {
|
|
*h = append(*h, x.(float64))
|
|
}
|
|
|
|
func (h *MinPriceHeap) Pop() interface{} {
|
|
old := *h
|
|
n := len(old)
|
|
x := old[n-1]
|
|
*h = old[0 : n-1]
|
|
return x
|
|
}
|
|
|
|
type MaxPriceHeap []float64
|
|
|
|
func (h MaxPriceHeap) Len() int { return len(h) }
|
|
func (h MaxPriceHeap) Less(i, j int) bool { return h[i] > h[j] }
|
|
func (h MaxPriceHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
|
|
func (h MaxPriceHeap) Peek() float64 {
|
|
if len(h) > 0 {
|
|
return h[0]
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (h *MaxPriceHeap) Push(x interface{}) {
|
|
*h = append(*h, x.(float64))
|
|
}
|
|
|
|
func (h *MaxPriceHeap) Pop() interface{} {
|
|
old := *h
|
|
n := len(old)
|
|
x := old[n-1]
|
|
*h = old[0 : n-1]
|
|
return x
|
|
}
|
|
|
|
// Subscribe adds a channel to receive market data updates
|
|
func (ob *OrderBook) Subscribe(ch chan MarketDataUpdate) {
|
|
ob.subMu.Lock()
|
|
defer ob.subMu.Unlock()
|
|
ob.subscribers = append(ob.subscribers, ch)
|
|
}
|
|
|
|
// Unsubscribe removes a channel from market data updates
|
|
func (ob *OrderBook) Unsubscribe(ch chan MarketDataUpdate) {
|
|
ob.subMu.Lock()
|
|
defer ob.subMu.Unlock()
|
|
for i, sub := range ob.subscribers {
|
|
if sub == ch {
|
|
ob.subscribers = append(ob.subscribers[:i], ob.subscribers[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// publishUpdate sends market data updates to all subscribers
|
|
func (ob *OrderBook) publishUpdate(update MarketDataUpdate) {
|
|
ob.subMu.RLock()
|
|
defer ob.subMu.RUnlock()
|
|
for _, ch := range ob.subscribers {
|
|
select {
|
|
case ch <- update:
|
|
default:
|
|
// Skip if channel is full
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reset clears the order book
|
|
func (ob *OrderBook) Reset() {
|
|
ob.mu.Lock()
|
|
defer ob.mu.Unlock()
|
|
|
|
ob.Orders = make(map[uint64]*Order)
|
|
ob.UserOrders = make(map[string][]uint64)
|
|
ob.Trades = []Trade{}
|
|
ob.Bids = NewOrderTree(Buy)
|
|
ob.Asks = NewOrderTree(Sell)
|
|
atomic.StorePointer(&ob.bids, unsafe.Pointer(ob.Bids))
|
|
atomic.StorePointer(&ob.asks, unsafe.Pointer(ob.Asks))
|
|
ob.ordersMap = sync.Map{}
|
|
ob.userOrdersMap = sync.Map{}
|
|
ob.LastOrderID = 0
|
|
ob.LastTradeID = 0
|
|
ob.LastUpdateID = 0
|
|
}
|