mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
366 lines
8.4 KiB
Go
366 lines
8.4 KiB
Go
package lx
|
|
|
|
import (
|
|
"math/rand"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestOrderBookBasicOperations(t *testing.T) {
|
|
book := NewOrderBook("BTC-USD")
|
|
book.EnableImmediateMatching = true
|
|
|
|
// Test adding buy order
|
|
buyOrder := &Order{
|
|
ID: 1,
|
|
Type: Limit,
|
|
Side: Buy,
|
|
Price: 50000,
|
|
Size: 1.0,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
orderID := book.AddOrder(buyOrder)
|
|
if orderID != 1 {
|
|
t.Errorf("Expected order ID 1 for first order, got %d", orderID)
|
|
}
|
|
|
|
// No trades should be created yet
|
|
if len(book.GetTrades()) != 0 {
|
|
t.Errorf("Expected no trades for first order, got %d", len(book.GetTrades()))
|
|
}
|
|
|
|
// Test adding sell order that matches
|
|
sellOrder := &Order{
|
|
ID: 2,
|
|
Type: Limit,
|
|
Side: Sell,
|
|
Price: 50000,
|
|
Size: 0.5,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
orderID2 := book.AddOrder(sellOrder)
|
|
if orderID2 != 2 {
|
|
t.Errorf("Expected order ID 2 for sell order, got %d", orderID2)
|
|
}
|
|
|
|
// Check that a trade was created
|
|
trades := book.GetTrades()
|
|
if len(trades) != 1 {
|
|
t.Errorf("Expected 1 trade, got %d", len(trades))
|
|
}
|
|
|
|
// Check trade was recorded correctly
|
|
if len(trades) > 0 && trades[0].Size != 0.5 {
|
|
t.Errorf("Expected trade size 0.5, got %f", trades[0].Size)
|
|
}
|
|
}
|
|
|
|
func TestOrderBookBestPrices(t *testing.T) {
|
|
book := NewOrderBook("ETH-USD")
|
|
|
|
// Add buy orders
|
|
book.AddOrder(&Order{
|
|
ID: 1, Type: Limit, Side: Buy, Price: 3000, Size: 1.0,
|
|
Timestamp: time.Now(),
|
|
})
|
|
book.AddOrder(&Order{
|
|
ID: 2, Type: Limit, Side: Buy, Price: 3001, Size: 1.0,
|
|
Timestamp: time.Now(),
|
|
})
|
|
|
|
// Add sell orders
|
|
book.AddOrder(&Order{
|
|
ID: 3, Type: Limit, Side: Sell, Price: 3002, Size: 1.0,
|
|
Timestamp: time.Now(),
|
|
})
|
|
book.AddOrder(&Order{
|
|
ID: 4, Type: Limit, Side: Sell, Price: 3003, Size: 1.0,
|
|
Timestamp: time.Now(),
|
|
})
|
|
|
|
bestBid := book.GetBestBid()
|
|
if bestBid != 3001 {
|
|
t.Errorf("Expected best bid 3001, got %f", bestBid)
|
|
}
|
|
|
|
bestAsk := book.GetBestAsk()
|
|
if bestAsk != 3002 {
|
|
t.Errorf("Expected best ask 3002, got %f", bestAsk)
|
|
}
|
|
}
|
|
|
|
func TestOrderBookCancelOrder(t *testing.T) {
|
|
book := NewOrderBook("SOL-USD")
|
|
|
|
order := &Order{
|
|
ID: 1, Type: Limit, Side: Buy, Price: 100, Size: 10,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
book.AddOrder(order)
|
|
|
|
// Cancel the order
|
|
err := book.CancelOrder(1)
|
|
if err != nil {
|
|
t.Errorf("Failed to cancel order: %v", err)
|
|
}
|
|
|
|
// Try to cancel again
|
|
err = book.CancelOrder(1)
|
|
if err == nil {
|
|
t.Error("Expected error when canceling non-existent order")
|
|
}
|
|
}
|
|
|
|
func TestOrderBookMarketOrder(t *testing.T) {
|
|
book := NewOrderBook("BTC-USD")
|
|
|
|
// Add liquidity
|
|
book.AddOrder(&Order{
|
|
ID: 1, Type: Limit, Side: Sell, Price: 50000, Size: 1.0,
|
|
Timestamp: time.Now(),
|
|
})
|
|
book.AddOrder(&Order{
|
|
ID: 2, Type: Limit, Side: Sell, Price: 50001, Size: 1.0,
|
|
Timestamp: time.Now(),
|
|
})
|
|
|
|
// Market buy order
|
|
marketOrder := &Order{
|
|
ID: 3, Type: Market, Side: Buy, Size: 1.5,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
// Market orders should get an ID and execute immediately
|
|
orderID := book.AddOrder(marketOrder)
|
|
if orderID != 3 {
|
|
t.Errorf("Expected order ID 3 for market order, got %d", orderID)
|
|
}
|
|
|
|
// Check that trades were created
|
|
trades := book.GetTrades()
|
|
// We expect at least 2 trades from the market order matching the two limit orders
|
|
if len(trades) < 2 {
|
|
t.Errorf("Expected at least 2 trades, got %d", len(trades))
|
|
}
|
|
|
|
// Check total traded size from market order
|
|
totalSize := 0.0
|
|
for _, trade := range trades {
|
|
totalSize += trade.Size
|
|
}
|
|
|
|
if totalSize != 1.5 {
|
|
t.Errorf("Expected total trade size 1.5, got %f", totalSize)
|
|
}
|
|
}
|
|
|
|
func TestOrderBookConcurrency(t *testing.T) {
|
|
book := NewOrderBook("STRESS-TEST")
|
|
book.EnableImmediateMatching = true
|
|
|
|
numGoroutines := 100
|
|
ordersPerGoroutine := 100
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(numGoroutines)
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
|
|
for j := 0; j < ordersPerGoroutine; j++ {
|
|
order := &Order{
|
|
ID: uint64(id*ordersPerGoroutine + j),
|
|
Type: Limit,
|
|
Side: Side(rand.Intn(2)),
|
|
Price: 50000 + float64(rand.Intn(1000)),
|
|
Size: rand.Float64() * 10,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
book.AddOrder(order)
|
|
|
|
// Randomly cancel some orders
|
|
if rand.Float32() < 0.3 {
|
|
book.CancelOrder(order.ID)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Verify book is in consistent state
|
|
bestBid := book.GetBestBid()
|
|
bestAsk := book.GetBestAsk()
|
|
|
|
// Just verify we have some orders
|
|
if bestBid == 0 && bestAsk == 0 {
|
|
t.Errorf("No orders in book after concurrent operations")
|
|
}
|
|
}
|
|
|
|
func TestOrderBookSelfTradePrevention(t *testing.T) {
|
|
book := NewOrderBook("STP-TEST")
|
|
book.EnableImmediateMatching = true
|
|
|
|
// Add orders from same user
|
|
userID := "trader1"
|
|
|
|
book.AddOrder(&Order{
|
|
ID: 1, Type: Limit, Side: Buy, Price: 100, Size: 10,
|
|
UserID: userID, Timestamp: time.Now(),
|
|
})
|
|
|
|
// This should not match with the previous order (self-trade prevention)
|
|
orderID := book.AddOrder(&Order{
|
|
ID: 2, Type: Limit, Side: Sell, Price: 100, Size: 10,
|
|
UserID: userID, Timestamp: time.Now(),
|
|
})
|
|
|
|
// Order should be rejected (ID = 0) due to self-trade
|
|
if orderID != 0 {
|
|
t.Error("Self-trade prevention failed: orders from same user matched")
|
|
}
|
|
|
|
// Order from different user should match and create order
|
|
orderID2 := book.AddOrder(&Order{
|
|
ID: 3, Type: Limit, Side: Sell, Price: 100, Size: 10,
|
|
UserID: "trader2", Timestamp: time.Now(),
|
|
})
|
|
|
|
// Should succeed and return order ID
|
|
if orderID2 != 3 {
|
|
t.Errorf("Expected order ID 3 from different user, got %d", orderID2)
|
|
}
|
|
|
|
// Verify a trade was created by checking the order book state
|
|
trades := book.GetTrades()
|
|
if len(trades) != 1 {
|
|
t.Errorf("Expected 1 trade to be created, got %d", len(trades))
|
|
}
|
|
}
|
|
|
|
func TestOrderBookPartialFills(t *testing.T) {
|
|
book := NewOrderBook("PARTIAL-TEST")
|
|
book.EnableImmediateMatching = true
|
|
|
|
// Large buy order
|
|
book.AddOrder(&Order{
|
|
ID: 1, Type: Limit, Side: Buy, Price: 100, Size: 100,
|
|
Timestamp: time.Now(),
|
|
})
|
|
|
|
// Small sell orders that partially fill
|
|
for i := 0; i < 5; i++ {
|
|
orderID := book.AddOrder(&Order{
|
|
ID: uint64(i + 2), Type: Limit, Side: Sell,
|
|
Price: 100, Size: 10, Timestamp: time.Now(),
|
|
})
|
|
|
|
// Each order should be added successfully
|
|
if orderID != uint64(i+2) {
|
|
t.Errorf("Expected order ID %d for partial fill %d, got %d", i+2, i, orderID)
|
|
}
|
|
}
|
|
|
|
// Verify all trades were created
|
|
trades := book.GetTrades()
|
|
if len(trades) != 5 {
|
|
t.Errorf("Expected 5 trades from partial fills, got %d", len(trades))
|
|
}
|
|
|
|
// Check remaining order
|
|
bestBid := book.GetBestBid()
|
|
if bestBid != 100 {
|
|
t.Errorf("Expected remaining buy order at 100, got %f", bestBid)
|
|
}
|
|
}
|
|
|
|
func TestOrderBookSnapshot(t *testing.T) {
|
|
book := NewOrderBook("SNAPSHOT-TEST")
|
|
|
|
// Add some orders
|
|
for i := 0; i < 10; i++ {
|
|
book.AddOrder(&Order{
|
|
ID: uint64(i), Type: Limit, Side: Buy,
|
|
Price: 100 - float64(i), Size: 1.0,
|
|
Timestamp: time.Now(),
|
|
})
|
|
book.AddOrder(&Order{
|
|
ID: uint64(i + 10), Type: Limit, Side: Sell,
|
|
Price: 101 + float64(i), Size: 1.0,
|
|
Timestamp: time.Now(),
|
|
})
|
|
}
|
|
|
|
snapshot := book.GetSnapshot()
|
|
|
|
if snapshot.Symbol != "SNAPSHOT-TEST" {
|
|
t.Errorf("Expected symbol SNAPSHOT-TEST, got %s", snapshot.Symbol)
|
|
}
|
|
|
|
if len(snapshot.Bids) != 10 {
|
|
t.Errorf("Expected 10 bids, got %d", len(snapshot.Bids))
|
|
}
|
|
|
|
if len(snapshot.Asks) != 10 {
|
|
t.Errorf("Expected 10 asks, got %d", len(snapshot.Asks))
|
|
}
|
|
|
|
// Check order is correct (best bid first)
|
|
if len(snapshot.Bids) > 0 && snapshot.Bids[0].Price != 100 {
|
|
t.Errorf("Expected best bid 100, got %f", snapshot.Bids[0].Price)
|
|
}
|
|
|
|
if len(snapshot.Asks) > 0 && snapshot.Asks[0].Price != 101 {
|
|
t.Errorf("Expected best ask 101, got %f", snapshot.Asks[0].Price)
|
|
}
|
|
}
|
|
|
|
func BenchmarkOrderBookAddOrder(b *testing.B) {
|
|
book := NewOrderBook("BENCH")
|
|
|
|
orders := make([]*Order, b.N)
|
|
for i := 0; i < b.N; i++ {
|
|
orders[i] = &Order{
|
|
ID: uint64(i),
|
|
Type: Limit,
|
|
Side: Side(i % 2),
|
|
Price: 50000 + float64(i%1000),
|
|
Size: 1.0,
|
|
Timestamp: time.Now(),
|
|
}
|
|
}
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
book.AddOrder(orders[i])
|
|
}
|
|
}
|
|
|
|
func BenchmarkOrderBookConcurrent(b *testing.B) {
|
|
book := NewOrderBook("BENCH-CONCURRENT")
|
|
|
|
b.RunParallel(func(pb *testing.PB) {
|
|
id := uint64(rand.Int63())
|
|
for pb.Next() {
|
|
order := &Order{
|
|
ID: id,
|
|
Type: Limit,
|
|
Side: Side(rand.Intn(2)),
|
|
Price: 50000 + float64(rand.Intn(1000)),
|
|
Size: rand.Float64() * 10,
|
|
Timestamp: time.Now(),
|
|
}
|
|
book.AddOrder(order)
|
|
id++
|
|
}
|
|
})
|
|
}
|