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]>
872 lines
19 KiB
Go
872 lines
19 KiB
Go
package client
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
"github.com/luxfi/dex/pkg/dex"
|
|
)
|
|
|
|
// TraderClient is the main client for traders to interact with LX
|
|
type TraderClient struct {
|
|
// Connection
|
|
wsConn *websocket.Conn
|
|
apiEndpoint string
|
|
wsEndpoint string
|
|
apiKey string
|
|
apiSecret string
|
|
|
|
// State
|
|
connected bool
|
|
authenticated bool
|
|
userID string
|
|
|
|
// Account information
|
|
marginAccount *dex.MarginAccount
|
|
positions map[string]*dex.MarginPosition
|
|
orders map[uint64]*dex.Order
|
|
balances map[string]*big.Int
|
|
|
|
// Market data
|
|
orderBooks map[string]*dex.OrderBookSnapshot
|
|
prices map[string]float64
|
|
trades map[string][]*dex.Trade
|
|
|
|
// Subscriptions
|
|
subscriptions map[string]bool
|
|
callbacks map[string]func(interface{})
|
|
|
|
// Channels
|
|
orderUpdates chan *OrderUpdate
|
|
positionUpdates chan *PositionUpdate
|
|
priceUpdates chan *PriceUpdate
|
|
tradeUpdates chan *TradeUpdate
|
|
errorChan chan error
|
|
|
|
// Control
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// OrderUpdate represents an order update event
|
|
type OrderUpdate struct {
|
|
Order *dex.Order
|
|
Status dex.OrderStatus
|
|
Timestamp time.Time
|
|
Message string
|
|
}
|
|
|
|
// PositionUpdate represents a position update event
|
|
type PositionUpdate struct {
|
|
Position *dex.MarginPosition
|
|
Action string // "opened", "modified", "closed", "liquidated"
|
|
Timestamp time.Time
|
|
}
|
|
|
|
// PriceUpdate represents a price update event
|
|
type PriceUpdate struct {
|
|
Symbol string
|
|
Price float64
|
|
Bid float64
|
|
Ask float64
|
|
Volume float64
|
|
Timestamp time.Time
|
|
}
|
|
|
|
// TradeUpdate represents a trade execution event
|
|
type TradeUpdate struct {
|
|
Trade *dex.Trade
|
|
Symbol string
|
|
Side string
|
|
Timestamp time.Time
|
|
}
|
|
|
|
// ClientConfig contains configuration for the trader client
|
|
type ClientConfig struct {
|
|
APIEndpoint string
|
|
WSEndpoint string
|
|
APIKey string
|
|
APISecret string
|
|
UserID string
|
|
}
|
|
|
|
// NewTraderClient creates a new trader client
|
|
func NewTraderClient(config ClientConfig) (*TraderClient, error) {
|
|
if config.APIEndpoint == "" || config.WSEndpoint == "" {
|
|
return nil, errors.New("API and WebSocket endpoints required")
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
client := &TraderClient{
|
|
apiEndpoint: config.APIEndpoint,
|
|
wsEndpoint: config.WSEndpoint,
|
|
apiKey: config.APIKey,
|
|
apiSecret: config.APISecret,
|
|
userID: config.UserID,
|
|
positions: make(map[string]*dex.MarginPosition),
|
|
orders: make(map[uint64]*dex.Order),
|
|
balances: make(map[string]*big.Int),
|
|
orderBooks: make(map[string]*dex.OrderBookSnapshot),
|
|
prices: make(map[string]float64),
|
|
trades: make(map[string][]*dex.Trade),
|
|
subscriptions: make(map[string]bool),
|
|
callbacks: make(map[string]func(interface{})),
|
|
orderUpdates: make(chan *OrderUpdate, 100),
|
|
positionUpdates: make(chan *PositionUpdate, 100),
|
|
priceUpdates: make(chan *PriceUpdate, 1000),
|
|
tradeUpdates: make(chan *TradeUpdate, 1000),
|
|
errorChan: make(chan error, 10),
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
// Connect establishes connection to the DEX
|
|
func (c *TraderClient) Connect() error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if c.connected {
|
|
return errors.New("already connected")
|
|
}
|
|
|
|
// Connect to WebSocket
|
|
conn, _, err := websocket.DefaultDialer.Dial(c.wsEndpoint, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("websocket connection failed: %w", err)
|
|
}
|
|
|
|
c.wsConn = conn
|
|
c.connected = true
|
|
|
|
// Start message handlers
|
|
go c.handleMessages()
|
|
go c.heartbeat()
|
|
|
|
// Authenticate
|
|
if err := c.authenticate(); err != nil {
|
|
c.Disconnect()
|
|
return fmt.Errorf("authentication failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Disconnect closes the connection
|
|
func (c *TraderClient) Disconnect() error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if !c.connected {
|
|
return nil
|
|
}
|
|
|
|
c.cancel()
|
|
|
|
if c.wsConn != nil {
|
|
c.wsConn.Close()
|
|
}
|
|
|
|
c.connected = false
|
|
c.authenticated = false
|
|
|
|
// Close channels
|
|
close(c.orderUpdates)
|
|
close(c.positionUpdates)
|
|
close(c.priceUpdates)
|
|
close(c.tradeUpdates)
|
|
close(c.errorChan)
|
|
|
|
return nil
|
|
}
|
|
|
|
// authenticate authenticates with the DEX
|
|
func (c *TraderClient) authenticate() error {
|
|
auth := map[string]interface{}{
|
|
"type": "auth",
|
|
"apiKey": c.apiKey,
|
|
"apiSecret": c.apiSecret,
|
|
"timestamp": time.Now().Unix(),
|
|
}
|
|
|
|
if err := c.wsConn.WriteJSON(auth); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Wait for auth response
|
|
var response map[string]interface{}
|
|
if err := c.wsConn.ReadJSON(&response); err != nil {
|
|
return err
|
|
}
|
|
|
|
if response["type"] == "auth_success" {
|
|
c.authenticated = true
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("authentication failed: %v", response["error"])
|
|
}
|
|
|
|
// Trading Methods
|
|
|
|
// PlaceOrder places a new order
|
|
func (c *TraderClient) PlaceOrder(order *dex.Order) (*dex.Order, error) {
|
|
if !c.authenticated {
|
|
return nil, errors.New("not authenticated")
|
|
}
|
|
|
|
order.User = c.userID
|
|
order.Timestamp = time.Now()
|
|
|
|
// Send order via WebSocket
|
|
msg := map[string]interface{}{
|
|
"type": "place_order",
|
|
"order": order,
|
|
}
|
|
|
|
if err := c.wsConn.WriteJSON(msg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Store order locally
|
|
c.mu.Lock()
|
|
c.orders[order.ID] = order
|
|
c.mu.Unlock()
|
|
|
|
return order, nil
|
|
}
|
|
|
|
// CancelOrder cancels an existing order
|
|
func (c *TraderClient) CancelOrder(orderID uint64) error {
|
|
if !c.authenticated {
|
|
return errors.New("not authenticated")
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "cancel_order",
|
|
"orderID": orderID,
|
|
}
|
|
|
|
return c.wsConn.WriteJSON(msg)
|
|
}
|
|
|
|
// ModifyOrder modifies an existing order
|
|
func (c *TraderClient) ModifyOrder(orderID uint64, newPrice, newSize float64) error {
|
|
if !c.authenticated {
|
|
return errors.New("not authenticated")
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "modify_order",
|
|
"orderID": orderID,
|
|
"newPrice": newPrice,
|
|
"newSize": newSize,
|
|
}
|
|
|
|
return c.wsConn.WriteJSON(msg)
|
|
}
|
|
|
|
// Margin Trading Methods
|
|
|
|
// OpenMarginPosition opens a new margin position
|
|
func (c *TraderClient) OpenMarginPosition(symbol string, side dex.Side, size, leverage float64) (*dex.MarginPosition, error) {
|
|
if !c.authenticated {
|
|
return nil, errors.New("not authenticated")
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "open_position",
|
|
"symbol": symbol,
|
|
"side": side,
|
|
"size": size,
|
|
"leverage": leverage,
|
|
}
|
|
|
|
if err := c.wsConn.WriteJSON(msg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Wait for position update
|
|
select {
|
|
case update := <-c.positionUpdates:
|
|
if update.Action == "opened" {
|
|
return update.Position, nil
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
return nil, errors.New("timeout waiting for position")
|
|
}
|
|
|
|
return nil, errors.New("failed to open position")
|
|
}
|
|
|
|
// ClosePosition closes an existing position
|
|
func (c *TraderClient) ClosePosition(positionID string, size float64) error {
|
|
if !c.authenticated {
|
|
return errors.New("not authenticated")
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "close_position",
|
|
"positionID": positionID,
|
|
"size": size,
|
|
}
|
|
|
|
return c.wsConn.WriteJSON(msg)
|
|
}
|
|
|
|
// ModifyLeverage modifies leverage for a position
|
|
func (c *TraderClient) ModifyLeverage(positionID string, newLeverage float64) error {
|
|
if !c.authenticated {
|
|
return errors.New("not authenticated")
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "modify_leverage",
|
|
"positionID": positionID,
|
|
"newLeverage": newLeverage,
|
|
}
|
|
|
|
return c.wsConn.WriteJSON(msg)
|
|
}
|
|
|
|
// Vault Operations
|
|
|
|
// DepositToVault deposits funds to a vault
|
|
func (c *TraderClient) DepositToVault(vaultID string, amount *big.Int) error {
|
|
if !c.authenticated {
|
|
return errors.New("not authenticated")
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "vault_deposit",
|
|
"vaultID": vaultID,
|
|
"amount": amount.String(),
|
|
}
|
|
|
|
return c.wsConn.WriteJSON(msg)
|
|
}
|
|
|
|
// WithdrawFromVault withdraws funds from a vault
|
|
func (c *TraderClient) WithdrawFromVault(vaultID string, shares *big.Int) error {
|
|
if !c.authenticated {
|
|
return errors.New("not authenticated")
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "vault_withdraw",
|
|
"vaultID": vaultID,
|
|
"shares": shares.String(),
|
|
}
|
|
|
|
return c.wsConn.WriteJSON(msg)
|
|
}
|
|
|
|
// Lending Operations
|
|
|
|
// Supply supplies assets to lending pool
|
|
func (c *TraderClient) Supply(asset string, amount *big.Int) error {
|
|
if !c.authenticated {
|
|
return errors.New("not authenticated")
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "lending_supply",
|
|
"asset": asset,
|
|
"amount": amount.String(),
|
|
}
|
|
|
|
return c.wsConn.WriteJSON(msg)
|
|
}
|
|
|
|
// Borrow borrows from lending pool
|
|
func (c *TraderClient) Borrow(asset string, amount *big.Int) error {
|
|
if !c.authenticated {
|
|
return errors.New("not authenticated")
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "lending_borrow",
|
|
"asset": asset,
|
|
"amount": amount.String(),
|
|
}
|
|
|
|
return c.wsConn.WriteJSON(msg)
|
|
}
|
|
|
|
// Repay repays borrowed amount
|
|
func (c *TraderClient) Repay(asset string, amount *big.Int) error {
|
|
if !c.authenticated {
|
|
return errors.New("not authenticated")
|
|
}
|
|
|
|
msg := map[string]interface{}{
|
|
"type": "lending_repay",
|
|
"asset": asset,
|
|
"amount": amount.String(),
|
|
}
|
|
|
|
return c.wsConn.WriteJSON(msg)
|
|
}
|
|
|
|
// Market Data Methods
|
|
|
|
// Subscribe subscribes to market data
|
|
func (c *TraderClient) Subscribe(channel string, symbols []string) error {
|
|
msg := map[string]interface{}{
|
|
"type": "subscribe",
|
|
"channel": channel,
|
|
"symbols": symbols,
|
|
}
|
|
|
|
if err := c.wsConn.WriteJSON(msg); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Mark as subscribed
|
|
c.mu.Lock()
|
|
for _, symbol := range symbols {
|
|
key := fmt.Sprintf("%s:%s", channel, symbol)
|
|
c.subscriptions[key] = true
|
|
}
|
|
c.mu.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Unsubscribe unsubscribes from market data
|
|
func (c *TraderClient) Unsubscribe(channel string, symbols []string) error {
|
|
msg := map[string]interface{}{
|
|
"type": "unsubscribe",
|
|
"channel": channel,
|
|
"symbols": symbols,
|
|
}
|
|
|
|
if err := c.wsConn.WriteJSON(msg); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Remove subscriptions
|
|
c.mu.Lock()
|
|
for _, symbol := range symbols {
|
|
key := fmt.Sprintf("%s:%s", channel, symbol)
|
|
delete(c.subscriptions, key)
|
|
}
|
|
c.mu.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetOrderBook returns current order book for a symbol
|
|
func (c *TraderClient) GetOrderBook(symbol string) (*dex.OrderBookSnapshot, error) {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
if ob, exists := c.orderBooks[symbol]; exists {
|
|
return ob, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("no order book for %s", symbol)
|
|
}
|
|
|
|
// GetPrice returns current price for a symbol
|
|
func (c *TraderClient) GetPrice(symbol string) (float64, error) {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
if price, exists := c.prices[symbol]; exists {
|
|
return price, nil
|
|
}
|
|
|
|
return 0, fmt.Errorf("no price for %s", symbol)
|
|
}
|
|
|
|
// Account Methods
|
|
|
|
// GetBalance returns balance for an asset
|
|
func (c *TraderClient) GetBalance(asset string) (*big.Int, error) {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
if balance, exists := c.balances[asset]; exists {
|
|
return balance, nil
|
|
}
|
|
|
|
return big.NewInt(0), nil
|
|
}
|
|
|
|
// GetPosition returns a specific position
|
|
func (c *TraderClient) GetPosition(positionID string) (*dex.MarginPosition, error) {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
if position, exists := c.positions[positionID]; exists {
|
|
return position, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("position %s not found", positionID)
|
|
}
|
|
|
|
// GetPositions returns all positions
|
|
func (c *TraderClient) GetPositions() map[string]*dex.MarginPosition {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
positions := make(map[string]*dex.MarginPosition)
|
|
for k, v := range c.positions {
|
|
positions[k] = v
|
|
}
|
|
|
|
return positions
|
|
}
|
|
|
|
// GetOrders returns all orders
|
|
func (c *TraderClient) GetOrders() map[uint64]*dex.Order {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
orders := make(map[uint64]*dex.Order)
|
|
for k, v := range c.orders {
|
|
orders[k] = v
|
|
}
|
|
|
|
return orders
|
|
}
|
|
|
|
// Callback Registration
|
|
|
|
// OnOrderUpdate registers callback for order updates
|
|
func (c *TraderClient) OnOrderUpdate(callback func(*OrderUpdate)) {
|
|
c.callbacks["order_update"] = func(data interface{}) {
|
|
if update, ok := data.(*OrderUpdate); ok {
|
|
callback(update)
|
|
}
|
|
}
|
|
}
|
|
|
|
// OnPositionUpdate registers callback for position updates
|
|
func (c *TraderClient) OnPositionUpdate(callback func(*PositionUpdate)) {
|
|
c.callbacks["position_update"] = func(data interface{}) {
|
|
if update, ok := data.(*PositionUpdate); ok {
|
|
callback(update)
|
|
}
|
|
}
|
|
}
|
|
|
|
// OnPriceUpdate registers callback for price updates
|
|
func (c *TraderClient) OnPriceUpdate(callback func(*PriceUpdate)) {
|
|
c.callbacks["price_update"] = func(data interface{}) {
|
|
if update, ok := data.(*PriceUpdate); ok {
|
|
callback(update)
|
|
}
|
|
}
|
|
}
|
|
|
|
// OnTradeUpdate registers callback for trade updates
|
|
func (c *TraderClient) OnTradeUpdate(callback func(*TradeUpdate)) {
|
|
c.callbacks["trade_update"] = func(data interface{}) {
|
|
if update, ok := data.(*TradeUpdate); ok {
|
|
callback(update)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Internal Methods
|
|
|
|
// handleMessages handles incoming WebSocket messages
|
|
func (c *TraderClient) handleMessages() {
|
|
for c.connected {
|
|
var msg map[string]interface{}
|
|
if err := c.wsConn.ReadJSON(&msg); err != nil {
|
|
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
|
c.errorChan <- err
|
|
}
|
|
break
|
|
}
|
|
|
|
c.processMessage(msg)
|
|
}
|
|
}
|
|
|
|
// processMessage processes a single message
|
|
func (c *TraderClient) processMessage(msg map[string]interface{}) {
|
|
msgType, ok := msg["type"].(string)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
switch msgType {
|
|
case "order_update":
|
|
c.handleOrderUpdate(msg)
|
|
case "position_update":
|
|
c.handlePositionUpdate(msg)
|
|
case "price_update":
|
|
c.handlePriceUpdate(msg)
|
|
case "trade_update":
|
|
c.handleTradeUpdate(msg)
|
|
case "orderbook_update":
|
|
c.handleOrderBookUpdate(msg)
|
|
case "balance_update":
|
|
c.handleBalanceUpdate(msg)
|
|
case "error":
|
|
c.handleError(msg)
|
|
}
|
|
}
|
|
|
|
// handleOrderUpdate handles order update messages
|
|
func (c *TraderClient) handleOrderUpdate(msg map[string]interface{}) {
|
|
// Parse order update
|
|
update := &OrderUpdate{
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
// Extract order data
|
|
if orderData, ok := msg["order"].(map[string]interface{}); ok {
|
|
// Convert to Order struct
|
|
orderJSON, _ := json.Marshal(orderData)
|
|
var order dex.Order
|
|
json.Unmarshal(orderJSON, &order)
|
|
update.Order = &order
|
|
|
|
// Update local state
|
|
c.mu.Lock()
|
|
c.orders[order.ID] = &order
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// Send to channel
|
|
select {
|
|
case c.orderUpdates <- update:
|
|
default:
|
|
}
|
|
|
|
// Call callback
|
|
if callback, exists := c.callbacks["order_update"]; exists {
|
|
callback(update)
|
|
}
|
|
}
|
|
|
|
// handlePositionUpdate handles position update messages
|
|
func (c *TraderClient) handlePositionUpdate(msg map[string]interface{}) {
|
|
update := &PositionUpdate{
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
if action, ok := msg["action"].(string); ok {
|
|
update.Action = action
|
|
}
|
|
|
|
// Extract position data
|
|
if posData, ok := msg["position"].(map[string]interface{}); ok {
|
|
// Convert to Position struct
|
|
posJSON, _ := json.Marshal(posData)
|
|
var position dex.MarginPosition
|
|
json.Unmarshal(posJSON, &position)
|
|
update.Position = &position
|
|
|
|
// Update local state
|
|
c.mu.Lock()
|
|
if update.Action == "closed" || update.Action == "liquidated" {
|
|
delete(c.positions, position.ID)
|
|
} else {
|
|
c.positions[position.ID] = &position
|
|
}
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// Send to channel
|
|
select {
|
|
case c.positionUpdates <- update:
|
|
default:
|
|
}
|
|
|
|
// Call callback
|
|
if callback, exists := c.callbacks["position_update"]; exists {
|
|
callback(update)
|
|
}
|
|
}
|
|
|
|
// handlePriceUpdate handles price update messages
|
|
func (c *TraderClient) handlePriceUpdate(msg map[string]interface{}) {
|
|
update := &PriceUpdate{
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
if symbol, ok := msg["symbol"].(string); ok {
|
|
update.Symbol = symbol
|
|
}
|
|
if price, ok := msg["price"].(float64); ok {
|
|
update.Price = price
|
|
|
|
// Update local state
|
|
c.mu.Lock()
|
|
c.prices[update.Symbol] = price
|
|
c.mu.Unlock()
|
|
}
|
|
if bid, ok := msg["bid"].(float64); ok {
|
|
update.Bid = bid
|
|
}
|
|
if ask, ok := msg["ask"].(float64); ok {
|
|
update.Ask = ask
|
|
}
|
|
if volume, ok := msg["volume"].(float64); ok {
|
|
update.Volume = volume
|
|
}
|
|
|
|
// Send to channel
|
|
select {
|
|
case c.priceUpdates <- update:
|
|
default:
|
|
}
|
|
|
|
// Call callback
|
|
if callback, exists := c.callbacks["price_update"]; exists {
|
|
callback(update)
|
|
}
|
|
}
|
|
|
|
// handleTradeUpdate handles trade update messages
|
|
func (c *TraderClient) handleTradeUpdate(msg map[string]interface{}) {
|
|
update := &TradeUpdate{
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
// Extract symbol
|
|
symbol := ""
|
|
if s, ok := msg["symbol"].(string); ok {
|
|
symbol = s
|
|
update.Symbol = symbol
|
|
}
|
|
|
|
// Extract trade data
|
|
if tradeData, ok := msg["trade"].(map[string]interface{}); ok {
|
|
// Convert to Trade struct
|
|
tradeJSON, _ := json.Marshal(tradeData)
|
|
var trade dex.Trade
|
|
json.Unmarshal(tradeJSON, &trade)
|
|
update.Trade = &trade
|
|
|
|
// Update local state if symbol is available
|
|
if symbol != "" {
|
|
c.mu.Lock()
|
|
if c.trades[symbol] == nil {
|
|
c.trades[symbol] = make([]*dex.Trade, 0)
|
|
}
|
|
c.trades[symbol] = append(c.trades[symbol], &trade)
|
|
|
|
// Limit trade history
|
|
if len(c.trades[symbol]) > 100 {
|
|
c.trades[symbol] = c.trades[symbol][1:]
|
|
}
|
|
c.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// Send to channel
|
|
select {
|
|
case c.tradeUpdates <- update:
|
|
default:
|
|
}
|
|
|
|
// Call callback
|
|
if callback, exists := c.callbacks["trade_update"]; exists {
|
|
callback(update)
|
|
}
|
|
}
|
|
|
|
// handleOrderBookUpdate handles order book update messages
|
|
func (c *TraderClient) handleOrderBookUpdate(msg map[string]interface{}) {
|
|
if symbol, ok := msg["symbol"].(string); ok {
|
|
if snapshot, ok := msg["snapshot"].(map[string]interface{}); ok {
|
|
// Convert to OrderBookSnapshot
|
|
snapshotJSON, _ := json.Marshal(snapshot)
|
|
var ob dex.OrderBookSnapshot
|
|
json.Unmarshal(snapshotJSON, &ob)
|
|
|
|
// Update local state
|
|
c.mu.Lock()
|
|
c.orderBooks[symbol] = &ob
|
|
c.mu.Unlock()
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleBalanceUpdate handles balance update messages
|
|
func (c *TraderClient) handleBalanceUpdate(msg map[string]interface{}) {
|
|
if balances, ok := msg["balances"].(map[string]interface{}); ok {
|
|
c.mu.Lock()
|
|
for asset, balance := range balances {
|
|
if balStr, ok := balance.(string); ok {
|
|
bal, _ := new(big.Int).SetString(balStr, 10)
|
|
c.balances[asset] = bal
|
|
}
|
|
}
|
|
c.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// handleError handles error messages
|
|
func (c *TraderClient) handleError(msg map[string]interface{}) {
|
|
if errMsg, ok := msg["error"].(string); ok {
|
|
err := fmt.Errorf("server error: %s", errMsg)
|
|
select {
|
|
case c.errorChan <- err:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
// heartbeat sends periodic heartbeat messages
|
|
func (c *TraderClient) heartbeat() {
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
if c.connected {
|
|
msg := map[string]interface{}{
|
|
"type": "ping",
|
|
}
|
|
c.wsConn.WriteJSON(msg)
|
|
}
|
|
case <-c.ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetOrderUpdates returns the order updates channel
|
|
func (c *TraderClient) GetOrderUpdates() <-chan *OrderUpdate {
|
|
return c.orderUpdates
|
|
}
|
|
|
|
// GetPositionUpdates returns the position updates channel
|
|
func (c *TraderClient) GetPositionUpdates() <-chan *PositionUpdate {
|
|
return c.positionUpdates
|
|
}
|
|
|
|
// GetPriceUpdates returns the price updates channel
|
|
func (c *TraderClient) GetPriceUpdates() <-chan *PriceUpdate {
|
|
return c.priceUpdates
|
|
}
|
|
|
|
// GetTradeUpdates returns the trade updates channel
|
|
func (c *TraderClient) GetTradeUpdates() <-chan *TradeUpdate {
|
|
return c.tradeUpdates
|
|
}
|
|
|
|
// GetErrors returns the error channel
|
|
func (c *TraderClient) GetErrors() <-chan error {
|
|
return c.errorChan
|
|
}
|