Files
zeekay 1722b23b89 dex: purge CLOB — DEX (service) + OrderBook (engine)
Decentralized exchange; the engine acts like an order book but is decentralized,
so 'Central Limit Order Book' is a misnomer. Branded/service surface → DEX
(RegisterDEX, DEXMethod*, dex_* wire). Matching engine + GPU → OrderBook
(OrderBookMatch/Source/Arena, source_orderbook.go), preserving the AMM-vs-orderbook
distinction. C-ABI → dex_orderbook_* (matches luxcpp). SDKs/clients/docs aligned.
CGO=0 + cgo builds clean; full suite 23 pkgs green (dchain/dex/lx/fix/gateway/zapwire).
Legit financial-term 'Central Limit Order Book' retained in hummingbot taxonomy.
2026-06-26 13:05:07 -07:00

589 lines
20 KiB
Python

"""Native LX DEX and LX AMM adapters."""
from decimal import Decimal
from typing import List, Optional
import time
import aiohttp
from lx_trading.adapters.base import VenueAdapter, VenueCapabilities
from lx_trading.config import NativeVenueConfig
from lx_trading.types import (
Balance,
Fee,
LiquidityResult,
LpPosition,
MarketInfo,
Order,
OrderRequest,
OrderStatus,
OrderType,
PoolInfo,
Side,
SwapQuote,
Ticker,
Trade,
TradingPair,
VenueType,
)
from lx_trading.orderbook import Orderbook
class LxDexAdapter(VenueAdapter):
"""LX DEX adapter for OrderBook trading."""
def __init__(self, name: str, config: NativeVenueConfig):
self._name = name
self._config = config
self._capabilities = VenueCapabilities.orderBook()
self._connected = False
self._latency: Optional[int] = None
self._session: Optional[aiohttp.ClientSession] = None
@property
def name(self) -> str:
return self._name
@property
def venue_type(self) -> VenueType:
return VenueType.NATIVE
@property
def capabilities(self) -> VenueCapabilities:
return self._capabilities
@property
def is_connected(self) -> bool:
return self._connected
@property
def latency_ms(self) -> Optional[int]:
return self._latency
async def connect(self) -> None:
self._session = aiohttp.ClientSession()
# Test connection
start = time.time()
async with self._session.get(f"{self._config.api_url}/api/v1/health") as resp:
await resp.json()
self._latency = int((time.time() - start) * 1000)
self._connected = True
async def disconnect(self) -> None:
if self._session:
await self._session.close()
self._connected = False
async def _request(self, method: str, path: str, **kwargs) -> dict:
if not self._session:
raise RuntimeError("Not connected")
url = f"{self._config.api_url}{path}"
headers = {}
if self._config.api_key:
headers["X-API-KEY"] = self._config.api_key
headers["X-TIMESTAMP"] = str(int(time.time() * 1000))
start = time.time()
async with self._session.request(method, url, headers=headers, **kwargs) as resp:
self._latency = int((time.time() - start) * 1000)
resp.raise_for_status()
return await resp.json()
async def get_markets(self) -> List[MarketInfo]:
data = await self._request("GET", "/api/v1/markets")
return [
MarketInfo(
symbol=m["symbol"],
base=m["base"],
quote=m["quote"],
price_precision=m.get("pricePrecision", 8),
quantity_precision=m.get("quantityPrecision", 8),
min_quantity=Decimal(str(m.get("minQuantity", 0))),
max_quantity=Decimal(str(m.get("maxQuantity", 0))) if m.get("maxQuantity") else None,
min_notional=Decimal(str(m.get("minNotional", 0))) if m.get("minNotional") else None,
tick_size=Decimal(str(m.get("tickSize", "0.00000001"))),
lot_size=Decimal(str(m.get("lotSize", "0.00000001"))),
)
for m in data
]
async def get_ticker(self, symbol: str) -> Ticker:
data = await self._request("GET", f"/api/v1/ticker/{symbol}")
return Ticker(
symbol=data["symbol"],
venue=self._name,
bid=Decimal(str(data["bid"])) if data.get("bid") else None,
ask=Decimal(str(data["ask"])) if data.get("ask") else None,
last=Decimal(str(data["last"])) if data.get("last") else None,
volume_24h=Decimal(str(data["volume24h"])) if data.get("volume24h") else None,
high_24h=Decimal(str(data["high24h"])) if data.get("high24h") else None,
low_24h=Decimal(str(data["low24h"])) if data.get("low24h") else None,
change_24h=Decimal(str(data["change24h"])) if data.get("change24h") else None,
timestamp=data.get("timestamp", 0),
)
async def get_orderbook(self, symbol: str, depth: Optional[int] = None) -> Orderbook:
path = f"/api/v1/orderbook/{symbol}"
if depth:
path += f"?depth={depth}"
data = await self._request("GET", path)
book = Orderbook(symbol, self._name)
for bid in data.get("bids", []):
book.add_bid(Decimal(str(bid[0])), Decimal(str(bid[1])))
for ask in data.get("asks", []):
book.add_ask(Decimal(str(ask[0])), Decimal(str(ask[1])))
book.sort()
return book
async def get_trades(self, symbol: str, limit: Optional[int] = None) -> List[Trade]:
path = f"/api/v1/trades/{symbol}"
if limit:
path += f"?limit={limit}"
data = await self._request("GET", path)
return [
Trade(
trade_id=t["id"],
order_id=t.get("orderId", ""),
symbol=symbol,
venue=self._name,
side=Side.BUY if t["side"] == "buy" else Side.SELL,
price=Decimal(str(t["price"])),
quantity=Decimal(str(t["quantity"])),
fee=Fee(
asset=t.get("feeAsset", ""),
amount=Decimal(str(t.get("feeAmount", 0))),
),
timestamp=t["timestamp"],
is_maker=t.get("isMaker", False),
)
for t in data
]
async def get_balances(self) -> List[Balance]:
data = await self._request("GET", "/api/v1/account/balances")
return [
Balance(
asset=b["asset"],
venue=self._name,
free=Decimal(str(b["free"])),
locked=Decimal(str(b["locked"])),
)
for b in data
]
async def get_balance(self, asset: str) -> Balance:
data = await self._request("GET", f"/api/v1/account/balance/{asset}")
return Balance(
asset=data["asset"],
venue=self._name,
free=Decimal(str(data["free"])),
locked=Decimal(str(data["locked"])),
)
async def get_open_orders(self, symbol: Optional[str] = None) -> List[Order]:
path = "/api/v1/orders?status=open"
if symbol:
path += f"&symbol={symbol}"
data = await self._request("GET", path)
return [self._convert_order(o) for o in data]
async def place_order(self, request: OrderRequest) -> Order:
body = {
"clientOrderId": request.client_order_id,
"symbol": request.symbol,
"side": request.side.value,
"type": request.order_type.value,
"quantity": str(request.quantity),
"timeInForce": request.time_in_force.value,
}
if request.price:
body["price"] = str(request.price)
data = await self._request("POST", "/api/v1/orders", json=body)
return self._convert_order(data)
async def cancel_order(self, order_id: str, symbol: str) -> Order:
data = await self._request("DELETE", f"/api/v1/orders/{order_id}", json={"symbol": symbol})
return self._convert_order(data)
async def cancel_all_orders(self, symbol: Optional[str] = None) -> List[Order]:
body = {}
if symbol:
body["symbol"] = symbol
data = await self._request("DELETE", "/api/v1/orders/all", json=body)
return [self._convert_order(o) for o in data]
def _convert_order(self, o: dict) -> Order:
status_map = {
"pending": OrderStatus.PENDING,
"open": OrderStatus.OPEN,
"partially_filled": OrderStatus.PARTIALLY_FILLED,
"filled": OrderStatus.FILLED,
"cancelled": OrderStatus.CANCELLED,
"rejected": OrderStatus.REJECTED,
"expired": OrderStatus.EXPIRED,
}
type_map = {
"market": OrderType.MARKET,
"limit": OrderType.LIMIT,
"stop_loss": OrderType.STOP_LOSS,
"stop_loss_limit": OrderType.STOP_LOSS_LIMIT,
}
quantity = Decimal(str(o.get("quantity", 0)))
filled = Decimal(str(o.get("filledQuantity", 0)))
return Order(
order_id=o["orderId"],
client_order_id=o.get("clientOrderId", ""),
symbol=o["symbol"],
venue=self._name,
side=Side.BUY if o["side"] == "buy" else Side.SELL,
order_type=type_map.get(o.get("type", "limit"), OrderType.LIMIT),
status=status_map.get(o.get("status", "open"), OrderStatus.OPEN),
quantity=quantity,
filled_quantity=filled,
remaining_quantity=quantity - filled,
price=Decimal(str(o.get("price"))) if o.get("price") else None,
average_price=Decimal(str(o.get("averagePrice"))) if o.get("averagePrice") else None,
created_at=o.get("createdAt", 0),
updated_at=o.get("updatedAt", 0),
)
class LxAmmAdapter(VenueAdapter):
"""LX AMM adapter for liquidity pool operations."""
def __init__(self, name: str, config: NativeVenueConfig):
self._name = name
self._config = config
self._capabilities = VenueCapabilities.amm()
self._connected = False
self._latency: Optional[int] = None
self._session: Optional[aiohttp.ClientSession] = None
@property
def name(self) -> str:
return self._name
@property
def venue_type(self) -> VenueType:
return VenueType.NATIVE
@property
def capabilities(self) -> VenueCapabilities:
return self._capabilities
@property
def is_connected(self) -> bool:
return self._connected
@property
def latency_ms(self) -> Optional[int]:
return self._latency
async def connect(self) -> None:
self._session = aiohttp.ClientSession()
self._connected = True
async def disconnect(self) -> None:
if self._session:
await self._session.close()
self._connected = False
async def _request(self, method: str, path: str, **kwargs) -> dict:
if not self._session:
raise RuntimeError("Not connected")
url = f"{self._config.api_url}{path}"
start = time.time()
async with self._session.request(method, url, **kwargs) as resp:
self._latency = int((time.time() - start) * 1000)
resp.raise_for_status()
return await resp.json()
async def get_markets(self) -> List[MarketInfo]:
data = await self._request("GET", "/api/v1/amm/pools")
return [
MarketInfo(
symbol=f"{p['baseToken']}-{p['quoteToken']}",
base=p["baseToken"],
quote=p["quoteToken"],
price_precision=8,
quantity_precision=8,
min_quantity=Decimal(0),
max_quantity=None,
min_notional=None,
tick_size=Decimal("0.00000001"),
lot_size=Decimal("0.00000001"),
)
for p in data
]
async def get_ticker(self, symbol: str) -> Ticker:
pair = TradingPair.from_symbol(symbol)
if not pair:
raise ValueError(f"Invalid symbol: {symbol}")
data = await self._request("GET", f"/api/v1/amm/price/{pair.base}/{pair.quote}")
price = Decimal(str(data.get("price", 0)))
return Ticker(
symbol=symbol,
venue=self._name,
bid=price,
ask=price,
last=price,
volume_24h=Decimal(str(data.get("volume24h"))) if data.get("volume24h") else None,
high_24h=None,
low_24h=None,
change_24h=None,
timestamp=int(time.time() * 1000),
)
async def get_orderbook(self, symbol: str, depth: Optional[int] = None) -> Orderbook:
raise NotImplementedError("AMM does not have orderbook")
async def get_trades(self, symbol: str, limit: Optional[int] = None) -> List[Trade]:
pair = TradingPair.from_symbol(symbol)
if not pair:
return []
path = f"/api/v1/amm/swaps/{pair.base}/{pair.quote}"
if limit:
path += f"?limit={limit}"
data = await self._request("GET", path)
return [
Trade(
trade_id=t["txHash"],
order_id=t["txHash"],
symbol=symbol,
venue=self._name,
side=Side.BUY if t.get("side") == "buy" else Side.SELL,
price=Decimal(str(t["price"])),
quantity=Decimal(str(t["amount"])),
fee=Fee(asset="", amount=Decimal(str(t.get("fee", 0)))),
timestamp=t["timestamp"],
is_maker=False,
)
for t in data
]
async def get_balances(self) -> List[Balance]:
data = await self._request("GET", "/api/v1/account/balances")
return [
Balance(
asset=b["asset"],
venue=self._name,
free=Decimal(str(b["free"])),
locked=Decimal(str(b.get("locked", 0))),
)
for b in data
]
async def get_balance(self, asset: str) -> Balance:
data = await self._request("GET", f"/api/v1/account/balance/{asset}")
return Balance(
asset=data["asset"],
venue=self._name,
free=Decimal(str(data["free"])),
locked=Decimal(str(data.get("locked", 0))),
)
async def get_open_orders(self, symbol: Optional[str] = None) -> List[Order]:
return [] # AMM doesn't have orders
async def place_order(self, request: OrderRequest) -> Order:
# Convert to swap
pair = TradingPair.from_symbol(request.symbol)
if not pair:
raise ValueError(f"Invalid symbol: {request.symbol}")
trade = await self.execute_swap(
pair.base,
pair.quote,
request.quantity,
request.side == Side.BUY,
Decimal("0.01"), # 1% default slippage
)
return Order(
order_id=trade.trade_id,
client_order_id=request.client_order_id,
symbol=request.symbol,
venue=self._name,
side=request.side,
order_type=OrderType.MARKET,
status=OrderStatus.FILLED,
quantity=request.quantity,
filled_quantity=trade.quantity,
remaining_quantity=Decimal(0),
price=trade.price,
average_price=trade.price,
created_at=trade.timestamp,
updated_at=trade.timestamp,
fees=[trade.fee],
)
async def cancel_order(self, order_id: str, symbol: str) -> Order:
raise NotImplementedError("AMM swaps cannot be cancelled")
async def cancel_all_orders(self, symbol: Optional[str] = None) -> List[Order]:
return []
# AMM specific
async def get_swap_quote(
self, base_token: str, quote_token: str, amount: Decimal, is_buy: bool
) -> SwapQuote:
data = await self._request(
"POST",
"/api/v1/amm/quote",
json={
"baseToken": base_token,
"quoteToken": quote_token,
"amount": str(amount),
"side": "buy" if is_buy else "sell",
},
)
return SwapQuote(
base_token=base_token,
quote_token=quote_token,
input_amount=amount,
output_amount=Decimal(str(data["outputAmount"])),
price=Decimal(str(data["price"])),
price_impact=Decimal(str(data.get("priceImpact", 0))),
fee=Decimal(str(data.get("fee", 0))),
route=data.get("route", []),
expires_at=int(time.time() * 1000) + 60000,
)
async def execute_swap(
self,
base_token: str,
quote_token: str,
amount: Decimal,
is_buy: bool,
slippage: Decimal,
) -> Trade:
data = await self._request(
"POST",
"/api/v1/amm/swap",
json={
"baseToken": base_token,
"quoteToken": quote_token,
"amount": str(amount),
"side": "buy" if is_buy else "sell",
"slippage": str(slippage),
},
)
return Trade(
trade_id=data["txHash"],
order_id=data["txHash"],
symbol=f"{base_token}-{quote_token}",
venue=self._name,
side=Side.BUY if is_buy else Side.SELL,
price=Decimal(str(data["price"])),
quantity=amount,
fee=Fee(asset="", amount=Decimal(str(data.get("fee", 0)))),
timestamp=int(time.time() * 1000),
is_maker=False,
)
async def get_pool_info(self, base_token: str, quote_token: str) -> PoolInfo:
data = await self._request("GET", f"/api/v1/amm/pool/{base_token}/{quote_token}")
return PoolInfo(
address=data["address"],
base_token=base_token,
quote_token=quote_token,
base_reserve=Decimal(str(data["baseReserve"])),
quote_reserve=Decimal(str(data["quoteReserve"])),
total_liquidity=Decimal(str(data.get("totalLiquidity", 0))),
fee_rate=Decimal(str(data.get("feeRate", "0.003"))),
apy=Decimal(str(data.get("apy"))) if data.get("apy") else None,
)
async def add_liquidity(
self,
base_token: str,
quote_token: str,
base_amount: Decimal,
quote_amount: Decimal,
slippage: Decimal,
) -> LiquidityResult:
data = await self._request(
"POST",
"/api/v1/amm/liquidity/add",
json={
"baseToken": base_token,
"quoteToken": quote_token,
"baseAmount": str(base_amount),
"quoteAmount": str(quote_amount),
"slippage": str(slippage),
},
)
return LiquidityResult(
tx_hash=data["txHash"],
pool_address=data.get("poolAddress", ""),
base_amount=base_amount,
quote_amount=quote_amount,
lp_tokens=Decimal(str(data.get("lpTokens", 0))),
share_percent=Decimal(str(data.get("sharePercent", 0))),
)
async def remove_liquidity(
self, pool_address: str, liquidity_amount: Decimal, slippage: Decimal
) -> LiquidityResult:
data = await self._request(
"POST",
"/api/v1/amm/liquidity/remove",
json={
"poolAddress": pool_address,
"liquidity": str(liquidity_amount),
"slippage": str(slippage),
},
)
return LiquidityResult(
tx_hash=data["txHash"],
pool_address=pool_address,
base_amount=Decimal(str(data.get("baseAmount", 0))),
quote_amount=Decimal(str(data.get("quoteAmount", 0))),
lp_tokens=liquidity_amount,
share_percent=Decimal(0),
)
async def get_lp_positions(self) -> List[LpPosition]:
data = await self._request("GET", "/api/v1/amm/positions")
return [
LpPosition(
pool_address=p["poolAddress"],
base_token=p["baseToken"],
quote_token=p["quoteToken"],
lp_tokens=Decimal(str(p.get("lpTokens", 0))),
base_amount=Decimal(str(p["baseAmount"])),
quote_amount=Decimal(str(p["quoteAmount"])),
share_percent=Decimal(str(p.get("sharePercent", 0))),
unrealized_pnl=Decimal(str(p.get("unrealizedPnl"))) if p.get("unrealizedPnl") else None,
)
for p in data
]