Files
dex/sdk/python/lux_dex/client.py
T
zeekay fae0efeb1b chore: normalize naming to LX across codebase
Apply consistent naming conventions throughout:
- Standardize product name to "LX" in documentation
- Update README files and API documentation
- Apply linter fixes to all source files
- Update Hummingbot connector documentation
2025-12-11 21:04:37 -08:00

369 lines
11 KiB
Python

"""
LX Python Client
"""
import json
import time
import threading
from typing import Dict, List, Optional, Callable, Any
from decimal import Decimal
import requests
import websocket
from .types import (
OrderType, OrderSide, OrderStatus, TimeInForce,
Order, Trade, OrderBook, OrderBookLevel, NodeInfo
)
from .exceptions import LXDexException, ConnectionError, OrderError
from .market_data import (
MarketDataClient, LiquidationMonitor,
MarketDataSource, LiquidationInfo, SettlementBatch, MarginInfo
)
class JSONRPCClient:
"""JSON-RPC 2.0 client for LX"""
def __init__(self, base_url: str):
self.base_url = base_url
self.session = requests.Session()
self.id_counter = 1
def call(self, method: str, params: Dict = None) -> Any:
"""Make a JSON-RPC call"""
payload = {
"jsonrpc": "2.0",
"method": method,
"params": params or {},
"id": self.id_counter
}
self.id_counter += 1
try:
response = self.session.post(
f"{self.base_url}/rpc",
json=payload,
headers={"Content-Type": "application/json"}
)
response.raise_for_status()
data = response.json()
if "error" in data:
raise LXDexException(f"RPC Error: {data['error']['message']}")
return data.get("result")
except requests.exceptions.RequestException as e:
raise ConnectionError(f"Connection failed: {str(e)}")
class LXDexClient:
"""Main client for interacting with LX"""
def __init__(
self,
json_rpc_url: str = "http://localhost:8080",
ws_url: str = "ws://localhost:8081",
api_key: Optional[str] = None
):
"""
Initialize LX client
Args:
json_rpc_url: URL for JSON-RPC API
ws_url: URL for WebSocket connection
api_key: Optional API key for authentication
"""
self.json_rpc = JSONRPCClient(json_rpc_url)
self.ws_url = ws_url
self.api_key = api_key
self.ws = None
self.ws_thread = None
self.ws_callbacks = {}
self.ws_running = False
# Initialize sub-clients
self.market_data = MarketDataClient(self.json_rpc)
self.liquidation_monitor = LiquidationMonitor(None) # Will set WS later
# Connection Management
def connect_websocket(self) -> None:
"""Connect to WebSocket for real-time data"""
if self.ws:
return
def on_message(ws, message):
try:
data = json.loads(message)
self._handle_ws_message(data)
except json.JSONDecodeError:
print(f"Failed to parse WebSocket message: {message}")
def on_error(ws, error):
print(f"WebSocket error: {error}")
def on_close(ws, close_status_code, close_msg):
print("WebSocket connection closed")
self.ws = None
self.ws_running = False
def on_open(ws):
print("WebSocket connected")
self.ws_running = True
# Set WebSocket for liquidation monitor
self.liquidation_monitor.ws = ws
self.ws = websocket.WebSocketApp(
self.ws_url,
on_message=on_message,
on_error=on_error,
on_close=on_close,
on_open=on_open
)
self.ws_thread = threading.Thread(target=self.ws.run_forever)
self.ws_thread.daemon = True
self.ws_thread.start()
# Wait for connection
timeout = 5
start = time.time()
while not self.ws_running and time.time() - start < timeout:
time.sleep(0.1)
if not self.ws_running:
raise ConnectionError("Failed to connect to WebSocket")
def disconnect(self) -> None:
"""Disconnect from WebSocket"""
if self.ws:
self.ws.close()
self.ws = None
self.ws_running = False
# Order Management
def place_order(
self,
symbol: str,
order_type: OrderType,
side: OrderSide,
price: float,
size: float,
user_id: Optional[str] = None,
client_id: Optional[str] = None,
time_in_force: TimeInForce = TimeInForce.GTC,
post_only: bool = False,
reduce_only: bool = False
) -> Order:
"""
Place a new order
Args:
symbol: Trading pair symbol (e.g., 'BTC-USD')
order_type: Type of order
side: Buy or sell
price: Order price
size: Order size
user_id: User identifier
client_id: Client order ID
time_in_force: Order time in force
post_only: Post-only order flag
reduce_only: Reduce-only order flag
Returns:
Order object with order details
"""
params = {
"symbol": symbol,
"type": order_type.value,
"side": side.value,
"price": price,
"size": size,
"userID": user_id or "default",
"clientID": client_id,
"timeInForce": time_in_force.value,
"postOnly": post_only,
"reduceOnly": reduce_only
}
result = self.json_rpc.call("lx_placeOrder", params)
if not result.get("orderId"):
raise OrderError(f"Order rejected: {result.get('message', 'Unknown error')}")
return Order(
order_id=result["orderId"],
symbol=symbol,
type=order_type,
side=side,
price=price,
size=size,
status=OrderStatus(result.get("status", "open")),
user_id=user_id,
client_id=client_id
)
def cancel_order(self, order_id: int) -> bool:
"""
Cancel an order
Args:
order_id: Order ID to cancel
Returns:
True if successful
"""
result = self.json_rpc.call("lx_cancelOrder", {"orderId": order_id})
if not result.get("success"):
raise OrderError(f"Failed to cancel order: {result.get('message')}")
return True
def get_order(self, order_id: int) -> Order:
"""
Get order details
Args:
order_id: Order ID
Returns:
Order object
"""
result = self.json_rpc.call("lx_getOrder", {"orderId": order_id})
if not result:
raise OrderError(f"Order {order_id} not found")
return Order(**result)
# Market Data
def get_order_book(self, symbol: str = "BTC-USD", depth: int = 10) -> OrderBook:
"""
Get order book snapshot
Args:
symbol: Trading pair symbol
depth: Number of price levels
Returns:
OrderBook object
"""
result = self.json_rpc.call("lx_getOrderBook", {
"symbol": symbol,
"depth": depth
})
return OrderBook(
symbol=result["Symbol"],
bids=[OrderBookLevel(price=b["Price"], size=b["Size"]) for b in result["Bids"]],
asks=[OrderBookLevel(price=a["Price"], size=a["Size"]) for a in result["Asks"]],
timestamp=result["Timestamp"]
)
def get_best_bid(self, symbol: str = "BTC-USD") -> float:
"""Get best bid price"""
result = self.json_rpc.call("lx_getBestBid", {"symbol": symbol})
return result["price"]
def get_best_ask(self, symbol: str = "BTC-USD") -> float:
"""Get best ask price"""
result = self.json_rpc.call("lx_getBestAsk", {"symbol": symbol})
return result["price"]
def get_trades(self, symbol: str = "BTC-USD", limit: int = 100) -> List[Trade]:
"""
Get recent trades
Args:
symbol: Trading pair symbol
limit: Maximum number of trades
Returns:
List of Trade objects
"""
result = self.json_rpc.call("lx_getTrades", {
"symbol": symbol,
"limit": limit
})
return [Trade(**trade) for trade in result]
# Node Information
def get_info(self) -> NodeInfo:
"""Get node information"""
result = self.json_rpc.call("lx_getInfo")
return NodeInfo(**result)
def ping(self) -> str:
"""Ping the server"""
return self.json_rpc.call("lx_ping")
# WebSocket Subscriptions
def subscribe(self, channel: str, callback: Callable) -> None:
"""
Subscribe to a WebSocket channel
Args:
channel: Channel name
callback: Function to call with updates
"""
if not self.ws:
self.connect_websocket()
self.ws_callbacks[channel] = callback
# Send subscription message
self.ws.send(json.dumps({
"type": "subscribe",
"channel": channel
}))
def unsubscribe(self, channel: str) -> None:
"""Unsubscribe from a channel"""
if channel in self.ws_callbacks:
del self.ws_callbacks[channel]
if self.ws:
self.ws.send(json.dumps({
"type": "unsubscribe",
"channel": channel
}))
def subscribe_order_book(self, symbol: str, callback: Callable[[OrderBook], None]) -> None:
"""Subscribe to order book updates"""
self.subscribe(f"orderbook:{symbol}", callback)
def subscribe_trades(self, symbol: str, callback: Callable[[Trade], None]) -> None:
"""Subscribe to trade updates"""
self.subscribe(f"trades:{symbol}", callback)
# Private methods
def _handle_ws_message(self, msg: Dict) -> None:
"""Handle incoming WebSocket message"""
channel = msg.get("channel")
data = msg.get("data")
if channel in self.ws_callbacks:
callback = self.ws_callbacks[channel]
try:
callback(data)
except Exception as e:
print(f"Error in WebSocket callback: {e}")
# Utility methods
@staticmethod
def format_price(price: float, decimals: int = 2) -> str:
"""Format price with specified decimal places"""
return f"{price:.{decimals}f}"
@staticmethod
def format_size(size: float, decimals: int = 8) -> str:
"""Format size with specified decimal places"""
return f"{size:.{decimals}f}"
@staticmethod
def calculate_total(price: float, size: float) -> float:
"""Calculate total value"""
return price * size