mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
The gateway spoke 8=FIX.4.4 and reported a fill as ExecType 1/2 — the pair FIX Latest retired precisely because it duplicated order state into the event field. Both fields carried the same thing, so nothing on the wire distinguished "a trade happened" from "the order is done", and a client could read either. Session layer is now FIXT.1.1; the application layer is FIX.5.0SP2 + EP307, named on the Logon by DefaultApplVerID(1137) and ApplExtID(1156). Every fill is ExecType=F (Trade) and OrdStatus alone moves Partial -> Filled. Nothing depended on the old BeginString, so the wire moved with the semantics instead of straddling both. This is the wire pkg/dexprotocol already documents (150=F, 39=1|2). A partially-filled IOC also never reached a terminal state: the last report said Partial and nothing followed, leaving a client waiting for an end that never came. It now closes with Canceled. Vocabulary, one word per concept: execReport -> execution, gateway OrderFill -> Fill, multisig ExecutionResult -> Result (it holds an error string, never an execution). Deletes the SDK's hand-rolled FIX encoder — it pinned a second protocol version, omitted BodyLength entirely so the acceptor would have rejected every frame, and sendFIXMessage returned nil without sending. Co-authored-by: Hanzo Dev <[email protected]>
258 lines
5.2 KiB
Go
258 lines
5.2 KiB
Go
//go:build grpc
|
|
|
|
// Go SDK for LX Trading Platform.
|
|
//
|
|
// This file contains the gRPC-backed client and is built only when the
|
|
// `grpc` build tag is set. The default build provides a stub in
|
|
// client_nogrpc.go whose NewClient returns an error directing callers
|
|
// to rebuild with `-tags=grpc`.
|
|
package lxsdk
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
)
|
|
|
|
// Side represents order side
|
|
type Side int
|
|
|
|
const (
|
|
Buy Side = iota
|
|
Sell
|
|
)
|
|
|
|
// OrderType represents order type
|
|
type OrderType int
|
|
|
|
const (
|
|
Market OrderType = iota
|
|
Limit
|
|
Stop
|
|
StopLimit
|
|
)
|
|
|
|
// Order represents a trading order
|
|
type Order struct {
|
|
ID string
|
|
Symbol string
|
|
Price float64
|
|
Quantity float64
|
|
Side Side
|
|
Type OrderType
|
|
Timestamp time.Time
|
|
}
|
|
|
|
// Trade represents an executed trade
|
|
type Trade struct {
|
|
ID string
|
|
OrderID string
|
|
Symbol string
|
|
Price float64
|
|
Quantity float64
|
|
Side Side
|
|
Timestamp time.Time
|
|
}
|
|
|
|
// Client is the main trading client
|
|
type Client struct {
|
|
conn *grpc.ClientConn
|
|
address string
|
|
|
|
// Performance metrics
|
|
ordersSent atomic.Int64
|
|
tradesRecv atomic.Int64
|
|
latencyNanos atomic.Int64
|
|
|
|
// Callbacks
|
|
onTrade func(*Trade)
|
|
onOrder func(*Order)
|
|
onError func(error)
|
|
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewClient creates a new trading client
|
|
func NewClient(address string) (*Client, error) {
|
|
conn, err := grpc.Dial(address,
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
grpc.WithDefaultCallOptions(
|
|
grpc.MaxCallRecvMsgSize(100*1024*1024), // 100MB
|
|
grpc.MaxCallSendMsgSize(100*1024*1024),
|
|
),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to connect: %w", err)
|
|
}
|
|
|
|
return &Client{conn: conn, address: address}, nil
|
|
}
|
|
|
|
// SendOrder sends a new order
|
|
func (c *Client) SendOrder(symbol string, price, quantity float64, side Side) (*Order, error) {
|
|
start := time.Now()
|
|
defer func() {
|
|
c.latencyNanos.Store(time.Since(start).Nanoseconds())
|
|
}()
|
|
|
|
order := &Order{
|
|
ID: fmt.Sprintf("ORD%d", time.Now().UnixNano()),
|
|
Symbol: symbol,
|
|
Price: price,
|
|
Quantity: quantity,
|
|
Side: side,
|
|
Type: Limit,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
c.ordersSent.Add(1)
|
|
|
|
if c.onOrder != nil {
|
|
c.onOrder(order)
|
|
}
|
|
|
|
return order, nil
|
|
}
|
|
|
|
// SendBulkOrders sends multiple orders efficiently
|
|
func (c *Client) SendBulkOrders(orders []*Order) error {
|
|
// Batch processing for high throughput
|
|
batch := make([]*Order, 0, 1000)
|
|
|
|
for _, order := range orders {
|
|
batch = append(batch, order)
|
|
|
|
if len(batch) >= 1000 {
|
|
if err := c.processBatch(batch); err != nil {
|
|
return err
|
|
}
|
|
batch = batch[:0]
|
|
}
|
|
}
|
|
|
|
if len(batch) > 0 {
|
|
return c.processBatch(batch)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CancelOrder cancels an existing order
|
|
func (c *Client) CancelOrder(orderID string) error {
|
|
// Implementation
|
|
return nil
|
|
}
|
|
|
|
// Subscribe subscribes to market data
|
|
func (c *Client) Subscribe(symbols []string, callback func(*Trade)) error {
|
|
c.onTrade = callback
|
|
// Implementation would set up streaming
|
|
return nil
|
|
}
|
|
|
|
// GetOrderBook gets current order book
|
|
func (c *Client) GetOrderBook(symbol string) (bids, asks []Order, err error) {
|
|
// Implementation
|
|
return nil, nil, nil
|
|
}
|
|
|
|
// Performance metrics methods
|
|
|
|
// GetMetrics returns performance metrics
|
|
func (c *Client) GetMetrics() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"orders_sent": c.ordersSent.Load(),
|
|
"trades_received": c.tradesRecv.Load(),
|
|
"latency_ns": c.latencyNanos.Load(),
|
|
"latency_ms": float64(c.latencyNanos.Load()) / 1e6,
|
|
}
|
|
}
|
|
|
|
// GetThroughput returns current throughput
|
|
func (c *Client) GetThroughput() float64 {
|
|
// Implementation would track time-based throughput
|
|
return float64(c.ordersSent.Load())
|
|
}
|
|
|
|
// Close closes the client connection
|
|
func (c *Client) Close() error {
|
|
if c.conn != nil {
|
|
return c.conn.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Private methods
|
|
|
|
func (c *Client) processBatch(orders []*Order) error {
|
|
// Batch processing implementation
|
|
for range orders {
|
|
c.ordersSent.Add(1)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// HighFrequencyClient is optimized for HFT
|
|
type HighFrequencyClient struct {
|
|
*Client
|
|
|
|
// Pre-allocated order pool
|
|
orderPool sync.Pool
|
|
|
|
// Lock-free queues
|
|
sendQueue chan *Order
|
|
recvQueue chan *Trade
|
|
}
|
|
|
|
// NewHighFrequencyClient creates HFT-optimized client
|
|
func NewHighFrequencyClient(address string) (*HighFrequencyClient, error) {
|
|
client, err := NewClient(address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hft := &HighFrequencyClient{
|
|
Client: client,
|
|
sendQueue: make(chan *Order, 100000),
|
|
recvQueue: make(chan *Trade, 100000),
|
|
}
|
|
|
|
hft.orderPool.New = func() interface{} {
|
|
return &Order{}
|
|
}
|
|
|
|
// Start worker goroutines
|
|
for i := 0; i < 10; i++ {
|
|
go hft.sendWorker()
|
|
}
|
|
|
|
return hft, nil
|
|
}
|
|
|
|
func (hft *HighFrequencyClient) sendWorker() {
|
|
for order := range hft.sendQueue {
|
|
hft.SendOrder(order.Symbol, order.Price, order.Quantity, order.Side)
|
|
hft.orderPool.Put(order)
|
|
}
|
|
}
|
|
|
|
// SendOrderAsync sends order without waiting
|
|
func (hft *HighFrequencyClient) SendOrderAsync(symbol string, price, quantity float64, side Side) {
|
|
order := hft.orderPool.Get().(*Order)
|
|
order.Symbol = symbol
|
|
order.Price = price
|
|
order.Quantity = quantity
|
|
order.Side = side
|
|
|
|
select {
|
|
case hft.sendQueue <- order:
|
|
default:
|
|
// Queue full, drop order
|
|
hft.orderPool.Put(order)
|
|
}
|
|
}
|