Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 21 additions & 10 deletions packages/sdk-go/websocket/orderbook/book.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,14 @@ type OrderBook struct {

const maxCachedPriceLength = 128

type cachedPriceValue struct {
decimal types.Decimal
val float64
}

type decimalCache struct {
mu sync.RWMutex
values map[string]types.Decimal
values map[string]cachedPriceValue
}

// NewOrderBook creates a new empty OrderBook for a symbol.
Expand All @@ -47,7 +52,7 @@ func NewOrderBook(symbol string) *OrderBook {
symbol: symbol,
bids: make([]PriceLevel, 0, 128),
asks: make([]PriceLevel, 0, 128),
priceCache: decimalCache{values: make(map[string]types.Decimal)},
priceCache: decimalCache{values: make(map[string]cachedPriceValue)},
}
}

Expand Down Expand Up @@ -127,7 +132,7 @@ func parseLevels(raw [][]string, allowZeroAmount bool, cache *decimalCache) ([]P
}

func parseLevel(priceStr, amountStr string, allowZeroAmount bool, cache *decimalCache) (PriceLevel, error) {
price, err := cachedPrice(cache, priceStr)
price, priceVal, err := cachedPrice(cache, priceStr)
if err != nil || !price.IsPositive() {
return PriceLevel{}, fmt.Errorf("price %q must be positive decimal", priceStr)
}
Expand All @@ -138,36 +143,42 @@ func parseLevel(priceStr, amountStr string, allowZeroAmount bool, cache *decimal
return PriceLevel{
Price: priceStr,
Amount: amountStr,
val: price.Float64(),
val: priceVal,
decimal: price,
amountDec: amount,
}, nil
}

func cachedPrice(cache *decimalCache, raw string) (types.Decimal, error) {
func cachedPrice(cache *decimalCache, raw string) (types.Decimal, float64, error) {
key := strings.TrimSpace(raw)
if cache != nil && len(key) <= maxCachedPriceLength {
cache.mu.RLock()
price, ok := cache.values[key]
cached, ok := cache.values[key]
cache.mu.RUnlock()
if ok {
return price, nil
return cached.decimal, cached.val, nil
}
}

price, err := types.ParseDecimal(raw)
if err != nil {
return types.Decimal{}, err
return types.Decimal{}, 0, err
}
priceVal := price.Float64()

if cache != nil && len(key) <= maxCachedPriceLength {
cache.mu.Lock()
if len(cache.values) >= 4096 {
clear(cache.values)
}
cache.values[key] = price
cache.values[key] = cachedPriceValue{
decimal: price,
val: priceVal,
}
cache.mu.Unlock()
}
return price, nil

return price, priceVal, nil
}

// ApplyDiff atomically applies a batch of bid and ask level updates and updates the sequence ID under a single lock.
Expand Down
74 changes: 74 additions & 0 deletions packages/sdk-go/websocket/orderbook/book_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -820,3 +820,77 @@ func TestLiveOrderBook_BBOListenerUnregister(t *testing.T) {
default:
}
}

func TestOrderBook_PriceCachePreservesFloatMetrics(t *testing.T) {
book := NewOrderBook("BTCUSD")

if err := book.ApplySnapshot(
1,
[][]string{{"65000.00", "1"}},
[][]string{{"65100.00", "1"}},
); err != nil {
t.Fatalf("ApplySnapshot failed: %v", err)
}

wantSpread, ok := book.Spread()
if !ok {
t.Fatal("expected spread")
}
wantMid, ok := book.Mid()
if !ok {
t.Fatal("expected mid")
}

// Repeated updates hit the price cache. Cached float values must remain
// identical to those produced when the prices were first parsed.
for i := 0; i < 100; i++ {
if err := book.ApplyLevel(true, "65000.00", "2"); err != nil {
t.Fatalf("ApplyLevel bid failed: %v", err)
}
if err := book.ApplyLevel(false, "65100.00", "2"); err != nil {
t.Fatalf("ApplyLevel ask failed: %v", err)
}

gotSpread, ok := book.Spread()
if !ok || gotSpread != wantSpread {
t.Fatalf("Spread = %v, %v; want %v, true", gotSpread, ok, wantSpread)
}
gotMid, ok := book.Mid()
if !ok || gotMid != wantMid {
t.Fatalf("Mid = %v, %v; want %v, true", gotMid, ok, wantMid)
}
}
}

func TestOrderBook_PriceCachePreservesExactPriceIdentity(t *testing.T) {
book := NewOrderBook("BTCUSD")

if err := book.ApplySnapshot(
1,
[][]string{
{"10000000000000000.01", "1"},
{"10000000000000000.02", "2"},
},
[][]string{{"10000000000000000.03", "1"}},
); err != nil {
t.Fatalf("ApplySnapshot failed: %v", err)
}

for i := 0; i < 100; i++ {
if err := book.ApplyLevel(true, "10000000000000000.01", "3"); err != nil {
t.Fatalf("ApplyLevel failed: %v", err)
}
}

bids := book.Bids(10)
if len(bids) != 2 {
t.Fatalf("got %d bids, want 2", len(bids))
}
if bids[0].Price != "10000000000000000.02" ||
bids[1].Price != "10000000000000000.01" {
t.Fatalf("cached prices changed exact ordering: %+v", bids)
}
if bids[1].Amount != "3" {
t.Fatalf("cached update changed wrong level: %+v", bids)
}
}