mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
The matching engine (pkg/lx, 45.5K LOC incl. tests) and the deterministic core layered on it (pkg/dex) are one matcher spread across two packages (dex imported lx in 12 files; lx imported neither). Fold lx into dex and drop the "lx" name entirely — pkg/dex is now the single matcher package. Pure rename/fold, ZERO logic change. Behavior proven identical: the merged pkg/dex runs 457 top-level tests (431 former-lx + 26 former-dex) and 489 test funcs (468 + 21), all green — matching, batch (BatchMatchEqualsSequential, run-to-run determinism, tile boundary), GPU/CPU parity, truncation, and CL swap math all pass unchanged. What moved: - git mv all 101 pkg/lx/*.go + 1 .c into pkg/dex/ (package lx -> package dex). - lx's swap_test.go -> swap_math_test.go (only filename collision with dex's existing swap_test.go). - Rewrote imports repo-wide: 54 pkg/lx import lines removed; 42 external importers (api, client, consensus, dchain, dpdk, lxgpu, marketdata, test/, tests/) repointed to pkg/dex with lx.X -> dex.X; the 12 in-package dex files had lx.X -> bare X. 717 selector lines rewritten. Stale pkg/lx comment refs updated (pkg/lxgpu left intact). Collisions resolved (2, both package-level vars): - ErrNoLiquidity: lx's pool-level error (dexError, used only in swap.go, never referenced via qualifier) renamed ErrPoolNoLiquidity to match the existing ErrPool* convention; dex's router-level ErrNoLiquidity (public, has errors.Is tests) kept. - priceMultiplierBig: identical after fold (both big.NewInt(PriceMultiplier)); deduped by deleting dex's copy. Not folded — pkg/orderbook: it is NOT the GPU seam (that is orderbook_gpu.go, which lives in the engine and folded in here). pkg/orderbook is a zero-importer, placeholder-cgo island whose 13 core domain identifiers (Order, OrderBook, Trade, OrderType, OrderStatus, PriceLevel, Buy, Sell, Market, Limit, Filled, PartiallyFilled, NewOrderBook) collide incompatibly with the engine's. Folding it verbatim breaks the build; folding via rename reintroduces the exact parallel duplication this fold removes. Left for owner decision (recommend deletion as dead code) — see refactor notes. Follow-on (separate repos, not touched): 19 files under luxfi/dex import pkg/lx externally — lux/precompile/dex, lux/precompile-dex/dex, lux/consensus/examples/01-simple-bridge, lux/_consensus-v12537/examples, lux/cex/pkg/engine — repoint to pkg/dex after this lands. Verified: CGO_ENABLED=0 GOWORK=off go build ./... clean; go vet ./... clean; go test ./... -count=1 all green (22 ok / 0 fail). Co-authored-by: Hanzo Dev <[email protected]>
1540 lines
35 KiB
Go
1540 lines
35 KiB
Go
package client
|
|
|
|
import (
|
|
"fmt"
|
|
"math/big"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/luxfi/dex/pkg/dex"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestNewTraderClient(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.lux.exchange",
|
|
WSEndpoint: "wss://ws.lux.exchange",
|
|
APIKey: "test_key",
|
|
APISecret: "test_secret",
|
|
UserID: "user123",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, client)
|
|
assert.Equal(t, "https://api.lux.exchange", client.apiEndpoint)
|
|
assert.Equal(t, "wss://ws.lux.exchange", client.wsEndpoint)
|
|
assert.Equal(t, "test_key", client.apiKey)
|
|
assert.Equal(t, "test_secret", client.apiSecret)
|
|
assert.Equal(t, "user123", client.userID)
|
|
assert.False(t, client.connected)
|
|
assert.False(t, client.authenticated)
|
|
}
|
|
|
|
func TestNewTraderClientMissingEndpoints(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config ClientConfig
|
|
}{
|
|
{
|
|
name: "MissingAPIEndpoint",
|
|
config: ClientConfig{WSEndpoint: "wss://ws.test.com"},
|
|
},
|
|
{
|
|
name: "MissingWSEndpoint",
|
|
config: ClientConfig{APIEndpoint: "https://api.test.com"},
|
|
},
|
|
{
|
|
name: "BothMissing",
|
|
config: ClientConfig{},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client, err := NewTraderClient(tt.config)
|
|
assert.Error(t, err)
|
|
assert.Nil(t, client)
|
|
assert.Contains(t, err.Error(), "endpoints required")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTraderClientInitialState(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
// Check maps are initialized
|
|
assert.NotNil(t, client.positions)
|
|
assert.NotNil(t, client.orders)
|
|
assert.NotNil(t, client.balances)
|
|
assert.NotNil(t, client.orderBooks)
|
|
assert.NotNil(t, client.prices)
|
|
assert.NotNil(t, client.trades)
|
|
assert.NotNil(t, client.subscriptions)
|
|
assert.NotNil(t, client.callbacks)
|
|
|
|
// Check channels are initialized
|
|
assert.NotNil(t, client.orderUpdates)
|
|
assert.NotNil(t, client.positionUpdates)
|
|
assert.NotNil(t, client.priceUpdates)
|
|
assert.NotNil(t, client.tradeUpdates)
|
|
assert.NotNil(t, client.errorChan)
|
|
|
|
// Check context
|
|
assert.NotNil(t, client.ctx)
|
|
assert.NotNil(t, client.cancel)
|
|
}
|
|
|
|
func TestOrderUpdateStruct(t *testing.T) {
|
|
update := OrderUpdate{
|
|
Order: nil, // Would be *dex.Order in real use
|
|
Status: 0, // Would be dex.OrderStatus
|
|
Timestamp: time.Now(),
|
|
Message: "Order filled",
|
|
}
|
|
|
|
assert.Equal(t, "Order filled", update.Message)
|
|
assert.False(t, update.Timestamp.IsZero())
|
|
}
|
|
|
|
func TestPositionUpdateStruct(t *testing.T) {
|
|
update := PositionUpdate{
|
|
Position: nil,
|
|
Action: "opened",
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
assert.Equal(t, "opened", update.Action)
|
|
assert.False(t, update.Timestamp.IsZero())
|
|
}
|
|
|
|
func TestPriceUpdateStruct(t *testing.T) {
|
|
update := PriceUpdate{
|
|
Symbol: "BTC/USDC",
|
|
Price: 50000.0,
|
|
Bid: 49990.0,
|
|
Ask: 50010.0,
|
|
Volume: 1000.0,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
assert.Equal(t, "BTC/USDC", update.Symbol)
|
|
assert.Equal(t, 50000.0, update.Price)
|
|
assert.Equal(t, 49990.0, update.Bid)
|
|
assert.Equal(t, 50010.0, update.Ask)
|
|
assert.Equal(t, 1000.0, update.Volume)
|
|
}
|
|
|
|
func TestTradeUpdateStruct(t *testing.T) {
|
|
update := TradeUpdate{
|
|
Trade: nil,
|
|
Symbol: "ETH/USDC",
|
|
Side: "buy",
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
assert.Equal(t, "ETH/USDC", update.Symbol)
|
|
assert.Equal(t, "buy", update.Side)
|
|
}
|
|
|
|
func TestClientConfigStruct(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.example.com",
|
|
WSEndpoint: "wss://ws.example.com",
|
|
APIKey: "key123",
|
|
APISecret: "secret456",
|
|
UserID: "user789",
|
|
}
|
|
|
|
assert.Equal(t, "https://api.example.com", config.APIEndpoint)
|
|
assert.Equal(t, "wss://ws.example.com", config.WSEndpoint)
|
|
assert.Equal(t, "key123", config.APIKey)
|
|
assert.Equal(t, "secret456", config.APISecret)
|
|
assert.Equal(t, "user789", config.UserID)
|
|
}
|
|
|
|
func TestClientChannelBufferSizes(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
// Check channel capacities
|
|
assert.Equal(t, 100, cap(client.orderUpdates))
|
|
assert.Equal(t, 100, cap(client.positionUpdates))
|
|
assert.Equal(t, 1000, cap(client.priceUpdates))
|
|
assert.Equal(t, 1000, cap(client.tradeUpdates))
|
|
assert.Equal(t, 10, cap(client.errorChan))
|
|
}
|
|
|
|
func TestDisconnectNotConnected(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
// Disconnect when not connected should be no-op
|
|
err = client.Disconnect()
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestConnectAlreadyConnected(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
// Manually set connected state
|
|
client.mu.Lock()
|
|
client.connected = true
|
|
client.mu.Unlock()
|
|
|
|
// Connect when already connected should return error
|
|
err = client.Connect()
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "already connected")
|
|
}
|
|
|
|
func TestPriceUpdateActions(t *testing.T) {
|
|
actions := []string{"opened", "modified", "closed", "liquidated"}
|
|
|
|
for _, action := range actions {
|
|
update := PositionUpdate{Action: action}
|
|
assert.Equal(t, action, update.Action)
|
|
}
|
|
}
|
|
|
|
func TestTradeSides(t *testing.T) {
|
|
sides := []string{"buy", "sell"}
|
|
|
|
for _, side := range sides {
|
|
update := TradeUpdate{Side: side}
|
|
assert.Equal(t, side, update.Side)
|
|
}
|
|
}
|
|
|
|
func TestGetBalanceEmpty(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
// Get balance for non-existent asset returns 0
|
|
balance, err := client.GetBalance("BTC")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(0), balance.Int64())
|
|
}
|
|
|
|
func TestGetBalanceExists(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
// Set balance directly
|
|
client.mu.Lock()
|
|
client.balances["ETH"] = big.NewInt(1000000000)
|
|
client.mu.Unlock()
|
|
|
|
balance, err := client.GetBalance("ETH")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(1000000000), balance.Int64())
|
|
}
|
|
|
|
func TestGetPositionNotFound(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
pos, err := client.GetPosition("nonexistent")
|
|
assert.Error(t, err)
|
|
assert.Nil(t, pos)
|
|
assert.Contains(t, err.Error(), "not found")
|
|
}
|
|
|
|
func TestGetPositionExists(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
// Add position directly
|
|
testPos := &dex.MarginPosition{
|
|
ID: "pos123",
|
|
Symbol: "BTC/USDC",
|
|
Leverage: 10,
|
|
}
|
|
client.mu.Lock()
|
|
client.positions["pos123"] = testPos
|
|
client.mu.Unlock()
|
|
|
|
pos, err := client.GetPosition("pos123")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "pos123", pos.ID)
|
|
assert.Equal(t, "BTC/USDC", pos.Symbol)
|
|
assert.Equal(t, float64(10), pos.Leverage)
|
|
}
|
|
|
|
func TestGetPositionsEmpty(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
positions := client.GetPositions()
|
|
assert.Empty(t, positions)
|
|
}
|
|
|
|
func TestGetPositionsMultiple(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
// Add positions
|
|
client.mu.Lock()
|
|
client.positions["pos1"] = &dex.MarginPosition{ID: "pos1", Symbol: "BTC/USDC"}
|
|
client.positions["pos2"] = &dex.MarginPosition{ID: "pos2", Symbol: "ETH/USDC"}
|
|
client.mu.Unlock()
|
|
|
|
positions := client.GetPositions()
|
|
assert.Len(t, positions, 2)
|
|
assert.NotNil(t, positions["pos1"])
|
|
assert.NotNil(t, positions["pos2"])
|
|
}
|
|
|
|
func TestGetOrdersEmpty(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
orders := client.GetOrders()
|
|
assert.Empty(t, orders)
|
|
}
|
|
|
|
func TestGetOrdersMultiple(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
// Add orders
|
|
client.mu.Lock()
|
|
client.orders[1] = &dex.Order{ID: 1, Symbol: "BTC/USDC"}
|
|
client.orders[2] = &dex.Order{ID: 2, Symbol: "ETH/USDC"}
|
|
client.orders[3] = &dex.Order{ID: 3, Symbol: "SOL/USDC"}
|
|
client.mu.Unlock()
|
|
|
|
orders := client.GetOrders()
|
|
assert.Len(t, orders, 3)
|
|
assert.NotNil(t, orders[1])
|
|
assert.NotNil(t, orders[2])
|
|
assert.NotNil(t, orders[3])
|
|
}
|
|
|
|
func TestGetPriceNotFound(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
price, err := client.GetPrice("BTC/USDC")
|
|
assert.Error(t, err)
|
|
assert.Equal(t, float64(0), price)
|
|
assert.Contains(t, err.Error(), "no price")
|
|
}
|
|
|
|
func TestGetPriceExists(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
// Set price directly
|
|
client.mu.Lock()
|
|
client.prices["BTC/USDC"] = 50000.0
|
|
client.mu.Unlock()
|
|
|
|
price, err := client.GetPrice("BTC/USDC")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 50000.0, price)
|
|
}
|
|
|
|
func TestGetOrderBookNotFound(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
ob, err := client.GetOrderBook("BTC/USDC")
|
|
assert.Error(t, err)
|
|
assert.Nil(t, ob)
|
|
assert.Contains(t, err.Error(), "no order book")
|
|
}
|
|
|
|
func TestGetOrderBookExists(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
// Set order book directly
|
|
testOB := &dex.OrderBookSnapshot{
|
|
Symbol: "ETH/USDC",
|
|
Bids: []dex.OrderLevel{{Price: 3000, Size: 10}},
|
|
Asks: []dex.OrderLevel{{Price: 3010, Size: 5}},
|
|
}
|
|
client.mu.Lock()
|
|
client.orderBooks["ETH/USDC"] = testOB
|
|
client.mu.Unlock()
|
|
|
|
ob, err := client.GetOrderBook("ETH/USDC")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "ETH/USDC", ob.Symbol)
|
|
assert.Len(t, ob.Bids, 1)
|
|
assert.Len(t, ob.Asks, 1)
|
|
}
|
|
|
|
func TestPlaceOrderNotAuthenticated(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
order := &dex.Order{Symbol: "BTC/USDC", Price: 50000}
|
|
result, err := client.PlaceOrder(order)
|
|
assert.Error(t, err)
|
|
assert.Nil(t, result)
|
|
assert.Contains(t, err.Error(), "not authenticated")
|
|
}
|
|
|
|
func TestCancelOrderNotAuthenticated(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
err = client.CancelOrder(12345)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not authenticated")
|
|
}
|
|
|
|
func TestModifyOrderNotAuthenticated(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
err = client.ModifyOrder(12345, 51000, 2.0)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not authenticated")
|
|
}
|
|
|
|
func TestOpenMarginPositionNotAuthenticated(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
pos, err := client.OpenMarginPosition("BTC/USDC", dex.Buy, 1.0, 10.0)
|
|
assert.Error(t, err)
|
|
assert.Nil(t, pos)
|
|
assert.Contains(t, err.Error(), "not authenticated")
|
|
}
|
|
|
|
func TestClosePositionNotAuthenticated(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
err = client.ClosePosition("pos123", 1.0)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not authenticated")
|
|
}
|
|
|
|
func TestModifyLeverageNotAuthenticated(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
err = client.ModifyLeverage("pos123", 20.0)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not authenticated")
|
|
}
|
|
|
|
func TestDepositToVaultNotAuthenticated(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
err = client.DepositToVault("vault1", big.NewInt(1000))
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not authenticated")
|
|
}
|
|
|
|
func TestWithdrawFromVaultNotAuthenticated(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
err = client.WithdrawFromVault("vault1", big.NewInt(500))
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not authenticated")
|
|
}
|
|
|
|
func TestSupplyNotAuthenticated(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
err = client.Supply("USDC", big.NewInt(10000))
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not authenticated")
|
|
}
|
|
|
|
func TestBorrowNotAuthenticated(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
err = client.Borrow("USDC", big.NewInt(5000))
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not authenticated")
|
|
}
|
|
|
|
func TestRepayNotAuthenticated(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
err = client.Repay("USDC", big.NewInt(2000))
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not authenticated")
|
|
}
|
|
|
|
func TestOnOrderUpdateCallback(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
var received *OrderUpdate
|
|
client.OnOrderUpdate(func(update *OrderUpdate) {
|
|
received = update
|
|
})
|
|
|
|
assert.NotNil(t, client.callbacks["order_update"])
|
|
|
|
// Test callback execution
|
|
testUpdate := &OrderUpdate{Message: "test order"}
|
|
client.callbacks["order_update"](testUpdate)
|
|
assert.Equal(t, "test order", received.Message)
|
|
}
|
|
|
|
func TestOnPositionUpdateCallback(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
var received *PositionUpdate
|
|
client.OnPositionUpdate(func(update *PositionUpdate) {
|
|
received = update
|
|
})
|
|
|
|
assert.NotNil(t, client.callbacks["position_update"])
|
|
|
|
// Test callback execution
|
|
testUpdate := &PositionUpdate{Action: "opened"}
|
|
client.callbacks["position_update"](testUpdate)
|
|
assert.Equal(t, "opened", received.Action)
|
|
}
|
|
|
|
func TestOnPriceUpdateCallback(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
var received *PriceUpdate
|
|
client.OnPriceUpdate(func(update *PriceUpdate) {
|
|
received = update
|
|
})
|
|
|
|
assert.NotNil(t, client.callbacks["price_update"])
|
|
|
|
// Test callback execution
|
|
testUpdate := &PriceUpdate{Symbol: "BTC/USDC", Price: 50000}
|
|
client.callbacks["price_update"](testUpdate)
|
|
assert.Equal(t, "BTC/USDC", received.Symbol)
|
|
assert.Equal(t, float64(50000), received.Price)
|
|
}
|
|
|
|
func TestOnTradeUpdateCallback(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
var received *TradeUpdate
|
|
client.OnTradeUpdate(func(update *TradeUpdate) {
|
|
received = update
|
|
})
|
|
|
|
assert.NotNil(t, client.callbacks["trade_update"])
|
|
|
|
// Test callback execution
|
|
testUpdate := &TradeUpdate{Symbol: "ETH/USDC", Side: "buy"}
|
|
client.callbacks["trade_update"](testUpdate)
|
|
assert.Equal(t, "ETH/USDC", received.Symbol)
|
|
assert.Equal(t, "buy", received.Side)
|
|
}
|
|
|
|
func TestProcessMessageOrderUpdate(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "order_update",
|
|
"order": map[string]interface{}{
|
|
"id": float64(123),
|
|
"symbol": "BTC/USDC",
|
|
},
|
|
}
|
|
|
|
client.processMessage(msg)
|
|
|
|
// Check if order was stored
|
|
client.mu.RLock()
|
|
_, exists := client.orders[123]
|
|
client.mu.RUnlock()
|
|
assert.True(t, exists)
|
|
}
|
|
|
|
func TestProcessMessagePositionUpdate(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "position_update",
|
|
"action": "opened",
|
|
"position": map[string]interface{}{
|
|
"id": "pos456",
|
|
"symbol": "ETH/USDC",
|
|
},
|
|
}
|
|
|
|
client.processMessage(msg)
|
|
|
|
// Check if position was stored
|
|
client.mu.RLock()
|
|
_, exists := client.positions["pos456"]
|
|
client.mu.RUnlock()
|
|
assert.True(t, exists)
|
|
}
|
|
|
|
func TestProcessMessagePositionClosed(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
// First add a position
|
|
client.mu.Lock()
|
|
client.positions["pos789"] = &dex.MarginPosition{ID: "pos789"}
|
|
client.mu.Unlock()
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "position_update",
|
|
"action": "closed",
|
|
"position": map[string]interface{}{
|
|
"id": "pos789",
|
|
},
|
|
}
|
|
|
|
client.processMessage(msg)
|
|
|
|
// Check if position was removed
|
|
client.mu.RLock()
|
|
_, exists := client.positions["pos789"]
|
|
client.mu.RUnlock()
|
|
assert.False(t, exists)
|
|
}
|
|
|
|
func TestProcessMessagePriceUpdate(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "price_update",
|
|
"symbol": "SOL/USDC",
|
|
"price": 150.5,
|
|
}
|
|
|
|
client.processMessage(msg)
|
|
|
|
// Check if price was stored
|
|
client.mu.RLock()
|
|
price := client.prices["SOL/USDC"]
|
|
client.mu.RUnlock()
|
|
assert.Equal(t, 150.5, price)
|
|
}
|
|
|
|
func TestProcessMessageBalanceUpdate(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
// handleBalanceUpdate expects "balances" map with string values
|
|
msg := map[string]interface{}{
|
|
"type": "balance_update",
|
|
"balances": map[string]interface{}{
|
|
"USDC": "1000000000000",
|
|
"ETH": "5000000000000000000",
|
|
},
|
|
}
|
|
|
|
client.processMessage(msg)
|
|
|
|
// Check if balances were stored
|
|
client.mu.RLock()
|
|
usdcBalance := client.balances["USDC"]
|
|
ethBalance := client.balances["ETH"]
|
|
client.mu.RUnlock()
|
|
assert.NotNil(t, usdcBalance)
|
|
assert.NotNil(t, ethBalance)
|
|
assert.Equal(t, "1000000000000", usdcBalance.String())
|
|
assert.Equal(t, "5000000000000000000", ethBalance.String())
|
|
}
|
|
|
|
func TestProcessMessageInvalidType(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
msg := map[string]interface{}{
|
|
"invalid": "no type field",
|
|
}
|
|
|
|
// Should not panic
|
|
client.processMessage(msg)
|
|
}
|
|
|
|
func TestProcessMessageUnknownType(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "unknown_type",
|
|
}
|
|
|
|
// Should not panic
|
|
client.processMessage(msg)
|
|
}
|
|
|
|
func TestConcurrentMapAccess(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
done := make(chan bool, 100)
|
|
|
|
// Concurrent writes
|
|
for i := 0; i < 50; i++ {
|
|
go func(idx int) {
|
|
client.mu.Lock()
|
|
client.prices[fmt.Sprintf("PAIR%d/USDC", idx)] = float64(idx * 100)
|
|
client.mu.Unlock()
|
|
done <- true
|
|
}(i)
|
|
}
|
|
|
|
// Concurrent reads
|
|
for i := 0; i < 50; i++ {
|
|
go func() {
|
|
_, _ = client.GetPrice("BTC/USDC")
|
|
done <- true
|
|
}()
|
|
}
|
|
|
|
// Wait for all
|
|
for i := 0; i < 100; i++ {
|
|
<-done
|
|
}
|
|
|
|
// Should have 50 prices
|
|
client.mu.RLock()
|
|
count := len(client.prices)
|
|
client.mu.RUnlock()
|
|
assert.Equal(t, 50, count)
|
|
}
|
|
|
|
func BenchmarkGetBalance(b *testing.B) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
client, _ := NewTraderClient(config)
|
|
client.balances["BTC"] = big.NewInt(1000000000)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
client.GetBalance("BTC")
|
|
}
|
|
}
|
|
|
|
func BenchmarkGetPrice(b *testing.B) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
client, _ := NewTraderClient(config)
|
|
client.prices["BTC/USDC"] = 50000.0
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
client.GetPrice("BTC/USDC")
|
|
}
|
|
}
|
|
|
|
func BenchmarkProcessMessage(b *testing.B) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
client, _ := NewTraderClient(config)
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "price_update",
|
|
"symbol": "BTC/USDC",
|
|
"price": 50000.0,
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
client.processMessage(msg)
|
|
}
|
|
}
|
|
|
|
// Test handleTradeUpdate
|
|
func TestHandleTradeUpdate(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "trade_update",
|
|
"symbol": "BTC/USDC",
|
|
"trade": map[string]interface{}{
|
|
"id": "trade123",
|
|
"price": 50000.0,
|
|
"quantity": 1.5,
|
|
},
|
|
}
|
|
|
|
client.handleTradeUpdate(msg)
|
|
|
|
// Check if trade was stored
|
|
client.mu.RLock()
|
|
trades := client.trades["BTC/USDC"]
|
|
client.mu.RUnlock()
|
|
assert.Len(t, trades, 1)
|
|
}
|
|
|
|
// Test handleTradeUpdate without symbol
|
|
func TestHandleTradeUpdateNoSymbol(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "trade_update",
|
|
"trade": map[string]interface{}{
|
|
"id": "trade123",
|
|
"price": 50000.0,
|
|
"quantity": 1.5,
|
|
},
|
|
}
|
|
|
|
// Should not panic
|
|
client.handleTradeUpdate(msg)
|
|
}
|
|
|
|
// Test handleTradeUpdate with trade history limit
|
|
func TestHandleTradeUpdateHistoryLimit(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
// Add more than 100 trades
|
|
for i := 0; i < 105; i++ {
|
|
msg := map[string]interface{}{
|
|
"type": "trade_update",
|
|
"symbol": "BTC/USDC",
|
|
"trade": map[string]interface{}{
|
|
"id": fmt.Sprintf("trade%d", i),
|
|
"price": float64(50000 + i),
|
|
},
|
|
}
|
|
client.handleTradeUpdate(msg)
|
|
}
|
|
|
|
// Check trade history is limited to 100
|
|
client.mu.RLock()
|
|
trades := client.trades["BTC/USDC"]
|
|
client.mu.RUnlock()
|
|
assert.LessOrEqual(t, len(trades), 100)
|
|
}
|
|
|
|
// Test handleOrderBookUpdate
|
|
func TestHandleOrderBookUpdate(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "orderbook_update",
|
|
"symbol": "ETH/USDC",
|
|
"snapshot": map[string]interface{}{
|
|
"symbol": "ETH/USDC",
|
|
"bids": []interface{}{},
|
|
"asks": []interface{}{},
|
|
},
|
|
}
|
|
|
|
client.handleOrderBookUpdate(msg)
|
|
|
|
// Check if order book was stored
|
|
client.mu.RLock()
|
|
ob := client.orderBooks["ETH/USDC"]
|
|
client.mu.RUnlock()
|
|
assert.NotNil(t, ob)
|
|
}
|
|
|
|
// Test handleOrderBookUpdate missing symbol
|
|
func TestHandleOrderBookUpdateNoSymbol(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "orderbook_update",
|
|
"snapshot": map[string]interface{}{
|
|
"bids": []interface{}{},
|
|
},
|
|
}
|
|
|
|
// Should not panic
|
|
client.handleOrderBookUpdate(msg)
|
|
}
|
|
|
|
// Test handleError
|
|
func TestHandleError(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "error",
|
|
"error": "test error message",
|
|
}
|
|
|
|
client.handleError(msg)
|
|
|
|
// Check error was sent to channel
|
|
select {
|
|
case err := <-client.errorChan:
|
|
assert.Contains(t, err.Error(), "test error message")
|
|
default:
|
|
t.Fatal("expected error in channel")
|
|
}
|
|
}
|
|
|
|
// Test handleError no error field
|
|
func TestHandleErrorNoField(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "error",
|
|
}
|
|
|
|
// Should not panic
|
|
client.handleError(msg)
|
|
}
|
|
|
|
// Test GetOrderUpdates channel access
|
|
func TestGetOrderUpdates(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
ch := client.GetOrderUpdates()
|
|
assert.NotNil(t, ch)
|
|
|
|
// Send an update and verify
|
|
go func() {
|
|
client.orderUpdates <- &OrderUpdate{Message: "test"}
|
|
}()
|
|
|
|
select {
|
|
case update := <-ch:
|
|
assert.Equal(t, "test", update.Message)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timeout waiting for order update")
|
|
}
|
|
}
|
|
|
|
// Test GetPositionUpdates channel access
|
|
func TestGetPositionUpdates(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
ch := client.GetPositionUpdates()
|
|
assert.NotNil(t, ch)
|
|
|
|
// Send an update and verify
|
|
go func() {
|
|
client.positionUpdates <- &PositionUpdate{Action: "opened"}
|
|
}()
|
|
|
|
select {
|
|
case update := <-ch:
|
|
assert.Equal(t, "opened", update.Action)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timeout waiting for position update")
|
|
}
|
|
}
|
|
|
|
// Test GetPriceUpdates channel access
|
|
func TestGetPriceUpdates(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
ch := client.GetPriceUpdates()
|
|
assert.NotNil(t, ch)
|
|
|
|
// Send an update and verify
|
|
go func() {
|
|
client.priceUpdates <- &PriceUpdate{Symbol: "BTC/USDC", Price: 50000}
|
|
}()
|
|
|
|
select {
|
|
case update := <-ch:
|
|
assert.Equal(t, "BTC/USDC", update.Symbol)
|
|
assert.Equal(t, float64(50000), update.Price)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timeout waiting for price update")
|
|
}
|
|
}
|
|
|
|
// Test GetTradeUpdates channel access
|
|
func TestGetTradeUpdates(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
ch := client.GetTradeUpdates()
|
|
assert.NotNil(t, ch)
|
|
|
|
// Send an update and verify
|
|
go func() {
|
|
client.tradeUpdates <- &TradeUpdate{Symbol: "ETH/USDC", Side: "buy"}
|
|
}()
|
|
|
|
select {
|
|
case update := <-ch:
|
|
assert.Equal(t, "ETH/USDC", update.Symbol)
|
|
assert.Equal(t, "buy", update.Side)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timeout waiting for trade update")
|
|
}
|
|
}
|
|
|
|
// Test GetErrors channel access
|
|
func TestGetErrors(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
ch := client.GetErrors()
|
|
assert.NotNil(t, ch)
|
|
|
|
// Send an error and verify
|
|
go func() {
|
|
client.errorChan <- fmt.Errorf("test error")
|
|
}()
|
|
|
|
select {
|
|
case err := <-ch:
|
|
assert.Contains(t, err.Error(), "test error")
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timeout waiting for error")
|
|
}
|
|
}
|
|
|
|
// Test processMessage with trade_update type
|
|
func TestProcessMessageTradeUpdate(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "trade_update",
|
|
"symbol": "BTC/USDC",
|
|
"trade": map[string]interface{}{
|
|
"id": "trade1",
|
|
"price": 51000.0,
|
|
},
|
|
}
|
|
|
|
client.processMessage(msg)
|
|
|
|
// Check if trade was stored
|
|
client.mu.RLock()
|
|
trades := client.trades["BTC/USDC"]
|
|
client.mu.RUnlock()
|
|
assert.Len(t, trades, 1)
|
|
}
|
|
|
|
// Test processMessage with orderbook_update type
|
|
func TestProcessMessageOrderbookUpdate(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "orderbook_update",
|
|
"symbol": "SOL/USDC",
|
|
"snapshot": map[string]interface{}{
|
|
"symbol": "SOL/USDC",
|
|
"bids": []interface{}{},
|
|
"asks": []interface{}{},
|
|
},
|
|
}
|
|
|
|
client.processMessage(msg)
|
|
|
|
// Check if orderbook was stored
|
|
client.mu.RLock()
|
|
ob := client.orderBooks["SOL/USDC"]
|
|
client.mu.RUnlock()
|
|
assert.NotNil(t, ob)
|
|
}
|
|
|
|
// Test processMessage with error type
|
|
func TestProcessMessageError(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "error",
|
|
"error": "some server error",
|
|
}
|
|
|
|
client.processMessage(msg)
|
|
|
|
// Check error was sent to channel
|
|
select {
|
|
case err := <-client.errorChan:
|
|
assert.Contains(t, err.Error(), "some server error")
|
|
default:
|
|
t.Fatal("expected error in channel")
|
|
}
|
|
}
|
|
|
|
// Test handlePriceUpdate with bid/ask
|
|
func TestHandlePriceUpdateWithBidAsk(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "price_update",
|
|
"symbol": "BTC/USDC",
|
|
"price": 50000.0,
|
|
"bid": 49990.0,
|
|
"ask": 50010.0,
|
|
"volume": 1000.0,
|
|
}
|
|
|
|
client.handlePriceUpdate(msg)
|
|
|
|
// Check if price was stored
|
|
client.mu.RLock()
|
|
price := client.prices["BTC/USDC"]
|
|
client.mu.RUnlock()
|
|
assert.Equal(t, 50000.0, price)
|
|
}
|
|
|
|
// Test handlePriceUpdate callback
|
|
func TestHandlePriceUpdateWithCallback(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
var received *PriceUpdate
|
|
client.OnPriceUpdate(func(update *PriceUpdate) {
|
|
received = update
|
|
})
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "price_update",
|
|
"symbol": "ETH/USDC",
|
|
"price": 3000.0,
|
|
"bid": 2999.0,
|
|
"ask": 3001.0,
|
|
}
|
|
|
|
client.handlePriceUpdate(msg)
|
|
|
|
// Check callback was called
|
|
require.NotNil(t, received)
|
|
assert.Equal(t, "ETH/USDC", received.Symbol)
|
|
assert.Equal(t, 3000.0, received.Price)
|
|
assert.Equal(t, 2999.0, received.Bid)
|
|
assert.Equal(t, 3001.0, received.Ask)
|
|
}
|
|
|
|
// Test handleOrderUpdate with callback
|
|
func TestHandleOrderUpdateWithCallback(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
var received *OrderUpdate
|
|
client.OnOrderUpdate(func(update *OrderUpdate) {
|
|
received = update
|
|
})
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "order_update",
|
|
"status": float64(1),
|
|
"order": map[string]interface{}{
|
|
"id": float64(456),
|
|
"symbol": "BTC/USDC",
|
|
"price": 51000.0,
|
|
"quantity": 1.0,
|
|
},
|
|
}
|
|
|
|
client.handleOrderUpdate(msg)
|
|
|
|
// Check callback was called
|
|
require.NotNil(t, received)
|
|
assert.NotNil(t, received.Order)
|
|
}
|
|
|
|
// Test handlePositionUpdate with callback
|
|
func TestHandlePositionUpdateWithCallback(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
var received *PositionUpdate
|
|
client.OnPositionUpdate(func(update *PositionUpdate) {
|
|
received = update
|
|
})
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "position_update",
|
|
"action": "modified",
|
|
"position": map[string]interface{}{
|
|
"id": "pos999",
|
|
"symbol": "BTC/USDC",
|
|
"leverage": 5.0,
|
|
},
|
|
}
|
|
|
|
client.handlePositionUpdate(msg)
|
|
|
|
// Check callback was called
|
|
require.NotNil(t, received)
|
|
assert.Equal(t, "modified", received.Action)
|
|
}
|
|
|
|
// Test handleTradeUpdate with callback
|
|
func TestHandleTradeUpdateWithCallback(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
var received *TradeUpdate
|
|
client.OnTradeUpdate(func(update *TradeUpdate) {
|
|
received = update
|
|
})
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "trade_update",
|
|
"symbol": "SOL/USDC",
|
|
"trade": map[string]interface{}{
|
|
"id": "tradeXYZ",
|
|
"price": 150.0,
|
|
"quantity": 10.0,
|
|
},
|
|
}
|
|
|
|
client.handleTradeUpdate(msg)
|
|
|
|
// Check callback was called
|
|
require.NotNil(t, received)
|
|
assert.Equal(t, "SOL/USDC", received.Symbol)
|
|
}
|
|
|
|
// Test position update with liquidated action
|
|
func TestHandlePositionUpdateLiquidated(t *testing.T) {
|
|
config := ClientConfig{
|
|
APIEndpoint: "https://api.test.com",
|
|
WSEndpoint: "wss://ws.test.com",
|
|
}
|
|
|
|
client, err := NewTraderClient(config)
|
|
require.NoError(t, err)
|
|
|
|
// Add a position first
|
|
client.mu.Lock()
|
|
client.positions["liqPos"] = &dex.MarginPosition{ID: "liqPos"}
|
|
client.mu.Unlock()
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "position_update",
|
|
"action": "liquidated",
|
|
"position": map[string]interface{}{
|
|
"id": "liqPos",
|
|
},
|
|
}
|
|
|
|
client.handlePositionUpdate(msg)
|
|
|
|
// Check if position was removed (liquidated = removed)
|
|
client.mu.RLock()
|
|
_, exists := client.positions["liqPos"]
|
|
client.mu.RUnlock()
|
|
assert.False(t, exists)
|
|
}
|