mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
SubmitMarketable and MatchOrders derived their returned fills by re-slicing the shared trade log (ob.Trades[startOffset:]) with an offset captured BEFORE matching. ob.Trades self-truncates at 100k entries (keeps the last 50k), so a match that crosses that boundary mid-match trims the log and leaves the pre-match offset pointing past the end -> panic 'slice bounds out of range'. The log feeds settlement, so fills had to stay exact. tryMatchImmediateLocked / MatchOrders now capture each fill as it is produced, independent of ob.Trades, and return that slice. Decomplects 'this match's fills' (a value the caller needs) from 'the trade log' (a bounded-retention buffer): correct across the truncation boundary by construction, deterministic across validators. Adds orderbook_truncation_test.go: three regression cases that cross the boundary through the production match path (single submits, one sweeping submit, legacy MatchOrders) with exact-fill and value-conservation asserts. They panic on the old code, pass on the fixed code. No sleeps, no stubs. Also: 0x9999 D-matches/C-settles docs (LLM.md, README, ARCHITECTURE), venue-canonical naming, fourpath-live proof tool, drop dead snowman-api key from run-lux-testnet.sh.
1591 lines
42 KiB
Go
1591 lines
42 KiB
Go
package lx
|
|
|
|
import (
|
|
"container/heap"
|
|
"fmt"
|
|
"math"
|
|
"math/big"
|
|
"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
|
|
}
|
|
|
|
// SubmitMarketable submits a marketable (crossing) order against the book and
|
|
// returns ONLY the trades that order generated, atomically. It is the single
|
|
// path a taker uses to get its fills back: a market order, or an IOC limit order
|
|
// whose Price bounds how far it crosses. Whatever does not fill is NOT rested —
|
|
// the remainder is discarded (IOC semantics), so a marketable order never leaves
|
|
// liquidity behind.
|
|
//
|
|
// The whole match-and-capture runs under one acquisition of ob.mu, so the
|
|
// returned slice is exactly this order's fills even under concurrent submission:
|
|
// no other goroutine can append to ob.Trades between the match and the capture.
|
|
// This is what lets the LP-9010 adapter derive a value-conserving BalanceDelta
|
|
// from server-side truth — it never trusts a client-supplied amount.
|
|
//
|
|
// order.Type is forced to the crossing form: Market keeps Market, anything else
|
|
// is treated as an IOC limit (Price is the worst price it will accept). The order
|
|
// is never added to the resting book.
|
|
func (ob *OrderBook) SubmitMarketable(order *Order) ([]Trade, error) {
|
|
if order == nil {
|
|
return nil, ErrInvalidOrder
|
|
}
|
|
if order.Size <= 0 {
|
|
return nil, ErrInvalidSize
|
|
}
|
|
// A limit-bounded marketable order needs a positive price; a pure market
|
|
// order does not (it sweeps the book unconditionally).
|
|
if order.Type != Market && order.Price <= 0 {
|
|
return nil, ErrInvalidPrice
|
|
}
|
|
if order.Price > MaxSafePrice {
|
|
return nil, fmt.Errorf("price %f exceeds max safe price %f: %w", order.Price, MaxSafePrice, ErrInvalidPrice)
|
|
}
|
|
|
|
ob.mu.Lock()
|
|
defer ob.mu.Unlock()
|
|
|
|
// CONSENSUS DETERMINISM: a marketable order is IOC and never rests, so its
|
|
// ID and Timestamp are ephemeral — they never participate in resting
|
|
// price-time priority and are never persisted as book state. We therefore
|
|
// MUST NOT stamp them from wall-clock time (time.Now) or a process-local
|
|
// counter (atomic.AddUint64 on LastOrderID): both are non-deterministic
|
|
// across validators replaying the same ordered block. The d-chain consensus
|
|
// path supplies a deterministic ID/Timestamp derived from block context when
|
|
// it wants one; anything left zero stays zero. The returned fills name real
|
|
// makers (resting orders, which DO carry deterministic IDs/timestamps), so
|
|
// value conservation is derived purely from book-resident truth.
|
|
order.Status = Open
|
|
order.RemainingSize = order.Size
|
|
order.Filled = 0
|
|
|
|
// tryMatchImmediateLocked returns exactly this order's fills, captured as they
|
|
// are produced — NOT re-sliced from ob.Trades, whose 100k->50k self-truncation
|
|
// would make a pre-match offset stale (and panic) once a hot market crosses the
|
|
// boundary mid-match. We hold ob.mu for the whole window, so the match is atomic.
|
|
// Copy into an exactly-sized slice so the returned value does not alias the
|
|
// matcher's internal append buffer.
|
|
produced := ob.tryMatchImmediateLocked(order)
|
|
fills := make([]Trade, len(produced))
|
|
copy(fills, produced)
|
|
return fills, nil
|
|
}
|
|
|
|
// 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 = uint64(len(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 += uint64(len(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 matches order against the opposite side and returns the
|
|
// fills it produced, in match order. The returned slice is built up locally as
|
|
// each fill is created, so it is exactly this call's fills and is INDEPENDENT of
|
|
// ob.Trades: the shared trade log self-truncates at 100k entries (keeps the last
|
|
// 50k), so any offset captured into ob.Trades before matching goes stale the
|
|
// moment a sustained-hot market crosses that boundary mid-match — re-slicing
|
|
// ob.Trades[offset:] then panics with "slice bounds out of range". Capturing fills
|
|
// here, before they reach the log, decomplects "this match's fills" (a value the
|
|
// caller needs) from "the trade log" (a bounded-retention buffer): correct across
|
|
// the truncation boundary and deterministic across validators by construction.
|
|
func (ob *OrderBook) tryMatchImmediateLocked(order *Order) []Trade {
|
|
var fills []Trade
|
|
|
|
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)
|
|
|
|
// EXACT-INTEGER QUANTITY LANE (value conservation). When BOTH the taker
|
|
// and the resting maker carry the integer quantity lane (CLOB / consensus
|
|
// path), the matched base is min(remainingUnits) computed in big.Int — the
|
|
// authoritative quantity, immune to the float64 precision loss that mints
|
|
// or burns value for 18-decimal token amounts above 2^53. The float
|
|
// tradeSize above still drives the crossing loop and legacy bookkeeping;
|
|
// the integer base/quote below are what settlement is derived from. If
|
|
// either side lacks the lane (legacy float API), tradeUnits stays nil and
|
|
// the fill carries no integer truth, preserving legacy behavior exactly.
|
|
takerUnits := ensureRemainingUnits(order)
|
|
makerUnits := ensureRemainingUnits(bestOrder)
|
|
var tradeUnits *big.Int
|
|
if takerUnits != nil && makerUnits != nil {
|
|
tradeUnits = minBig(takerUnits, makerUnits)
|
|
}
|
|
|
|
// Create trade. CONSENSUS DETERMINISM: stamp the trade with the
|
|
// INCOMING order's timestamp (the d-chain supplies a block-derived,
|
|
// deterministic value via SubmitMarketable) rather than wall-clock, so
|
|
// every validator replaying the same ordered block emits byte-identical
|
|
// fills. Legacy non-consensus callers that leave the timestamp zero fall
|
|
// back to wall-clock, preserving their prior behavior. The fill also
|
|
// carries both maker and taker identity so settlement can credit the
|
|
// real resting maker (value-conservation source of truth).
|
|
ts := order.Timestamp
|
|
if ts.IsZero() {
|
|
ts = time.Now()
|
|
}
|
|
ob.LastTradeID++
|
|
trade := Trade{
|
|
ID: ob.LastTradeID,
|
|
Price: bestOrder.Price,
|
|
Size: tradeSize,
|
|
Timestamp: ts,
|
|
TakerSide: order.Side,
|
|
}
|
|
if tradeUnits != nil {
|
|
// Quote is an exact integer function of the matched base and the
|
|
// maker's resting price (its PriceInt key) — never of any float size.
|
|
priceInt := PriceInt(bestOrder.Price * PriceMultiplier)
|
|
trade.BaseUnits = tradeUnits
|
|
trade.QuoteUnits = quoteUnitsFor(tradeUnits, priceInt)
|
|
// Decrement the exact integer remainders so a multi-level / partial
|
|
// sweep stays integer-consistent across iterations.
|
|
takerUnits.Sub(takerUnits, tradeUnits)
|
|
makerUnits.Sub(makerUnits, tradeUnits)
|
|
}
|
|
if order.Side == Buy {
|
|
trade.BuyOrder, trade.SellOrder = order.ID, bestOrder.ID
|
|
trade.Buyer, trade.Seller = order.User, bestOrder.User
|
|
trade.BuyUserID, trade.SellUserID = order.UserID, bestOrder.UserID
|
|
} else {
|
|
trade.BuyOrder, trade.SellOrder = bestOrder.ID, order.ID
|
|
trade.Buyer, trade.Seller = bestOrder.User, order.User
|
|
trade.BuyUserID, trade.SellUserID = bestOrder.UserID, order.UserID
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Capture this fill for the caller BEFORE it enters the shared log, then
|
|
// append to the log. fills is this call's authoritative return value and is
|
|
// never re-derived from ob.Trades, so the truncation below cannot corrupt it.
|
|
fills = append(fills, 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:]
|
|
}
|
|
}
|
|
|
|
return fills
|
|
}
|
|
|
|
// 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()
|
|
|
|
// trades accumulates this session's fills as they are produced; it is the
|
|
// authoritative return value. We deliberately do NOT re-slice ob.Trades[start:]
|
|
// at the end: ob.Trades self-truncates at 100k entries (keeps the last 50k), so
|
|
// an offset captured here goes stale and panics ("slice bounds out of range")
|
|
// the moment the log crosses that boundary inside this loop on a hot market.
|
|
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"
|
|
}
|
|
|
|
// Capture the fill for the caller BEFORE it enters the shared log, then
|
|
// append to the log. trades is independent of ob.Trades, so the truncation
|
|
// below cannot corrupt this session's returned fills.
|
|
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 exactly the fills generated in this MatchOrders call.
|
|
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
|
|
}
|