mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
414 lines
11 KiB
Go
414 lines
11 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"github.com/luxfi/dex/pkg/log"
|
|
"github.com/luxfi/dex/pkg/lx"
|
|
"github.com/luxfi/rpc"
|
|
)
|
|
|
|
// ZAP wire message types for DEX (zero-copy binary protocol)
|
|
const (
|
|
MsgPlaceOrder uint8 = iota + 1
|
|
MsgCancelOrder
|
|
MsgModifyOrder
|
|
MsgGetBestBid
|
|
MsgGetBestAsk
|
|
MsgGetOrderBook
|
|
MsgGetOrder
|
|
MsgOrderAck
|
|
MsgOrderReject
|
|
MsgTrade
|
|
MsgMarketData
|
|
)
|
|
|
|
// Wire sizes for fixed-format messages (cache-aligned)
|
|
const (
|
|
OrderWireSize = 64 // Order: symbol(8) + id(8) + price(8) + size(8) + side(1) + type(1) + flags(2) + ts(8) + user(16) + pad(4)
|
|
CancelWireSize = 32 // Cancel: order_id(8) + user(16) + pad(8)
|
|
AckWireSize = 24 // Ack: order_id(8) + status(1) + seq(8) + pad(7)
|
|
QuoteWireSize = 24 // Quote: price(8) + size(8) + count(4) + pad(4)
|
|
TradeWireSize = 48 // Trade: id(8) + price(8) + size(8) + buyer(8) + seller(8) + ts(8)
|
|
)
|
|
|
|
// ZAPServer provides ultra-low-latency order handling for HFT
|
|
type ZAPServer struct {
|
|
orderBook *lx.OrderBook
|
|
server rpc.Server
|
|
logger log.Logger
|
|
addr string
|
|
|
|
// Statistics
|
|
ordersProcessed atomic.Uint64
|
|
tradesExecuted atomic.Uint64
|
|
cancelProcessed atomic.Uint64
|
|
|
|
// Pre-allocated response buffers (per-connection pools)
|
|
bufferPool sync.Pool
|
|
|
|
// Sequence counter for order acks
|
|
sequence atomic.Uint64
|
|
}
|
|
|
|
// NewZAPServer creates a new ZAP server for ultra-low-latency trading
|
|
func NewZAPServer(orderBook *lx.OrderBook, addr string, logger log.Logger) *ZAPServer {
|
|
return &ZAPServer{
|
|
orderBook: orderBook,
|
|
addr: addr,
|
|
logger: logger,
|
|
bufferPool: sync.Pool{
|
|
New: func() interface{} {
|
|
// Pre-allocate 4KB buffer for responses
|
|
return make([]byte, 4096)
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Start starts the ZAP server
|
|
func (s *ZAPServer) Start(ctx context.Context) error {
|
|
server, err := rpc.Listen(s.addr)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to start ZAP server: %w", err)
|
|
}
|
|
s.server = server
|
|
|
|
// Register raw handlers for zero-copy processing
|
|
s.server.RegisterRaw("place_order", s.handlePlaceOrder)
|
|
s.server.RegisterRaw("cancel_order", s.handleCancelOrder)
|
|
s.server.RegisterRaw("modify_order", s.handleModifyOrder)
|
|
s.server.RegisterRaw("best_bid", s.handleGetBestBid)
|
|
s.server.RegisterRaw("best_ask", s.handleGetBestAsk)
|
|
s.server.RegisterRaw("orderbook", s.handleGetOrderBook)
|
|
s.server.RegisterRaw("order", s.handleGetOrder)
|
|
|
|
s.logger.Info("ZAP server started", "addr", s.server.Addr())
|
|
|
|
go s.server.Serve(ctx)
|
|
return nil
|
|
}
|
|
|
|
// Stop stops the ZAP server
|
|
func (s *ZAPServer) Stop() error {
|
|
if s.server != nil {
|
|
return s.server.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Addr returns the server address
|
|
func (s *ZAPServer) Addr() string {
|
|
if s.server != nil {
|
|
return s.server.Addr()
|
|
}
|
|
return s.addr
|
|
}
|
|
|
|
// Stats returns server statistics
|
|
func (s *ZAPServer) Stats() (orders, trades, cancels uint64) {
|
|
return s.ordersProcessed.Load(), s.tradesExecuted.Load(), s.cancelProcessed.Load()
|
|
}
|
|
|
|
// handlePlaceOrder handles order placement with zero-copy
|
|
// Wire format (64 bytes):
|
|
//
|
|
// [0:8] symbol (8 bytes, null-padded)
|
|
// [8:16] order_id (uint64, network byte order)
|
|
// [16:24] price (float64, IEEE 754)
|
|
// [24:32] size (float64, IEEE 754)
|
|
// [32] side (uint8: 0=buy, 1=sell)
|
|
// [33] order_type (uint8: 0=limit, 1=market, 2=stop)
|
|
// [34:36] flags (uint16: post-only, reduce-only, STP)
|
|
// [36:44] timestamp (uint64, unix nanos)
|
|
// [44:60] user_id (16 bytes, null-padded)
|
|
// [60:64] padding
|
|
func (s *ZAPServer) handlePlaceOrder(ctx context.Context, payload []byte) ([]byte, error) {
|
|
if len(payload) < OrderWireSize {
|
|
return s.encodeReject(0, "invalid message size"), nil
|
|
}
|
|
|
|
// Zero-copy decode order from wire format
|
|
order := s.decodeOrder(payload)
|
|
if order == nil {
|
|
return s.encodeReject(0, "invalid order data"), nil
|
|
}
|
|
|
|
// Validate order
|
|
if order.Price <= 0 || order.Size <= 0 {
|
|
return s.encodeReject(order.ID, "invalid price or size"), nil
|
|
}
|
|
|
|
// Process order through matching engine
|
|
// AddOrder returns the order ID (0 if rejected)
|
|
orderID := s.orderBook.AddOrder(order)
|
|
if orderID == 0 {
|
|
return s.encodeReject(order.ID, "order rejected"), nil
|
|
}
|
|
|
|
s.ordersProcessed.Add(1)
|
|
|
|
// Return ack with sequence number
|
|
return s.encodeAck(orderID, 0, s.sequence.Add(1)), nil
|
|
}
|
|
|
|
// handleCancelOrder handles order cancellation
|
|
// Wire format (32 bytes):
|
|
//
|
|
// [0:8] order_id (uint64)
|
|
// [8:24] user_id (16 bytes)
|
|
// [24:32] padding
|
|
func (s *ZAPServer) handleCancelOrder(ctx context.Context, payload []byte) ([]byte, error) {
|
|
if len(payload) < CancelWireSize {
|
|
return s.encodeReject(0, "invalid message size"), nil
|
|
}
|
|
|
|
orderID := binary.BigEndian.Uint64(payload[0:8])
|
|
|
|
// Cancel the order
|
|
err := s.orderBook.CancelOrder(orderID)
|
|
if err != nil {
|
|
return s.encodeReject(orderID, err.Error()), nil
|
|
}
|
|
|
|
s.cancelProcessed.Add(1)
|
|
return s.encodeAck(orderID, 1, s.sequence.Add(1)), nil
|
|
}
|
|
|
|
// handleModifyOrder handles order modification
|
|
func (s *ZAPServer) handleModifyOrder(ctx context.Context, payload []byte) ([]byte, error) {
|
|
if len(payload) < OrderWireSize {
|
|
return s.encodeReject(0, "invalid message size"), nil
|
|
}
|
|
|
|
order := s.decodeOrder(payload)
|
|
if order == nil {
|
|
return s.encodeReject(0, "invalid order data"), nil
|
|
}
|
|
|
|
// Modify is cancel + add
|
|
_ = s.orderBook.CancelOrder(order.ID)
|
|
orderID := s.orderBook.AddOrder(order)
|
|
if orderID == 0 {
|
|
return s.encodeReject(order.ID, "order rejected"), nil
|
|
}
|
|
|
|
return s.encodeAck(orderID, 0, s.sequence.Add(1)), nil
|
|
}
|
|
|
|
// handleGetBestBid returns the best bid price/size
|
|
func (s *ZAPServer) handleGetBestBid(ctx context.Context, payload []byte) ([]byte, error) {
|
|
price := s.orderBook.GetBestBid()
|
|
if price == 0 {
|
|
return s.encodeQuote(0, 0, 0), nil
|
|
}
|
|
// Get depth for size/count info
|
|
depth := s.orderBook.GetDepth(1)
|
|
if depth != nil && len(depth.Bids) > 0 {
|
|
return s.encodeQuote(depth.Bids[0].Price, depth.Bids[0].Size, depth.Bids[0].Count), nil
|
|
}
|
|
return s.encodeQuote(price, 0, 0), nil
|
|
}
|
|
|
|
// handleGetBestAsk returns the best ask price/size
|
|
func (s *ZAPServer) handleGetBestAsk(ctx context.Context, payload []byte) ([]byte, error) {
|
|
price := s.orderBook.GetBestAsk()
|
|
if price == 0 {
|
|
return s.encodeQuote(0, 0, 0), nil
|
|
}
|
|
// Get depth for size/count info
|
|
depth := s.orderBook.GetDepth(1)
|
|
if depth != nil && len(depth.Asks) > 0 {
|
|
return s.encodeQuote(depth.Asks[0].Price, depth.Asks[0].Size, depth.Asks[0].Count), nil
|
|
}
|
|
return s.encodeQuote(price, 0, 0), nil
|
|
}
|
|
|
|
// handleGetOrderBook returns top N levels
|
|
func (s *ZAPServer) handleGetOrderBook(ctx context.Context, payload []byte) ([]byte, error) {
|
|
levels := 10
|
|
if len(payload) >= 4 {
|
|
levels = int(binary.BigEndian.Uint32(payload[0:4]))
|
|
if levels > 100 {
|
|
levels = 100
|
|
}
|
|
}
|
|
|
|
// Get orderbook depth
|
|
depth := s.orderBook.GetDepth(levels)
|
|
if depth == nil {
|
|
return s.encodeQuote(0, 0, 0), nil
|
|
}
|
|
|
|
// Encode response: [4 bytes: bid count][4 bytes: ask count][bids...][asks...]
|
|
bidCount := len(depth.Bids)
|
|
askCount := len(depth.Asks)
|
|
size := 8 + bidCount*QuoteWireSize + askCount*QuoteWireSize
|
|
resp := s.getBuffer()[:size]
|
|
|
|
binary.BigEndian.PutUint32(resp[0:4], uint32(bidCount))
|
|
binary.BigEndian.PutUint32(resp[4:8], uint32(askCount))
|
|
|
|
offset := 8
|
|
|
|
// Encode bids
|
|
for _, level := range depth.Bids {
|
|
s.encodeQuoteAt(resp[offset:], level.Price, level.Size, level.Count)
|
|
offset += QuoteWireSize
|
|
}
|
|
|
|
// Encode asks
|
|
for _, level := range depth.Asks {
|
|
s.encodeQuoteAt(resp[offset:], level.Price, level.Size, level.Count)
|
|
offset += QuoteWireSize
|
|
}
|
|
|
|
return resp[:offset], nil
|
|
}
|
|
|
|
// handleGetOrder returns a specific order
|
|
func (s *ZAPServer) handleGetOrder(ctx context.Context, payload []byte) ([]byte, error) {
|
|
if len(payload) < 8 {
|
|
return nil, fmt.Errorf("invalid order id")
|
|
}
|
|
|
|
orderID := binary.BigEndian.Uint64(payload[0:8])
|
|
order := s.orderBook.GetOrder(orderID)
|
|
if order == nil {
|
|
return s.encodeReject(orderID, "order not found"), nil
|
|
}
|
|
|
|
return s.encodeOrderResponse(order), nil
|
|
}
|
|
|
|
// decodeOrder decodes order from wire format (zero-copy)
|
|
func (s *ZAPServer) decodeOrder(data []byte) *lx.Order {
|
|
if len(data) < OrderWireSize {
|
|
return nil
|
|
}
|
|
|
|
// Extract symbol (8 bytes, trim null padding)
|
|
symbol := trimNull(data[0:8])
|
|
|
|
order := &lx.Order{
|
|
ID: binary.BigEndian.Uint64(data[8:16]),
|
|
Price: decodeFloat64(data[16:24]),
|
|
Size: decodeFloat64(data[24:32]),
|
|
Side: lx.Side(data[32]),
|
|
Type: lx.OrderType(data[33]),
|
|
Flags: lx.OrderFlags(binary.BigEndian.Uint16(data[34:36])),
|
|
Timestamp: time.Unix(0, int64(binary.BigEndian.Uint64(data[36:44]))),
|
|
UserID: string(trimNull(data[44:60])),
|
|
Symbol: string(symbol),
|
|
}
|
|
|
|
// Validate basic fields
|
|
if order.Price < 0 || order.Size < 0 {
|
|
return nil
|
|
}
|
|
|
|
return order
|
|
}
|
|
|
|
// encodeAck encodes an order acknowledgment
|
|
func (s *ZAPServer) encodeAck(orderID uint64, status uint8, seq uint64) []byte {
|
|
resp := s.getBuffer()[:AckWireSize]
|
|
binary.BigEndian.PutUint64(resp[0:8], orderID)
|
|
resp[8] = status
|
|
binary.BigEndian.PutUint64(resp[9:17], seq)
|
|
return resp[:AckWireSize]
|
|
}
|
|
|
|
// encodeReject encodes an order rejection
|
|
func (s *ZAPServer) encodeReject(orderID uint64, reason string) []byte {
|
|
// Rejection: order_id(8) + status(1=rejected) + reason_len(2) + reason(variable)
|
|
reasonBytes := []byte(reason)
|
|
if len(reasonBytes) > 256 {
|
|
reasonBytes = reasonBytes[:256]
|
|
}
|
|
|
|
size := 11 + len(reasonBytes)
|
|
resp := s.getBuffer()[:size]
|
|
binary.BigEndian.PutUint64(resp[0:8], orderID)
|
|
resp[8] = 2 // Rejected status
|
|
binary.BigEndian.PutUint16(resp[9:11], uint16(len(reasonBytes)))
|
|
copy(resp[11:], reasonBytes)
|
|
return resp[:size]
|
|
}
|
|
|
|
// encodeQuote encodes a price level quote
|
|
func (s *ZAPServer) encodeQuote(price, size float64, count int) []byte {
|
|
resp := s.getBuffer()[:QuoteWireSize]
|
|
s.encodeQuoteAt(resp, price, size, count)
|
|
return resp[:QuoteWireSize]
|
|
}
|
|
|
|
// encodeQuoteAt encodes a quote at a specific buffer position
|
|
func (s *ZAPServer) encodeQuoteAt(buf []byte, price, size float64, count int) {
|
|
encodeFloat64(buf[0:8], price)
|
|
encodeFloat64(buf[8:16], size)
|
|
binary.BigEndian.PutUint32(buf[16:20], uint32(count))
|
|
}
|
|
|
|
// encodeOrderResponse encodes a full order response
|
|
func (s *ZAPServer) encodeOrderResponse(order *lx.Order) []byte {
|
|
resp := s.getBuffer()[:OrderWireSize]
|
|
|
|
// Symbol (8 bytes, null-padded)
|
|
copy(resp[0:8], padNull([]byte(order.Symbol), 8))
|
|
binary.BigEndian.PutUint64(resp[8:16], order.ID)
|
|
encodeFloat64(resp[16:24], order.Price)
|
|
encodeFloat64(resp[24:32], order.Size)
|
|
resp[32] = byte(order.Side)
|
|
resp[33] = byte(order.Type)
|
|
binary.BigEndian.PutUint16(resp[34:36], uint16(order.Flags))
|
|
binary.BigEndian.PutUint64(resp[36:44], uint64(order.Timestamp.UnixNano()))
|
|
copy(resp[44:60], padNull([]byte(order.UserID), 16))
|
|
|
|
return resp[:OrderWireSize]
|
|
}
|
|
|
|
// getBuffer gets a buffer from the pool
|
|
func (s *ZAPServer) getBuffer() []byte {
|
|
return s.bufferPool.Get().([]byte)
|
|
}
|
|
|
|
// Helper functions for binary encoding
|
|
|
|
func decodeFloat64(b []byte) float64 {
|
|
bits := binary.BigEndian.Uint64(b)
|
|
return *(*float64)(unsafe.Pointer(&bits))
|
|
}
|
|
|
|
func encodeFloat64(b []byte, f float64) {
|
|
bits := *(*uint64)(unsafe.Pointer(&f))
|
|
binary.BigEndian.PutUint64(b, bits)
|
|
}
|
|
|
|
func trimNull(b []byte) []byte {
|
|
for i := len(b) - 1; i >= 0; i-- {
|
|
if b[i] != 0 {
|
|
return b[:i+1]
|
|
}
|
|
}
|
|
return b[:0]
|
|
}
|
|
|
|
func padNull(b []byte, size int) []byte {
|
|
if len(b) >= size {
|
|
return b[:size]
|
|
}
|
|
result := make([]byte, size)
|
|
copy(result, b)
|
|
return result
|
|
}
|