mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
docs: align all examples with @luxfi/trading DEX/AMM exports
- Update arbitrage.mdx with new import patterns - Update sdk/index.mdx with correct package names and examples - Update getting-started.mdx TypeScript examples - Update tutorials/first-trade.mdx complete walkthrough - Update tutorials/index.mdx quick reference - Update main index.mdx SDK examples - All docs now use clean DEX() and AMM() imports - Build verified: 144 pages generated successfully
This commit is contained in:
@@ -137,22 +137,14 @@ curl -X POST http://localhost:8080/rpc \
|
||||
### Using TypeScript SDK
|
||||
|
||||
```typescript
|
||||
import { LXDexClient } from '@luxfi/dex-sdk';
|
||||
import { DEX } from '@luxfi/trading'
|
||||
|
||||
const client = new LXDexClient({
|
||||
jsonRpcUrl: 'http://localhost:8080/rpc',
|
||||
});
|
||||
const dex = await DEX({ rpcUrl: 'http://localhost:8080/rpc' })
|
||||
|
||||
// Place a buy order
|
||||
const order = await client.placeOrder({
|
||||
symbol: 'BTC-USD',
|
||||
type: 'limit',
|
||||
side: 'buy',
|
||||
price: 50000,
|
||||
size: 1.0
|
||||
});
|
||||
const order = await dex.limitBuy('BTC-USD', '1.0', '50000')
|
||||
|
||||
console.log('Order ID:', order.orderId);
|
||||
console.log('Order ID:', order.orderId)
|
||||
```
|
||||
|
||||
### Using Python SDK
|
||||
|
||||
@@ -146,21 +146,15 @@ func main() {
|
||||
### TypeScript/JavaScript
|
||||
|
||||
```typescript
|
||||
import { LxDex } from '@luxfi/dex-sdk'
|
||||
import { DEX } from '@luxfi/trading'
|
||||
|
||||
const dex = new LxDex({
|
||||
endpoint: 'https://dex.lux.network',
|
||||
const dex = await DEX({
|
||||
rpcUrl: 'https://api.lux.network/rpc',
|
||||
apiKey: 'your-api-key'
|
||||
})
|
||||
|
||||
// Place order
|
||||
const order = await dex.placeOrder({
|
||||
symbol: 'BTC-USD',
|
||||
side: 'buy',
|
||||
type: 'limit',
|
||||
price: '50000',
|
||||
quantity: '1.5'
|
||||
})
|
||||
const order = await dex.limitBuy('BTC-USD', '1.5', '50000')
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
@@ -17,12 +17,12 @@ LX provides multiple SDKs and APIs for integration:
|
||||
|
||||
## Implementation Status
|
||||
|
||||
| SDK | Repository | Status |
|
||||
|-----|------------|--------|
|
||||
| TypeScript | [`@luxfi/dex-sdk`](https://github.com/luxfi/dex/tree/main/sdk/typescript) | Complete |
|
||||
| Python | [`luxfi-dex`](https://github.com/luxfi/dex/tree/main/sdk/python) | Complete |
|
||||
| Go | [`github.com/luxfi/dex/sdk/go`](https://github.com/luxfi/dex/tree/main/sdk/go) | Complete |
|
||||
| Rust | [`luxfi-dex`](https://github.com/luxfi/dex/tree/main/sdk/rust) | In Progress |
|
||||
| SDK | Package | Repository | Status |
|
||||
|-----|---------|------------|--------|
|
||||
| TypeScript | [`@luxfi/trading`](https://npmjs.com/package/@luxfi/trading) | [`sdk/lx-trading-ts`](https://github.com/luxfi/dex/tree/main/sdk/lx-trading-ts) | Complete |
|
||||
| Python | [`luxfi-dex`](https://pypi.org/project/luxfi-dex/) | [`sdk/python`](https://github.com/luxfi/dex/tree/main/sdk/python) | Complete |
|
||||
| Go | `github.com/luxfi/dex/sdk/go` | [`sdk/go`](https://github.com/luxfi/dex/tree/main/sdk/go) | Complete |
|
||||
| Rust | [`lux-dex`](https://crates.io/crates/lux-dex) | [`sdk/rust`](https://github.com/luxfi/dex/tree/main/sdk/rust) | Complete |
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -47,69 +47,59 @@ This starts:
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npm install @luxfi/dex-sdk
|
||||
npm install @luxfi/trading
|
||||
# or
|
||||
pnpm add @luxfi/dex-sdk
|
||||
pnpm add @luxfi/trading
|
||||
# or
|
||||
yarn add @luxfi/dex-sdk
|
||||
yarn add @luxfi/trading
|
||||
```
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```typescript
|
||||
import { LXDexClient } from '@luxfi/dex-sdk';
|
||||
import { DEX } from '@luxfi/trading'
|
||||
|
||||
const client = new LXDexClient({
|
||||
jsonRpcUrl: 'http://localhost:8080/rpc',
|
||||
wsUrl: 'ws://localhost:8081'
|
||||
});
|
||||
// Simple initialization
|
||||
const dex = await DEX({ rpcUrl: 'http://localhost:8080/rpc' })
|
||||
|
||||
// Place an order
|
||||
const order = await client.placeOrder({
|
||||
symbol: 'BTC-USD',
|
||||
type: 'limit',
|
||||
side: 'buy',
|
||||
price: 50000,
|
||||
size: 1.0
|
||||
});
|
||||
const order = await dex.limitBuy('BTC-USD', '1.0', '50000')
|
||||
|
||||
console.log('Order placed:', order.orderId);
|
||||
console.log('Order placed:', order.orderId)
|
||||
```
|
||||
|
||||
### WebSocket Subscriptions
|
||||
|
||||
```typescript
|
||||
// Subscribe to order book
|
||||
client.subscribeOrderBook('BTC-USD', (book) => {
|
||||
console.log('Best bid:', book.bids[0]);
|
||||
console.log('Best ask:', book.asks[0]);
|
||||
});
|
||||
dex.subscribeOrderBook('BTC-USD', (book) => {
|
||||
console.log('Best bid:', book.bids[0])
|
||||
console.log('Best ask:', book.asks[0])
|
||||
})
|
||||
|
||||
// Subscribe to trades
|
||||
client.subscribeTrades('BTC-USD', (trade) => {
|
||||
console.log('New trade:', trade);
|
||||
});
|
||||
dex.subscribeTrades('BTC-USD', (trade) => {
|
||||
console.log('New trade:', trade)
|
||||
})
|
||||
|
||||
// Subscribe to your orders
|
||||
client.subscribeUserOrders((order) => {
|
||||
console.log('Order update:', order);
|
||||
});
|
||||
dex.subscribeUserOrders((order) => {
|
||||
console.log('Order update:', order)
|
||||
})
|
||||
```
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
```typescript
|
||||
const client = new LXDexClient({
|
||||
jsonRpcUrl: 'https://api.lux.network/rpc',
|
||||
wsUrl: 'wss://api.lux.network/ws',
|
||||
apiKey: 'your-api-key',
|
||||
apiSecret: 'your-api-secret',
|
||||
timeout: 30000,
|
||||
retry: {
|
||||
maxRetries: 3,
|
||||
backoffMs: 1000
|
||||
}
|
||||
});
|
||||
import { Client, Config, NativeVenueConfig } from '@luxfi/trading'
|
||||
|
||||
const config = new Config()
|
||||
.withNative('lx-dex', NativeVenueConfig.lxDex('https://api.lux.network/rpc'))
|
||||
.withApiKey('your-api-key')
|
||||
.withTimeout(30000)
|
||||
|
||||
const client = new Client(config)
|
||||
await client.connect()
|
||||
```
|
||||
|
||||
## Python SDK
|
||||
@@ -306,12 +296,12 @@ resp, err := client.PlaceOrder(context.Background(), &pb.PlaceOrderRequest{
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const order = await client.placeOrder({...});
|
||||
const order = await dex.limitBuy('BTC-USD', '1.0', '50000')
|
||||
} catch (error) {
|
||||
if (error.code === 'INSUFFICIENT_BALANCE') {
|
||||
console.error('Not enough funds');
|
||||
console.error('Not enough funds')
|
||||
} else if (error.code === 'RATE_LIMIT_EXCEEDED') {
|
||||
console.error('Too many requests');
|
||||
console.error('Too many requests')
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -361,10 +351,10 @@ if err != nil {
|
||||
### API Key Setup
|
||||
|
||||
```typescript
|
||||
const client = new LXDexClient({
|
||||
apiKey: 'your-api-key',
|
||||
apiSecret: 'your-api-secret'
|
||||
});
|
||||
const dex = await DEX({
|
||||
rpcUrl: 'https://api.lux.network/rpc',
|
||||
apiKey: 'your-api-key'
|
||||
})
|
||||
```
|
||||
|
||||
### JWT Authentication
|
||||
|
||||
@@ -174,17 +174,20 @@ Chain A (Lux) Chain B (Ethereum)
|
||||
### TypeScript
|
||||
|
||||
```typescript
|
||||
import { DEX, Client } from '@luxfi/trading'
|
||||
|
||||
// For advanced arbitrage features
|
||||
import {
|
||||
LxDex,
|
||||
UnifiedArbitrage,
|
||||
LxFirstArbitrage,
|
||||
CrossChainRouter,
|
||||
Scanner
|
||||
} from '@luxfi/dex-sdk'
|
||||
} from '@luxfi/trading/arbitrage'
|
||||
|
||||
// Initialize the DEX client
|
||||
const dex = new LxDex({
|
||||
endpoint: 'wss://dex.lux.network/ws',
|
||||
const dex = await DEX({
|
||||
rpcUrl: 'https://api.lux.network/rpc',
|
||||
wsUrl: 'wss://dex.lux.network/ws',
|
||||
apiKey: process.env.LX_API_KEY
|
||||
})
|
||||
|
||||
|
||||
@@ -47,29 +47,29 @@ INFO Node ready for trading
|
||||
### TypeScript
|
||||
|
||||
```typescript
|
||||
import { LXDexClient } from '@luxfi/dex-sdk';
|
||||
import { DEX } from '@luxfi/trading'
|
||||
|
||||
// Create client instance
|
||||
const client = new LXDexClient({
|
||||
jsonRpcUrl: 'http://localhost:8080/rpc',
|
||||
const dex = await DEX({
|
||||
rpcUrl: 'http://localhost:8080/rpc',
|
||||
wsUrl: 'ws://localhost:8081'
|
||||
});
|
||||
})
|
||||
|
||||
// Test connection
|
||||
async function testConnection() {
|
||||
try {
|
||||
const info = await client.getInfo();
|
||||
console.log('Connected to LX');
|
||||
console.log('Node version:', info.version);
|
||||
console.log('Network:', info.network);
|
||||
return true;
|
||||
const info = await dex.getInfo()
|
||||
console.log('Connected to LX')
|
||||
console.log('Node version:', info.version)
|
||||
console.log('Network:', info.network)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Connection failed:', error.message);
|
||||
return false;
|
||||
console.error('Connection failed:', error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
testConnection();
|
||||
testConnection()
|
||||
```
|
||||
|
||||
### Python
|
||||
@@ -167,33 +167,33 @@ Before placing an order, check the current market state:
|
||||
|
||||
```typescript
|
||||
async function checkOrderBook() {
|
||||
const book = await client.getOrderBook('BTC-USD', 5);
|
||||
const book = await dex.getOrderBook('BTC-USD', 5)
|
||||
|
||||
console.log('\n=== BTC-USD Order Book ===');
|
||||
console.log('\nAsks (Sell Orders):');
|
||||
console.log('\n=== BTC-USD Order Book ===')
|
||||
console.log('\nAsks (Sell Orders):')
|
||||
book.asks.slice().reverse().forEach(([price, size]) => {
|
||||
console.log(` ${price.toFixed(2)} | ${size.toFixed(4)} BTC`);
|
||||
});
|
||||
console.log(` ${price.toFixed(2)} | ${size.toFixed(4)} BTC`)
|
||||
})
|
||||
|
||||
console.log('\n--- Spread ---');
|
||||
console.log('\n--- Spread ---')
|
||||
|
||||
console.log('\nBids (Buy Orders):');
|
||||
console.log('\nBids (Buy Orders):')
|
||||
book.bids.forEach(([price, size]) => {
|
||||
console.log(` ${price.toFixed(2)} | ${size.toFixed(4)} BTC`);
|
||||
});
|
||||
console.log(` ${price.toFixed(2)} | ${size.toFixed(4)} BTC`)
|
||||
})
|
||||
|
||||
// Calculate spread
|
||||
const bestBid = book.bids[0][0];
|
||||
const bestAsk = book.asks[0][0];
|
||||
const spread = bestAsk - bestBid;
|
||||
const spreadPercent = (spread / bestBid) * 100;
|
||||
const bestBid = book.bids[0][0]
|
||||
const bestAsk = book.asks[0][0]
|
||||
const spread = bestAsk - bestBid
|
||||
const spreadPercent = (spread / bestBid) * 100
|
||||
|
||||
console.log(`\nSpread: $${spread.toFixed(2)} (${spreadPercent.toFixed(3)}%)`);
|
||||
console.log(`\nSpread: $${spread.toFixed(2)} (${spreadPercent.toFixed(3)}%)`)
|
||||
|
||||
return book;
|
||||
return book
|
||||
}
|
||||
|
||||
checkOrderBook();
|
||||
checkOrderBook()
|
||||
```
|
||||
|
||||
### Python
|
||||
@@ -292,33 +292,26 @@ Now let us place a limit buy order:
|
||||
```typescript
|
||||
async function placeLimitOrder() {
|
||||
try {
|
||||
const order = await client.placeOrder({
|
||||
symbol: 'BTC-USD',
|
||||
side: 'buy',
|
||||
type: 'limit',
|
||||
price: 49500, // Price below market
|
||||
size: 0.1, // 0.1 BTC
|
||||
timeInForce: 'GTC' // Good Till Cancelled
|
||||
});
|
||||
const order = await dex.limitBuy('BTC-USD', '0.1', '49500', { timeInForce: 'GTC' })
|
||||
|
||||
console.log('\n=== Order Placed ===');
|
||||
console.log('Order ID:', order.orderId);
|
||||
console.log('Symbol:', order.symbol);
|
||||
console.log('Side:', order.side);
|
||||
console.log('Type:', order.type);
|
||||
console.log('Price:', order.price);
|
||||
console.log('Size:', order.size);
|
||||
console.log('Status:', order.status);
|
||||
console.log('Created:', new Date(order.createdAt).toISOString());
|
||||
console.log('\n=== Order Placed ===')
|
||||
console.log('Order ID:', order.orderId)
|
||||
console.log('Symbol:', order.symbol)
|
||||
console.log('Side:', order.side)
|
||||
console.log('Type:', order.type)
|
||||
console.log('Price:', order.price)
|
||||
console.log('Size:', order.size)
|
||||
console.log('Status:', order.status)
|
||||
console.log('Created:', new Date(order.createdAt).toISOString())
|
||||
|
||||
return order;
|
||||
return order
|
||||
} catch (error) {
|
||||
console.error('Failed to place order:', error.message);
|
||||
throw error;
|
||||
console.error('Failed to place order:', error.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const myOrder = await placeLimitOrder();
|
||||
const myOrder = await placeLimitOrder()
|
||||
```
|
||||
|
||||
### Python
|
||||
@@ -425,26 +418,26 @@ Monitor your order:
|
||||
|
||||
```typescript
|
||||
async function checkOrderStatus(orderId: string) {
|
||||
const order = await client.getOrder(orderId);
|
||||
const order = await dex.getOrder(orderId)
|
||||
|
||||
console.log('\n=== Order Status ===');
|
||||
console.log('Order ID:', order.orderId);
|
||||
console.log('Status:', order.status);
|
||||
console.log('Filled:', order.filledSize, '/', order.size);
|
||||
console.log('Remaining:', order.remainingSize);
|
||||
console.log('Average Fill Price:', order.avgFillPrice || 'N/A');
|
||||
console.log('\n=== Order Status ===')
|
||||
console.log('Order ID:', order.orderId)
|
||||
console.log('Status:', order.status)
|
||||
console.log('Filled:', order.filledSize, '/', order.size)
|
||||
console.log('Remaining:', order.remainingSize)
|
||||
console.log('Average Fill Price:', order.avgFillPrice || 'N/A')
|
||||
|
||||
if (order.fills && order.fills.length > 0) {
|
||||
console.log('\nFills:');
|
||||
console.log('\nFills:')
|
||||
order.fills.forEach((fill, i) => {
|
||||
console.log(` ${i + 1}. ${fill.size} @ ${fill.price} (${fill.timestamp})`);
|
||||
});
|
||||
console.log(` ${i + 1}. ${fill.size} @ ${fill.price} (${fill.timestamp})`)
|
||||
})
|
||||
}
|
||||
|
||||
return order;
|
||||
return order
|
||||
}
|
||||
|
||||
await checkOrderStatus(myOrder.orderId);
|
||||
await checkOrderStatus(myOrder.orderId)
|
||||
```
|
||||
|
||||
### Python
|
||||
@@ -516,23 +509,18 @@ For immediate execution, use a market order:
|
||||
|
||||
```typescript
|
||||
async function placeMarketOrder() {
|
||||
const order = await client.placeOrder({
|
||||
symbol: 'BTC-USD',
|
||||
side: 'buy',
|
||||
type: 'market',
|
||||
size: 0.05 // 0.05 BTC
|
||||
});
|
||||
const order = await dex.buy('BTC-USD', '0.05') // Market buy 0.05 BTC
|
||||
|
||||
console.log('\n=== Market Order Executed ===');
|
||||
console.log('Order ID:', order.orderId);
|
||||
console.log('Status:', order.status);
|
||||
console.log('Filled:', order.filledSize);
|
||||
console.log('Average Price:', order.avgFillPrice);
|
||||
console.log('\n=== Market Order Executed ===')
|
||||
console.log('Order ID:', order.orderId)
|
||||
console.log('Status:', order.status)
|
||||
console.log('Filled:', order.filledSize)
|
||||
console.log('Average Price:', order.avgFillPrice)
|
||||
|
||||
return order;
|
||||
return order
|
||||
}
|
||||
|
||||
await placeMarketOrder();
|
||||
await placeMarketOrder()
|
||||
```
|
||||
|
||||
### Python
|
||||
@@ -598,24 +586,24 @@ Cancel your open limit order:
|
||||
```typescript
|
||||
async function cancelOrder(orderId: string) {
|
||||
try {
|
||||
const result = await client.cancelOrder(orderId);
|
||||
const result = await dex.cancelOrder(orderId)
|
||||
|
||||
console.log('\n=== Order Cancelled ===');
|
||||
console.log('Order ID:', result.orderId);
|
||||
console.log('Status:', result.status);
|
||||
console.log('Cancelled At:', new Date(result.cancelledAt).toISOString());
|
||||
console.log('\n=== Order Cancelled ===')
|
||||
console.log('Order ID:', result.orderId)
|
||||
console.log('Status:', result.status)
|
||||
console.log('Cancelled At:', new Date(result.cancelledAt).toISOString())
|
||||
|
||||
return result;
|
||||
return result
|
||||
} catch (error) {
|
||||
if (error.code === 'ORDER_NOT_FOUND') {
|
||||
console.log('Order not found (may already be filled or cancelled)');
|
||||
console.log('Order not found (may already be filled or cancelled)')
|
||||
} else {
|
||||
throw error;
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await cancelOrder(myOrder.orderId);
|
||||
await cancelOrder(myOrder.orderId)
|
||||
```
|
||||
|
||||
### Python
|
||||
@@ -676,52 +664,39 @@ Here is a complete script combining all steps:
|
||||
### TypeScript
|
||||
|
||||
```typescript
|
||||
import { LXDexClient } from '@luxfi/dex-sdk';
|
||||
import { DEX } from '@luxfi/trading'
|
||||
|
||||
async function main() {
|
||||
// Initialize client
|
||||
const client = new LXDexClient({
|
||||
jsonRpcUrl: 'http://localhost:8080/rpc'
|
||||
});
|
||||
const dex = await DEX({ rpcUrl: 'http://localhost:8080/rpc' })
|
||||
|
||||
// 1. Test connection
|
||||
const info = await client.getInfo();
|
||||
console.log('Connected to', info.network);
|
||||
const info = await dex.getInfo()
|
||||
console.log('Connected to', info.network)
|
||||
|
||||
// 2. Check orderbook
|
||||
const book = await client.getOrderBook('BTC-USD', 5);
|
||||
console.log(`Best bid: ${book.bids[0][0]}, Best ask: ${book.asks[0][0]}`);
|
||||
const book = await dex.getOrderBook('BTC-USD', 5)
|
||||
console.log(`Best bid: ${book.bids[0][0]}, Best ask: ${book.asks[0][0]}`)
|
||||
|
||||
// 3. Place limit order
|
||||
const limitOrder = await client.placeOrder({
|
||||
symbol: 'BTC-USD',
|
||||
side: 'buy',
|
||||
type: 'limit',
|
||||
price: 49500,
|
||||
size: 0.1
|
||||
});
|
||||
console.log('Limit order placed:', limitOrder.orderId);
|
||||
const limitOrder = await dex.limitBuy('BTC-USD', '0.1', '49500')
|
||||
console.log('Limit order placed:', limitOrder.orderId)
|
||||
|
||||
// 4. Place market order
|
||||
const marketOrder = await client.placeOrder({
|
||||
symbol: 'BTC-USD',
|
||||
side: 'buy',
|
||||
type: 'market',
|
||||
size: 0.05
|
||||
});
|
||||
console.log('Market order filled at:', marketOrder.avgFillPrice);
|
||||
const marketOrder = await dex.buy('BTC-USD', '0.05')
|
||||
console.log('Market order filled at:', marketOrder.avgFillPrice)
|
||||
|
||||
// 5. Check and cancel limit order
|
||||
const status = await client.getOrder(limitOrder.orderId);
|
||||
const status = await dex.getOrder(limitOrder.orderId)
|
||||
if (status.status === 'open') {
|
||||
await client.cancelOrder(limitOrder.orderId);
|
||||
console.log('Limit order cancelled');
|
||||
await dex.cancelOrder(limitOrder.orderId)
|
||||
console.log('Limit order cancelled')
|
||||
}
|
||||
|
||||
console.log('\nTutorial complete!');
|
||||
console.log('\nTutorial complete!')
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
main().catch(console.error)
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
@@ -95,7 +95,7 @@ Choose your preferred language:
|
||||
|
||||
```bash
|
||||
# TypeScript/JavaScript
|
||||
npm install @luxfi/dex-sdk
|
||||
npm install @luxfi/trading
|
||||
|
||||
# Python
|
||||
pip install luxfi-dex
|
||||
@@ -146,17 +146,19 @@ If you get stuck:
|
||||
### Common Operations
|
||||
|
||||
```typescript
|
||||
import { DEX } from '@luxfi/trading'
|
||||
|
||||
// Quick reference - TypeScript
|
||||
const client = new LXDexClient({ endpoint: 'http://localhost:8080/rpc' });
|
||||
const dex = await DEX({ rpcUrl: 'http://localhost:8080/rpc' })
|
||||
|
||||
// Place order
|
||||
await client.placeOrder({ symbol: 'BTC-USD', side: 'buy', type: 'limit', price: 50000, size: 0.1 });
|
||||
await dex.limitBuy('BTC-USD', '0.1', '50000')
|
||||
|
||||
// Cancel order
|
||||
await client.cancelOrder(orderId);
|
||||
await dex.cancelOrder(orderId)
|
||||
|
||||
// Get orderbook
|
||||
const book = await client.getOrderBook('BTC-USD', 20);
|
||||
const book = await dex.getOrderBook('BTC-USD', 20)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -72,3 +72,9 @@ libc = "0.2"
|
||||
tokio-test = "0.4"
|
||||
criterion = "0.5"
|
||||
mockall = "0.12"
|
||||
rand = "0.8"
|
||||
rust_decimal_macros = "1.33"
|
||||
|
||||
[[example]]
|
||||
name = "arbitrage_bot"
|
||||
path = "examples/arbitrage_bot.rs"
|
||||
|
||||
@@ -22,14 +22,9 @@ use rust_decimal_macros::dec;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::time::interval;
|
||||
|
||||
use lx_trading_core::{
|
||||
arbitrage::{
|
||||
CrossChainConfig, CrossChainInfo, CrossChainRouter, CrossChainTransport,
|
||||
ChainType, LxFirstArbitrage, LxFirstConfig, LxFirstOpportunity,
|
||||
LxPrice, Scanner, ScannerConfig, UnifiedArbitrage, UnifiedArbConfig,
|
||||
VenuePrice,
|
||||
},
|
||||
LxDex, LxDexConfig,
|
||||
use lx_trading::arbitrage::{
|
||||
ChainType, CrossChainConfig, CrossChainInfo, CrossChainRouter, LxFirstArbitrage,
|
||||
LxFirstConfig, LxFirstOpportunity, LxPrice, Scanner, ScannerConfig, VenuePrice,
|
||||
};
|
||||
|
||||
// ============================================
|
||||
@@ -37,9 +32,7 @@ use lx_trading_core::{
|
||||
// ============================================
|
||||
|
||||
struct ArbitrageBot {
|
||||
dex: Arc<LxDex>,
|
||||
lx_first: Arc<RwLock<LxFirstArbitrage>>,
|
||||
unified: Arc<RwLock<UnifiedArbitrage>>,
|
||||
lx_first: Arc<LxFirstArbitrage>,
|
||||
scanner: Arc<RwLock<Scanner>>,
|
||||
router: Arc<CrossChainRouter>,
|
||||
running: Arc<RwLock<bool>>,
|
||||
@@ -60,21 +53,6 @@ impl ArbitrageBot {
|
||||
println!("{}", "=".repeat(60));
|
||||
println!();
|
||||
|
||||
// Initialize DEX client
|
||||
let endpoint = std::env::var("LX_DEX_ENDPOINT")
|
||||
.unwrap_or_else(|_| "wss://dex.lux.network/ws".to_string());
|
||||
let api_key = std::env::var("LX_API_KEY").ok();
|
||||
|
||||
let dex = Arc::new(
|
||||
LxDex::new(LxDexConfig {
|
||||
endpoint,
|
||||
api_key,
|
||||
..Default::default()
|
||||
})
|
||||
.await?,
|
||||
);
|
||||
println!("[OK] Connected to LX DEX");
|
||||
|
||||
// Initialize LX-First strategy
|
||||
let lx_config = LxFirstConfig {
|
||||
max_staleness_ms: 2000,
|
||||
@@ -96,34 +74,9 @@ impl ArbitrageBot {
|
||||
.into_iter()
|
||||
.collect(),
|
||||
};
|
||||
let lx_first = Arc::new(RwLock::new(LxFirstArbitrage::new(lx_config)));
|
||||
let lx_first = Arc::new(LxFirstArbitrage::new(lx_config));
|
||||
println!("[OK] LX-First strategy initialized");
|
||||
|
||||
// Initialize Unified Arbitrage
|
||||
let unified_config = UnifiedArbConfig {
|
||||
min_spread_bps: dec!(10),
|
||||
min_profit: dec!(5),
|
||||
max_position_size: dec!(10000),
|
||||
max_total_exposure: dec!(100000),
|
||||
symbols: vec![
|
||||
"BTC-USDC".into(),
|
||||
"ETH-USDC".into(),
|
||||
"LUX-USDC".into(),
|
||||
],
|
||||
venue_priority: vec![
|
||||
"lx_dex".into(),
|
||||
"binance".into(),
|
||||
"mexc".into(),
|
||||
"lx_amm".into(),
|
||||
],
|
||||
scan_interval_ms: 100,
|
||||
execute_timeout_ms: 5000,
|
||||
max_daily_loss: dec!(1000),
|
||||
max_trades_per_day: 100,
|
||||
};
|
||||
let unified = Arc::new(RwLock::new(UnifiedArbitrage::new(unified_config)));
|
||||
println!("[OK] Unified arbitrage initialized");
|
||||
|
||||
// Initialize Scanner
|
||||
let scanner_config = ScannerConfig {
|
||||
min_spread_bps: dec!(10),
|
||||
@@ -196,9 +149,7 @@ impl ArbitrageBot {
|
||||
println!("[OK] Cross-chain router initialized");
|
||||
|
||||
Ok(Self {
|
||||
dex,
|
||||
lx_first,
|
||||
unified,
|
||||
scanner,
|
||||
router,
|
||||
running: Arc::new(RwLock::new(false)),
|
||||
@@ -207,18 +158,11 @@ impl ArbitrageBot {
|
||||
}
|
||||
|
||||
async fn start(&self) -> Result<()> {
|
||||
// Start all systems
|
||||
{
|
||||
let mut arb = self.lx_first.write().await;
|
||||
arb.start();
|
||||
}
|
||||
{
|
||||
let mut unified = self.unified.write().await;
|
||||
unified.start().await?;
|
||||
}
|
||||
// Start systems
|
||||
self.lx_first.start().await;
|
||||
{
|
||||
let mut scanner = self.scanner.write().await;
|
||||
scanner.start().await?;
|
||||
scanner.start().await;
|
||||
}
|
||||
|
||||
*self.running.write().await = true;
|
||||
@@ -240,15 +184,6 @@ impl ArbitrageBot {
|
||||
Self::simulate_price_feeds(lx_first, running).await;
|
||||
});
|
||||
|
||||
// Spawn opportunity handler
|
||||
let lx_first = self.lx_first.clone();
|
||||
let router = self.router.clone();
|
||||
let stats = self.stats.clone();
|
||||
let running = self.running.clone();
|
||||
tokio::spawn(async move {
|
||||
Self::handle_opportunities(lx_first, router, stats, running).await;
|
||||
});
|
||||
|
||||
// Spawn stats reporter
|
||||
let stats = self.stats.clone();
|
||||
let running = self.running.clone();
|
||||
@@ -263,14 +198,7 @@ impl ArbitrageBot {
|
||||
println!("\nShutting down...");
|
||||
*self.running.write().await = false;
|
||||
|
||||
{
|
||||
let mut arb = self.lx_first.write().await;
|
||||
arb.stop();
|
||||
}
|
||||
{
|
||||
let mut unified = self.unified.write().await;
|
||||
unified.stop().await;
|
||||
}
|
||||
self.lx_first.stop().await;
|
||||
{
|
||||
let mut scanner = self.scanner.write().await;
|
||||
scanner.stop().await;
|
||||
@@ -280,10 +208,10 @@ impl ArbitrageBot {
|
||||
}
|
||||
|
||||
async fn simulate_price_feeds(
|
||||
lx_first: Arc<RwLock<LxFirstArbitrage>>,
|
||||
lx_first: Arc<LxFirstArbitrage>,
|
||||
running: Arc<RwLock<bool>>,
|
||||
) {
|
||||
use rand::Rng;
|
||||
use rand::{rngs::StdRng, Rng, SeedableRng};
|
||||
|
||||
let base_prices = [
|
||||
("BTC-USDC", dec!(50000)),
|
||||
@@ -292,6 +220,7 @@ impl ArbitrageBot {
|
||||
];
|
||||
|
||||
let mut interval = interval(Duration::from_millis(100));
|
||||
let mut rng = StdRng::from_entropy();
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
@@ -300,68 +229,44 @@ impl ArbitrageBot {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
for (symbol, base) in &base_prices {
|
||||
// Simulate LX DEX price (the oracle)
|
||||
let variance: f64 = rng.gen_range(-0.001..0.001);
|
||||
let lx_mid = *base * Decimal::try_from(1.0 + variance).unwrap();
|
||||
|
||||
{
|
||||
let mut arb = lx_first.write().await;
|
||||
arb.update_lx_price(LxPrice {
|
||||
lx_first
|
||||
.update_lx_price(LxPrice {
|
||||
symbol: symbol.to_string(),
|
||||
bid: lx_mid * dec!(0.9999),
|
||||
ask: lx_mid * dec!(1.0001),
|
||||
mid: lx_mid,
|
||||
timestamp: chrono::Utc::now().timestamp_millis(),
|
||||
block_num: rng.gen_range(1000000..2000000),
|
||||
});
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
// Simulate stale CEX prices
|
||||
for (venue, latency) in [("binance", 50i64), ("mexc", 100i64)] {
|
||||
let divergence: f64 = rng.gen_range(-0.002..0.002);
|
||||
let venue_mid = *base * Decimal::try_from(1.0 + divergence).unwrap();
|
||||
|
||||
let mut arb = lx_first.write().await;
|
||||
arb.update_venue_price(VenuePrice {
|
||||
venue: venue.to_string(),
|
||||
symbol: symbol.to_string(),
|
||||
bid: venue_mid * dec!(0.9998),
|
||||
ask: venue_mid * dec!(1.0002),
|
||||
timestamp: chrono::Utc::now().timestamp_millis() - latency,
|
||||
latency: latency as i32,
|
||||
stale: false,
|
||||
});
|
||||
lx_first
|
||||
.update_venue_price(VenuePrice {
|
||||
venue: venue.to_string(),
|
||||
symbol: symbol.to_string(),
|
||||
bid: venue_mid * dec!(0.9998),
|
||||
ask: venue_mid * dec!(1.0002),
|
||||
timestamp: chrono::Utc::now().timestamp_millis() - latency,
|
||||
latency,
|
||||
stale: false,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_opportunities(
|
||||
lx_first: Arc<RwLock<LxFirstArbitrage>>,
|
||||
router: Arc<CrossChainRouter>,
|
||||
stats: Arc<RwLock<Stats>>,
|
||||
running: Arc<RwLock<bool>>,
|
||||
) {
|
||||
// In a real implementation, opportunities would be received via callbacks
|
||||
// For this example, we'll poll for demonstration
|
||||
let mut interval = interval(Duration::from_millis(100));
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
if !*running.read().await {
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for opportunities (simplified)
|
||||
// In production, use callbacks from lx_first.on_opportunity()
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_lx_first(
|
||||
async fn handle_opportunity(
|
||||
opp: &LxFirstOpportunity,
|
||||
router: &CrossChainRouter,
|
||||
stats: &Arc<RwLock<Stats>>,
|
||||
@@ -393,8 +298,8 @@ impl ArbitrageBot {
|
||||
let transport = router.determine_transport(&buy_chain, sell_chain);
|
||||
let latency = router.estimate_latency(&buy_chain, sell_chain);
|
||||
|
||||
println!(" Transport: {:?}", transport);
|
||||
println!(" Est. Latency: {}ms", latency);
|
||||
println!(" Transport: {transport:?}");
|
||||
println!(" Est. Latency: {latency}ms");
|
||||
|
||||
if opp.side == "buy" {
|
||||
println!(" Buying on {}...", opp.stale_venue);
|
||||
@@ -441,7 +346,7 @@ impl ArbitrageBot {
|
||||
println!(" Total PnL: ${:.2}", s.total_pnl);
|
||||
if s.total_executions > 0 {
|
||||
let avg_pnl = s.total_pnl / Decimal::from(s.total_executions);
|
||||
println!(" Avg PnL: ${:.2}", avg_pnl);
|
||||
println!(" Avg PnL: ${avg_pnl:.2}");
|
||||
}
|
||||
println!("{}", "-".repeat(40));
|
||||
}
|
||||
@@ -460,8 +365,8 @@ impl ArbitrageBot {
|
||||
let win_rate =
|
||||
(s.total_executions as f64 / s.total_opportunities as f64) * 100.0;
|
||||
let avg_pnl = s.total_pnl / Decimal::from(s.total_executions);
|
||||
println!("Execution Rate: {:.1}%", win_rate);
|
||||
println!("Avg PnL per Trade: ${:.2}", avg_pnl);
|
||||
println!("Execution Rate: {win_rate:.1}%");
|
||||
println!("Avg PnL per Trade: ${avg_pnl:.2}");
|
||||
}
|
||||
println!("{}", "=".repeat(50));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
//! Basic Trading Example
|
||||
//!
|
||||
//! This example demonstrates simple spot trading operations with the LX Trading SDK.
|
||||
//! It shows how to:
|
||||
//! - Configure and initialize the trading client
|
||||
//! - Connect to trading venues
|
||||
//! - Fetch market data (tickers, orderbooks)
|
||||
//! - Place and manage orders
|
||||
//!
|
||||
//! # Running
|
||||
//!
|
||||
//! ```bash
|
||||
//! cargo run --example basic_trading
|
||||
//! ```
|
||||
|
||||
use anyhow::Result;
|
||||
use rust_decimal::Decimal;
|
||||
use rust_decimal_macros::dec;
|
||||
|
||||
use lx_trading::{
|
||||
config::{NativeVenueConfig, RiskConfig},
|
||||
Config, OrderRequest, Side, UnifiedClient,
|
||||
};
|
||||
|
||||
/// Example configuration for paper trading
|
||||
fn create_config() -> Config {
|
||||
let mut config = Config::default();
|
||||
|
||||
// Configure native LX DEX venue
|
||||
config.native.insert(
|
||||
"lx_dex".to_string(),
|
||||
NativeVenueConfig::lx_dex("https://api.dex.lux.network")
|
||||
.testnet()
|
||||
.with_websocket("wss://ws.dex.lux.network"),
|
||||
);
|
||||
|
||||
// Configure risk management
|
||||
config.risk = RiskConfig {
|
||||
enabled: true,
|
||||
max_position_size: dec!(100),
|
||||
max_order_size: dec!(10),
|
||||
max_daily_loss: dec!(1000),
|
||||
max_open_orders: 50,
|
||||
kill_switch_enabled: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// General settings
|
||||
config.general.log_level = "info".to_string();
|
||||
config.general.smart_routing = true;
|
||||
config.general.timeout_ms = 30000;
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
/// Print orderbook summary
|
||||
fn print_orderbook_summary(symbol: &str, book: &lx_trading::orderbook::Orderbook) {
|
||||
println!("\n=== Orderbook: {} ===", symbol);
|
||||
println!("Best Bid: {:?}", book.best_bid());
|
||||
println!("Best Ask: {:?}", book.best_ask());
|
||||
println!("Spread: {:?}", book.spread());
|
||||
println!("Mid Price: {:?}", book.mid_price());
|
||||
println!("Bid Levels: {}", book.bids.len());
|
||||
println!("Ask Levels: {}", book.asks.len());
|
||||
}
|
||||
|
||||
/// Print ticker information
|
||||
fn print_ticker(ticker: &lx_trading::Ticker) {
|
||||
println!("\n=== Ticker: {} ===", ticker.symbol);
|
||||
println!("Bid: {:?}", ticker.bid);
|
||||
println!("Ask: {:?}", ticker.ask);
|
||||
println!("Last: {:?}", ticker.last);
|
||||
println!("24h Volume: {:?}", ticker.volume_24h);
|
||||
println!("24h Change: {:?}%", ticker.change_24h);
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Initialize logging
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
println!("=== LX Trading SDK - Basic Trading Example ===\n");
|
||||
|
||||
// Create configuration
|
||||
let config = create_config();
|
||||
println!("[1] Configuration created");
|
||||
|
||||
// Initialize client
|
||||
let client = UnifiedClient::new(config)?;
|
||||
println!("[2] UnifiedClient created");
|
||||
|
||||
// In a real scenario, we would initialize venues
|
||||
// For this example, we'll demonstrate the API without actual connections
|
||||
println!("[3] Venues would be initialized with: client.init().await?");
|
||||
|
||||
// Example: Market data operations (would work with connected venue)
|
||||
println!("\n--- Market Data Operations ---");
|
||||
|
||||
// Simulated ticker data for demonstration
|
||||
let ticker = lx_trading::Ticker {
|
||||
symbol: "BTC-USDC".to_string(),
|
||||
venue: "lx_dex".to_string(),
|
||||
bid: Some(dec!(50000)),
|
||||
ask: Some(dec!(50010)),
|
||||
last: Some(dec!(50005)),
|
||||
volume_24h: Some(dec!(1000)),
|
||||
high_24h: Some(dec!(51000)),
|
||||
low_24h: Some(dec!(49000)),
|
||||
change_24h: Some(dec!(2.5)),
|
||||
timestamp: chrono::Utc::now().timestamp_millis(),
|
||||
};
|
||||
print_ticker(&ticker);
|
||||
|
||||
// Simulated orderbook
|
||||
let mut book = lx_trading::orderbook::Orderbook::new("BTC-USDC", "lx_dex");
|
||||
book.add_bid(dec!(50000), dec!(1.5));
|
||||
book.add_bid(dec!(49990), dec!(2.0));
|
||||
book.add_bid(dec!(49980), dec!(3.0));
|
||||
book.add_ask(dec!(50010), dec!(1.0));
|
||||
book.add_ask(dec!(50020), dec!(2.5));
|
||||
book.add_ask(dec!(50030), dec!(4.0));
|
||||
book.sort();
|
||||
print_orderbook_summary("BTC-USDC", &book);
|
||||
|
||||
// Example: Order creation
|
||||
println!("\n--- Order Operations ---");
|
||||
|
||||
// Create a market buy order
|
||||
let market_order = OrderRequest::market("BTC-USDC", Side::Buy, dec!(0.1));
|
||||
println!("\nMarket Order: {:?}", market_order);
|
||||
|
||||
// Create a limit buy order
|
||||
let limit_order = OrderRequest::limit("BTC-USDC", Side::Buy, dec!(0.5), dec!(49500))
|
||||
.with_venue("lx_dex")
|
||||
.with_client_id("my-order-001");
|
||||
println!("\nLimit Order: {:?}", limit_order);
|
||||
|
||||
// Create a post-only limit order (maker only)
|
||||
let maker_order = OrderRequest::limit("ETH-USDC", Side::Sell, dec!(10.0), dec!(3100))
|
||||
.with_venue("lx_dex")
|
||||
.with_client_id("maker-001")
|
||||
.post_only();
|
||||
println!("\nPost-Only Order: {:?}", maker_order);
|
||||
|
||||
// Example: Calculate execution cost using orderbook
|
||||
println!("\n--- Execution Analysis ---");
|
||||
let buy_quantity = dec!(2.5);
|
||||
if let Some(vwap) = book.vwap_buy(buy_quantity) {
|
||||
println!(
|
||||
"VWAP to buy {} BTC: ${}",
|
||||
buy_quantity,
|
||||
vwap.round_dp(2)
|
||||
);
|
||||
if let Some(best_ask) = book.best_ask() {
|
||||
let slippage = ((vwap - best_ask) / best_ask) * dec!(100);
|
||||
println!("Expected slippage: {}%", slippage.round_dp(4));
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Use math functions
|
||||
println!("\n--- Financial Math ---");
|
||||
let entry = dec!(50000);
|
||||
let current = dec!(52000);
|
||||
let quantity = dec!(1.0);
|
||||
let pnl = lx_trading::position_pnl(entry, current, quantity, Side::Buy);
|
||||
let pnl_pct = lx_trading::position_pnl_percent(entry, current, Side::Buy);
|
||||
println!("Position P&L: ${} ({:.2}%)", pnl, pnl_pct);
|
||||
|
||||
// Kelly criterion for position sizing
|
||||
let kelly = lx_trading::kelly_criterion(dec!(0.55), dec!(2.0), dec!(1.0));
|
||||
println!("Kelly Criterion: {:.2}% of capital", kelly * dec!(100));
|
||||
|
||||
println!("\n=== Example Complete ===");
|
||||
println!("\nIn production:");
|
||||
println!("1. Use real API credentials");
|
||||
println!("2. Connect to venues: client.init().await?");
|
||||
println!("3. Place orders: client.place_order(order).await?");
|
||||
println!("4. Monitor fills via WebSocket streams");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
//! Concurrent Execution Example
|
||||
//!
|
||||
//! This example demonstrates async patterns for high-performance trading:
|
||||
//! - Concurrent order submission to multiple venues
|
||||
//! - Parallel market data fetching
|
||||
//! - Task coordination with tokio
|
||||
//! - Error handling in concurrent contexts
|
||||
//!
|
||||
//! # Running
|
||||
//!
|
||||
//! ```bash
|
||||
//! cargo run --example concurrent_execution
|
||||
//! ```
|
||||
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::Result;
|
||||
use futures::future::join_all;
|
||||
use parking_lot::RwLock;
|
||||
use rust_decimal::Decimal;
|
||||
use rust_decimal_macros::dec;
|
||||
use tokio::sync::{mpsc, Semaphore};
|
||||
use tokio::time::timeout;
|
||||
|
||||
use lx_trading::{OrderRequest, OrderStatus, Side};
|
||||
|
||||
/// Simulated order result
|
||||
#[derive(Debug, Clone)]
|
||||
struct OrderResult {
|
||||
venue: String,
|
||||
order_id: String,
|
||||
symbol: String,
|
||||
side: Side,
|
||||
quantity: Decimal,
|
||||
price: Decimal,
|
||||
status: OrderStatus,
|
||||
latency_ms: u64,
|
||||
}
|
||||
|
||||
/// Execution statistics
|
||||
struct ExecutionStats {
|
||||
orders_submitted: AtomicU64,
|
||||
orders_filled: AtomicU64,
|
||||
orders_rejected: AtomicU64,
|
||||
total_latency_us: AtomicU64,
|
||||
}
|
||||
|
||||
impl ExecutionStats {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
orders_submitted: AtomicU64::new(0),
|
||||
orders_filled: AtomicU64::new(0),
|
||||
orders_rejected: AtomicU64::new(0),
|
||||
total_latency_us: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn record_order(&self, result: &OrderResult) {
|
||||
self.orders_submitted.fetch_add(1, Ordering::Relaxed);
|
||||
self.total_latency_us
|
||||
.fetch_add(result.latency_ms * 1000, Ordering::Relaxed);
|
||||
|
||||
match result.status {
|
||||
OrderStatus::Filled | OrderStatus::PartiallyFilled => {
|
||||
self.orders_filled.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
OrderStatus::Rejected => {
|
||||
self.orders_rejected.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_summary(&self) {
|
||||
let submitted = self.orders_submitted.load(Ordering::Relaxed);
|
||||
let filled = self.orders_filled.load(Ordering::Relaxed);
|
||||
let rejected = self.orders_rejected.load(Ordering::Relaxed);
|
||||
let total_latency = self.total_latency_us.load(Ordering::Relaxed);
|
||||
|
||||
let avg_latency = if submitted > 0 {
|
||||
total_latency / submitted / 1000 // Convert to ms
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let fill_rate = if submitted > 0 {
|
||||
(filled as f64 / submitted as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
println!("\n=== Execution Statistics ===");
|
||||
println!("Orders Submitted: {}", submitted);
|
||||
println!("Orders Filled: {}", filled);
|
||||
println!("Orders Rejected: {}", rejected);
|
||||
println!("Fill Rate: {:.1}%", fill_rate);
|
||||
println!("Average Latency: {}ms", avg_latency);
|
||||
}
|
||||
}
|
||||
|
||||
/// Simulate order execution with random latency and outcome
|
||||
async fn execute_order(
|
||||
venue: &str,
|
||||
request: OrderRequest,
|
||||
stats: Arc<ExecutionStats>,
|
||||
) -> Result<OrderResult> {
|
||||
use rand::Rng;
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
// Generate random values before await
|
||||
let (latency_ms, outcome_filled, outcome_rejected): (u64, bool, bool) = {
|
||||
let mut rng = rand::thread_rng();
|
||||
(
|
||||
rng.gen_range(10..50),
|
||||
rng.gen_bool(0.85),
|
||||
rng.gen_bool(0.5),
|
||||
)
|
||||
};
|
||||
|
||||
// Simulate network latency (10-50ms)
|
||||
tokio::time::sleep(Duration::from_millis(latency_ms)).await;
|
||||
|
||||
// Generate order ID
|
||||
let order_id = {
|
||||
let mut rng = rand::thread_rng();
|
||||
format!("{}-{}", venue, rng.gen::<u32>())
|
||||
};
|
||||
|
||||
// Use pre-generated random outcomes
|
||||
let status = if outcome_filled {
|
||||
OrderStatus::Filled
|
||||
} else if outcome_rejected {
|
||||
OrderStatus::Rejected
|
||||
} else {
|
||||
OrderStatus::PartiallyFilled
|
||||
};
|
||||
|
||||
let result = OrderResult {
|
||||
venue: venue.to_string(),
|
||||
order_id,
|
||||
symbol: request.symbol.clone(),
|
||||
side: request.side.clone(),
|
||||
quantity: request.quantity,
|
||||
price: request.price.unwrap_or(dec!(50000)),
|
||||
status,
|
||||
latency_ms: start.elapsed().as_millis() as u64,
|
||||
};
|
||||
|
||||
stats.record_order(&result);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Submit orders to multiple venues concurrently
|
||||
async fn submit_to_venues(
|
||||
venues: &[&str],
|
||||
request: OrderRequest,
|
||||
stats: Arc<ExecutionStats>,
|
||||
) -> Vec<Result<OrderResult>> {
|
||||
let futures: Vec<_> = venues
|
||||
.iter()
|
||||
.map(|venue| {
|
||||
let req = request.clone();
|
||||
let stats = Arc::clone(&stats);
|
||||
async move { execute_order(venue, req, stats).await }
|
||||
})
|
||||
.collect();
|
||||
|
||||
join_all(futures).await
|
||||
}
|
||||
|
||||
/// Fetch market data from multiple venues concurrently
|
||||
async fn fetch_market_data(venues: &[&str], symbol: &str) -> Vec<(String, Decimal)> {
|
||||
use rand::Rng;
|
||||
|
||||
let futures: Vec<_> = venues
|
||||
.iter()
|
||||
.map(|venue| async move {
|
||||
// Simulate fetch latency
|
||||
let mut rng = rand::thread_rng();
|
||||
let latency: u64 = rng.gen_range(5..30);
|
||||
tokio::time::sleep(Duration::from_millis(latency)).await;
|
||||
|
||||
// Simulate price with small venue-specific offset
|
||||
let base = dec!(50000);
|
||||
let offset: f64 = rng.gen_range(-20.0..20.0);
|
||||
let price = base + Decimal::try_from(offset).unwrap_or_default();
|
||||
|
||||
(venue.to_string(), price)
|
||||
})
|
||||
.collect();
|
||||
|
||||
join_all(futures).await
|
||||
}
|
||||
|
||||
/// Rate-limited order submission using semaphore
|
||||
async fn rate_limited_orders(
|
||||
orders: Vec<OrderRequest>,
|
||||
venue: &str,
|
||||
max_concurrent: usize,
|
||||
stats: Arc<ExecutionStats>,
|
||||
) -> Vec<Result<OrderResult>> {
|
||||
let semaphore = Arc::new(Semaphore::new(max_concurrent));
|
||||
|
||||
let futures: Vec<_> = orders
|
||||
.into_iter()
|
||||
.map(|order| {
|
||||
let permit = Arc::clone(&semaphore);
|
||||
let stats = Arc::clone(&stats);
|
||||
let venue = venue.to_string();
|
||||
async move {
|
||||
let _permit = permit.acquire().await.unwrap();
|
||||
execute_order(&venue, order, stats).await
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
join_all(futures).await
|
||||
}
|
||||
|
||||
/// Process stream of orders with bounded concurrency
|
||||
async fn process_order_stream(
|
||||
mut rx: mpsc::Receiver<OrderRequest>,
|
||||
venue: String,
|
||||
stats: Arc<ExecutionStats>,
|
||||
max_concurrent: usize,
|
||||
) {
|
||||
let semaphore = Arc::new(Semaphore::new(max_concurrent));
|
||||
let mut handles = vec![];
|
||||
|
||||
while let Some(order) = rx.recv().await {
|
||||
let permit = Arc::clone(&semaphore);
|
||||
let stats = Arc::clone(&stats);
|
||||
let venue = venue.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let _permit = permit.acquire().await.unwrap();
|
||||
let result = execute_order(&venue, order, stats).await;
|
||||
if let Ok(r) = &result {
|
||||
println!(
|
||||
" [{}] {} {} @ {} -> {:?} ({}ms)",
|
||||
r.venue,
|
||||
if r.side == Side::Buy { "BUY" } else { "SELL" },
|
||||
r.quantity,
|
||||
r.price,
|
||||
r.status,
|
||||
r.latency_ms
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for all in-flight orders
|
||||
for handle in handles {
|
||||
let _ = handle.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
println!("=== LX Trading SDK - Concurrent Execution Example ===\n");
|
||||
|
||||
let stats = Arc::new(ExecutionStats::new());
|
||||
let venues = vec!["lx_dex", "binance", "mexc"];
|
||||
|
||||
// Example 1: Parallel market data fetching
|
||||
println!("--- Example 1: Parallel Market Data Fetch ---");
|
||||
println!("Fetching BTC-USDC prices from {} venues...", venues.len());
|
||||
|
||||
let start = Instant::now();
|
||||
let prices = fetch_market_data(&venues, "BTC-USDC").await;
|
||||
let fetch_time = start.elapsed();
|
||||
|
||||
for (venue, price) in &prices {
|
||||
println!(" {}: ${}", venue, price);
|
||||
}
|
||||
println!("Total fetch time: {:?} (parallel)", fetch_time);
|
||||
|
||||
// Find best price
|
||||
if let Some((best_venue, best_price)) = prices.iter().min_by_key(|(_, p)| *p) {
|
||||
println!("Best ask: {} at ${}", best_venue, best_price);
|
||||
}
|
||||
|
||||
// Example 2: Concurrent order submission
|
||||
println!("\n--- Example 2: Concurrent Order Submission ---");
|
||||
let order = OrderRequest::limit("BTC-USDC", Side::Buy, dec!(0.1), dec!(50000));
|
||||
|
||||
println!("Submitting order to {} venues concurrently...", venues.len());
|
||||
let start = Instant::now();
|
||||
let results = submit_to_venues(&venues, order, Arc::clone(&stats)).await;
|
||||
let submit_time = start.elapsed();
|
||||
|
||||
for result in results {
|
||||
match result {
|
||||
Ok(r) => {
|
||||
println!(
|
||||
" [{}] Order {} -> {:?} ({}ms)",
|
||||
r.venue, r.order_id, r.status, r.latency_ms
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" [ERROR] {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("Total submit time: {:?} (parallel)", submit_time);
|
||||
|
||||
// Example 3: Rate-limited batch orders
|
||||
println!("\n--- Example 3: Rate-Limited Batch Orders ---");
|
||||
|
||||
let batch_orders: Vec<_> = (0..20)
|
||||
.map(|i| {
|
||||
let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
|
||||
let price = dec!(50000) + Decimal::from(i as i64 * 10);
|
||||
OrderRequest::limit("BTC-USDC", side, dec!(0.05), price)
|
||||
})
|
||||
.collect();
|
||||
|
||||
println!(
|
||||
"Submitting {} orders with max 5 concurrent...",
|
||||
batch_orders.len()
|
||||
);
|
||||
let start = Instant::now();
|
||||
let results = rate_limited_orders(batch_orders, "lx_dex", 5, Arc::clone(&stats)).await;
|
||||
let batch_time = start.elapsed();
|
||||
|
||||
let successful = results.iter().filter(|r| r.is_ok()).count();
|
||||
println!("Completed: {}/{} in {:?}", successful, results.len(), batch_time);
|
||||
|
||||
// Example 4: Stream-based order processing
|
||||
println!("\n--- Example 4: Stream-Based Order Processing ---");
|
||||
|
||||
let (tx, rx) = mpsc::channel(100);
|
||||
let stats_clone = Arc::clone(&stats);
|
||||
|
||||
// Spawn order processor
|
||||
let processor = tokio::spawn(async move {
|
||||
process_order_stream(rx, "lx_dex".to_string(), stats_clone, 3).await;
|
||||
});
|
||||
|
||||
// Send orders through stream
|
||||
println!("Sending 10 orders through stream processor...");
|
||||
for i in 0..10 {
|
||||
let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
|
||||
let order = OrderRequest::market("ETH-USDC", side, dec!(0.5));
|
||||
tx.send(order).await?;
|
||||
}
|
||||
|
||||
// Close channel and wait for completion
|
||||
drop(tx);
|
||||
let _ = timeout(Duration::from_secs(5), processor).await;
|
||||
|
||||
// Example 5: Timeout handling
|
||||
println!("\n--- Example 5: Timeout Handling ---");
|
||||
|
||||
let slow_order = async {
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
OrderResult {
|
||||
venue: "slow_venue".to_string(),
|
||||
order_id: "timeout-test".to_string(),
|
||||
symbol: "BTC-USDC".to_string(),
|
||||
side: Side::Buy,
|
||||
quantity: dec!(1.0),
|
||||
price: dec!(50000),
|
||||
status: OrderStatus::Filled,
|
||||
latency_ms: 10000,
|
||||
}
|
||||
};
|
||||
|
||||
match timeout(Duration::from_millis(100), slow_order).await {
|
||||
Ok(result) => {
|
||||
println!("Order completed: {:?}", result.status);
|
||||
}
|
||||
Err(_) => {
|
||||
println!("Order timed out after 100ms (expected)");
|
||||
}
|
||||
}
|
||||
|
||||
// Print final statistics
|
||||
stats.print_summary();
|
||||
|
||||
println!("\n=== Example Complete ===");
|
||||
println!("\nKey patterns demonstrated:");
|
||||
println!("1. join_all() for parallel execution");
|
||||
println!("2. Semaphore for rate limiting");
|
||||
println!("3. mpsc channels for stream processing");
|
||||
println!("4. timeout() for deadline enforcement");
|
||||
println!("5. Arc for shared state across tasks");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
//! Market Making Strategy Example
|
||||
//!
|
||||
//! This example demonstrates a simple market making strategy that:
|
||||
//! - Maintains quotes on both sides of the orderbook
|
||||
//! - Adjusts spread based on inventory risk
|
||||
//! - Uses risk management to prevent excessive exposure
|
||||
//!
|
||||
//! # Strategy Overview
|
||||
//!
|
||||
//! Market makers profit from the bid-ask spread while managing inventory risk.
|
||||
//! This example shows:
|
||||
//! - Quote calculation with inventory skew
|
||||
//! - Position-based spread adjustment
|
||||
//! - Risk limits and monitoring
|
||||
//!
|
||||
//! # Running
|
||||
//!
|
||||
//! ```bash
|
||||
//! cargo run --example market_maker
|
||||
//! ```
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use parking_lot::RwLock;
|
||||
use rust_decimal::Decimal;
|
||||
use rust_decimal_macros::dec;
|
||||
use tokio::time::interval;
|
||||
|
||||
use lx_trading::{
|
||||
math::{mid_price, spread_bps},
|
||||
orderbook::Orderbook,
|
||||
Order, OrderRequest, Side, Ticker,
|
||||
};
|
||||
|
||||
/// Market maker configuration
|
||||
#[derive(Debug, Clone)]
|
||||
struct MakerConfig {
|
||||
/// Trading symbol
|
||||
symbol: String,
|
||||
/// Target spread in basis points
|
||||
target_spread_bps: Decimal,
|
||||
/// Maximum spread in basis points
|
||||
max_spread_bps: Decimal,
|
||||
/// Order size for each quote
|
||||
order_size: Decimal,
|
||||
/// Maximum position (long or short)
|
||||
max_position: Decimal,
|
||||
/// Inventory skew factor (how much to widen spread per unit of inventory)
|
||||
inventory_skew: Decimal,
|
||||
/// Quote refresh interval
|
||||
refresh_interval: Duration,
|
||||
}
|
||||
|
||||
impl Default for MakerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
symbol: "BTC-USDC".to_string(),
|
||||
target_spread_bps: dec!(10), // 10 bps = 0.1%
|
||||
max_spread_bps: dec!(50), // 50 bps = 0.5%
|
||||
order_size: dec!(0.1), // 0.1 BTC per side
|
||||
max_position: dec!(1.0), // Max 1 BTC long or short
|
||||
inventory_skew: dec!(5), // 5 bps per 0.1 BTC inventory
|
||||
refresh_interval: Duration::from_millis(500),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Market maker state
|
||||
struct MakerState {
|
||||
/// Current position (positive = long, negative = short)
|
||||
position: Decimal,
|
||||
/// Active bid order
|
||||
bid_order: Option<Order>,
|
||||
/// Active ask order
|
||||
ask_order: Option<Order>,
|
||||
/// Total realized PnL
|
||||
realized_pnl: Decimal,
|
||||
/// Number of trades executed
|
||||
trade_count: u64,
|
||||
/// Total volume traded
|
||||
volume: Decimal,
|
||||
}
|
||||
|
||||
impl Default for MakerState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
position: Decimal::ZERO,
|
||||
bid_order: None,
|
||||
ask_order: None,
|
||||
realized_pnl: Decimal::ZERO,
|
||||
trade_count: 0,
|
||||
volume: Decimal::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate quotes based on mid price and inventory
|
||||
fn calculate_quotes(
|
||||
config: &MakerConfig,
|
||||
state: &MakerState,
|
||||
reference_price: Decimal,
|
||||
) -> (Decimal, Decimal) {
|
||||
// Base spread
|
||||
let base_spread = reference_price * config.target_spread_bps / dec!(10000);
|
||||
|
||||
// Inventory adjustment: widen spread on the side where we have exposure
|
||||
let inventory_adjustment = state.position.abs() * config.inventory_skew / dec!(10000);
|
||||
let skew = state.position * config.inventory_skew / dec!(10000) * reference_price;
|
||||
|
||||
// Calculate bid and ask with inventory skew
|
||||
// If long (position > 0): lower bid more, raise ask less (encourage sells)
|
||||
// If short (position < 0): lower bid less, raise ask more (encourage buys)
|
||||
let half_spread = base_spread / dec!(2) + inventory_adjustment * reference_price;
|
||||
|
||||
let bid_price = reference_price - half_spread - skew;
|
||||
let ask_price = reference_price + half_spread - skew;
|
||||
|
||||
// Enforce maximum spread
|
||||
let max_half_spread = reference_price * config.max_spread_bps / dec!(10000) / dec!(2);
|
||||
let clamped_bid = bid_price.max(reference_price - max_half_spread);
|
||||
let clamped_ask = ask_price.min(reference_price + max_half_spread);
|
||||
|
||||
(clamped_bid.round_dp(2), clamped_ask.round_dp(2))
|
||||
}
|
||||
|
||||
/// Check if position is within limits
|
||||
fn check_position_limits(config: &MakerConfig, state: &MakerState, side: Side) -> bool {
|
||||
match side {
|
||||
Side::Buy => state.position < config.max_position,
|
||||
Side::Sell => state.position > -config.max_position,
|
||||
}
|
||||
}
|
||||
|
||||
/// Print market maker status
|
||||
fn print_status(config: &MakerConfig, state: &MakerState, bid: Decimal, ask: Decimal) {
|
||||
let spread_bps = (ask - bid) / ((ask + bid) / dec!(2)) * dec!(10000);
|
||||
|
||||
println!("\n--- Market Maker Status ---");
|
||||
println!("Symbol: {}", config.symbol);
|
||||
println!("Position: {} BTC", state.position);
|
||||
println!("Bid: ${} | Ask: ${}", bid, ask);
|
||||
println!("Spread: {:.1} bps", spread_bps);
|
||||
println!("Trades: {} | Volume: {} BTC", state.trade_count, state.volume);
|
||||
println!("Realized PnL: ${:.2}", state.realized_pnl);
|
||||
println!(
|
||||
"Position Limit: {:.1}%",
|
||||
(state.position.abs() / config.max_position) * dec!(100)
|
||||
);
|
||||
}
|
||||
|
||||
/// Simulate a fill
|
||||
fn simulate_fill(state: &mut MakerState, side: Side, price: Decimal, quantity: Decimal) {
|
||||
match side {
|
||||
Side::Buy => {
|
||||
state.position += quantity;
|
||||
}
|
||||
Side::Sell => {
|
||||
state.position -= quantity;
|
||||
}
|
||||
}
|
||||
state.trade_count += 1;
|
||||
state.volume += quantity;
|
||||
|
||||
// Simple PnL estimation (in reality, need to track entry prices)
|
||||
println!(
|
||||
" [FILL] {} {:.4} BTC @ ${}",
|
||||
if side == Side::Buy { "BUY" } else { "SELL" },
|
||||
quantity,
|
||||
price
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
println!("=== LX Trading SDK - Market Maker Example ===\n");
|
||||
|
||||
let config = MakerConfig::default();
|
||||
let state = Arc::new(RwLock::new(MakerState::default()));
|
||||
|
||||
println!("Configuration:");
|
||||
println!(" Symbol: {}", config.symbol);
|
||||
println!(" Target Spread: {} bps", config.target_spread_bps);
|
||||
println!(" Order Size: {} BTC", config.order_size);
|
||||
println!(" Max Position: {} BTC", config.max_position);
|
||||
println!(" Refresh Interval: {:?}", config.refresh_interval);
|
||||
println!();
|
||||
|
||||
// Simulate market data feed with some price variation
|
||||
let base_price = dec!(50000);
|
||||
let mut price_offset = dec!(0);
|
||||
let mut iteration = 0;
|
||||
|
||||
// Run for a limited number of iterations for the example
|
||||
let max_iterations = 10;
|
||||
let mut ticker_interval = interval(config.refresh_interval);
|
||||
|
||||
println!("Starting market making simulation...\n");
|
||||
|
||||
while iteration < max_iterations {
|
||||
ticker_interval.tick().await;
|
||||
iteration += 1;
|
||||
|
||||
// Simulate price movement
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
let change: f64 = rng.gen_range(-0.001..0.001);
|
||||
price_offset += Decimal::try_from(change * 50000.0).unwrap_or_default();
|
||||
let current_price = base_price + price_offset;
|
||||
|
||||
println!("\n[Iteration {}] Reference Price: ${:.2}", iteration, current_price);
|
||||
|
||||
// Calculate quotes
|
||||
let (bid_price, ask_price) = {
|
||||
let s = state.read();
|
||||
calculate_quotes(&config, &s, current_price)
|
||||
};
|
||||
|
||||
// Create orders (in reality, would send to exchange)
|
||||
let mut s = state.write();
|
||||
|
||||
// Check if we can quote on each side
|
||||
let can_bid = check_position_limits(&config, &s, Side::Buy);
|
||||
let can_ask = check_position_limits(&config, &s, Side::Sell);
|
||||
|
||||
if can_bid {
|
||||
let bid_order = OrderRequest::limit(
|
||||
&config.symbol,
|
||||
Side::Buy,
|
||||
config.order_size,
|
||||
bid_price,
|
||||
)
|
||||
.post_only();
|
||||
println!(" BID: {} @ ${}", config.order_size, bid_price);
|
||||
|
||||
// Simulate random fill (30% chance)
|
||||
if rng.gen_bool(0.3) {
|
||||
simulate_fill(&mut s, Side::Buy, bid_price, config.order_size);
|
||||
}
|
||||
} else {
|
||||
println!(" BID: SKIPPED (position limit)");
|
||||
}
|
||||
|
||||
if can_ask {
|
||||
let ask_order = OrderRequest::limit(
|
||||
&config.symbol,
|
||||
Side::Sell,
|
||||
config.order_size,
|
||||
ask_price,
|
||||
)
|
||||
.post_only();
|
||||
println!(" ASK: {} @ ${}", config.order_size, ask_price);
|
||||
|
||||
// Simulate random fill (30% chance)
|
||||
if rng.gen_bool(0.3) {
|
||||
simulate_fill(&mut s, Side::Sell, ask_price, config.order_size);
|
||||
}
|
||||
} else {
|
||||
println!(" ASK: SKIPPED (position limit)");
|
||||
}
|
||||
|
||||
// Print status every 5 iterations
|
||||
if iteration % 5 == 0 {
|
||||
print_status(&config, &s, bid_price, ask_price);
|
||||
}
|
||||
}
|
||||
|
||||
// Final status
|
||||
{
|
||||
let s = state.read();
|
||||
println!("\n=== Final Summary ===");
|
||||
println!("Total Trades: {}", s.trade_count);
|
||||
println!("Total Volume: {} BTC", s.volume);
|
||||
println!("Final Position: {} BTC", s.position);
|
||||
println!("Realized PnL: ${:.2}", s.realized_pnl);
|
||||
}
|
||||
|
||||
println!("\n=== Example Complete ===");
|
||||
println!("\nKey concepts demonstrated:");
|
||||
println!("1. Bid-ask spread calculation");
|
||||
println!("2. Inventory-based quote adjustment");
|
||||
println!("3. Position limit checks");
|
||||
println!("4. Post-only orders for maker rebates");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
//! WebSocket Client Example
|
||||
//!
|
||||
//! This example demonstrates real-time market data streaming:
|
||||
//! - Connecting to WebSocket feeds
|
||||
//! - Subscribing to orderbook, trade, and ticker channels
|
||||
//! - Processing streaming events
|
||||
//! - Managing connection lifecycle
|
||||
//!
|
||||
//! # Running
|
||||
//!
|
||||
//! ```bash
|
||||
//! cargo run --example websocket_client
|
||||
//! ```
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use rust_decimal::Decimal;
|
||||
use rust_decimal_macros::dec;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::{interval, timeout};
|
||||
|
||||
use lx_trading::{
|
||||
ws::{Fill, OrderUpdate, OrderbookUpdate, WsConfig, WsEvent},
|
||||
PriceLevel, Side, Ticker, Trade,
|
||||
};
|
||||
|
||||
/// Event statistics
|
||||
struct EventStats {
|
||||
orderbook_updates: AtomicU64,
|
||||
trades: AtomicU64,
|
||||
tickers: AtomicU64,
|
||||
order_updates: AtomicU64,
|
||||
fills: AtomicU64,
|
||||
errors: AtomicU64,
|
||||
}
|
||||
|
||||
impl EventStats {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
orderbook_updates: AtomicU64::new(0),
|
||||
trades: AtomicU64::new(0),
|
||||
tickers: AtomicU64::new(0),
|
||||
order_updates: AtomicU64::new(0),
|
||||
fills: AtomicU64::new(0),
|
||||
errors: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_summary(&self) {
|
||||
println!("\n=== Event Statistics ===");
|
||||
println!(
|
||||
"Orderbook Updates: {}",
|
||||
self.orderbook_updates.load(Ordering::Relaxed)
|
||||
);
|
||||
println!("Trades: {}", self.trades.load(Ordering::Relaxed));
|
||||
println!("Tickers: {}", self.tickers.load(Ordering::Relaxed));
|
||||
println!("Order Updates: {}", self.order_updates.load(Ordering::Relaxed));
|
||||
println!("Fills: {}", self.fills.load(Ordering::Relaxed));
|
||||
println!("Errors: {}", self.errors.load(Ordering::Relaxed));
|
||||
}
|
||||
}
|
||||
|
||||
/// Process an orderbook update
|
||||
fn handle_orderbook_update(update: &OrderbookUpdate, stats: &EventStats) {
|
||||
stats.orderbook_updates.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
let update_type = if update.is_snapshot { "SNAPSHOT" } else { "DELTA" };
|
||||
println!(
|
||||
"[ORDERBOOK] {} {} | Bids: {} | Asks: {}",
|
||||
update.symbol,
|
||||
update_type,
|
||||
update.bids.len(),
|
||||
update.asks.len()
|
||||
);
|
||||
|
||||
// Show best levels
|
||||
if let (Some(best_bid), Some(best_ask)) = (update.bids.first(), update.asks.first()) {
|
||||
println!(
|
||||
" Best: {} @ {} | {} @ {}",
|
||||
best_bid.quantity, best_bid.price, best_ask.quantity, best_ask.price
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Process a trade
|
||||
fn handle_trade(trade: &Trade, stats: &EventStats) {
|
||||
stats.trades.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
let side_str = match trade.side {
|
||||
Side::Buy => "BUY ",
|
||||
Side::Sell => "SELL",
|
||||
};
|
||||
|
||||
println!(
|
||||
"[TRADE] {} {} {} @ {} | Fee: {} {}",
|
||||
trade.symbol, side_str, trade.quantity, trade.price, trade.fee.amount, trade.fee.asset
|
||||
);
|
||||
}
|
||||
|
||||
/// Process a ticker update
|
||||
fn handle_ticker(ticker: &Ticker, stats: &EventStats) {
|
||||
stats.tickers.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
let spread = match (ticker.bid, ticker.ask) {
|
||||
(Some(b), Some(a)) => Some(a - b),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
println!(
|
||||
"[TICKER] {} | Bid: {:?} | Ask: {:?} | Spread: {:?}",
|
||||
ticker.symbol, ticker.bid, ticker.ask, spread
|
||||
);
|
||||
}
|
||||
|
||||
/// Process an order update
|
||||
fn handle_order_update(update: &OrderUpdate, stats: &EventStats) {
|
||||
stats.order_updates.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
println!(
|
||||
"[ORDER] {} | Status: {:?} | Filled: {} | ID: {}",
|
||||
update.symbol, update.status, update.filled_quantity, update.order_id
|
||||
);
|
||||
}
|
||||
|
||||
/// Process a fill notification
|
||||
fn handle_fill(fill: &Fill, stats: &EventStats) {
|
||||
stats.fills.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
let side_str = match fill.side {
|
||||
Side::Buy => "BUY ",
|
||||
Side::Sell => "SELL",
|
||||
};
|
||||
|
||||
println!(
|
||||
"[FILL] {} {} {} @ {} | Order: {}",
|
||||
fill.symbol, side_str, fill.quantity, fill.price, fill.order_id
|
||||
);
|
||||
}
|
||||
|
||||
/// Simulate WebSocket events for demonstration
|
||||
async fn simulate_events(tx: mpsc::Sender<WsEvent>, running: Arc<AtomicBool>) {
|
||||
use rand::{rngs::StdRng, Rng, SeedableRng};
|
||||
|
||||
let mut interval = interval(Duration::from_millis(200));
|
||||
let mut rng = StdRng::from_entropy();
|
||||
let base_price = dec!(50000);
|
||||
let mut price_offset = dec!(0);
|
||||
|
||||
while running.load(Ordering::Relaxed) {
|
||||
interval.tick().await;
|
||||
|
||||
// Simulate price movement
|
||||
let change: f64 = rng.gen_range(-0.0005..0.0005);
|
||||
price_offset += Decimal::try_from(change * 50000.0).unwrap_or_default();
|
||||
let current_price = base_price + price_offset;
|
||||
|
||||
// Generate random event type
|
||||
let event_type: u8 = rng.gen_range(0..5);
|
||||
|
||||
let event = match event_type {
|
||||
0 => {
|
||||
// Orderbook update
|
||||
let is_snapshot = rng.gen_bool(0.1);
|
||||
WsEvent::OrderbookUpdate(OrderbookUpdate {
|
||||
symbol: "BTC-USDC".to_string(),
|
||||
venue: "lx_dex".to_string(),
|
||||
is_snapshot,
|
||||
bids: vec![
|
||||
PriceLevel::new(current_price - dec!(5), Decimal::try_from(rng.gen_range(0.1..2.0) as f64).unwrap_or_default()),
|
||||
PriceLevel::new(current_price - dec!(10), Decimal::try_from(rng.gen_range(0.5..3.0) as f64).unwrap_or_default()),
|
||||
],
|
||||
asks: vec![
|
||||
PriceLevel::new(current_price + dec!(5), Decimal::try_from(rng.gen_range(0.1..2.0) as f64).unwrap_or_default()),
|
||||
PriceLevel::new(current_price + dec!(10), Decimal::try_from(rng.gen_range(0.5..3.0) as f64).unwrap_or_default()),
|
||||
],
|
||||
timestamp: chrono::Utc::now().timestamp_millis(),
|
||||
sequence: rng.gen(),
|
||||
})
|
||||
}
|
||||
1 => {
|
||||
// Trade
|
||||
WsEvent::Trade(Trade {
|
||||
trade_id: format!("trade-{}", rng.gen::<u64>()),
|
||||
order_id: format!("order-{}", rng.gen::<u64>()),
|
||||
symbol: "BTC-USDC".to_string(),
|
||||
venue: "lx_dex".to_string(),
|
||||
side: if rng.gen_bool(0.5) { Side::Buy } else { Side::Sell },
|
||||
price: current_price,
|
||||
quantity: Decimal::try_from(rng.gen_range(0.01..0.5) as f64).unwrap_or_default(),
|
||||
fee: lx_trading::Fee {
|
||||
asset: "USDC".to_string(),
|
||||
amount: Decimal::try_from(rng.gen_range(0.01..1.0) as f64).unwrap_or_default(),
|
||||
rate: Some(dec!(0.001)),
|
||||
},
|
||||
timestamp: chrono::Utc::now().timestamp_millis(),
|
||||
is_maker: rng.gen_bool(0.5),
|
||||
})
|
||||
}
|
||||
2 => {
|
||||
// Ticker
|
||||
WsEvent::Ticker(Ticker {
|
||||
symbol: "BTC-USDC".to_string(),
|
||||
venue: "lx_dex".to_string(),
|
||||
bid: Some(current_price - dec!(5)),
|
||||
ask: Some(current_price + dec!(5)),
|
||||
last: Some(current_price),
|
||||
volume_24h: Some(dec!(1000)),
|
||||
high_24h: Some(base_price + dec!(500)),
|
||||
low_24h: Some(base_price - dec!(500)),
|
||||
change_24h: Some((price_offset / base_price) * dec!(100)),
|
||||
timestamp: chrono::Utc::now().timestamp_millis(),
|
||||
})
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if tx.send(event).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
println!("=== LX Trading SDK - WebSocket Client Example ===\n");
|
||||
|
||||
// Create WebSocket configuration
|
||||
let ws_config = WsConfig::new("wss://ws.dex.lux.network/stream", "lx_dex");
|
||||
|
||||
println!("WebSocket Configuration:");
|
||||
println!(" URL: {}", ws_config.url);
|
||||
println!(" Venue: {}", ws_config.venue);
|
||||
println!(" Auto Reconnect: {}", ws_config.auto_reconnect);
|
||||
println!(" Ping Interval: {:?}", ws_config.ping_interval);
|
||||
println!();
|
||||
|
||||
// Initialize statistics
|
||||
let stats = Arc::new(EventStats::new());
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
|
||||
// Create event channel
|
||||
let (tx, mut rx) = mpsc::channel::<WsEvent>(1024);
|
||||
|
||||
// Start event simulator (in production, this would be actual WebSocket connection)
|
||||
let running_clone = running.clone();
|
||||
let simulator = tokio::spawn(async move {
|
||||
simulate_events(tx, running_clone).await;
|
||||
});
|
||||
|
||||
println!("Subscriptions:");
|
||||
println!(" - Orderbook (BTC-USDC)");
|
||||
println!(" - Trades (BTC-USDC)");
|
||||
println!(" - Ticker (BTC-USDC)");
|
||||
println!();
|
||||
|
||||
println!("Processing events for 5 seconds...\n");
|
||||
|
||||
// Process events with timeout
|
||||
let stats_clone = stats.clone();
|
||||
let event_processor = tokio::spawn(async move {
|
||||
let timeout_duration = Duration::from_secs(5);
|
||||
let result = timeout(timeout_duration, async {
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
WsEvent::OrderbookUpdate(update) => {
|
||||
handle_orderbook_update(&update, &stats_clone);
|
||||
}
|
||||
WsEvent::Trade(trade) => {
|
||||
handle_trade(&trade, &stats_clone);
|
||||
}
|
||||
WsEvent::Ticker(ticker) => {
|
||||
handle_ticker(&ticker, &stats_clone);
|
||||
}
|
||||
WsEvent::OrderUpdate(update) => {
|
||||
handle_order_update(&update, &stats_clone);
|
||||
}
|
||||
WsEvent::Fill(fill) => {
|
||||
handle_fill(&fill, &stats_clone);
|
||||
}
|
||||
WsEvent::Connected { venue } => {
|
||||
println!("[CONNECTED] {}", venue);
|
||||
}
|
||||
WsEvent::Disconnected { venue, reason } => {
|
||||
println!("[DISCONNECTED] {} - {}", venue, reason);
|
||||
}
|
||||
WsEvent::Error { venue, message } => {
|
||||
stats_clone.errors.fetch_add(1, Ordering::Relaxed);
|
||||
println!("[ERROR] {} - {}", venue, message);
|
||||
}
|
||||
WsEvent::Subscribed { channel, symbol } => {
|
||||
println!("[SUBSCRIBED] {}:{}", channel, symbol);
|
||||
}
|
||||
WsEvent::Unsubscribed { channel, symbol } => {
|
||||
println!("[UNSUBSCRIBED] {}:{}", channel, symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
if result.is_err() {
|
||||
println!("\nTimeout reached, stopping event processing...");
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for processor
|
||||
let _ = event_processor.await;
|
||||
|
||||
// Stop simulator
|
||||
running.store(false, Ordering::Relaxed);
|
||||
let _ = simulator.await;
|
||||
|
||||
// Print statistics
|
||||
stats.print_summary();
|
||||
|
||||
println!("\n=== Example Complete ===");
|
||||
println!("\nKey concepts demonstrated:");
|
||||
println!("1. WebSocket configuration");
|
||||
println!("2. Channel subscriptions (orderbook, trades, ticker)");
|
||||
println!("3. Event processing patterns");
|
||||
println!("4. Statistics collection");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -224,10 +224,10 @@ pub trait VenueAdapter: Send + Sync {
|
||||
/// Get swap quote
|
||||
async fn get_swap_quote(
|
||||
&self,
|
||||
base_token: &str,
|
||||
quote_token: &str,
|
||||
amount: Decimal,
|
||||
is_buy: bool,
|
||||
_base_token: &str,
|
||||
_quote_token: &str,
|
||||
_amount: Decimal,
|
||||
_is_buy: bool,
|
||||
) -> Result<SwapQuote> {
|
||||
Err(crate::error::Error::NotImplemented(
|
||||
"AMM swap not supported".into(),
|
||||
@@ -237,11 +237,11 @@ pub trait VenueAdapter: Send + Sync {
|
||||
/// Execute swap
|
||||
async fn execute_swap(
|
||||
&self,
|
||||
base_token: &str,
|
||||
quote_token: &str,
|
||||
amount: Decimal,
|
||||
is_buy: bool,
|
||||
slippage_percent: Decimal,
|
||||
_base_token: &str,
|
||||
_quote_token: &str,
|
||||
_amount: Decimal,
|
||||
_is_buy: bool,
|
||||
_slippage_percent: Decimal,
|
||||
) -> Result<Trade> {
|
||||
Err(crate::error::Error::NotImplemented(
|
||||
"AMM swap not supported".into(),
|
||||
@@ -249,7 +249,7 @@ pub trait VenueAdapter: Send + Sync {
|
||||
}
|
||||
|
||||
/// Get pool information
|
||||
async fn get_pool_info(&self, base_token: &str, quote_token: &str) -> Result<PoolInfo> {
|
||||
async fn get_pool_info(&self, _base_token: &str, _quote_token: &str) -> Result<PoolInfo> {
|
||||
Err(crate::error::Error::NotImplemented(
|
||||
"Pool info not supported".into(),
|
||||
))
|
||||
@@ -258,11 +258,11 @@ pub trait VenueAdapter: Send + Sync {
|
||||
/// Add liquidity to pool
|
||||
async fn add_liquidity(
|
||||
&self,
|
||||
base_token: &str,
|
||||
quote_token: &str,
|
||||
base_amount: Decimal,
|
||||
quote_amount: Decimal,
|
||||
slippage_percent: Decimal,
|
||||
_base_token: &str,
|
||||
_quote_token: &str,
|
||||
_base_amount: Decimal,
|
||||
_quote_amount: Decimal,
|
||||
_slippage_percent: Decimal,
|
||||
) -> Result<LiquidityResult> {
|
||||
Err(crate::error::Error::NotImplemented(
|
||||
"Add liquidity not supported".into(),
|
||||
@@ -272,9 +272,9 @@ pub trait VenueAdapter: Send + Sync {
|
||||
/// Remove liquidity from pool
|
||||
async fn remove_liquidity(
|
||||
&self,
|
||||
pool_address: &str,
|
||||
liquidity_amount: Decimal,
|
||||
slippage_percent: Decimal,
|
||||
_pool_address: &str,
|
||||
_liquidity_amount: Decimal,
|
||||
_slippage_percent: Decimal,
|
||||
) -> Result<LiquidityResult> {
|
||||
Err(crate::error::Error::NotImplemented(
|
||||
"Remove liquidity not supported".into(),
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
use async_trait::async_trait;
|
||||
use parking_lot::RwLock;
|
||||
use rust_decimal::prelude::FromStr;
|
||||
use rust_decimal::Decimal;
|
||||
use std::collections::HashMap;
|
||||
use std::process::Stdio;
|
||||
@@ -26,6 +27,7 @@ pub struct CcxtAdapter {
|
||||
capabilities: VenueCapabilities,
|
||||
connected: AtomicBool,
|
||||
latency: AtomicU64,
|
||||
#[allow(dead_code)]
|
||||
markets_cache: RwLock<HashMap<String, MarketInfo>>,
|
||||
}
|
||||
|
||||
@@ -97,7 +99,7 @@ print(json.dumps(result))
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| Error::CcxtError(format!("Failed to execute Python: {}", e)))?;
|
||||
.map_err(|e| Error::CcxtError(format!("Failed to execute Python: {e}")))?;
|
||||
|
||||
let latency = start.elapsed().as_millis() as u64;
|
||||
self.latency.store(latency, Ordering::Relaxed);
|
||||
@@ -105,14 +107,13 @@ print(json.dumps(result))
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(Error::CcxtError(format!(
|
||||
"CCXT {} failed: {}",
|
||||
method, stderr
|
||||
"CCXT {method} failed: {stderr}"
|
||||
)));
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
serde_json::from_str(&stdout).map_err(|e| {
|
||||
Error::DeserializationError(format!("Failed to parse CCXT response: {}", e))
|
||||
Error::DeserializationError(format!("Failed to parse CCXT response: {e}"))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -151,13 +152,11 @@ print(json.dumps(result))
|
||||
|
||||
let price: Option<Decimal> = ccxt_order["price"]
|
||||
.as_f64()
|
||||
.map(|f| Decimal::try_from(f).ok())
|
||||
.flatten();
|
||||
.and_then(|f| Decimal::try_from(f).ok());
|
||||
|
||||
let average: Option<Decimal> = ccxt_order["average"]
|
||||
.as_f64()
|
||||
.map(|f| Decimal::try_from(f).ok())
|
||||
.flatten();
|
||||
.and_then(|f| Decimal::try_from(f).ok());
|
||||
|
||||
Ok(Order {
|
||||
order_id: ccxt_order["id"].as_str().unwrap_or("").to_string(),
|
||||
@@ -295,12 +294,10 @@ impl VenueAdapter for CcxtAdapter {
|
||||
.unwrap_or_default(),
|
||||
max_quantity: market["limits"]["amount"]["max"]
|
||||
.as_f64()
|
||||
.map(|f| Decimal::try_from(f).ok())
|
||||
.flatten(),
|
||||
.and_then(|f| Decimal::try_from(f).ok()),
|
||||
min_notional: market["limits"]["cost"]["min"]
|
||||
.as_f64()
|
||||
.map(|f| Decimal::try_from(f).ok())
|
||||
.flatten(),
|
||||
.and_then(|f| Decimal::try_from(f).ok()),
|
||||
tick_size: market["precision"]["price"]
|
||||
.as_f64()
|
||||
.map(|p| Decimal::from(1) / Decimal::from(10u64.pow(p as u32)))
|
||||
@@ -325,13 +322,13 @@ impl VenueAdapter for CcxtAdapter {
|
||||
Ok(Ticker {
|
||||
symbol: ticker["symbol"].as_str().unwrap_or("").to_string(),
|
||||
venue: self.name.clone(),
|
||||
bid: ticker["bid"].as_f64().map(|f| Decimal::try_from(f).ok()).flatten(),
|
||||
ask: ticker["ask"].as_f64().map(|f| Decimal::try_from(f).ok()).flatten(),
|
||||
last: ticker["last"].as_f64().map(|f| Decimal::try_from(f).ok()).flatten(),
|
||||
volume_24h: ticker["baseVolume"].as_f64().map(|f| Decimal::try_from(f).ok()).flatten(),
|
||||
high_24h: ticker["high"].as_f64().map(|f| Decimal::try_from(f).ok()).flatten(),
|
||||
low_24h: ticker["low"].as_f64().map(|f| Decimal::try_from(f).ok()).flatten(),
|
||||
change_24h: ticker["percentage"].as_f64().map(|f| Decimal::try_from(f).ok()).flatten(),
|
||||
bid: ticker["bid"].as_f64().and_then(|f| Decimal::try_from(f).ok()),
|
||||
ask: ticker["ask"].as_f64().and_then(|f| Decimal::try_from(f).ok()),
|
||||
last: ticker["last"].as_f64().and_then(|f| Decimal::try_from(f).ok()),
|
||||
volume_24h: ticker["baseVolume"].as_f64().and_then(|f| Decimal::try_from(f).ok()),
|
||||
high_24h: ticker["high"].as_f64().and_then(|f| Decimal::try_from(f).ok()),
|
||||
low_24h: ticker["low"].as_f64().and_then(|f| Decimal::try_from(f).ok()),
|
||||
change_24h: ticker["percentage"].as_f64().and_then(|f| Decimal::try_from(f).ok()),
|
||||
timestamp: ticker["timestamp"].as_i64().unwrap_or(0),
|
||||
})
|
||||
}
|
||||
@@ -347,13 +344,13 @@ impl VenueAdapter for CcxtAdapter {
|
||||
result.push(Ticker {
|
||||
symbol: ticker["symbol"].as_str().unwrap_or("").to_string(),
|
||||
venue: self.name.clone(),
|
||||
bid: ticker["bid"].as_f64().map(|f| Decimal::try_from(f).ok()).flatten(),
|
||||
ask: ticker["ask"].as_f64().map(|f| Decimal::try_from(f).ok()).flatten(),
|
||||
last: ticker["last"].as_f64().map(|f| Decimal::try_from(f).ok()).flatten(),
|
||||
volume_24h: ticker["baseVolume"].as_f64().map(|f| Decimal::try_from(f).ok()).flatten(),
|
||||
high_24h: ticker["high"].as_f64().map(|f| Decimal::try_from(f).ok()).flatten(),
|
||||
low_24h: ticker["low"].as_f64().map(|f| Decimal::try_from(f).ok()).flatten(),
|
||||
change_24h: ticker["percentage"].as_f64().map(|f| Decimal::try_from(f).ok()).flatten(),
|
||||
bid: ticker["bid"].as_f64().and_then(|f| Decimal::try_from(f).ok()),
|
||||
ask: ticker["ask"].as_f64().and_then(|f| Decimal::try_from(f).ok()),
|
||||
last: ticker["last"].as_f64().and_then(|f| Decimal::try_from(f).ok()),
|
||||
volume_24h: ticker["baseVolume"].as_f64().and_then(|f| Decimal::try_from(f).ok()),
|
||||
high_24h: ticker["high"].as_f64().and_then(|f| Decimal::try_from(f).ok()),
|
||||
low_24h: ticker["low"].as_f64().and_then(|f| Decimal::try_from(f).ok()),
|
||||
change_24h: ticker["percentage"].as_f64().and_then(|f| Decimal::try_from(f).ok()),
|
||||
timestamp: ticker["timestamp"].as_i64().unwrap_or(0),
|
||||
});
|
||||
}
|
||||
@@ -558,8 +555,3 @@ impl VenueAdapter for CcxtAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
use rust_decimal::prelude::FromStr;
|
||||
|
||||
fn decimal_from_str(s: &str) -> Result<Decimal> {
|
||||
Decimal::from_str(s).map_err(|e| Error::DeserializationError(e.to_string()))
|
||||
}
|
||||
|
||||
@@ -70,13 +70,12 @@ impl HummingbotAdapter {
|
||||
let status = response.status();
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
return Err(Error::HummingbotError(format!(
|
||||
"Gateway error [{}]: {}",
|
||||
status, text
|
||||
"Gateway error [{status}]: {text}"
|
||||
)));
|
||||
}
|
||||
|
||||
response.json().await.map_err(|e| {
|
||||
Error::DeserializationError(format!("Failed to parse Gateway response: {}", e))
|
||||
Error::DeserializationError(format!("Failed to parse Gateway response: {e}"))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -176,7 +175,7 @@ impl VenueAdapter for HummingbotAdapter {
|
||||
}
|
||||
|
||||
markets.push(MarketInfo {
|
||||
symbol: format!("{}-{}", base, quote),
|
||||
symbol: format!("{base}-{quote}"),
|
||||
base: base.to_string(),
|
||||
quote: quote.to_string(),
|
||||
price_precision: 8,
|
||||
@@ -431,7 +430,7 @@ impl VenueAdapter for HummingbotAdapter {
|
||||
Ok(Trade {
|
||||
trade_id: tx_hash.clone(),
|
||||
order_id: tx_hash,
|
||||
symbol: format!("{}-{}", base_token, quote_token),
|
||||
symbol: format!("{base_token}-{quote_token}"),
|
||||
venue: self.name.clone(),
|
||||
side: if is_buy { Side::Buy } else { Side::Sell },
|
||||
price,
|
||||
|
||||
@@ -5,7 +5,6 @@ use parking_lot::RwLock;
|
||||
use rust_decimal::Decimal;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::adapters::adapter::*;
|
||||
use crate::config::NativeVenueConfig;
|
||||
@@ -22,6 +21,7 @@ pub struct LxDexAdapter {
|
||||
latency: AtomicU64,
|
||||
client: reqwest::Client,
|
||||
balances: RwLock<HashMap<String, Balance>>,
|
||||
#[allow(dead_code)]
|
||||
open_orders: RwLock<HashMap<String, Order>>,
|
||||
}
|
||||
|
||||
@@ -157,25 +157,25 @@ impl VenueAdapter for LxDexAdapter {
|
||||
}
|
||||
|
||||
async fn get_ticker(&self, symbol: &str) -> Result<Ticker> {
|
||||
let path = format!("/api/v1/ticker/{}", symbol);
|
||||
let path = format!("/api/v1/ticker/{symbol}");
|
||||
self.signed_request(reqwest::Method::GET, &path, None).await
|
||||
}
|
||||
|
||||
async fn get_tickers(&self, symbols: &[String]) -> Result<Vec<Ticker>> {
|
||||
let query = symbols.join(",");
|
||||
let path = format!("/api/v1/tickers?symbols={}", query);
|
||||
let path = format!("/api/v1/tickers?symbols={query}");
|
||||
self.signed_request(reqwest::Method::GET, &path, None).await
|
||||
}
|
||||
|
||||
async fn get_orderbook(&self, symbol: &str, depth: Option<usize>) -> Result<Orderbook> {
|
||||
let depth = depth.unwrap_or(100);
|
||||
let path = format!("/api/v1/orderbook/{}?depth={}", symbol, depth);
|
||||
let path = format!("/api/v1/orderbook/{symbol}?depth={depth}");
|
||||
self.signed_request(reqwest::Method::GET, &path, None).await
|
||||
}
|
||||
|
||||
async fn get_trades(&self, symbol: &str, limit: Option<usize>) -> Result<Vec<Trade>> {
|
||||
let limit = limit.unwrap_or(100);
|
||||
let path = format!("/api/v1/trades/{}?limit={}", symbol, limit);
|
||||
let path = format!("/api/v1/trades/{symbol}?limit={limit}");
|
||||
self.signed_request(reqwest::Method::GET, &path, None).await
|
||||
}
|
||||
|
||||
@@ -199,20 +199,20 @@ impl VenueAdapter for LxDexAdapter {
|
||||
return Ok(balance.clone());
|
||||
}
|
||||
|
||||
let path = format!("/api/v1/account/balance/{}", asset);
|
||||
let path = format!("/api/v1/account/balance/{asset}");
|
||||
self.signed_request(reqwest::Method::GET, &path, None).await
|
||||
}
|
||||
|
||||
async fn get_open_orders(&self, symbol: Option<&str>) -> Result<Vec<Order>> {
|
||||
let path = match symbol {
|
||||
Some(s) => format!("/api/v1/orders?symbol={}&status=open", s),
|
||||
Some(s) => format!("/api/v1/orders?symbol={s}&status=open"),
|
||||
None => "/api/v1/orders?status=open".to_string(),
|
||||
};
|
||||
self.signed_request(reqwest::Method::GET, &path, None).await
|
||||
}
|
||||
|
||||
async fn get_order(&self, order_id: &str, symbol: &str) -> Result<Order> {
|
||||
let path = format!("/api/v1/orders/{}?symbol={}", order_id, symbol);
|
||||
let path = format!("/api/v1/orders/{order_id}?symbol={symbol}");
|
||||
self.signed_request(reqwest::Method::GET, &path, None).await
|
||||
}
|
||||
|
||||
@@ -225,10 +225,10 @@ impl VenueAdapter for LxDexAdapter {
|
||||
let mut params = vec![];
|
||||
|
||||
if let Some(s) = symbol {
|
||||
params.push(format!("symbol={}", s));
|
||||
params.push(format!("symbol={s}"));
|
||||
}
|
||||
if let Some(l) = limit {
|
||||
params.push(format!("limit={}", l));
|
||||
params.push(format!("limit={l}"));
|
||||
}
|
||||
|
||||
if !params.is_empty() {
|
||||
@@ -249,7 +249,7 @@ impl VenueAdapter for LxDexAdapter {
|
||||
}
|
||||
|
||||
async fn cancel_order(&self, order_id: &str, symbol: &str) -> Result<Order> {
|
||||
let path = format!("/api/v1/orders/{}", order_id);
|
||||
let path = format!("/api/v1/orders/{order_id}");
|
||||
let body = serde_json::json!({ "symbol": symbol });
|
||||
self.signed_request(reqwest::Method::DELETE, &path, Some(body)).await
|
||||
}
|
||||
@@ -391,7 +391,7 @@ impl VenueAdapter for LxAmmAdapter {
|
||||
}
|
||||
|
||||
async fn get_ticker(&self, symbol: &str) -> Result<Ticker> {
|
||||
let path = format!("/api/v1/amm/price/{}", symbol);
|
||||
let path = format!("/api/v1/amm/price/{symbol}");
|
||||
self.request(reqwest::Method::GET, &path, None).await
|
||||
}
|
||||
|
||||
@@ -411,7 +411,7 @@ impl VenueAdapter for LxAmmAdapter {
|
||||
|
||||
async fn get_trades(&self, symbol: &str, limit: Option<usize>) -> Result<Vec<Trade>> {
|
||||
let limit = limit.unwrap_or(100);
|
||||
let path = format!("/api/v1/amm/swaps/{}?limit={}", symbol, limit);
|
||||
let path = format!("/api/v1/amm/swaps/{symbol}?limit={limit}");
|
||||
self.request(reqwest::Method::GET, &path, None).await
|
||||
}
|
||||
|
||||
@@ -420,7 +420,7 @@ impl VenueAdapter for LxAmmAdapter {
|
||||
}
|
||||
|
||||
async fn get_balance(&self, asset: &str) -> Result<Balance> {
|
||||
let path = format!("/api/v1/account/balance/{}", asset);
|
||||
let path = format!("/api/v1/account/balance/{asset}");
|
||||
self.request(reqwest::Method::GET, &path, None).await
|
||||
}
|
||||
|
||||
@@ -531,7 +531,7 @@ impl VenueAdapter for LxAmmAdapter {
|
||||
}
|
||||
|
||||
async fn get_pool_info(&self, base_token: &str, quote_token: &str) -> Result<PoolInfo> {
|
||||
let path = format!("/api/v1/amm/pool/{}/{}", base_token, quote_token);
|
||||
let path = format!("/api/v1/amm/pool/{base_token}/{quote_token}");
|
||||
self.request(reqwest::Method::GET, &path, None).await
|
||||
}
|
||||
|
||||
|
||||
@@ -185,6 +185,7 @@ impl LxFirstArbitrage {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn create_opportunity(
|
||||
&self,
|
||||
symbol: &str,
|
||||
|
||||
@@ -259,7 +259,7 @@ async fn find_opportunity<C: TradingClient>(
|
||||
let now = current_time_ms();
|
||||
|
||||
Some(UnifiedOpportunity {
|
||||
id: format!("arb-{}-{}", symbol, now),
|
||||
id: format!("arb-{symbol}-{now}"),
|
||||
symbol: symbol.to_string(),
|
||||
timestamp: now,
|
||||
expires_at: now + 5000,
|
||||
|
||||
@@ -38,7 +38,7 @@ impl Config {
|
||||
/// Load configuration from file
|
||||
pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
|
||||
let content = std::fs::read_to_string(path.as_ref())
|
||||
.map_err(|e| Error::ConfigError(format!("Failed to read config file: {}", e)))?;
|
||||
.map_err(|e| Error::ConfigError(format!("Failed to read config file: {e}")))?;
|
||||
|
||||
let config: Config = toml::from_str(&content)?;
|
||||
config.validate()?;
|
||||
@@ -66,7 +66,7 @@ impl Config {
|
||||
for (name, cfg) in &self.native {
|
||||
if cfg.api_url.is_empty() {
|
||||
return Err(Error::InvalidConfig {
|
||||
field: format!("native.{}.api_url", name),
|
||||
field: format!("native.{name}.api_url"),
|
||||
message: "API URL is required".into(),
|
||||
});
|
||||
}
|
||||
@@ -75,7 +75,7 @@ impl Config {
|
||||
for (name, cfg) in &self.ccxt {
|
||||
if cfg.exchange_id.is_empty() {
|
||||
return Err(Error::InvalidConfig {
|
||||
field: format!("ccxt.{}.exchange_id", name),
|
||||
field: format!("ccxt.{name}.exchange_id"),
|
||||
message: "Exchange ID is required".into(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Unified trading engine with smart order routing.
|
||||
|
||||
use dashmap::DashMap;
|
||||
use rust_decimal::prelude::FromStr;
|
||||
use rust_decimal::Decimal;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
@@ -283,7 +284,7 @@ impl UnifiedClient {
|
||||
|
||||
/// Place order on default venue
|
||||
pub async fn place_order(&self, request: OrderRequest) -> Result<Order> {
|
||||
if let Some(venue) = &request.venue {
|
||||
if request.venue.is_some() {
|
||||
return self.place_order_on(request.clone()).await;
|
||||
}
|
||||
|
||||
@@ -458,12 +459,6 @@ impl UnifiedClient {
|
||||
}
|
||||
}
|
||||
|
||||
use rust_decimal::prelude::FromStr;
|
||||
|
||||
fn decimal_from_str(s: &str) -> Result<Decimal> {
|
||||
Decimal::from_str(s).map_err(|e| Error::DeserializationError(e.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -164,7 +164,7 @@ impl From<reqwest::Error> for Error {
|
||||
if err.is_timeout() {
|
||||
Error::Timeout { timeout_ms: 30000 }
|
||||
} else if err.is_connect() {
|
||||
Error::NetworkError(format!("Connection failed: {}", err))
|
||||
Error::NetworkError(format!("Connection failed: {err}"))
|
||||
} else {
|
||||
Error::NetworkError(err.to_string())
|
||||
}
|
||||
@@ -185,19 +185,19 @@ impl From<tokio_tungstenite::tungstenite::Error> for Error {
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
Error::Internal(format!("IO error: {}", err))
|
||||
Error::Internal(format!("IO error: {err}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for Error {
|
||||
fn from(err: url::ParseError) -> Self {
|
||||
Error::ConfigError(format!("Invalid URL: {}", err))
|
||||
Error::ConfigError(format!("Invalid URL: {err}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<toml::de::Error> for Error {
|
||||
fn from(err: toml::de::Error) -> Self {
|
||||
Error::ConfigError(format!("TOML parse error: {}", err))
|
||||
Error::ConfigError(format!("TOML parse error: {err}"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! - Request/response interceptors
|
||||
|
||||
use parking_lot::RwLock;
|
||||
use reqwest::{Client, Method, Request, Response, StatusCode};
|
||||
use reqwest::{Client, Method, Response, StatusCode};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
@@ -173,16 +173,18 @@ impl RateLimiter {
|
||||
}
|
||||
|
||||
/// Acquire a token (wait if necessary)
|
||||
#[allow(clippy::await_holding_lock)]
|
||||
pub async fn acquire(&self) {
|
||||
loop {
|
||||
self.refill();
|
||||
|
||||
let mut tokens = self.tokens.write();
|
||||
if *tokens >= 1.0 {
|
||||
*tokens -= 1.0;
|
||||
return;
|
||||
}
|
||||
drop(tokens);
|
||||
{
|
||||
let mut tokens = self.tokens.write();
|
||||
if *tokens >= 1.0 {
|
||||
*tokens -= 1.0;
|
||||
return;
|
||||
}
|
||||
} // Guard dropped here before await
|
||||
|
||||
// Wait for token
|
||||
let wait_time = Duration::from_secs_f64(1.0 / self.requests_per_second);
|
||||
@@ -293,7 +295,7 @@ impl HttpClient {
|
||||
.pool_max_idle_per_host(config.pool_max_idle_per_host)
|
||||
.user_agent(&config.user_agent)
|
||||
.build()
|
||||
.map_err(|e| Error::Internal(format!("Failed to create HTTP client: {}", e)))?;
|
||||
.map_err(|e| Error::Internal(format!("Failed to create HTTP client: {e}")))?;
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
@@ -401,7 +403,7 @@ impl HttpClient {
|
||||
if status.is_success() {
|
||||
self.metrics.successful_requests.fetch_add(1, Ordering::Relaxed);
|
||||
let text = response.text().await.map_err(|e| {
|
||||
Error::NetworkError(format!("Failed to read response: {}", e))
|
||||
Error::NetworkError(format!("Failed to read response: {e}"))
|
||||
})?;
|
||||
return serde_json::from_str(&text).map_err(|e| {
|
||||
Error::DeserializationError(format!(
|
||||
@@ -530,7 +532,7 @@ impl HttpClient {
|
||||
|
||||
let status = response.status();
|
||||
let text = response.text().await.map_err(|e| {
|
||||
Error::NetworkError(format!("Failed to read response: {}", e))
|
||||
Error::NetworkError(format!("Failed to read response: {e}"))
|
||||
})?;
|
||||
|
||||
if status.is_success() {
|
||||
|
||||
@@ -44,7 +44,7 @@ impl MetricsCollector {
|
||||
let mut latencies = self.latencies.write();
|
||||
latencies
|
||||
.entry(operation.to_string())
|
||||
.or_insert_with(LatencyTracker::new)
|
||||
.or_default()
|
||||
.record(latency);
|
||||
}
|
||||
|
||||
@@ -578,7 +578,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_percentile() {
|
||||
let samples: Vec<Duration> = (1..=100)
|
||||
.map(|i| Duration::from_millis(i))
|
||||
.map(Duration::from_millis)
|
||||
.collect();
|
||||
|
||||
// Percentile calculation uses floor, so p50 of 100 items = index 50 = 51ms
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! Provides unified streaming interface across all venues.
|
||||
|
||||
use futures::{Stream, StreamExt};
|
||||
use futures::Stream;
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use std::pin::Pin;
|
||||
@@ -10,7 +10,6 @@ use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::types::*;
|
||||
use crate::ws::{Fill, OrderUpdate, OrderbookUpdate, WsEvent};
|
||||
|
||||
@@ -103,11 +102,10 @@ impl OrderbookStream {
|
||||
tokio::spawn(async move {
|
||||
while let Ok(event) = event_rx.recv().await {
|
||||
if let WsEvent::OrderbookUpdate(update) = event {
|
||||
if update.symbol == symbol_filter {
|
||||
if tx.send(update).await.is_err() {
|
||||
if update.symbol == symbol_filter
|
||||
&& tx.send(update).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -149,11 +147,10 @@ impl TradeStream {
|
||||
tokio::spawn(async move {
|
||||
while let Ok(event) = event_rx.recv().await {
|
||||
if let WsEvent::Trade(trade) = event {
|
||||
if trade.symbol == symbol_filter {
|
||||
if tx.send(trade).await.is_err() {
|
||||
if trade.symbol == symbol_filter
|
||||
&& tx.send(trade).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -195,11 +192,10 @@ impl TickerStream {
|
||||
tokio::spawn(async move {
|
||||
while let Ok(event) = event_rx.recv().await {
|
||||
if let WsEvent::Ticker(ticker) = event {
|
||||
if ticker.symbol == symbol_filter {
|
||||
if tx.send(ticker).await.is_err() {
|
||||
if ticker.symbol == symbol_filter
|
||||
&& tx.send(ticker).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -22,7 +22,6 @@ use tokio_tungstenite::{
|
||||
use url::Url;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::orderbook::Orderbook;
|
||||
use crate::types::*;
|
||||
|
||||
/// WebSocket event types
|
||||
@@ -169,6 +168,7 @@ pub enum SubscriptionChannel {
|
||||
enum WsCommand {
|
||||
Subscribe(Subscription),
|
||||
Unsubscribe(Subscription),
|
||||
#[allow(dead_code)]
|
||||
SendMessage(String),
|
||||
Disconnect,
|
||||
}
|
||||
@@ -213,7 +213,7 @@ impl WsConnection {
|
||||
|
||||
let url = Url::parse(&self.config.url)?;
|
||||
let (ws_stream, _) = connect_async(url).await.map_err(|e| {
|
||||
Error::WebsocketError(format!("Failed to connect: {}", e))
|
||||
Error::WebsocketError(format!("Failed to connect: {e}"))
|
||||
})?;
|
||||
|
||||
let (write, read) = ws_stream.split();
|
||||
@@ -295,7 +295,7 @@ impl WsConnection {
|
||||
self.command_tx
|
||||
.send(WsCommand::Subscribe(sub.clone()))
|
||||
.await
|
||||
.map_err(|e| Error::WebsocketError(format!("Failed to send subscribe: {}", e)))?;
|
||||
.map_err(|e| Error::WebsocketError(format!("Failed to send subscribe: {e}")))?;
|
||||
|
||||
let key = format!("{}:{}", channel_to_str(channel), symbol);
|
||||
self.subscriptions.write().insert(key);
|
||||
@@ -313,7 +313,7 @@ impl WsConnection {
|
||||
self.command_tx
|
||||
.send(WsCommand::Unsubscribe(sub))
|
||||
.await
|
||||
.map_err(|e| Error::WebsocketError(format!("Failed to send unsubscribe: {}", e)))?;
|
||||
.map_err(|e| Error::WebsocketError(format!("Failed to send unsubscribe: {e}")))?;
|
||||
|
||||
let key = format!("{}:{}", channel_to_str(channel), symbol);
|
||||
self.subscriptions.write().remove(&key);
|
||||
@@ -381,12 +381,13 @@ impl Stream for WsEventStream {
|
||||
}
|
||||
|
||||
/// WebSocket message handler
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn run_ws_loop<S, R>(
|
||||
config: WsConfig,
|
||||
state: Arc<RwLock<ConnectionState>>,
|
||||
connected: AtomicBool,
|
||||
reconnect_count: AtomicU64,
|
||||
subscriptions: Arc<RwLock<HashSet<String>>>,
|
||||
_subscriptions: Arc<RwLock<HashSet<String>>>,
|
||||
event_tx: broadcast::Sender<WsEvent>,
|
||||
mut command_rx: mpsc::Receiver<WsCommand>,
|
||||
mut write: S,
|
||||
@@ -529,14 +530,11 @@ fn parse_ws_message(text: &str, venue: &str) -> Result<WsEvent> {
|
||||
}
|
||||
|
||||
Err(Error::DeserializationError(format!(
|
||||
"Unknown message format: {}",
|
||||
text
|
||||
"Unknown message format: {text}"
|
||||
)))
|
||||
}
|
||||
|
||||
fn parse_orderbook_update(value: &serde_json::Value, venue: &str) -> Result<OrderbookUpdate> {
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
let symbol = value
|
||||
.get("symbol")
|
||||
.or_else(|| value.get("s"))
|
||||
@@ -556,8 +554,8 @@ fn parse_orderbook_update(value: &serde_json::Value, venue: &str) -> Result<Orde
|
||||
if let Some(bid_array) = value.get("bids").or_else(|| value.get("b")).and_then(|v| v.as_array()) {
|
||||
for bid in bid_array {
|
||||
if let (Some(price), Some(qty)) = (
|
||||
bid.get(0).or_else(|| bid.get("price")).and_then(|v| parse_decimal(v)),
|
||||
bid.get(1).or_else(|| bid.get("quantity")).and_then(|v| parse_decimal(v)),
|
||||
bid.get(0).or_else(|| bid.get("price")).and_then(parse_decimal),
|
||||
bid.get(1).or_else(|| bid.get("quantity")).and_then(parse_decimal),
|
||||
) {
|
||||
bids.push(PriceLevel::new(price, qty));
|
||||
}
|
||||
@@ -567,8 +565,8 @@ fn parse_orderbook_update(value: &serde_json::Value, venue: &str) -> Result<Orde
|
||||
if let Some(ask_array) = value.get("asks").or_else(|| value.get("a")).and_then(|v| v.as_array()) {
|
||||
for ask in ask_array {
|
||||
if let (Some(price), Some(qty)) = (
|
||||
ask.get(0).or_else(|| ask.get("price")).and_then(|v| parse_decimal(v)),
|
||||
ask.get(1).or_else(|| ask.get("quantity")).and_then(|v| parse_decimal(v)),
|
||||
ask.get(0).or_else(|| ask.get("price")).and_then(parse_decimal),
|
||||
ask.get(1).or_else(|| ask.get("quantity")).and_then(parse_decimal),
|
||||
) {
|
||||
asks.push(PriceLevel::new(price, qty));
|
||||
}
|
||||
@@ -634,14 +632,14 @@ fn parse_trade(value: &serde_json::Value, venue: &str) -> Result<Trade> {
|
||||
let price = value
|
||||
.get("price")
|
||||
.or_else(|| value.get("p"))
|
||||
.and_then(|v| parse_decimal(v))
|
||||
.and_then(parse_decimal)
|
||||
.unwrap_or_default();
|
||||
|
||||
let quantity = value
|
||||
.get("quantity")
|
||||
.or_else(|| value.get("q"))
|
||||
.or_else(|| value.get("amount"))
|
||||
.and_then(|v| parse_decimal(v))
|
||||
.and_then(parse_decimal)
|
||||
.unwrap_or_default();
|
||||
|
||||
let timestamp = value
|
||||
@@ -686,37 +684,37 @@ fn parse_ticker(value: &serde_json::Value, venue: &str) -> Result<Ticker> {
|
||||
let bid = value
|
||||
.get("bid")
|
||||
.or_else(|| value.get("b"))
|
||||
.and_then(|v| parse_decimal(v));
|
||||
.and_then(parse_decimal);
|
||||
|
||||
let ask = value
|
||||
.get("ask")
|
||||
.or_else(|| value.get("a"))
|
||||
.and_then(|v| parse_decimal(v));
|
||||
.and_then(parse_decimal);
|
||||
|
||||
let last = value
|
||||
.get("last")
|
||||
.or_else(|| value.get("c"))
|
||||
.and_then(|v| parse_decimal(v));
|
||||
.and_then(parse_decimal);
|
||||
|
||||
let volume_24h = value
|
||||
.get("volume")
|
||||
.or_else(|| value.get("v"))
|
||||
.and_then(|v| parse_decimal(v));
|
||||
.and_then(parse_decimal);
|
||||
|
||||
let high_24h = value
|
||||
.get("high")
|
||||
.or_else(|| value.get("h"))
|
||||
.and_then(|v| parse_decimal(v));
|
||||
.and_then(parse_decimal);
|
||||
|
||||
let low_24h = value
|
||||
.get("low")
|
||||
.or_else(|| value.get("l"))
|
||||
.and_then(|v| parse_decimal(v));
|
||||
.and_then(parse_decimal);
|
||||
|
||||
let change_24h = value
|
||||
.get("change")
|
||||
.or_else(|| value.get("P"))
|
||||
.and_then(|v| parse_decimal(v));
|
||||
.and_then(parse_decimal);
|
||||
|
||||
let timestamp = value
|
||||
.get("timestamp")
|
||||
@@ -776,18 +774,18 @@ fn parse_order_update(value: &serde_json::Value, venue: &str) -> Result<OrderUpd
|
||||
let filled_quantity = value
|
||||
.get("filled_quantity")
|
||||
.or_else(|| value.get("z"))
|
||||
.and_then(|v| parse_decimal(v))
|
||||
.and_then(parse_decimal)
|
||||
.unwrap_or_default();
|
||||
|
||||
let remaining_quantity = value
|
||||
.get("remaining_quantity")
|
||||
.and_then(|v| parse_decimal(v))
|
||||
.and_then(parse_decimal)
|
||||
.unwrap_or_default();
|
||||
|
||||
let average_price = value
|
||||
.get("average_price")
|
||||
.or_else(|| value.get("ap"))
|
||||
.and_then(|v| parse_decimal(v));
|
||||
.and_then(parse_decimal);
|
||||
|
||||
let timestamp = value
|
||||
.get("timestamp")
|
||||
@@ -809,8 +807,6 @@ fn parse_order_update(value: &serde_json::Value, venue: &str) -> Result<OrderUpd
|
||||
}
|
||||
|
||||
fn parse_fill(value: &serde_json::Value, venue: &str) -> Result<Fill> {
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
let trade_id = value
|
||||
.get("trade_id")
|
||||
.or_else(|| value.get("t"))
|
||||
@@ -842,19 +838,19 @@ fn parse_fill(value: &serde_json::Value, venue: &str) -> Result<Fill> {
|
||||
let price = value
|
||||
.get("price")
|
||||
.or_else(|| value.get("p"))
|
||||
.and_then(|v| parse_decimal(v))
|
||||
.and_then(parse_decimal)
|
||||
.unwrap_or_default();
|
||||
|
||||
let quantity = value
|
||||
.get("quantity")
|
||||
.or_else(|| value.get("q"))
|
||||
.and_then(|v| parse_decimal(v))
|
||||
.and_then(parse_decimal)
|
||||
.unwrap_or_default();
|
||||
|
||||
let fee_amount = value
|
||||
.get("fee")
|
||||
.or_else(|| value.get("n"))
|
||||
.and_then(|v| parse_decimal(v))
|
||||
.and_then(parse_decimal)
|
||||
.unwrap_or_default();
|
||||
|
||||
let fee_asset = value
|
||||
|
||||
@@ -1,4 +1,28 @@
|
||||
//! Integration tests for lx-trading-core.
|
||||
//!
|
||||
//! This module provides comprehensive integration tests for the LX Trading SDK.
|
||||
//! Tests are organized by component and cover:
|
||||
//!
|
||||
//! - Configuration parsing and validation
|
||||
//! - Trading types (pairs, orders, balances)
|
||||
//! - Orderbook operations and aggregation
|
||||
//! - Risk management and position tracking
|
||||
//! - HTTP client and retry logic
|
||||
//! - Metrics collection
|
||||
//! - Financial mathematics
|
||||
//! - WebSocket configuration
|
||||
//! - Error handling and classification
|
||||
//!
|
||||
//! # Running Tests
|
||||
//!
|
||||
//! ```bash
|
||||
//! cargo test --test integration_tests
|
||||
//! ```
|
||||
//!
|
||||
//! # Test Coverage
|
||||
//!
|
||||
//! These tests validate the public API contracts and ensure correct behavior
|
||||
//! across the SDK's core functionality.
|
||||
|
||||
use lx_trading::*;
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright 2024 Lux Partners Limited. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
Package adapters provides venue adapter implementations for the trading SDK.
|
||||
|
||||
This package implements the trading.VenueAdapter interface for various
|
||||
trading venues:
|
||||
|
||||
# Native Adapters
|
||||
|
||||
LxDexAdapter connects to the native LX DEX (Central Limit Order Book):
|
||||
|
||||
adapter := adapters.NewLxDexAdapter("lx_dex", &trading.NativeVenueConfig{
|
||||
VenueType: "dex",
|
||||
APIURL: "https://api.lx.exchange",
|
||||
APIKey: apiKey,
|
||||
APISecret: apiSecret,
|
||||
})
|
||||
|
||||
LxAmmAdapter connects to the native LX AMM:
|
||||
|
||||
adapter := adapters.NewLxAmmAdapter("lx_amm", &trading.NativeVenueConfig{
|
||||
VenueType: "amm",
|
||||
APIURL: "https://api.amm.lux.network",
|
||||
})
|
||||
|
||||
# CCXT Adapter
|
||||
|
||||
CcxtAdapter connects to CCXT-supported exchanges via an HTTP gateway.
|
||||
This requires running a CCXT gateway service:
|
||||
|
||||
adapter := adapters.NewCcxtAdapter("binance", &trading.CcxtConfig{
|
||||
ExchangeID: "binance",
|
||||
APIKey: apiKey,
|
||||
APISecret: apiSecret,
|
||||
BaseURL: "http://localhost:8080", // CCXT gateway
|
||||
})
|
||||
|
||||
Supported exchanges include: Binance, MEXC, OKX, Kraken, Coinbase, and
|
||||
any other exchange supported by CCXT.
|
||||
|
||||
# Hummingbot Gateway Adapter
|
||||
|
||||
HummingbotAdapter connects to DEXs via Hummingbot Gateway:
|
||||
|
||||
adapter := adapters.NewHummingbotAdapter("uniswap", &trading.HummingbotConfig{
|
||||
Host: "localhost",
|
||||
Port: 15888,
|
||||
Connector: "uniswap",
|
||||
Chain: "ethereum",
|
||||
Network: "mainnet",
|
||||
})
|
||||
|
||||
# Adapter Registration
|
||||
|
||||
Adapters are automatically registered with the trading package via init():
|
||||
|
||||
import _ "github.com/luxfi/trading/adapters"
|
||||
|
||||
This enables the trading.Client to create adapters based on configuration.
|
||||
|
||||
# Implementing Custom Adapters
|
||||
|
||||
To implement a custom adapter, implement the trading.VenueAdapter interface:
|
||||
|
||||
type VenueAdapter interface {
|
||||
Name() string
|
||||
VenueType() VenueType
|
||||
Capabilities() VenueCapabilities
|
||||
IsConnected() bool
|
||||
LatencyMs() *int64
|
||||
Info() VenueInfo
|
||||
Connect(ctx context.Context) error
|
||||
Disconnect(ctx context.Context) error
|
||||
GetMarkets(ctx context.Context) ([]MarketInfo, error)
|
||||
GetTicker(ctx context.Context, symbol string) (Ticker, error)
|
||||
GetOrderbook(ctx context.Context, symbol string, depth int) (*Orderbook, error)
|
||||
GetTrades(ctx context.Context, symbol string, limit int) ([]Trade, error)
|
||||
GetBalances(ctx context.Context) ([]Balance, error)
|
||||
GetBalance(ctx context.Context, asset string) (Balance, error)
|
||||
GetOpenOrders(ctx context.Context, symbol string) ([]Order, error)
|
||||
PlaceOrder(ctx context.Context, request OrderRequest) (Order, error)
|
||||
CancelOrder(ctx context.Context, orderID, symbol string) (Order, error)
|
||||
CancelAllOrders(ctx context.Context, symbol string) ([]Order, error)
|
||||
// AMM methods (return ErrNotSupported if not applicable)
|
||||
GetSwapQuote(...) (SwapQuote, error)
|
||||
ExecuteSwap(...) (Trade, error)
|
||||
GetPoolInfo(...) (PoolInfo, error)
|
||||
AddLiquidity(...) (LiquidityResult, error)
|
||||
RemoveLiquidity(...) (LiquidityResult, error)
|
||||
GetLpPositions(ctx context.Context) ([]LpPosition, error)
|
||||
}
|
||||
|
||||
Use trading.BaseAdapter to get default implementations of common methods.
|
||||
*/
|
||||
package adapters
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2024 Lux Partners Limited. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
Package arbitrage provides unified liquidity arbitrage capabilities.
|
||||
|
||||
This package implements cross-venue arbitrage detection and execution
|
||||
using the unified trading SDK. The core strategy is "LX First" - since
|
||||
LX DEX has the fastest price updates (nanosecond updates, 200ms blocks),
|
||||
it serves as the price oracle while other venues are inherently stale.
|
||||
|
||||
# Architecture
|
||||
|
||||
The arbitrage system consists of three main components:
|
||||
|
||||
1. Scanner: Continuously monitors orderbooks across all connected venues
|
||||
2. Opportunity Detector: Identifies profitable cross-venue spreads
|
||||
3. Executor: Simultaneously executes buy and sell legs
|
||||
|
||||
# Quick Start
|
||||
|
||||
// Create trading client (see trading package documentation)
|
||||
client := trading.NewClient(config)
|
||||
client.Connect(ctx)
|
||||
|
||||
// Create adapter for arbitrage system
|
||||
arbClient := &ClientAdapter{Client: client}
|
||||
|
||||
// Configure arbitrage
|
||||
arbConfig := arbitrage.UnifiedArbConfig{
|
||||
MinSpreadBps: decimal.NewFromInt(10), // 10 bps minimum
|
||||
MinProfit: decimal.NewFromInt(5), // $5 minimum profit
|
||||
MaxPositionSize: decimal.NewFromInt(10000),
|
||||
Symbols: []string{"BTC-USDC", "ETH-USDC"},
|
||||
VenuePriority: []string{"lx_dex", "binance"},
|
||||
ScanInterval: 100 * time.Millisecond,
|
||||
ExecuteTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
arb := arbitrage.NewUnifiedArbitrage(arbClient, arbConfig)
|
||||
arb.Start()
|
||||
defer arb.Stop()
|
||||
|
||||
# Opportunity Detection
|
||||
|
||||
An opportunity is detected when:
|
||||
- Best bid on venue A > Best ask on venue B
|
||||
- Spread exceeds MinSpreadBps threshold
|
||||
- Net profit (after fees) exceeds MinProfit threshold
|
||||
- Position size is within risk limits
|
||||
|
||||
# Execution Strategy
|
||||
|
||||
When an opportunity is found, the system:
|
||||
1. Validates the opportunity hasn't expired
|
||||
2. Executes buy and sell orders simultaneously
|
||||
3. Tracks fill rates and actual profit
|
||||
4. Updates position and PnL tracking
|
||||
|
||||
# Risk Management
|
||||
|
||||
Built-in risk controls include:
|
||||
- Maximum position size per asset
|
||||
- Maximum total exposure across all assets
|
||||
- Maximum daily loss limit
|
||||
- Maximum trades per day
|
||||
- Per-trade profit threshold
|
||||
|
||||
# Statistics
|
||||
|
||||
Monitor arbitrage performance:
|
||||
|
||||
stats := arb.GetStats()
|
||||
fmt.Printf("Executions: %d, Win Rate: %.1f%%, PnL: $%s\n",
|
||||
stats.TotalExecutions,
|
||||
stats.WinRate * 100,
|
||||
stats.TotalPnL.StringFixed(2))
|
||||
|
||||
# Implementing TradingClient
|
||||
|
||||
To use the arbitrage system, implement the TradingClient interface:
|
||||
|
||||
type TradingClient interface {
|
||||
AggregatedOrderbook(ctx context.Context, symbol string) (*AggregatedBook, error)
|
||||
PlaceOrder(ctx context.Context, req OrderRequest) (*Order, error)
|
||||
GetConnectedVenues() []VenueInfo
|
||||
}
|
||||
*/
|
||||
package arbitrage
|
||||
@@ -0,0 +1,288 @@
|
||||
// Copyright 2024 Lux Partners Limited. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package trading
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// BenchmarkParseTradingPair measures trading pair parsing performance.
|
||||
// This is a critical path for every market data update.
|
||||
func BenchmarkParseTradingPair(b *testing.B) {
|
||||
symbols := []string{
|
||||
"BTC-USDC",
|
||||
"ETHUSDT",
|
||||
"LUX/USD",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ParseTradingPair(symbols[i%len(symbols)])
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkOrderbookAddBid measures orderbook bid insertion performance.
|
||||
func BenchmarkOrderbookAddBid(b *testing.B) {
|
||||
book := NewOrderbook("BTC-USDC", "test")
|
||||
price := decimal.NewFromFloat(50000)
|
||||
qty := decimal.NewFromFloat(1.0)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
book.AddBid(price, qty)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkOrderbookSort measures orderbook sorting performance.
|
||||
// Sorting is required after orderbook updates.
|
||||
func BenchmarkOrderbookSort(b *testing.B) {
|
||||
for _, depth := range []int{10, 100, 1000} {
|
||||
b.Run(itoa(depth)+"_levels", func(b *testing.B) {
|
||||
book := NewOrderbook("BTC-USDC", "test")
|
||||
basePrice := 50000.0
|
||||
|
||||
for i := 0; i < depth; i++ {
|
||||
book.AddBid(decimal.NewFromFloat(basePrice-float64(i)), decimal.NewFromFloat(1.0))
|
||||
book.AddAsk(decimal.NewFromFloat(basePrice+float64(i)+1), decimal.NewFromFloat(1.0))
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
book.Sort()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkOrderbookBestBid measures best bid retrieval performance.
|
||||
// This is called frequently for spread and arbitrage calculations.
|
||||
func BenchmarkOrderbookBestBid(b *testing.B) {
|
||||
book := NewOrderbook("BTC-USDC", "test")
|
||||
for i := 0; i < 100; i++ {
|
||||
book.AddBid(decimal.NewFromFloat(50000-float64(i)), decimal.NewFromFloat(1.0))
|
||||
}
|
||||
book.Sort()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
book.BestBid()
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkOrderbookVwapBuy measures VWAP calculation performance.
|
||||
// VWAP is used for large order execution planning.
|
||||
func BenchmarkOrderbookVwapBuy(b *testing.B) {
|
||||
book := NewOrderbook("BTC-USDC", "test")
|
||||
for i := 0; i < 100; i++ {
|
||||
book.AddAsk(decimal.NewFromFloat(50000+float64(i)*10), decimal.NewFromFloat(1.0))
|
||||
}
|
||||
book.Sort()
|
||||
|
||||
qty := decimal.NewFromFloat(50)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
book.VwapBuy(qty)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkBlackScholes measures Black-Scholes pricing performance.
|
||||
// Options pricing is computationally intensive.
|
||||
func BenchmarkBlackScholes(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
BlackScholes(100, 100, 1, 0.05, 0.2, true)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkCalculateGreeks measures Greek calculation performance.
|
||||
func BenchmarkCalculateGreeks(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
CalculateGreeks(100, 100, 1, 0.05, 0.2, true)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkImpliedVolatility measures IV calculation performance.
|
||||
// IV calculation uses Newton-Raphson iteration.
|
||||
func BenchmarkImpliedVolatility(b *testing.B) {
|
||||
price := BlackScholes(100, 100, 1, 0.05, 0.2, true)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ImpliedVolatility(price, 100, 100, 1, 0.05, true)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkConstantProductPrice measures AMM pricing performance.
|
||||
func BenchmarkConstantProductPrice(b *testing.B) {
|
||||
reserveX := decimal.NewFromFloat(1000)
|
||||
reserveY := decimal.NewFromFloat(2000000)
|
||||
amountIn := decimal.NewFromFloat(10)
|
||||
feeRate := decimal.NewFromFloat(0.003)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ConstantProductPrice(reserveX, reserveY, amountIn, feeRate, true)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkRiskManagerValidateOrder measures order validation performance.
|
||||
// Every order goes through risk validation.
|
||||
func BenchmarkRiskManagerValidateOrder(b *testing.B) {
|
||||
config := RiskConfig{
|
||||
Enabled: true,
|
||||
MaxOrderSize: decimal.NewFromFloat(1000),
|
||||
MaxPositionSize: decimal.NewFromFloat(10000),
|
||||
MaxOpenOrders: 100,
|
||||
}
|
||||
|
||||
rm := NewRiskManager(config)
|
||||
order := OrderRequest{
|
||||
Symbol: "BTC-USDC",
|
||||
Side: SideBuy,
|
||||
Quantity: decimal.NewFromFloat(5),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
rm.ValidateOrder(order)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkRiskManagerUpdatePosition measures position update performance.
|
||||
func BenchmarkRiskManagerUpdatePosition(b *testing.B) {
|
||||
config := RiskConfig{Enabled: true}
|
||||
rm := NewRiskManager(config)
|
||||
qty := decimal.NewFromFloat(1)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if i%2 == 0 {
|
||||
rm.UpdatePosition("BTC", qty, SideBuy)
|
||||
} else {
|
||||
rm.UpdatePosition("BTC", qty, SideSell)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkVolatility measures volatility calculation performance.
|
||||
func BenchmarkVolatility(b *testing.B) {
|
||||
returns := make([]float64, 252)
|
||||
for i := range returns {
|
||||
returns[i] = 0.01 * float64(i%10-5)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Volatility(returns, true, 252)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkSharpeRatio measures Sharpe ratio calculation performance.
|
||||
func BenchmarkSharpeRatio(b *testing.B) {
|
||||
returns := make([]float64, 252)
|
||||
for i := range returns {
|
||||
returns[i] = 0.01 * float64(i%10-5)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
SharpeRatio(returns, 0.02, 252)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkMaxDrawdown measures max drawdown calculation performance.
|
||||
func BenchmarkMaxDrawdown(b *testing.B) {
|
||||
prices := make([]float64, 1000)
|
||||
for i := range prices {
|
||||
prices[i] = 100 + float64(i%50-25)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
MaxDrawdown(prices)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkVaR measures Value at Risk calculation performance.
|
||||
func BenchmarkVaR(b *testing.B) {
|
||||
returns := make([]float64, 1000)
|
||||
for i := range returns {
|
||||
returns[i] = 0.01 * float64(i%20-10)
|
||||
}
|
||||
|
||||
b.Run("historical", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
VaR(returns, 0.95, "historical")
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("parametric", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
VaR(returns, 0.95, "parametric")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkAggregatedOrderbook measures orderbook aggregation performance.
|
||||
// Aggregation is done on every market data update in multi-venue mode.
|
||||
func BenchmarkAggregatedOrderbook(b *testing.B) {
|
||||
// Create orderbooks for 3 venues
|
||||
books := make([]*Orderbook, 3)
|
||||
venues := []string{"lx_dex", "binance", "mexc"}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
books[i] = NewOrderbook("BTC-USDC", venues[i])
|
||||
for j := 0; j < 10; j++ {
|
||||
books[i].AddBid(decimal.NewFromFloat(50000-float64(j*10+i)), decimal.NewFromFloat(1.0))
|
||||
books[i].AddAsk(decimal.NewFromFloat(50100+float64(j*10+i)), decimal.NewFromFloat(1.0))
|
||||
}
|
||||
books[i].Sort()
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
agg := NewAggregatedOrderbook("BTC-USDC")
|
||||
for _, book := range books {
|
||||
agg.AddOrderbook(book)
|
||||
}
|
||||
agg.BestBid()
|
||||
agg.BestAsk()
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkDecimalOperations measures decimal arithmetic performance.
|
||||
// Decimal operations are ubiquitous in trading calculations.
|
||||
func BenchmarkDecimalOperations(b *testing.B) {
|
||||
a := decimal.NewFromFloat(50000.12345678)
|
||||
c := decimal.NewFromFloat(1.23456789)
|
||||
|
||||
b.Run("add", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
a.Add(c)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("mul", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
a.Mul(c)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("div", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
a.Div(c)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("cmp", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
a.GreaterThan(c)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright 2024 Lux Partners Limited. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
Package trading provides a unified high-frequency trading SDK with multi-venue support.
|
||||
|
||||
The SDK abstracts multiple trading venues behind a single interface:
|
||||
- Native LX DEX (Central Limit Order Book)
|
||||
- Native LX AMM (Automated Market Maker)
|
||||
- CCXT exchanges (Binance, MEXC, OKX, etc.) via HTTP gateway
|
||||
- Hummingbot Gateway connectors (Uniswap, PancakeSwap, etc.)
|
||||
|
||||
# Quick Start
|
||||
|
||||
Create a client with venue configurations:
|
||||
|
||||
config := trading.NewConfig().
|
||||
WithNative("lx_dex", trading.NewLxDexConfig("https://api.lx.exchange")).
|
||||
WithCcxt("binance", trading.NewCcxtConfig("binance").
|
||||
WithCredentials(apiKey, apiSecret))
|
||||
|
||||
client := trading.NewClient(config)
|
||||
ctx := context.Background()
|
||||
|
||||
if err := client.Connect(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
# Market Data
|
||||
|
||||
Get orderbooks from single or multiple venues:
|
||||
|
||||
// Single venue orderbook
|
||||
book, err := client.Orderbook(ctx, "BTC-USDC", trading.WithVenue("lx_dex"))
|
||||
|
||||
// Aggregated orderbook from all venues
|
||||
aggBook, err := client.AggregatedOrderbook(ctx, "BTC-USDC")
|
||||
|
||||
# Order Execution
|
||||
|
||||
Place orders with automatic smart routing or target specific venues:
|
||||
|
||||
// Smart routed market buy
|
||||
order, err := client.Buy(ctx, "BTC-USDC", decimal.NewFromFloat(0.1))
|
||||
|
||||
// Limit order on specific venue
|
||||
order, err := client.LimitBuy(ctx, "BTC-USDC",
|
||||
decimal.NewFromFloat(0.1),
|
||||
decimal.NewFromFloat(50000),
|
||||
trading.WithVenue("binance"))
|
||||
|
||||
# Execution Algorithms
|
||||
|
||||
Built-in execution algorithms for large orders:
|
||||
|
||||
// TWAP execution over 10 minutes
|
||||
twap := trading.NewTwapExecutor(client, trading.TwapConfig{
|
||||
Symbol: "BTC-USDC",
|
||||
Side: trading.SideBuy,
|
||||
TotalQuantity: decimal.NewFromFloat(10),
|
||||
DurationSeconds: 600,
|
||||
NumSlices: 20,
|
||||
})
|
||||
orders, err := twap.Execute(ctx)
|
||||
|
||||
# Risk Management
|
||||
|
||||
Built-in risk controls:
|
||||
|
||||
config.Risk = trading.RiskConfig{
|
||||
Enabled: true,
|
||||
MaxPositionSize: decimal.NewFromFloat(100000),
|
||||
MaxOrderSize: decimal.NewFromFloat(10000),
|
||||
MaxDailyLoss: decimal.NewFromFloat(5000),
|
||||
KillSwitchEnabled: true,
|
||||
}
|
||||
|
||||
# AMM Operations
|
||||
|
||||
For AMM venues, swap and liquidity operations are available:
|
||||
|
||||
// Get swap quote
|
||||
quote, err := client.Quote(ctx, "ETH", "USDC",
|
||||
decimal.NewFromFloat(1), true, "lx_amm")
|
||||
|
||||
// Execute swap with slippage tolerance
|
||||
trade, err := client.Swap(ctx, "ETH", "USDC",
|
||||
decimal.NewFromFloat(1), true,
|
||||
decimal.NewFromFloat(0.005), "lx_amm")
|
||||
|
||||
# Thread Safety
|
||||
|
||||
The Client is safe for concurrent use. All methods properly synchronize
|
||||
access to shared state using read-write mutexes.
|
||||
|
||||
# Error Handling
|
||||
|
||||
All errors are wrapped with context using fmt.Errorf with %w for proper
|
||||
error chain inspection:
|
||||
|
||||
if errors.Is(err, trading.ErrVenueNotFound) {
|
||||
// Handle venue not found
|
||||
}
|
||||
|
||||
var tradingErr *trading.TradingError
|
||||
if errors.As(err, &tradingErr) {
|
||||
if tradingErr.IsRetryable() {
|
||||
// Retry the operation
|
||||
}
|
||||
}
|
||||
*/
|
||||
package trading
|
||||
+97
-26
@@ -1,4 +1,7 @@
|
||||
// Package trading provides a unified HFT trading SDK with multi-venue support.
|
||||
// Copyright 2024 Lux Partners Limited. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package trading
|
||||
|
||||
import (
|
||||
@@ -6,29 +9,71 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Standard errors.
|
||||
// Standard sentinel errors for the trading SDK.
|
||||
// Use errors.Is to check for these errors in error chains.
|
||||
var (
|
||||
ErrNotConnected = errors.New("not connected to venue")
|
||||
ErrVenueNotFound = errors.New("venue not found")
|
||||
ErrOrderNotFound = errors.New("order not found")
|
||||
ErrInvalidSymbol = errors.New("invalid trading symbol")
|
||||
ErrInvalidQuantity = errors.New("invalid quantity")
|
||||
ErrInvalidPrice = errors.New("invalid price")
|
||||
ErrInsufficientBalance = errors.New("insufficient balance")
|
||||
ErrRateLimited = errors.New("rate limited")
|
||||
ErrTimeout = errors.New("request timeout")
|
||||
ErrCancelled = errors.New("operation cancelled")
|
||||
// ErrNotConnected indicates the venue adapter is not connected.
|
||||
ErrNotConnected = errors.New("trading: not connected to venue")
|
||||
|
||||
// ErrVenueNotFound indicates the requested venue does not exist.
|
||||
ErrVenueNotFound = errors.New("trading: venue not found")
|
||||
|
||||
// ErrOrderNotFound indicates the order does not exist.
|
||||
ErrOrderNotFound = errors.New("trading: order not found")
|
||||
|
||||
// ErrInvalidSymbol indicates an invalid trading symbol format.
|
||||
ErrInvalidSymbol = errors.New("trading: invalid trading symbol")
|
||||
|
||||
// ErrInvalidQuantity indicates an invalid order quantity.
|
||||
ErrInvalidQuantity = errors.New("trading: invalid quantity")
|
||||
|
||||
// ErrInvalidPrice indicates an invalid order price.
|
||||
ErrInvalidPrice = errors.New("trading: invalid price")
|
||||
|
||||
// ErrInsufficientBalance indicates insufficient balance for the operation.
|
||||
ErrInsufficientBalance = errors.New("trading: insufficient balance")
|
||||
|
||||
// ErrRateLimited indicates the request was rate limited by the venue.
|
||||
ErrRateLimited = errors.New("trading: rate limited")
|
||||
|
||||
// ErrTimeout indicates the operation timed out.
|
||||
ErrTimeout = errors.New("trading: request timeout")
|
||||
|
||||
// ErrCancelled indicates the operation was cancelled.
|
||||
ErrCancelled = errors.New("trading: operation cancelled")
|
||||
)
|
||||
|
||||
// TradingError represents a trading-specific error.
|
||||
// TradingError represents a trading-specific error with rich context.
|
||||
// It implements the error interface and supports error wrapping via Unwrap.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// err := NewTradingError("INVALID_ORDER", "order quantity too small").
|
||||
// WithVenue("binance").
|
||||
// WithCause(originalErr).
|
||||
// WithRetryable()
|
||||
//
|
||||
// if err.IsRetryable() {
|
||||
// // retry logic
|
||||
// }
|
||||
type TradingError struct {
|
||||
Code string
|
||||
Message string
|
||||
Venue string
|
||||
// Code is a machine-readable error code (e.g., "RATE_LIMITED", "INVALID_ORDER").
|
||||
Code string
|
||||
|
||||
// Message is a human-readable error description.
|
||||
Message string
|
||||
|
||||
// Venue is the trading venue where the error occurred.
|
||||
Venue string
|
||||
|
||||
// Retryable indicates if the operation can be retried.
|
||||
Retryable bool
|
||||
Cause error
|
||||
|
||||
// Cause is the underlying error that caused this error.
|
||||
Cause error
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e *TradingError) Error() string {
|
||||
if e.Venue != "" {
|
||||
return fmt.Sprintf("[%s] %s: %s", e.Venue, e.Code, e.Message)
|
||||
@@ -36,16 +81,18 @@ func (e *TradingError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying cause for use with errors.Is and errors.As.
|
||||
func (e *TradingError) Unwrap() error {
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
// IsRetryable returns true if the error is retryable.
|
||||
// IsRetryable returns true if the error indicates a transient failure
|
||||
// that may succeed on retry.
|
||||
func (e *TradingError) IsRetryable() bool {
|
||||
return e.Retryable
|
||||
}
|
||||
|
||||
// NewTradingError creates a new trading error.
|
||||
// NewTradingError creates a new TradingError with the given code and message.
|
||||
func NewTradingError(code, message string) *TradingError {
|
||||
return &TradingError{
|
||||
Code: code,
|
||||
@@ -53,36 +100,54 @@ func NewTradingError(code, message string) *TradingError {
|
||||
}
|
||||
}
|
||||
|
||||
// WithVenue adds venue information to the error.
|
||||
// WithVenue returns a copy of the error with venue information added.
|
||||
func (e *TradingError) WithVenue(venue string) *TradingError {
|
||||
e.Venue = venue
|
||||
return e
|
||||
}
|
||||
|
||||
// WithRetryable marks the error as retryable.
|
||||
// WithRetryable returns a copy of the error marked as retryable.
|
||||
func (e *TradingError) WithRetryable() *TradingError {
|
||||
e.Retryable = true
|
||||
return e
|
||||
}
|
||||
|
||||
// WithCause adds a cause to the error.
|
||||
// WithCause returns a copy of the error with the underlying cause set.
|
||||
// This enables error chain inspection via errors.Is and errors.As.
|
||||
func (e *TradingError) WithCause(cause error) *TradingError {
|
||||
e.Cause = cause
|
||||
return e
|
||||
}
|
||||
|
||||
// Wrap creates a new TradingError that wraps the given error.
|
||||
// This is a convenience function for wrapping errors with context.
|
||||
func Wrap(err error, code, message string) *TradingError {
|
||||
return &TradingError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Cause: err,
|
||||
}
|
||||
}
|
||||
|
||||
// RiskError represents a risk limit exceeded error.
|
||||
// It provides details about which limit was exceeded and the current/max values.
|
||||
type RiskError struct {
|
||||
Limit string
|
||||
// Limit is the name of the exceeded limit (e.g., "position_size", "daily_loss").
|
||||
Limit string
|
||||
|
||||
// Current is the current value that triggered the limit.
|
||||
Current string
|
||||
Max string
|
||||
|
||||
// Max is the maximum allowed value.
|
||||
Max string
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e *RiskError) Error() string {
|
||||
return fmt.Sprintf("risk limit exceeded: %s (current: %s, max: %s)", e.Limit, e.Current, e.Max)
|
||||
return fmt.Sprintf("trading: risk limit exceeded: %s (current: %s, max: %s)", e.Limit, e.Current, e.Max)
|
||||
}
|
||||
|
||||
// NewRiskError creates a new risk error.
|
||||
// NewRiskError creates a new RiskError for the given limit.
|
||||
func NewRiskError(limit, current, max string) *RiskError {
|
||||
return &RiskError{
|
||||
Limit: limit,
|
||||
@@ -90,3 +155,9 @@ func NewRiskError(limit, current, max string) *RiskError {
|
||||
Max: max,
|
||||
}
|
||||
}
|
||||
|
||||
// IsRiskError returns true if err is or wraps a RiskError.
|
||||
func IsRiskError(err error) bool {
|
||||
var riskErr *RiskError
|
||||
return errors.As(err, &riskErr)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
// Copyright 2024 Lux Partners Limited. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package trading_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
trading "github.com/luxfi/trading"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// ExampleParseTradingPair demonstrates parsing various trading pair formats.
|
||||
func ExampleParseTradingPair() {
|
||||
// The SDK supports multiple trading pair formats
|
||||
pairs := []string{
|
||||
"BTC-USDC", // Dash separator
|
||||
"ETH/USDT", // Slash separator (CCXT style)
|
||||
"LUX_USD", // Underscore separator
|
||||
"BTCUSDT", // No separator (exchange native)
|
||||
}
|
||||
|
||||
for _, p := range pairs {
|
||||
pair := trading.ParseTradingPair(p)
|
||||
fmt.Printf("%s -> Base: %s, Quote: %s\n", p, pair.Base, pair.Quote)
|
||||
}
|
||||
// Output:
|
||||
// BTC-USDC -> Base: BTC, Quote: USDC
|
||||
// ETH/USDT -> Base: ETH, Quote: USDT
|
||||
// LUX_USD -> Base: LUX, Quote: USD
|
||||
// BTCUSDT -> Base: BTC, Quote: USDT
|
||||
}
|
||||
|
||||
// ExampleTradingPair demonstrates TradingPair operations.
|
||||
func ExampleTradingPair() {
|
||||
pair := trading.TradingPair{Base: "BTC", Quote: "USDC"}
|
||||
|
||||
// Default string format uses dash separator
|
||||
fmt.Println("String:", pair.String())
|
||||
|
||||
// CCXT format uses slash separator
|
||||
fmt.Println("CCXT:", pair.ToCCXT())
|
||||
// Output:
|
||||
// String: BTC-USDC
|
||||
// CCXT: BTC/USDC
|
||||
}
|
||||
|
||||
// ExampleOrderbook demonstrates orderbook operations.
|
||||
func ExampleOrderbook() {
|
||||
book := trading.NewOrderbook("BTC-USDC", "lx_dex")
|
||||
|
||||
// Add bid levels (buy orders)
|
||||
book.AddBid(decimal.NewFromFloat(50000), decimal.NewFromFloat(1.0))
|
||||
book.AddBid(decimal.NewFromFloat(49900), decimal.NewFromFloat(2.0))
|
||||
book.AddBid(decimal.NewFromFloat(49800), decimal.NewFromFloat(3.0))
|
||||
|
||||
// Add ask levels (sell orders)
|
||||
book.AddAsk(decimal.NewFromFloat(50100), decimal.NewFromFloat(0.5))
|
||||
book.AddAsk(decimal.NewFromFloat(50200), decimal.NewFromFloat(1.5))
|
||||
book.AddAsk(decimal.NewFromFloat(50300), decimal.NewFromFloat(2.5))
|
||||
|
||||
// Sort to ensure proper ordering
|
||||
book.Sort()
|
||||
|
||||
// Get market metrics
|
||||
fmt.Printf("Best Bid: %s\n", book.BestBid().StringFixed(2))
|
||||
fmt.Printf("Best Ask: %s\n", book.BestAsk().StringFixed(2))
|
||||
fmt.Printf("Mid Price: %s\n", book.MidPrice().StringFixed(2))
|
||||
fmt.Printf("Spread: %s\n", book.Spread().StringFixed(2))
|
||||
// Output:
|
||||
// Best Bid: 50000.00
|
||||
// Best Ask: 50100.00
|
||||
// Mid Price: 50050.00
|
||||
// Spread: 100.00
|
||||
}
|
||||
|
||||
// ExampleOrderbook_VwapBuy demonstrates VWAP calculation for buying.
|
||||
func ExampleOrderbook_VwapBuy() {
|
||||
book := trading.NewOrderbook("BTC-USDC", "lx_dex")
|
||||
|
||||
// Add ask levels: we buy from asks
|
||||
book.AddAsk(decimal.NewFromFloat(50000), decimal.NewFromFloat(1.0))
|
||||
book.AddAsk(decimal.NewFromFloat(50100), decimal.NewFromFloat(2.0))
|
||||
book.AddAsk(decimal.NewFromFloat(50200), decimal.NewFromFloat(3.0))
|
||||
book.Sort()
|
||||
|
||||
// Calculate VWAP for buying 2.5 BTC
|
||||
// Will sweep: 1.0 @ 50000, 1.5 @ 50100
|
||||
// VWAP = (1.0*50000 + 1.5*50100) / 2.5 = 125150 / 2.5 = 50060
|
||||
vwap := book.VwapBuy(decimal.NewFromFloat(2.5))
|
||||
fmt.Printf("VWAP for buying 2.5 BTC: %s\n", vwap.StringFixed(2))
|
||||
// Output:
|
||||
// VWAP for buying 2.5 BTC: 50060.00
|
||||
}
|
||||
|
||||
// ExampleNewMarketOrder demonstrates creating a market order.
|
||||
func ExampleNewMarketOrder() {
|
||||
order := trading.NewMarketOrder("BTC-USDC", trading.SideBuy, decimal.NewFromFloat(0.5))
|
||||
|
||||
fmt.Printf("Symbol: %s\n", order.Symbol)
|
||||
fmt.Printf("Side: %s\n", order.Side)
|
||||
fmt.Printf("Type: %s\n", order.OrderType)
|
||||
fmt.Printf("Quantity: %s\n", order.Quantity.StringFixed(4))
|
||||
// Output:
|
||||
// Symbol: BTC-USDC
|
||||
// Side: buy
|
||||
// Type: market
|
||||
// Quantity: 0.5000
|
||||
}
|
||||
|
||||
// ExampleNewLimitOrder demonstrates creating a limit order.
|
||||
func ExampleNewLimitOrder() {
|
||||
price := decimal.NewFromFloat(50000)
|
||||
order := trading.NewLimitOrder("BTC-USDC", trading.SideSell, decimal.NewFromFloat(1.0), price)
|
||||
|
||||
fmt.Printf("Symbol: %s\n", order.Symbol)
|
||||
fmt.Printf("Side: %s\n", order.Side)
|
||||
fmt.Printf("Type: %s\n", order.OrderType)
|
||||
fmt.Printf("Quantity: %s\n", order.Quantity.StringFixed(4))
|
||||
fmt.Printf("Price: %s\n", order.Price.StringFixed(2))
|
||||
fmt.Printf("TimeInForce: %s\n", order.TimeInForce)
|
||||
// Output:
|
||||
// Symbol: BTC-USDC
|
||||
// Side: sell
|
||||
// Type: limit
|
||||
// Quantity: 1.0000
|
||||
// Price: 50000.00
|
||||
// TimeInForce: GTC
|
||||
}
|
||||
|
||||
// ExampleRiskManager demonstrates risk management operations.
|
||||
func ExampleRiskManager() {
|
||||
config := trading.RiskConfig{
|
||||
Enabled: true,
|
||||
MaxOrderSize: decimal.NewFromFloat(10),
|
||||
MaxOpenOrders: 5,
|
||||
}
|
||||
|
||||
rm := trading.NewRiskManager(config)
|
||||
|
||||
// Validate a valid order
|
||||
order := trading.OrderRequest{
|
||||
Symbol: "BTC-USDC",
|
||||
Side: trading.SideBuy,
|
||||
Quantity: decimal.NewFromFloat(5),
|
||||
}
|
||||
|
||||
if err := rm.ValidateOrder(order); err != nil {
|
||||
fmt.Println("Order rejected:", err)
|
||||
} else {
|
||||
fmt.Println("Order accepted")
|
||||
}
|
||||
|
||||
// Try an oversized order
|
||||
order.Quantity = decimal.NewFromFloat(15)
|
||||
if err := rm.ValidateOrder(order); err != nil {
|
||||
fmt.Println("Large order rejected")
|
||||
}
|
||||
// Output:
|
||||
// Order accepted
|
||||
// Large order rejected
|
||||
}
|
||||
|
||||
// Example_killSwitch demonstrates the kill switch functionality.
|
||||
func Example_killSwitch() {
|
||||
config := trading.RiskConfig{
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
rm := trading.NewRiskManager(config)
|
||||
|
||||
fmt.Println("Kill switch active:", rm.IsKilled())
|
||||
|
||||
// Activate kill switch
|
||||
rm.Kill()
|
||||
fmt.Println("After Kill(): active:", rm.IsKilled())
|
||||
|
||||
// Any order will be rejected
|
||||
order := trading.OrderRequest{
|
||||
Symbol: "BTC-USDC",
|
||||
Side: trading.SideBuy,
|
||||
Quantity: decimal.NewFromFloat(1),
|
||||
}
|
||||
if err := rm.ValidateOrder(order); err != nil {
|
||||
fmt.Println("Order rejected due to kill switch")
|
||||
}
|
||||
|
||||
// Reset to resume trading
|
||||
rm.Reset()
|
||||
fmt.Println("After Reset(): active:", rm.IsKilled())
|
||||
// Output:
|
||||
// Kill switch active: false
|
||||
// After Kill(): active: true
|
||||
// Order rejected due to kill switch
|
||||
// After Reset(): active: false
|
||||
}
|
||||
|
||||
// ExampleConfig demonstrates SDK configuration.
|
||||
func ExampleConfig() {
|
||||
config := trading.NewConfig().
|
||||
WithNative("lx_dex", trading.NewLxDexConfig("https://api.lx.exchange")).
|
||||
WithCcxt("binance", trading.NewCcxtConfig("binance").
|
||||
WithCredentials("api_key", "api_secret")).
|
||||
WithSmartRouting(true).
|
||||
WithVenuePriority("lx_dex", "binance")
|
||||
|
||||
fmt.Printf("Native venues: %d\n", len(config.Native))
|
||||
fmt.Printf("CCXT venues: %d\n", len(config.Ccxt))
|
||||
fmt.Printf("Smart routing: %v\n", config.General.SmartRouting)
|
||||
// Output:
|
||||
// Native venues: 1
|
||||
// CCXT venues: 1
|
||||
// Smart routing: true
|
||||
}
|
||||
|
||||
// ExampleBlackScholes demonstrates options pricing.
|
||||
func ExampleBlackScholes() {
|
||||
// Price a call option:
|
||||
// Spot = $100, Strike = $100, Time = 1 year
|
||||
// Risk-free rate = 5%, Volatility = 20%
|
||||
callPrice := trading.BlackScholes(100, 100, 1, 0.05, 0.2, true)
|
||||
putPrice := trading.BlackScholes(100, 100, 1, 0.05, 0.2, false)
|
||||
|
||||
fmt.Printf("ATM Call Price: $%.2f\n", callPrice)
|
||||
fmt.Printf("ATM Put Price: $%.2f\n", putPrice)
|
||||
// Output:
|
||||
// ATM Call Price: $10.45
|
||||
// ATM Put Price: $5.57
|
||||
}
|
||||
|
||||
// ExampleCalculateGreeks demonstrates Greek calculations.
|
||||
func ExampleCalculateGreeks() {
|
||||
// Calculate Greeks for an ATM call option
|
||||
greeks := trading.CalculateGreeks(100, 100, 1, 0.05, 0.2, true)
|
||||
|
||||
fmt.Printf("Delta: %.4f\n", greeks.Delta)
|
||||
fmt.Printf("Gamma: %.4f\n", greeks.Gamma)
|
||||
fmt.Printf("Vega: %.4f\n", greeks.Vega)
|
||||
fmt.Printf("Theta: %.4f\n", greeks.Theta)
|
||||
// Output:
|
||||
// Delta: 0.6368
|
||||
// Gamma: 0.0188
|
||||
// Vega: 0.3752
|
||||
// Theta: -0.0176
|
||||
}
|
||||
|
||||
// ExampleConstantProductPrice demonstrates AMM pricing.
|
||||
func ExampleConstantProductPrice() {
|
||||
// Pool with 1000 ETH and 2,000,000 USDC
|
||||
reserveETH := decimal.NewFromFloat(1000)
|
||||
reserveUSDC := decimal.NewFromFloat(2000000)
|
||||
amountIn := decimal.NewFromFloat(10) // Swap 10 ETH
|
||||
feeRate := decimal.NewFromFloat(0.003) // 0.3% fee
|
||||
|
||||
// Swap ETH -> USDC
|
||||
output, price := trading.ConstantProductPrice(
|
||||
reserveETH, reserveUSDC, amountIn, feeRate, true)
|
||||
|
||||
fmt.Printf("Swap 10 ETH for: %s USDC\n", output.StringFixed(2))
|
||||
fmt.Printf("Effective price: %s USDC/ETH\n", price.StringFixed(2))
|
||||
// Output:
|
||||
// Swap 10 ETH for: 19743.16 USDC
|
||||
// Effective price: 1974.32 USDC/ETH
|
||||
}
|
||||
|
||||
// ExampleVolatility demonstrates volatility calculation.
|
||||
func ExampleVolatility() {
|
||||
// Daily returns over 5 days
|
||||
returns := []float64{0.01, -0.02, 0.015, -0.01, 0.02}
|
||||
|
||||
// Non-annualized volatility
|
||||
vol := trading.Volatility(returns, false, 252)
|
||||
fmt.Printf("Daily volatility: %.4f\n", vol)
|
||||
|
||||
// Annualized volatility (252 trading days)
|
||||
volAnnual := trading.Volatility(returns, true, 252)
|
||||
fmt.Printf("Annual volatility: %.4f\n", volAnnual)
|
||||
// Output:
|
||||
// Daily volatility: 0.0172
|
||||
// Annual volatility: 0.2727
|
||||
}
|
||||
|
||||
// ExampleMaxDrawdown demonstrates maximum drawdown calculation.
|
||||
func ExampleMaxDrawdown() {
|
||||
// Price series with a drawdown
|
||||
prices := []float64{100, 110, 105, 120, 90, 95, 100}
|
||||
|
||||
dd, peakIdx, troughIdx := trading.MaxDrawdown(prices)
|
||||
|
||||
fmt.Printf("Maximum Drawdown: %.2f%%\n", dd*100)
|
||||
fmt.Printf("Peak at index %d (price: %.0f)\n", peakIdx, prices[peakIdx])
|
||||
fmt.Printf("Trough at index %d (price: %.0f)\n", troughIdx, prices[troughIdx])
|
||||
// Output:
|
||||
// Maximum Drawdown: 25.00%
|
||||
// Peak at index 3 (price: 120)
|
||||
// Trough at index 4 (price: 90)
|
||||
}
|
||||
|
||||
// ExampleTradingError demonstrates error handling.
|
||||
func ExampleTradingError() {
|
||||
err := trading.NewTradingError("RATE_LIMITED", "too many requests").
|
||||
WithVenue("binance").
|
||||
WithRetryable()
|
||||
|
||||
fmt.Println("Error:", err.Error())
|
||||
fmt.Println("Retryable:", err.IsRetryable())
|
||||
// Output:
|
||||
// Error: [binance] RATE_LIMITED: too many requests
|
||||
// Retryable: true
|
||||
}
|
||||
|
||||
// ExampleNewAggregatedOrderbook demonstrates multi-venue orderbook aggregation.
|
||||
func ExampleNewAggregatedOrderbook() {
|
||||
agg := trading.NewAggregatedOrderbook("BTC-USDC")
|
||||
|
||||
// Add orderbook from venue 1
|
||||
book1 := trading.NewOrderbook("BTC-USDC", "lx_dex")
|
||||
book1.AddBid(decimal.NewFromFloat(50000), decimal.NewFromFloat(1.0))
|
||||
book1.AddAsk(decimal.NewFromFloat(50100), decimal.NewFromFloat(1.0))
|
||||
book1.Sort()
|
||||
agg.AddOrderbook(book1)
|
||||
|
||||
// Add orderbook from venue 2 with better prices
|
||||
book2 := trading.NewOrderbook("BTC-USDC", "binance")
|
||||
book2.AddBid(decimal.NewFromFloat(50050), decimal.NewFromFloat(0.5)) // Higher bid
|
||||
book2.AddAsk(decimal.NewFromFloat(50080), decimal.NewFromFloat(0.5)) // Lower ask
|
||||
book2.Sort()
|
||||
agg.AddOrderbook(book2)
|
||||
|
||||
// Best prices across venues
|
||||
bestBid := agg.BestBid()
|
||||
bestAsk := agg.BestAsk()
|
||||
|
||||
fmt.Printf("Best Bid: %s @ %s\n", bestBid.Price.StringFixed(2), bestBid.Venue)
|
||||
fmt.Printf("Best Ask: %s @ %s\n", bestAsk.Price.StringFixed(2), bestAsk.Venue)
|
||||
// Output:
|
||||
// Best Bid: 50050.00 @ binance
|
||||
// Best Ask: 50080.00 @ binance
|
||||
}
|
||||
@@ -0,0 +1,623 @@
|
||||
// Copyright 2024 Lux Partners Limited. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Example: Advanced Cross-Venue Arbitrage
|
||||
//
|
||||
// This example demonstrates sophisticated arbitrage strategies:
|
||||
// - Cross-venue price monitoring
|
||||
// - Triangular arbitrage detection
|
||||
// - Execution timing optimization
|
||||
// - Slippage and fee accounting
|
||||
// - Position tracking and hedging
|
||||
//
|
||||
// The strategy exploits price differences between LX DEX (fastest)
|
||||
// and other venues (CEX, external DEX) that are inherently slower.
|
||||
//
|
||||
// Run with: go run main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sort"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
trading "github.com/luxfi/trading"
|
||||
_ "github.com/luxfi/trading/adapters"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// AdvancedArbitrage implements sophisticated arbitrage detection and execution.
|
||||
type AdvancedArbitrage struct {
|
||||
client *trading.Client
|
||||
config ArbConfig
|
||||
|
||||
// State
|
||||
mu sync.RWMutex
|
||||
totalPnL decimal.Decimal
|
||||
executions []Execution
|
||||
opportunities int64
|
||||
skipped int64
|
||||
|
||||
// Control
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// ArbConfig configures the arbitrage system.
|
||||
type ArbConfig struct {
|
||||
// Symbols to monitor
|
||||
Symbols []string
|
||||
|
||||
// Minimum spread to trade (basis points)
|
||||
MinSpreadBps decimal.Decimal
|
||||
|
||||
// Minimum profit per trade (quote currency)
|
||||
MinProfit decimal.Decimal
|
||||
|
||||
// Maximum position per asset
|
||||
MaxPosition decimal.Decimal
|
||||
|
||||
// Fee rates per venue (used for profit calculation)
|
||||
VenueFees map[string]decimal.Decimal
|
||||
|
||||
// Venue priority (faster first)
|
||||
VenuePriority []string
|
||||
|
||||
// Scan interval
|
||||
ScanInterval time.Duration
|
||||
|
||||
// Execution timeout
|
||||
ExecutionTimeout time.Duration
|
||||
|
||||
// Maximum concurrent executions
|
||||
MaxConcurrent int
|
||||
|
||||
// Risk limits
|
||||
MaxDailyLoss decimal.Decimal
|
||||
MaxTradesPerHour int
|
||||
CooldownAfterLoss time.Duration
|
||||
}
|
||||
|
||||
// Opportunity represents a detected arbitrage opportunity.
|
||||
type Opportunity struct {
|
||||
ID string
|
||||
Symbol string
|
||||
Timestamp time.Time
|
||||
ExpiresAt time.Time
|
||||
|
||||
// Buy side (best ask)
|
||||
BuyVenue string
|
||||
BuyPrice decimal.Decimal
|
||||
BuyQuantity decimal.Decimal
|
||||
BuyFeeRate decimal.Decimal
|
||||
|
||||
// Sell side (best bid)
|
||||
SellVenue string
|
||||
SellPrice decimal.Decimal
|
||||
SellQuantity decimal.Decimal
|
||||
SellFeeRate decimal.Decimal
|
||||
|
||||
// Calculated metrics
|
||||
Spread decimal.Decimal
|
||||
SpreadBps decimal.Decimal
|
||||
MaxQuantity decimal.Decimal
|
||||
GrossProfit decimal.Decimal
|
||||
TotalFees decimal.Decimal
|
||||
NetProfit decimal.Decimal
|
||||
|
||||
// Quality score (0-1)
|
||||
Score float64
|
||||
}
|
||||
|
||||
// Execution represents an executed arbitrage.
|
||||
type Execution struct {
|
||||
Opportunity *Opportunity
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
BuyOrder *trading.Order
|
||||
SellOrder *trading.Order
|
||||
ActualProfit decimal.Decimal
|
||||
Status string
|
||||
Error error
|
||||
}
|
||||
|
||||
// DefaultArbConfig returns sensible defaults.
|
||||
func DefaultArbConfig() ArbConfig {
|
||||
return ArbConfig{
|
||||
Symbols: []string{"BTC-USDC", "ETH-USDC", "LUX-USDC"},
|
||||
MinSpreadBps: decimal.NewFromInt(15),
|
||||
MinProfit: decimal.NewFromFloat(5),
|
||||
MaxPosition: decimal.NewFromFloat(10000),
|
||||
VenueFees: map[string]decimal.Decimal{
|
||||
"lx_dex": decimal.NewFromFloat(0.001), // 0.1%
|
||||
"lx_amm": decimal.NewFromFloat(0.003), // 0.3%
|
||||
"binance": decimal.NewFromFloat(0.001), // 0.1%
|
||||
"mexc": decimal.NewFromFloat(0.002), // 0.2%
|
||||
},
|
||||
VenuePriority: []string{"lx_dex", "binance", "mexc", "lx_amm"},
|
||||
ScanInterval: 50 * time.Millisecond,
|
||||
ExecutionTimeout: 3 * time.Second,
|
||||
MaxConcurrent: 3,
|
||||
MaxDailyLoss: decimal.NewFromFloat(500),
|
||||
MaxTradesPerHour: 100,
|
||||
CooldownAfterLoss: 5 * time.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
// NewAdvancedArbitrage creates a new arbitrage system.
|
||||
func NewAdvancedArbitrage(client *trading.Client, config ArbConfig) *AdvancedArbitrage {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &AdvancedArbitrage{
|
||||
client: client,
|
||||
config: config,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins the arbitrage system.
|
||||
func (arb *AdvancedArbitrage) Start() {
|
||||
log.Println("Starting advanced arbitrage system")
|
||||
log.Printf("Monitoring: %v", arb.config.Symbols)
|
||||
log.Printf("Min spread: %s bps, Min profit: $%s",
|
||||
arb.config.MinSpreadBps, arb.config.MinProfit)
|
||||
|
||||
arb.wg.Add(1)
|
||||
go arb.scanLoop()
|
||||
}
|
||||
|
||||
// Stop stops the arbitrage system.
|
||||
func (arb *AdvancedArbitrage) Stop() {
|
||||
arb.cancel()
|
||||
arb.wg.Wait()
|
||||
arb.printFinalStats()
|
||||
}
|
||||
|
||||
// scanLoop continuously scans for opportunities.
|
||||
func (arb *AdvancedArbitrage) scanLoop() {
|
||||
defer arb.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(arb.config.ScanInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
semaphore := make(chan struct{}, arb.config.MaxConcurrent)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-arb.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
for _, symbol := range arb.config.Symbols {
|
||||
opp := arb.scan(symbol)
|
||||
if opp == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
arb.mu.Lock()
|
||||
arb.opportunities++
|
||||
arb.mu.Unlock()
|
||||
|
||||
// Try to execute if slot available
|
||||
select {
|
||||
case semaphore <- struct{}{}:
|
||||
go func(o *Opportunity) {
|
||||
defer func() { <-semaphore }()
|
||||
arb.execute(o)
|
||||
}(opp)
|
||||
default:
|
||||
arb.mu.Lock()
|
||||
arb.skipped++
|
||||
arb.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// scan looks for arbitrage opportunities in a symbol.
|
||||
func (arb *AdvancedArbitrage) scan(symbol string) *Opportunity {
|
||||
// Get aggregated orderbook
|
||||
book, err := arb.client.AggregatedOrderbook(arb.ctx, symbol)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
bestBid := book.BestBid()
|
||||
bestAsk := book.BestAsk()
|
||||
|
||||
if bestBid == nil || bestAsk == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check for cross-venue arbitrage
|
||||
// Profitable if: bid on venue A > ask on venue B
|
||||
if bestBid.Venue == bestAsk.Venue {
|
||||
// Same venue - need to find cross-venue opportunity
|
||||
return arb.findCrossVenueOpportunity(symbol, book)
|
||||
}
|
||||
|
||||
if bestBid.Price.LessThanOrEqual(bestAsk.Price) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return arb.buildOpportunity(symbol, bestBid, bestAsk)
|
||||
}
|
||||
|
||||
// findCrossVenueOpportunity looks for opportunities across different venues.
|
||||
func (arb *AdvancedArbitrage) findCrossVenueOpportunity(symbol string, book *trading.AggregatedOrderbook) *Opportunity {
|
||||
// Get all bids and asks by venue
|
||||
type venueLevel struct {
|
||||
venue string
|
||||
price decimal.Decimal
|
||||
quantity decimal.Decimal
|
||||
}
|
||||
|
||||
var bids, asks []venueLevel
|
||||
|
||||
// Collect levels from aggregated book
|
||||
for priceStr, levels := range book.Bids {
|
||||
for _, level := range levels {
|
||||
price, _ := decimal.NewFromString(priceStr)
|
||||
bids = append(bids, venueLevel{
|
||||
venue: level.Venue,
|
||||
price: price,
|
||||
quantity: level.Quantity,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for priceStr, levels := range book.Asks {
|
||||
for _, level := range levels {
|
||||
price, _ := decimal.NewFromString(priceStr)
|
||||
asks = append(asks, venueLevel{
|
||||
venue: level.Venue,
|
||||
price: price,
|
||||
quantity: level.Quantity,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort: bids descending by price, asks ascending by price
|
||||
sort.Slice(bids, func(i, j int) bool {
|
||||
return bids[i].price.GreaterThan(bids[j].price)
|
||||
})
|
||||
sort.Slice(asks, func(i, j int) bool {
|
||||
return asks[i].price.LessThan(asks[j].price)
|
||||
})
|
||||
|
||||
// Find best cross-venue opportunity
|
||||
var bestOpp *Opportunity
|
||||
bestProfit := decimal.Zero
|
||||
|
||||
for _, bid := range bids {
|
||||
for _, ask := range asks {
|
||||
if bid.venue == ask.venue {
|
||||
continue // Same venue
|
||||
}
|
||||
|
||||
if bid.price.LessThanOrEqual(ask.price) {
|
||||
continue // No profit
|
||||
}
|
||||
|
||||
opp := arb.buildOpportunityFromLevels(symbol,
|
||||
bid.venue, bid.price, bid.quantity,
|
||||
ask.venue, ask.price, ask.quantity)
|
||||
|
||||
if opp != nil && opp.NetProfit.GreaterThan(bestProfit) {
|
||||
bestOpp = opp
|
||||
bestProfit = opp.NetProfit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestOpp
|
||||
}
|
||||
|
||||
// buildOpportunity creates an opportunity from aggregated levels.
|
||||
func (arb *AdvancedArbitrage) buildOpportunity(symbol string,
|
||||
bestBid, bestAsk *trading.AggregatedLevel) *Opportunity {
|
||||
|
||||
return arb.buildOpportunityFromLevels(symbol,
|
||||
bestBid.Venue, bestBid.Price, bestBid.Quantity,
|
||||
bestAsk.Venue, bestAsk.Price, bestAsk.Quantity)
|
||||
}
|
||||
|
||||
// buildOpportunityFromLevels creates an opportunity from price levels.
|
||||
func (arb *AdvancedArbitrage) buildOpportunityFromLevels(symbol,
|
||||
sellVenue string, sellPrice, sellQty decimal.Decimal,
|
||||
buyVenue string, buyPrice, buyQty decimal.Decimal) *Opportunity {
|
||||
|
||||
if buyPrice.IsZero() {
|
||||
return nil
|
||||
}
|
||||
|
||||
spread := sellPrice.Sub(buyPrice)
|
||||
spreadBps := spread.Div(buyPrice).Mul(decimal.NewFromInt(10000))
|
||||
|
||||
if spreadBps.LessThan(arb.config.MinSpreadBps) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Calculate max quantity
|
||||
maxQty := decimal.Min(buyQty, sellQty)
|
||||
maxQty = decimal.Min(maxQty, arb.config.MaxPosition)
|
||||
|
||||
// Calculate fees
|
||||
buyFee := arb.config.VenueFees[buyVenue]
|
||||
if buyFee.IsZero() {
|
||||
buyFee = decimal.NewFromFloat(0.002) // Default 0.2%
|
||||
}
|
||||
sellFee := arb.config.VenueFees[sellVenue]
|
||||
if sellFee.IsZero() {
|
||||
sellFee = decimal.NewFromFloat(0.002)
|
||||
}
|
||||
|
||||
buyValue := buyPrice.Mul(maxQty)
|
||||
sellValue := sellPrice.Mul(maxQty)
|
||||
grossProfit := sellValue.Sub(buyValue)
|
||||
|
||||
totalFees := buyValue.Mul(buyFee).Add(sellValue.Mul(sellFee))
|
||||
netProfit := grossProfit.Sub(totalFees)
|
||||
|
||||
if netProfit.LessThan(arb.config.MinProfit) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Calculate quality score
|
||||
score := arb.calculateScore(spreadBps, netProfit, maxQty)
|
||||
|
||||
return &Opportunity{
|
||||
ID: fmt.Sprintf("arb-%s-%d", symbol, time.Now().UnixNano()),
|
||||
Symbol: symbol,
|
||||
Timestamp: time.Now(),
|
||||
ExpiresAt: time.Now().Add(2 * time.Second),
|
||||
BuyVenue: buyVenue,
|
||||
BuyPrice: buyPrice,
|
||||
BuyQuantity: buyQty,
|
||||
BuyFeeRate: buyFee,
|
||||
SellVenue: sellVenue,
|
||||
SellPrice: sellPrice,
|
||||
SellQuantity: sellQty,
|
||||
SellFeeRate: sellFee,
|
||||
Spread: spread,
|
||||
SpreadBps: spreadBps,
|
||||
MaxQuantity: maxQty,
|
||||
GrossProfit: grossProfit,
|
||||
TotalFees: totalFees,
|
||||
NetProfit: netProfit,
|
||||
Score: score,
|
||||
}
|
||||
}
|
||||
|
||||
// calculateScore calculates opportunity quality score.
|
||||
func (arb *AdvancedArbitrage) calculateScore(spreadBps, profit, quantity decimal.Decimal) float64 {
|
||||
// Weighted scoring
|
||||
spreadScore := spreadBps.Div(decimal.NewFromInt(100)).InexactFloat64() // Higher spread = better
|
||||
profitScore := profit.Div(decimal.NewFromInt(100)).InexactFloat64() // Higher profit = better
|
||||
sizeScore := quantity.Div(arb.config.MaxPosition).InexactFloat64() // Larger = better
|
||||
|
||||
// Weights
|
||||
score := spreadScore*0.3 + profitScore*0.5 + sizeScore*0.2
|
||||
|
||||
// Clamp to 0-1
|
||||
if score > 1 {
|
||||
score = 1
|
||||
}
|
||||
if score < 0 {
|
||||
score = 0
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
// execute executes an arbitrage opportunity.
|
||||
func (arb *AdvancedArbitrage) execute(opp *Opportunity) {
|
||||
// Check expiry
|
||||
if time.Now().After(opp.ExpiresAt) {
|
||||
return
|
||||
}
|
||||
|
||||
exec := &Execution{
|
||||
Opportunity: opp,
|
||||
StartTime: time.Now(),
|
||||
Status: "executing",
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(arb.ctx, arb.config.ExecutionTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Execute both legs simultaneously
|
||||
var wg sync.WaitGroup
|
||||
var buyOrder, sellOrder trading.Order
|
||||
var buyErr, sellErr error
|
||||
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
buyOrder, buyErr = arb.client.LimitBuy(ctx,
|
||||
opp.Symbol,
|
||||
opp.MaxQuantity,
|
||||
opp.BuyPrice,
|
||||
trading.WithVenue(opp.BuyVenue))
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
sellOrder, sellErr = arb.client.LimitSell(ctx,
|
||||
opp.Symbol,
|
||||
opp.MaxQuantity,
|
||||
opp.SellPrice,
|
||||
trading.WithVenue(opp.SellVenue))
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
exec.EndTime = time.Now()
|
||||
exec.BuyOrder = &buyOrder
|
||||
exec.SellOrder = &sellOrder
|
||||
|
||||
// Handle errors
|
||||
if buyErr != nil || sellErr != nil {
|
||||
exec.Status = "failed"
|
||||
exec.Error = fmt.Errorf("buy: %v, sell: %v", buyErr, sellErr)
|
||||
log.Printf("Arbitrage failed: %v", exec.Error)
|
||||
arb.recordExecution(exec)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate actual profit
|
||||
buyValue := buyOrder.FilledQuantity.Mul(*buyOrder.AveragePrice)
|
||||
sellValue := sellOrder.FilledQuantity.Mul(*sellOrder.AveragePrice)
|
||||
exec.ActualProfit = sellValue.Sub(buyValue)
|
||||
|
||||
// Subtract fees
|
||||
for _, fee := range buyOrder.Fees {
|
||||
exec.ActualProfit = exec.ActualProfit.Sub(fee.Amount)
|
||||
}
|
||||
for _, fee := range sellOrder.Fees {
|
||||
exec.ActualProfit = exec.ActualProfit.Sub(fee.Amount)
|
||||
}
|
||||
|
||||
exec.Status = "completed"
|
||||
|
||||
log.Printf("Arbitrage executed: %s buy@%s(%s) sell@%s(%s) profit=$%s",
|
||||
opp.Symbol,
|
||||
opp.BuyVenue, opp.BuyPrice.StringFixed(2),
|
||||
opp.SellVenue, opp.SellPrice.StringFixed(2),
|
||||
exec.ActualProfit.StringFixed(2))
|
||||
|
||||
arb.recordExecution(exec)
|
||||
}
|
||||
|
||||
// recordExecution records an execution.
|
||||
func (arb *AdvancedArbitrage) recordExecution(exec *Execution) {
|
||||
arb.mu.Lock()
|
||||
defer arb.mu.Unlock()
|
||||
|
||||
arb.executions = append(arb.executions, *exec)
|
||||
if exec.Status == "completed" {
|
||||
arb.totalPnL = arb.totalPnL.Add(exec.ActualProfit)
|
||||
}
|
||||
}
|
||||
|
||||
// GetStats returns current statistics.
|
||||
func (arb *AdvancedArbitrage) GetStats() (opportunities, executed, skipped int64, pnl decimal.Decimal) {
|
||||
arb.mu.RLock()
|
||||
defer arb.mu.RUnlock()
|
||||
|
||||
return arb.opportunities, int64(len(arb.executions)), arb.skipped, arb.totalPnL
|
||||
}
|
||||
|
||||
// printFinalStats prints final statistics.
|
||||
func (arb *AdvancedArbitrage) printFinalStats() {
|
||||
arb.mu.RLock()
|
||||
defer arb.mu.RUnlock()
|
||||
|
||||
successful := 0
|
||||
for _, exec := range arb.executions {
|
||||
if exec.Status == "completed" && exec.ActualProfit.GreaterThan(decimal.Zero) {
|
||||
successful++
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\n=== Arbitrage Final Stats ===")
|
||||
fmt.Printf("Opportunities Found: %d\n", arb.opportunities)
|
||||
fmt.Printf("Executions Attempted: %d\n", len(arb.executions))
|
||||
fmt.Printf("Successful: %d\n", successful)
|
||||
fmt.Printf("Skipped (busy): %d\n", arb.skipped)
|
||||
fmt.Printf("Total PnL: $%s\n", arb.totalPnL.StringFixed(2))
|
||||
if len(arb.executions) > 0 {
|
||||
fmt.Printf("Win Rate: %.1f%%\n",
|
||||
float64(successful)/float64(len(arb.executions))*100)
|
||||
}
|
||||
fmt.Println("=============================")
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Create configuration
|
||||
config := trading.NewConfig()
|
||||
|
||||
// Add LX DEX
|
||||
config.WithNative("lx_dex", trading.NewLxDexConfig(
|
||||
getEnv("LX_DEX_URL", "https://api.dex.lux.network"),
|
||||
).WithCredentials(
|
||||
os.Getenv("LX_DEX_KEY"),
|
||||
os.Getenv("LX_DEX_SECRET"),
|
||||
))
|
||||
|
||||
// Add LX AMM
|
||||
config.WithNative("lx_amm", trading.NewLxAmmConfig(
|
||||
getEnv("LX_AMM_URL", "https://api.amm.lux.network"),
|
||||
))
|
||||
|
||||
// Add CCXT exchanges
|
||||
if key := os.Getenv("BINANCE_KEY"); key != "" {
|
||||
config.WithCcxt("binance", trading.NewCcxtConfig("binance").
|
||||
WithCredentials(key, os.Getenv("BINANCE_SECRET")))
|
||||
}
|
||||
|
||||
if key := os.Getenv("MEXC_KEY"); key != "" {
|
||||
config.WithCcxt("mexc", trading.NewCcxtConfig("mexc").
|
||||
WithCredentials(key, os.Getenv("MEXC_SECRET")))
|
||||
}
|
||||
|
||||
// Risk config
|
||||
config.Risk = trading.RiskConfig{
|
||||
Enabled: true,
|
||||
MaxPositionSize: decimal.NewFromFloat(50000),
|
||||
MaxOrderSize: decimal.NewFromFloat(10000),
|
||||
MaxDailyLoss: decimal.NewFromFloat(1000),
|
||||
KillSwitchEnabled: true,
|
||||
}
|
||||
|
||||
// Create client
|
||||
client := trading.NewClient(config)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := client.Connect(ctx); err != nil {
|
||||
log.Fatalf("Failed to connect: %v", err)
|
||||
}
|
||||
defer client.Disconnect(context.Background())
|
||||
|
||||
// Create arbitrage system
|
||||
arbConfig := DefaultArbConfig()
|
||||
arb := NewAdvancedArbitrage(client, arbConfig)
|
||||
arb.Start()
|
||||
|
||||
// Stats ticker
|
||||
statsTicker := time.NewTicker(30 * time.Second)
|
||||
defer statsTicker.Stop()
|
||||
|
||||
// Signal handling
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-sigCh:
|
||||
arb.Stop()
|
||||
return
|
||||
case <-statsTicker.C:
|
||||
opps, exec, skip, pnl := arb.GetStats()
|
||||
log.Printf("Stats: opps=%d exec=%d skip=%d pnl=$%s",
|
||||
opps, exec, skip, pnl.StringFixed(2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
// Copyright 2024 Lux Partners Limited. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Example: Basic Spot Trading
|
||||
//
|
||||
// This example demonstrates the fundamental trading operations:
|
||||
// - Connecting to trading venues
|
||||
// - Fetching market data (orderbooks, tickers)
|
||||
// - Placing market and limit orders
|
||||
// - Checking balances and open orders
|
||||
//
|
||||
// Run with: go run main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
trading "github.com/luxfi/trading"
|
||||
_ "github.com/luxfi/trading/adapters" // Register adapters
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create configuration
|
||||
config := trading.NewConfig()
|
||||
|
||||
// Add LX DEX (native venue)
|
||||
config.WithNative("lx_dex", trading.NewLxDexConfig(
|
||||
getEnv("LX_DEX_URL", "https://api.dex.lux.network"),
|
||||
).WithCredentials(
|
||||
os.Getenv("LX_DEX_KEY"),
|
||||
os.Getenv("LX_DEX_SECRET"),
|
||||
))
|
||||
|
||||
// Add Binance via CCXT (optional)
|
||||
if key := os.Getenv("BINANCE_KEY"); key != "" {
|
||||
config.WithCcxt("binance", trading.NewCcxtConfig("binance").
|
||||
WithCredentials(key, os.Getenv("BINANCE_SECRET")).
|
||||
WithBaseURL(getEnv("CCXT_GATEWAY", "http://localhost:8080")))
|
||||
}
|
||||
|
||||
// Enable smart routing (will pick best venue automatically)
|
||||
config.WithSmartRouting(true)
|
||||
|
||||
// Basic risk limits
|
||||
config.Risk = trading.RiskConfig{
|
||||
Enabled: true,
|
||||
MaxOrderSize: decimal.NewFromFloat(1000), // Max $1000 per order
|
||||
}
|
||||
|
||||
// Create client
|
||||
client := trading.NewClient(config)
|
||||
|
||||
// Connect with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
log.Println("Connecting to venues...")
|
||||
if err := client.Connect(ctx); err != nil {
|
||||
log.Fatalf("Failed to connect: %v", err)
|
||||
}
|
||||
defer client.Disconnect(context.Background())
|
||||
|
||||
// List connected venues
|
||||
venues := client.Venues()
|
||||
log.Printf("Connected to %d venues:", len(venues))
|
||||
for _, v := range venues {
|
||||
log.Printf(" - %s (%s)", v.Name, v.VenueType)
|
||||
}
|
||||
|
||||
// Use a background context for trading operations
|
||||
ctx = context.Background()
|
||||
|
||||
// Example 1: Get ticker
|
||||
ticker, err := client.Ticker(ctx, "BTC-USDC")
|
||||
if err != nil {
|
||||
log.Printf("Failed to get ticker: %v", err)
|
||||
} else {
|
||||
printTicker(ticker)
|
||||
}
|
||||
|
||||
// Example 2: Get orderbook
|
||||
book, err := client.Orderbook(ctx, "BTC-USDC")
|
||||
if err != nil {
|
||||
log.Printf("Failed to get orderbook: %v", err)
|
||||
} else {
|
||||
printOrderbook(book)
|
||||
}
|
||||
|
||||
// Example 3: Get aggregated orderbook from all venues
|
||||
aggBook, err := client.AggregatedOrderbook(ctx, "BTC-USDC")
|
||||
if err != nil {
|
||||
log.Printf("Failed to get aggregated orderbook: %v", err)
|
||||
} else {
|
||||
printAggregatedOrderbook(aggBook)
|
||||
}
|
||||
|
||||
// Example 4: Get balances
|
||||
balances, err := client.Balances(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get balances: %v", err)
|
||||
} else {
|
||||
printBalances(balances)
|
||||
}
|
||||
|
||||
// Example 5: Place a limit order (uncomment to execute)
|
||||
// This places a limit buy order 5% below current price
|
||||
/*
|
||||
if ticker.Bid != nil {
|
||||
limitPrice := ticker.Bid.Mul(decimal.NewFromFloat(0.95))
|
||||
order, err := client.LimitBuy(ctx, "BTC-USDC",
|
||||
decimal.NewFromFloat(0.001), // 0.001 BTC
|
||||
limitPrice,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Failed to place order: %v", err)
|
||||
} else {
|
||||
printOrder(order)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// Example 6: Place a market order (uncomment to execute)
|
||||
/*
|
||||
order, err := client.Buy(ctx, "BTC-USDC", decimal.NewFromFloat(0.001))
|
||||
if err != nil {
|
||||
log.Printf("Failed to place market order: %v", err)
|
||||
} else {
|
||||
printOrder(order)
|
||||
}
|
||||
*/
|
||||
|
||||
// Example 7: Get open orders
|
||||
openOrders, err := client.OpenOrders(ctx, "BTC-USDC")
|
||||
if err != nil {
|
||||
log.Printf("Failed to get open orders: %v", err)
|
||||
} else {
|
||||
log.Printf("\nOpen Orders: %d", len(openOrders))
|
||||
for _, o := range openOrders {
|
||||
printOrder(o)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("\nBasic trading example complete.")
|
||||
}
|
||||
|
||||
func printTicker(t trading.Ticker) {
|
||||
fmt.Printf("\n=== Ticker: %s (%s) ===\n", t.Symbol, t.Venue)
|
||||
if t.Bid != nil {
|
||||
fmt.Printf(" Bid: %s\n", t.Bid.StringFixed(2))
|
||||
}
|
||||
if t.Ask != nil {
|
||||
fmt.Printf(" Ask: %s\n", t.Ask.StringFixed(2))
|
||||
}
|
||||
if t.Last != nil {
|
||||
fmt.Printf(" Last: %s\n", t.Last.StringFixed(2))
|
||||
}
|
||||
if spread := t.Spread(); spread != nil {
|
||||
fmt.Printf(" Spread: %s\n", spread.StringFixed(4))
|
||||
}
|
||||
if t.Volume24H != nil {
|
||||
fmt.Printf(" Volume: %s\n", t.Volume24H.StringFixed(2))
|
||||
}
|
||||
}
|
||||
|
||||
func printOrderbook(book *trading.Orderbook) {
|
||||
fmt.Printf("\n=== Orderbook: %s (%s) ===\n", book.Symbol, book.Venue)
|
||||
fmt.Println("Asks:")
|
||||
for i := min(4, len(book.Asks)) - 1; i >= 0; i-- {
|
||||
a := book.Asks[i]
|
||||
fmt.Printf(" %s @ %s\n", a.Quantity.StringFixed(4), a.Price.StringFixed(2))
|
||||
}
|
||||
fmt.Println(" ---")
|
||||
fmt.Println("Bids:")
|
||||
for i := 0; i < min(5, len(book.Bids)); i++ {
|
||||
b := book.Bids[i]
|
||||
fmt.Printf(" %s @ %s\n", b.Quantity.StringFixed(4), b.Price.StringFixed(2))
|
||||
}
|
||||
|
||||
if mid := book.MidPrice(); mid != nil {
|
||||
fmt.Printf("\nMid Price: %s\n", mid.StringFixed(2))
|
||||
}
|
||||
if spread := book.SpreadPercent(); spread != nil {
|
||||
fmt.Printf("Spread: %s%%\n", spread.StringFixed(4))
|
||||
}
|
||||
}
|
||||
|
||||
func printAggregatedOrderbook(book *trading.AggregatedOrderbook) {
|
||||
fmt.Printf("\n=== Aggregated Orderbook: %s ===\n", book.Symbol)
|
||||
|
||||
bestBid := book.BestBid()
|
||||
bestAsk := book.BestAsk()
|
||||
|
||||
if bestBid != nil {
|
||||
fmt.Printf("Best Bid: %s @ %s (%s)\n",
|
||||
bestBid.Quantity.StringFixed(4),
|
||||
bestBid.Price.StringFixed(2),
|
||||
bestBid.Venue)
|
||||
}
|
||||
if bestAsk != nil {
|
||||
fmt.Printf("Best Ask: %s @ %s (%s)\n",
|
||||
bestAsk.Quantity.StringFixed(4),
|
||||
bestAsk.Price.StringFixed(2),
|
||||
bestAsk.Venue)
|
||||
}
|
||||
|
||||
// Show aggregated levels
|
||||
fmt.Println("\nAggregated Bids:")
|
||||
bids := book.AggregatedBids()
|
||||
for i := 0; i < min(3, len(bids)); i++ {
|
||||
fmt.Printf(" %s @ %s\n", bids[i].Quantity.StringFixed(4), bids[i].Price.StringFixed(2))
|
||||
}
|
||||
|
||||
fmt.Println("Aggregated Asks:")
|
||||
asks := book.AggregatedAsks()
|
||||
for i := 0; i < min(3, len(asks)); i++ {
|
||||
fmt.Printf(" %s @ %s\n", asks[i].Quantity.StringFixed(4), asks[i].Price.StringFixed(2))
|
||||
}
|
||||
}
|
||||
|
||||
func printBalances(balances []trading.AggregatedBalance) {
|
||||
fmt.Println("\n=== Balances ===")
|
||||
for _, b := range balances {
|
||||
if b.Total().GreaterThan(decimal.Zero) {
|
||||
fmt.Printf(" %s: %s (free: %s, locked: %s)\n",
|
||||
b.Asset,
|
||||
b.Total().StringFixed(8),
|
||||
b.TotalFree.StringFixed(8),
|
||||
b.TotalLocked.StringFixed(8))
|
||||
|
||||
// Show per-venue breakdown
|
||||
for _, vb := range b.ByVenue {
|
||||
fmt.Printf(" [%s] %s\n", vb.Venue, vb.Total().StringFixed(8))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printOrder(o trading.Order) {
|
||||
fmt.Printf("\n=== Order: %s ===\n", o.OrderID)
|
||||
fmt.Printf(" Symbol: %s\n", o.Symbol)
|
||||
fmt.Printf(" Venue: %s\n", o.Venue)
|
||||
fmt.Printf(" Side: %s\n", o.Side)
|
||||
fmt.Printf(" Type: %s\n", o.OrderType)
|
||||
fmt.Printf(" Status: %s\n", o.Status)
|
||||
fmt.Printf(" Quantity: %s\n", o.Quantity.StringFixed(8))
|
||||
fmt.Printf(" Filled: %s (%s%%)\n",
|
||||
o.FilledQuantity.StringFixed(8),
|
||||
o.FillPercent().StringFixed(1))
|
||||
if o.Price != nil {
|
||||
fmt.Printf(" Price: %s\n", o.Price.StringFixed(2))
|
||||
}
|
||||
if o.AveragePrice != nil {
|
||||
fmt.Printf(" Avg Fill: %s\n", o.AveragePrice.StringFixed(2))
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
// Copyright 2024 Lux Partners Limited. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Example: Market Making with Spreads
|
||||
//
|
||||
// This example demonstrates a simple market making strategy:
|
||||
// - Placing bid/ask orders around the mid price
|
||||
// - Managing order inventory
|
||||
// - Adjusting spreads based on volatility
|
||||
// - Position management and hedging
|
||||
//
|
||||
// WARNING: This is an educational example. Real market making
|
||||
// requires much more sophisticated risk management.
|
||||
//
|
||||
// Run with: go run main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
trading "github.com/luxfi/trading"
|
||||
_ "github.com/luxfi/trading/adapters"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// MarketMaker implements a simple market making strategy.
|
||||
type MarketMaker struct {
|
||||
client *trading.Client
|
||||
config MarketMakerConfig
|
||||
|
||||
// State
|
||||
mu sync.RWMutex
|
||||
position decimal.Decimal
|
||||
activeOrders map[string]trading.Order
|
||||
totalPnL decimal.Decimal
|
||||
tradesCount int
|
||||
lastMidPrice decimal.Decimal
|
||||
volatility decimal.Decimal
|
||||
|
||||
// Control
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// MarketMakerConfig configures the market maker.
|
||||
type MarketMakerConfig struct {
|
||||
// Trading parameters
|
||||
Symbol string
|
||||
Venue string
|
||||
BaseSpreadBps decimal.Decimal // Base spread in basis points
|
||||
OrderSize decimal.Decimal // Size per order
|
||||
MaxPosition decimal.Decimal // Maximum position (absolute)
|
||||
NumLevels int // Number of price levels
|
||||
LevelSpacingBps decimal.Decimal // Spacing between levels
|
||||
|
||||
// Risk parameters
|
||||
MaxDailyLoss decimal.Decimal
|
||||
PositionSkewBps decimal.Decimal // Skew spread based on position
|
||||
|
||||
// Timing
|
||||
QuoteInterval time.Duration
|
||||
CancelAgeSeconds int
|
||||
}
|
||||
|
||||
// DefaultMarketMakerConfig returns sensible defaults.
|
||||
func DefaultMarketMakerConfig() MarketMakerConfig {
|
||||
return MarketMakerConfig{
|
||||
Symbol: "BTC-USDC",
|
||||
Venue: "lx_dex",
|
||||
BaseSpreadBps: decimal.NewFromInt(10), // 0.10%
|
||||
OrderSize: decimal.NewFromFloat(0.01), // 0.01 BTC
|
||||
MaxPosition: decimal.NewFromFloat(0.1), // 0.1 BTC
|
||||
NumLevels: 3,
|
||||
LevelSpacingBps: decimal.NewFromInt(5), // 0.05%
|
||||
MaxDailyLoss: decimal.NewFromFloat(100), // $100
|
||||
PositionSkewBps: decimal.NewFromInt(2), // Skew per position unit
|
||||
QuoteInterval: time.Second,
|
||||
CancelAgeSeconds: 30,
|
||||
}
|
||||
}
|
||||
|
||||
// NewMarketMaker creates a new market maker.
|
||||
func NewMarketMaker(client *trading.Client, config MarketMakerConfig) *MarketMaker {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &MarketMaker{
|
||||
client: client,
|
||||
config: config,
|
||||
activeOrders: make(map[string]trading.Order),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins market making.
|
||||
func (mm *MarketMaker) Start() {
|
||||
log.Printf("Starting market maker for %s on %s", mm.config.Symbol, mm.config.Venue)
|
||||
log.Printf("Config: spread=%sbps, size=%s, levels=%d",
|
||||
mm.config.BaseSpreadBps, mm.config.OrderSize, mm.config.NumLevels)
|
||||
|
||||
mm.wg.Add(2)
|
||||
go mm.quoteLoop()
|
||||
go mm.monitorLoop()
|
||||
}
|
||||
|
||||
// Stop stops market making and cancels all orders.
|
||||
func (mm *MarketMaker) Stop() {
|
||||
log.Println("Stopping market maker...")
|
||||
mm.cancel()
|
||||
mm.wg.Wait()
|
||||
|
||||
// Cancel all active orders
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := mm.client.CancelAllOrders(ctx, mm.config.Symbol, trading.WithVenue(mm.config.Venue))
|
||||
if err != nil {
|
||||
log.Printf("Error cancelling orders: %v", err)
|
||||
}
|
||||
|
||||
mm.printStats()
|
||||
}
|
||||
|
||||
// quoteLoop continuously updates quotes.
|
||||
func (mm *MarketMaker) quoteLoop() {
|
||||
defer mm.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(mm.config.QuoteInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-mm.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
mm.updateQuotes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateQuotes places new bid/ask orders.
|
||||
func (mm *MarketMaker) updateQuotes() {
|
||||
// Get current mid price
|
||||
ticker, err := mm.client.Ticker(mm.ctx, mm.config.Symbol, trading.WithVenue(mm.config.Venue))
|
||||
if err != nil {
|
||||
log.Printf("Failed to get ticker: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
midPrice := ticker.MidPrice()
|
||||
if midPrice == nil {
|
||||
log.Println("No mid price available")
|
||||
return
|
||||
}
|
||||
|
||||
mm.mu.Lock()
|
||||
mm.lastMidPrice = *midPrice
|
||||
position := mm.position
|
||||
mm.mu.Unlock()
|
||||
|
||||
// Calculate spread with position skew
|
||||
// If long, widen ask (want to sell), tighten bid
|
||||
// If short, widen bid (want to buy), tighten ask
|
||||
halfSpread := mm.config.BaseSpreadBps.Div(decimal.NewFromInt(2))
|
||||
positionSkew := position.Mul(mm.config.PositionSkewBps)
|
||||
|
||||
bidSpread := halfSpread.Add(positionSkew) // Wider if long
|
||||
askSpread := halfSpread.Sub(positionSkew) // Tighter if long
|
||||
|
||||
// Ensure minimum spread
|
||||
minSpread := decimal.NewFromInt(1) // 0.01%
|
||||
if bidSpread.LessThan(minSpread) {
|
||||
bidSpread = minSpread
|
||||
}
|
||||
if askSpread.LessThan(minSpread) {
|
||||
askSpread = minSpread
|
||||
}
|
||||
|
||||
// Cancel stale orders first
|
||||
mm.cancelStaleOrders()
|
||||
|
||||
// Check position limits
|
||||
canBuy := position.LessThan(mm.config.MaxPosition)
|
||||
canSell := position.Neg().LessThan(mm.config.MaxPosition)
|
||||
|
||||
// Place orders at multiple levels
|
||||
for level := 0; level < mm.config.NumLevels; level++ {
|
||||
levelOffset := mm.config.LevelSpacingBps.Mul(decimal.NewFromInt(int64(level)))
|
||||
|
||||
// Bid price: mid * (1 - (bidSpread + levelOffset) / 10000)
|
||||
bidBps := bidSpread.Add(levelOffset)
|
||||
bidMultiplier := decimal.NewFromInt(1).Sub(bidBps.Div(decimal.NewFromInt(10000)))
|
||||
bidPrice := midPrice.Mul(bidMultiplier)
|
||||
|
||||
// Ask price: mid * (1 + (askSpread + levelOffset) / 10000)
|
||||
askBps := askSpread.Add(levelOffset)
|
||||
askMultiplier := decimal.NewFromInt(1).Add(askBps.Div(decimal.NewFromInt(10000)))
|
||||
askPrice := midPrice.Mul(askMultiplier)
|
||||
|
||||
// Place bid
|
||||
if canBuy {
|
||||
mm.placeOrder(trading.SideBuy, mm.config.OrderSize, bidPrice, level)
|
||||
}
|
||||
|
||||
// Place ask
|
||||
if canSell {
|
||||
mm.placeOrder(trading.SideSell, mm.config.OrderSize, askPrice, level)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// placeOrder places a single order.
|
||||
func (mm *MarketMaker) placeOrder(side trading.Side, quantity, price decimal.Decimal, level int) {
|
||||
order, err := mm.client.PlaceOrder(mm.ctx, trading.OrderRequest{
|
||||
ClientOrderID: fmt.Sprintf("mm-%s-%d-%d", side, level, time.Now().UnixNano()),
|
||||
Symbol: mm.config.Symbol,
|
||||
Side: side,
|
||||
OrderType: trading.OrderTypeLimit,
|
||||
Quantity: quantity,
|
||||
Price: &price,
|
||||
TimeInForce: trading.TimeInForceGTC,
|
||||
Venue: mm.config.Venue,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Failed to place %s order at %s: %v", side, price.StringFixed(2), err)
|
||||
return
|
||||
}
|
||||
|
||||
mm.mu.Lock()
|
||||
mm.activeOrders[order.OrderID] = order
|
||||
mm.mu.Unlock()
|
||||
|
||||
log.Printf("Placed %s %s @ %s (order: %s)",
|
||||
side, quantity.StringFixed(4), price.StringFixed(2), order.OrderID)
|
||||
}
|
||||
|
||||
// cancelStaleOrders cancels orders older than threshold.
|
||||
func (mm *MarketMaker) cancelStaleOrders() {
|
||||
mm.mu.Lock()
|
||||
ordersToCancel := make([]string, 0)
|
||||
threshold := time.Now().Add(-time.Duration(mm.config.CancelAgeSeconds) * time.Second)
|
||||
|
||||
for id, order := range mm.activeOrders {
|
||||
if order.CreatedAt.Before(threshold) {
|
||||
ordersToCancel = append(ordersToCancel, id)
|
||||
}
|
||||
}
|
||||
mm.mu.Unlock()
|
||||
|
||||
for _, id := range ordersToCancel {
|
||||
_, err := mm.client.CancelOrder(mm.ctx, id, mm.config.Symbol, mm.config.Venue)
|
||||
if err != nil {
|
||||
log.Printf("Failed to cancel order %s: %v", id, err)
|
||||
} else {
|
||||
mm.mu.Lock()
|
||||
delete(mm.activeOrders, id)
|
||||
mm.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// monitorLoop monitors fills and updates position.
|
||||
func (mm *MarketMaker) monitorLoop() {
|
||||
defer mm.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-mm.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
mm.checkFills()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkFills checks for filled orders and updates position.
|
||||
func (mm *MarketMaker) checkFills() {
|
||||
openOrders, err := mm.client.OpenOrders(mm.ctx, mm.config.Symbol)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Build map of current open orders
|
||||
openOrderIDs := make(map[string]bool)
|
||||
for _, o := range openOrders {
|
||||
openOrderIDs[o.OrderID] = true
|
||||
}
|
||||
|
||||
// Check which active orders have been filled
|
||||
mm.mu.Lock()
|
||||
defer mm.mu.Unlock()
|
||||
|
||||
for id, order := range mm.activeOrders {
|
||||
if !openOrderIDs[id] {
|
||||
// Order is no longer open - assume filled
|
||||
// In production, you would fetch the actual fill details
|
||||
delete(mm.activeOrders, id)
|
||||
mm.tradesCount++
|
||||
|
||||
// Update position
|
||||
if order.Side == trading.SideBuy {
|
||||
mm.position = mm.position.Add(order.Quantity)
|
||||
} else {
|
||||
mm.position = mm.position.Sub(order.Quantity)
|
||||
}
|
||||
|
||||
log.Printf("Order filled: %s %s %s (position: %s)",
|
||||
order.Side, order.Quantity.StringFixed(4), mm.config.Symbol,
|
||||
mm.position.StringFixed(4))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// printStats prints market maker statistics.
|
||||
func (mm *MarketMaker) printStats() {
|
||||
mm.mu.RLock()
|
||||
defer mm.mu.RUnlock()
|
||||
|
||||
fmt.Println("\n=== Market Maker Stats ===")
|
||||
fmt.Printf("Symbol: %s\n", mm.config.Symbol)
|
||||
fmt.Printf("Venue: %s\n", mm.config.Venue)
|
||||
fmt.Printf("Position: %s\n", mm.position.StringFixed(8))
|
||||
fmt.Printf("Total Trades: %d\n", mm.tradesCount)
|
||||
fmt.Printf("Active Orders: %d\n", len(mm.activeOrders))
|
||||
fmt.Printf("Last Mid: %s\n", mm.lastMidPrice.StringFixed(2))
|
||||
fmt.Println("==========================")
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Create configuration
|
||||
config := trading.NewConfig()
|
||||
|
||||
// Add venue
|
||||
venue := getEnv("MM_VENUE", "lx_dex")
|
||||
if venue == "lx_dex" {
|
||||
config.WithNative(venue, trading.NewLxDexConfig(
|
||||
getEnv("LX_DEX_URL", "https://api.dex.lux.network"),
|
||||
).WithCredentials(
|
||||
os.Getenv("LX_DEX_KEY"),
|
||||
os.Getenv("LX_DEX_SECRET"),
|
||||
))
|
||||
} else {
|
||||
config.WithCcxt(venue, trading.NewCcxtConfig(venue).
|
||||
WithCredentials(
|
||||
os.Getenv(venue+"_KEY"),
|
||||
os.Getenv(venue+"_SECRET"),
|
||||
))
|
||||
}
|
||||
|
||||
// Risk config
|
||||
config.Risk = trading.RiskConfig{
|
||||
Enabled: true,
|
||||
MaxPositionSize: decimal.NewFromFloat(1), // 1 BTC max position
|
||||
MaxOrderSize: decimal.NewFromFloat(0.1), // 0.1 BTC max order
|
||||
MaxDailyLoss: decimal.NewFromFloat(500), // $500 daily loss limit
|
||||
MaxOpenOrders: 50,
|
||||
}
|
||||
|
||||
// Create client
|
||||
client := trading.NewClient(config)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := client.Connect(ctx); err != nil {
|
||||
log.Fatalf("Failed to connect: %v", err)
|
||||
}
|
||||
defer client.Disconnect(context.Background())
|
||||
|
||||
// Create market maker
|
||||
mmConfig := DefaultMarketMakerConfig()
|
||||
mmConfig.Symbol = getEnv("MM_SYMBOL", "BTC-USDC")
|
||||
mmConfig.Venue = venue
|
||||
|
||||
// Parse config from environment
|
||||
if spread := os.Getenv("MM_SPREAD_BPS"); spread != "" {
|
||||
mmConfig.BaseSpreadBps, _ = decimal.NewFromString(spread)
|
||||
}
|
||||
if size := os.Getenv("MM_ORDER_SIZE"); size != "" {
|
||||
mmConfig.OrderSize, _ = decimal.NewFromString(size)
|
||||
}
|
||||
|
||||
mm := NewMarketMaker(client, mmConfig)
|
||||
mm.Start()
|
||||
|
||||
// Wait for signal
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
<-sigCh
|
||||
mm.Stop()
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
@@ -0,0 +1,524 @@
|
||||
// Copyright 2024 Lux Partners Limited. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Example: Position and Risk Tracking
|
||||
//
|
||||
// This example demonstrates comprehensive risk management:
|
||||
// - Position tracking across venues
|
||||
// - PnL monitoring
|
||||
// - Risk limit enforcement
|
||||
// - Kill switch functionality
|
||||
// - Portfolio-level metrics
|
||||
//
|
||||
// Run with: go run main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
trading "github.com/luxfi/trading"
|
||||
_ "github.com/luxfi/trading/adapters"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// RiskMonitor provides real-time risk monitoring and enforcement.
|
||||
type RiskMonitor struct {
|
||||
client *trading.Client
|
||||
riskManager *trading.RiskManager
|
||||
|
||||
// Configuration
|
||||
config RiskConfig
|
||||
|
||||
// State
|
||||
mu sync.RWMutex
|
||||
positions map[string]Position // asset -> position
|
||||
pnlHistory []PnLSnapshot
|
||||
alerts []Alert
|
||||
lastBalance map[string]decimal.Decimal
|
||||
|
||||
// Control
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// RiskConfig configures risk monitoring.
|
||||
type RiskConfig struct {
|
||||
// Position limits
|
||||
MaxPositionUSD decimal.Decimal
|
||||
MaxPositionAsset map[string]decimal.Decimal
|
||||
|
||||
// PnL limits
|
||||
MaxDailyLoss decimal.Decimal
|
||||
MaxDrawdown decimal.Decimal
|
||||
ProfitTarget decimal.Decimal
|
||||
|
||||
// Alert thresholds
|
||||
AlertPositionPct decimal.Decimal // Alert when position > X% of limit
|
||||
AlertLossPct decimal.Decimal // Alert when loss > X% of limit
|
||||
|
||||
// Kill switch
|
||||
EnableKillSwitch bool
|
||||
KillOnMaxLoss bool
|
||||
|
||||
// Monitoring
|
||||
CheckInterval time.Duration
|
||||
}
|
||||
|
||||
// Position represents a position in an asset.
|
||||
type Position struct {
|
||||
Asset string
|
||||
Quantity decimal.Decimal
|
||||
AvgEntryPrice decimal.Decimal
|
||||
MarketPrice decimal.Decimal
|
||||
UnrealizedPnL decimal.Decimal
|
||||
RealizedPnL decimal.Decimal
|
||||
ByVenue map[string]decimal.Decimal
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// PnLSnapshot is a point-in-time PnL record.
|
||||
type PnLSnapshot struct {
|
||||
Timestamp time.Time
|
||||
UnrealizedPnL decimal.Decimal
|
||||
RealizedPnL decimal.Decimal
|
||||
TotalPnL decimal.Decimal
|
||||
}
|
||||
|
||||
// Alert represents a risk alert.
|
||||
type Alert struct {
|
||||
Timestamp time.Time
|
||||
Level string // "WARNING", "CRITICAL"
|
||||
Type string
|
||||
Message string
|
||||
}
|
||||
|
||||
// DefaultRiskConfig returns sensible defaults.
|
||||
func DefaultRiskConfig() RiskConfig {
|
||||
return RiskConfig{
|
||||
MaxPositionUSD: decimal.NewFromFloat(100000),
|
||||
MaxPositionAsset: make(map[string]decimal.Decimal),
|
||||
MaxDailyLoss: decimal.NewFromFloat(5000),
|
||||
MaxDrawdown: decimal.NewFromFloat(10000),
|
||||
ProfitTarget: decimal.NewFromFloat(10000),
|
||||
AlertPositionPct: decimal.NewFromFloat(80),
|
||||
AlertLossPct: decimal.NewFromFloat(50),
|
||||
EnableKillSwitch: true,
|
||||
KillOnMaxLoss: true,
|
||||
CheckInterval: 5 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// NewRiskMonitor creates a new risk monitor.
|
||||
func NewRiskMonitor(client *trading.Client, config RiskConfig) *RiskMonitor {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &RiskMonitor{
|
||||
client: client,
|
||||
riskManager: client.RiskManager(),
|
||||
config: config,
|
||||
positions: make(map[string]Position),
|
||||
lastBalance: make(map[string]decimal.Decimal),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins risk monitoring.
|
||||
func (rm *RiskMonitor) Start() {
|
||||
log.Println("Starting risk monitor")
|
||||
log.Printf("Max position: $%s, Max daily loss: $%s",
|
||||
rm.config.MaxPositionUSD, rm.config.MaxDailyLoss)
|
||||
|
||||
rm.wg.Add(1)
|
||||
go rm.monitorLoop()
|
||||
}
|
||||
|
||||
// Stop stops risk monitoring.
|
||||
func (rm *RiskMonitor) Stop() {
|
||||
rm.cancel()
|
||||
rm.wg.Wait()
|
||||
rm.printFinalReport()
|
||||
}
|
||||
|
||||
// monitorLoop continuously monitors risk metrics.
|
||||
func (rm *RiskMonitor) monitorLoop() {
|
||||
defer rm.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(rm.config.CheckInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-rm.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
rm.checkRisk()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkRisk performs risk checks.
|
||||
func (rm *RiskMonitor) checkRisk() {
|
||||
// Update positions from balances
|
||||
rm.updatePositions()
|
||||
|
||||
// Check PnL
|
||||
rm.checkPnL()
|
||||
|
||||
// Check position limits
|
||||
rm.checkPositionLimits()
|
||||
|
||||
// Check kill switch
|
||||
rm.checkKillSwitch()
|
||||
}
|
||||
|
||||
// updatePositions updates position tracking from venue balances.
|
||||
func (rm *RiskMonitor) updatePositions() {
|
||||
balances, err := rm.client.Balances(rm.ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
rm.mu.Lock()
|
||||
defer rm.mu.Unlock()
|
||||
|
||||
for _, bal := range balances {
|
||||
pos, ok := rm.positions[bal.Asset]
|
||||
if !ok {
|
||||
pos = Position{
|
||||
Asset: bal.Asset,
|
||||
ByVenue: make(map[string]decimal.Decimal),
|
||||
}
|
||||
}
|
||||
|
||||
// Track quantity by venue
|
||||
for _, vb := range bal.ByVenue {
|
||||
pos.ByVenue[vb.Venue] = vb.Total()
|
||||
}
|
||||
|
||||
// Total quantity
|
||||
pos.Quantity = bal.Total()
|
||||
pos.UpdatedAt = time.Now()
|
||||
|
||||
// Calculate PnL change
|
||||
lastTotal, hasLast := rm.lastBalance[bal.Asset]
|
||||
if hasLast {
|
||||
change := bal.Total().Sub(lastTotal)
|
||||
if !change.IsZero() {
|
||||
// Simplified: treat balance changes as realized PnL
|
||||
// In production, track actual trade fills
|
||||
pos.RealizedPnL = pos.RealizedPnL.Add(change)
|
||||
}
|
||||
}
|
||||
rm.lastBalance[bal.Asset] = bal.Total()
|
||||
|
||||
rm.positions[bal.Asset] = pos
|
||||
}
|
||||
}
|
||||
|
||||
// checkPnL checks PnL limits.
|
||||
func (rm *RiskMonitor) checkPnL() {
|
||||
// Get risk manager state
|
||||
pnl := rm.riskManager.DailyPnL()
|
||||
|
||||
rm.mu.Lock()
|
||||
|
||||
// Record snapshot
|
||||
snapshot := PnLSnapshot{
|
||||
Timestamp: time.Now(),
|
||||
TotalPnL: pnl,
|
||||
}
|
||||
rm.pnlHistory = append(rm.pnlHistory, snapshot)
|
||||
|
||||
// Keep last 24 hours
|
||||
cutoff := time.Now().Add(-24 * time.Hour)
|
||||
filtered := rm.pnlHistory[:0]
|
||||
for _, s := range rm.pnlHistory {
|
||||
if s.Timestamp.After(cutoff) {
|
||||
filtered = append(filtered, s)
|
||||
}
|
||||
}
|
||||
rm.pnlHistory = filtered
|
||||
|
||||
rm.mu.Unlock()
|
||||
|
||||
// Check loss limit
|
||||
if pnl.IsNegative() {
|
||||
lossAbs := pnl.Abs()
|
||||
lossPct := lossAbs.Div(rm.config.MaxDailyLoss).Mul(decimal.NewFromInt(100))
|
||||
|
||||
if lossPct.GreaterThan(rm.config.AlertLossPct) {
|
||||
rm.addAlert("WARNING", "LOSS",
|
||||
fmt.Sprintf("Daily loss at %s%% of limit ($%s)",
|
||||
lossPct.StringFixed(1), lossAbs.StringFixed(2)))
|
||||
}
|
||||
|
||||
if lossAbs.GreaterThanOrEqual(rm.config.MaxDailyLoss) {
|
||||
rm.addAlert("CRITICAL", "LOSS",
|
||||
fmt.Sprintf("Daily loss limit reached: $%s", lossAbs.StringFixed(2)))
|
||||
|
||||
if rm.config.KillOnMaxLoss {
|
||||
rm.triggerKillSwitch("Max daily loss reached")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check profit target
|
||||
if pnl.GreaterThanOrEqual(rm.config.ProfitTarget) {
|
||||
rm.addAlert("WARNING", "PROFIT",
|
||||
fmt.Sprintf("Profit target reached: $%s", pnl.StringFixed(2)))
|
||||
}
|
||||
}
|
||||
|
||||
// checkPositionLimits checks position limits.
|
||||
func (rm *RiskMonitor) checkPositionLimits() {
|
||||
rm.mu.RLock()
|
||||
positions := rm.positions
|
||||
rm.mu.RUnlock()
|
||||
|
||||
for _, pos := range positions {
|
||||
if pos.Quantity.IsZero() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check asset-specific limit
|
||||
if limit, ok := rm.config.MaxPositionAsset[pos.Asset]; ok {
|
||||
usagePct := pos.Quantity.Abs().Div(limit).Mul(decimal.NewFromInt(100))
|
||||
|
||||
if usagePct.GreaterThan(rm.config.AlertPositionPct) {
|
||||
rm.addAlert("WARNING", "POSITION",
|
||||
fmt.Sprintf("%s position at %s%% of limit (%s / %s)",
|
||||
pos.Asset, usagePct.StringFixed(1),
|
||||
pos.Quantity.StringFixed(4), limit.StringFixed(4)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkKillSwitch checks if kill switch should be triggered.
|
||||
func (rm *RiskMonitor) checkKillSwitch() {
|
||||
if rm.riskManager.IsKilled() {
|
||||
rm.addAlert("CRITICAL", "KILL_SWITCH", "Kill switch is active - trading halted")
|
||||
}
|
||||
}
|
||||
|
||||
// triggerKillSwitch activates the kill switch.
|
||||
func (rm *RiskMonitor) triggerKillSwitch(reason string) {
|
||||
if !rm.config.EnableKillSwitch {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("KILL SWITCH ACTIVATED: %s", reason)
|
||||
rm.riskManager.Kill()
|
||||
rm.addAlert("CRITICAL", "KILL_SWITCH", reason)
|
||||
|
||||
// Cancel all orders
|
||||
for _, symbol := range []string{"BTC-USDC", "ETH-USDC", "LUX-USDC"} {
|
||||
rm.client.CancelAllOrders(rm.ctx, symbol)
|
||||
}
|
||||
}
|
||||
|
||||
// addAlert adds a risk alert.
|
||||
func (rm *RiskMonitor) addAlert(level, alertType, message string) {
|
||||
rm.mu.Lock()
|
||||
defer rm.mu.Unlock()
|
||||
|
||||
alert := Alert{
|
||||
Timestamp: time.Now(),
|
||||
Level: level,
|
||||
Type: alertType,
|
||||
Message: message,
|
||||
}
|
||||
|
||||
rm.alerts = append(rm.alerts, alert)
|
||||
|
||||
// Log the alert
|
||||
log.Printf("[%s] %s: %s", level, alertType, message)
|
||||
|
||||
// Keep last 100 alerts
|
||||
if len(rm.alerts) > 100 {
|
||||
rm.alerts = rm.alerts[len(rm.alerts)-100:]
|
||||
}
|
||||
}
|
||||
|
||||
// GetSummary returns a risk summary.
|
||||
func (rm *RiskMonitor) GetSummary() RiskSummary {
|
||||
rm.mu.RLock()
|
||||
defer rm.mu.RUnlock()
|
||||
|
||||
summary := RiskSummary{
|
||||
Timestamp: time.Now(),
|
||||
DailyPnL: rm.riskManager.DailyPnL(),
|
||||
IsKilled: rm.riskManager.IsKilled(),
|
||||
Positions: make(map[string]decimal.Decimal),
|
||||
AlertCount: len(rm.alerts),
|
||||
}
|
||||
|
||||
for asset, pos := range rm.positions {
|
||||
if !pos.Quantity.IsZero() {
|
||||
summary.Positions[asset] = pos.Quantity
|
||||
}
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
// RiskSummary provides a snapshot of risk state.
|
||||
type RiskSummary struct {
|
||||
Timestamp time.Time
|
||||
DailyPnL decimal.Decimal
|
||||
IsKilled bool
|
||||
Positions map[string]decimal.Decimal
|
||||
AlertCount int
|
||||
}
|
||||
|
||||
// PrintStatus prints current risk status.
|
||||
func (rm *RiskMonitor) PrintStatus() {
|
||||
summary := rm.GetSummary()
|
||||
|
||||
fmt.Printf("\n=== Risk Status (%s) ===\n", summary.Timestamp.Format("15:04:05"))
|
||||
fmt.Printf("Daily PnL: $%s\n", summary.DailyPnL.StringFixed(2))
|
||||
fmt.Printf("Kill Switch: %v\n", summary.IsKilled)
|
||||
fmt.Printf("Alerts: %d\n", summary.AlertCount)
|
||||
|
||||
if len(summary.Positions) > 0 {
|
||||
fmt.Println("\nPositions:")
|
||||
for asset, qty := range summary.Positions {
|
||||
fmt.Printf(" %s: %s\n", asset, qty.StringFixed(8))
|
||||
}
|
||||
}
|
||||
|
||||
// Recent alerts
|
||||
rm.mu.RLock()
|
||||
if len(rm.alerts) > 0 {
|
||||
fmt.Println("\nRecent Alerts:")
|
||||
start := len(rm.alerts) - 5
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
for _, a := range rm.alerts[start:] {
|
||||
fmt.Printf(" [%s] %s: %s (%s)\n",
|
||||
a.Level, a.Type, a.Message,
|
||||
a.Timestamp.Format("15:04:05"))
|
||||
}
|
||||
}
|
||||
rm.mu.RUnlock()
|
||||
|
||||
fmt.Println("=============================")
|
||||
}
|
||||
|
||||
// printFinalReport prints final risk report.
|
||||
func (rm *RiskMonitor) printFinalReport() {
|
||||
rm.mu.RLock()
|
||||
defer rm.mu.RUnlock()
|
||||
|
||||
fmt.Println("\n=== Final Risk Report ===")
|
||||
fmt.Printf("Session End: %s\n", time.Now().Format("2006-01-02 15:04:05"))
|
||||
fmt.Printf("Daily PnL: $%s\n", rm.riskManager.DailyPnL().StringFixed(2))
|
||||
fmt.Printf("Kill Switch: %v\n", rm.riskManager.IsKilled())
|
||||
fmt.Printf("Total Alerts: %d\n", len(rm.alerts))
|
||||
|
||||
// Count alerts by type
|
||||
alertCounts := make(map[string]int)
|
||||
for _, a := range rm.alerts {
|
||||
alertCounts[a.Type]++
|
||||
}
|
||||
if len(alertCounts) > 0 {
|
||||
fmt.Println("\nAlerts by Type:")
|
||||
for t, c := range alertCounts {
|
||||
fmt.Printf(" %s: %d\n", t, c)
|
||||
}
|
||||
}
|
||||
|
||||
// Final positions
|
||||
fmt.Println("\nFinal Positions:")
|
||||
for asset, pos := range rm.positions {
|
||||
if !pos.Quantity.IsZero() {
|
||||
fmt.Printf(" %s: %s\n", asset, pos.Quantity.StringFixed(8))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("=========================")
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Create configuration
|
||||
config := trading.NewConfig()
|
||||
|
||||
// Add venues
|
||||
config.WithNative("lx_dex", trading.NewLxDexConfig(
|
||||
getEnv("LX_DEX_URL", "https://api.dex.lux.network"),
|
||||
).WithCredentials(
|
||||
os.Getenv("LX_DEX_KEY"),
|
||||
os.Getenv("LX_DEX_SECRET"),
|
||||
))
|
||||
|
||||
if key := os.Getenv("BINANCE_KEY"); key != "" {
|
||||
config.WithCcxt("binance", trading.NewCcxtConfig("binance").
|
||||
WithCredentials(key, os.Getenv("BINANCE_SECRET")))
|
||||
}
|
||||
|
||||
// Risk config for the client
|
||||
config.Risk = trading.RiskConfig{
|
||||
Enabled: true,
|
||||
MaxPositionSize: decimal.NewFromFloat(100000),
|
||||
MaxOrderSize: decimal.NewFromFloat(10000),
|
||||
MaxDailyLoss: decimal.NewFromFloat(5000),
|
||||
MaxOpenOrders: 100,
|
||||
KillSwitchEnabled: true,
|
||||
}
|
||||
|
||||
// Create client
|
||||
client := trading.NewClient(config)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := client.Connect(ctx); err != nil {
|
||||
log.Fatalf("Failed to connect: %v", err)
|
||||
}
|
||||
defer client.Disconnect(context.Background())
|
||||
|
||||
// Create risk monitor
|
||||
riskConfig := DefaultRiskConfig()
|
||||
riskConfig.MaxPositionAsset["BTC"] = decimal.NewFromFloat(1)
|
||||
riskConfig.MaxPositionAsset["ETH"] = decimal.NewFromFloat(10)
|
||||
|
||||
monitor := NewRiskMonitor(client, riskConfig)
|
||||
monitor.Start()
|
||||
defer monitor.Stop()
|
||||
|
||||
// Signal handling
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
// Status ticker
|
||||
statusTicker := time.NewTicker(10 * time.Second)
|
||||
defer statusTicker.Stop()
|
||||
|
||||
log.Println("Risk monitor started. Press Ctrl+C to exit.")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-sigCh:
|
||||
return
|
||||
case <-statusTicker.C:
|
||||
monitor.PrintStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
// Copyright 2024 Lux Partners Limited. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Example: Real-time Price Feeds via Polling
|
||||
//
|
||||
// This example demonstrates real-time market data streaming:
|
||||
// - Continuous ticker updates
|
||||
// - Orderbook depth tracking
|
||||
// - Trade feed monitoring
|
||||
// - Multi-symbol price aggregation
|
||||
//
|
||||
// Note: This example uses polling. For true WebSocket streaming,
|
||||
// implement the StreamingAdapter interface in your venue adapter.
|
||||
//
|
||||
// Run with: go run main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
trading "github.com/luxfi/trading"
|
||||
_ "github.com/luxfi/trading/adapters"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// PriceFeed tracks real-time prices across venues.
|
||||
type PriceFeed struct {
|
||||
client *trading.Client
|
||||
|
||||
// Configuration
|
||||
symbols []string
|
||||
pollInterval time.Duration
|
||||
venues []string
|
||||
|
||||
// State
|
||||
mu sync.RWMutex
|
||||
prices map[string]SymbolPrices // symbol -> prices
|
||||
orderbooks map[string]*trading.AggregatedOrderbook
|
||||
lastUpdated map[string]time.Time
|
||||
callbacks []PriceCallback
|
||||
|
||||
// Control
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// SymbolPrices holds prices for a symbol across venues.
|
||||
type SymbolPrices struct {
|
||||
Symbol string
|
||||
Prices map[string]VenuePrice // venue -> price
|
||||
BestBid decimal.Decimal
|
||||
BestAsk decimal.Decimal
|
||||
MidPrice decimal.Decimal
|
||||
Spread decimal.Decimal
|
||||
SpreadBps decimal.Decimal
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// VenuePrice holds price data from a specific venue.
|
||||
type VenuePrice struct {
|
||||
Venue string
|
||||
Bid decimal.Decimal
|
||||
Ask decimal.Decimal
|
||||
Last decimal.Decimal
|
||||
Volume24H decimal.Decimal
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// PriceCallback is called when prices update.
|
||||
type PriceCallback func(symbol string, prices SymbolPrices)
|
||||
|
||||
// NewPriceFeed creates a new price feed.
|
||||
func NewPriceFeed(client *trading.Client, symbols []string, pollInterval time.Duration) *PriceFeed {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Get venue names from client
|
||||
var venues []string
|
||||
for _, v := range client.Venues() {
|
||||
venues = append(venues, v.Name)
|
||||
}
|
||||
|
||||
return &PriceFeed{
|
||||
client: client,
|
||||
symbols: symbols,
|
||||
pollInterval: pollInterval,
|
||||
venues: venues,
|
||||
prices: make(map[string]SymbolPrices),
|
||||
orderbooks: make(map[string]*trading.AggregatedOrderbook),
|
||||
lastUpdated: make(map[string]time.Time),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
// OnPriceUpdate registers a callback for price updates.
|
||||
func (pf *PriceFeed) OnPriceUpdate(cb PriceCallback) {
|
||||
pf.mu.Lock()
|
||||
pf.callbacks = append(pf.callbacks, cb)
|
||||
pf.mu.Unlock()
|
||||
}
|
||||
|
||||
// Start begins the price feed.
|
||||
func (pf *PriceFeed) Start() {
|
||||
log.Printf("Starting price feed for %v", pf.symbols)
|
||||
log.Printf("Poll interval: %s", pf.pollInterval)
|
||||
|
||||
pf.wg.Add(2)
|
||||
go pf.tickerLoop()
|
||||
go pf.orderbookLoop()
|
||||
}
|
||||
|
||||
// Stop stops the price feed.
|
||||
func (pf *PriceFeed) Stop() {
|
||||
pf.cancel()
|
||||
pf.wg.Wait()
|
||||
}
|
||||
|
||||
// tickerLoop polls for ticker updates.
|
||||
func (pf *PriceFeed) tickerLoop() {
|
||||
defer pf.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(pf.pollInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-pf.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
pf.updateTickers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// orderbookLoop polls for orderbook updates.
|
||||
func (pf *PriceFeed) orderbookLoop() {
|
||||
defer pf.wg.Done()
|
||||
|
||||
// Orderbooks update slightly slower
|
||||
ticker := time.NewTicker(pf.pollInterval * 2)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-pf.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
pf.updateOrderbooks()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateTickers fetches latest tickers.
|
||||
func (pf *PriceFeed) updateTickers() {
|
||||
for _, symbol := range pf.symbols {
|
||||
tickers, err := pf.client.Tickers(pf.ctx, symbol)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
sp := SymbolPrices{
|
||||
Symbol: symbol,
|
||||
Prices: make(map[string]VenuePrice),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
var bestBid, bestAsk *decimal.Decimal
|
||||
|
||||
for _, t := range tickers {
|
||||
vp := VenuePrice{
|
||||
Venue: t.Venue,
|
||||
UpdatedAt: t.Timestamp,
|
||||
}
|
||||
|
||||
if t.Bid != nil {
|
||||
vp.Bid = *t.Bid
|
||||
if bestBid == nil || t.Bid.GreaterThan(*bestBid) {
|
||||
bestBid = t.Bid
|
||||
}
|
||||
}
|
||||
if t.Ask != nil {
|
||||
vp.Ask = *t.Ask
|
||||
if bestAsk == nil || t.Ask.LessThan(*bestAsk) {
|
||||
bestAsk = t.Ask
|
||||
}
|
||||
}
|
||||
if t.Last != nil {
|
||||
vp.Last = *t.Last
|
||||
}
|
||||
if t.Volume24H != nil {
|
||||
vp.Volume24H = *t.Volume24H
|
||||
}
|
||||
|
||||
sp.Prices[t.Venue] = vp
|
||||
}
|
||||
|
||||
if bestBid != nil {
|
||||
sp.BestBid = *bestBid
|
||||
}
|
||||
if bestAsk != nil {
|
||||
sp.BestAsk = *bestAsk
|
||||
}
|
||||
if bestBid != nil && bestAsk != nil {
|
||||
sp.MidPrice = bestBid.Add(*bestAsk).Div(decimal.NewFromInt(2))
|
||||
sp.Spread = bestAsk.Sub(*bestBid)
|
||||
if !bestAsk.IsZero() {
|
||||
sp.SpreadBps = sp.Spread.Div(*bestAsk).Mul(decimal.NewFromInt(10000))
|
||||
}
|
||||
}
|
||||
|
||||
pf.mu.Lock()
|
||||
pf.prices[symbol] = sp
|
||||
pf.lastUpdated[symbol] = time.Now()
|
||||
callbacks := append([]PriceCallback{}, pf.callbacks...)
|
||||
pf.mu.Unlock()
|
||||
|
||||
// Notify callbacks
|
||||
for _, cb := range callbacks {
|
||||
cb(symbol, sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateOrderbooks fetches latest orderbooks.
|
||||
func (pf *PriceFeed) updateOrderbooks() {
|
||||
for _, symbol := range pf.symbols {
|
||||
book, err := pf.client.AggregatedOrderbook(pf.ctx, symbol)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
pf.mu.Lock()
|
||||
pf.orderbooks[symbol] = book
|
||||
pf.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// GetPrice returns current prices for a symbol.
|
||||
func (pf *PriceFeed) GetPrice(symbol string) (SymbolPrices, bool) {
|
||||
pf.mu.RLock()
|
||||
defer pf.mu.RUnlock()
|
||||
sp, ok := pf.prices[symbol]
|
||||
return sp, ok
|
||||
}
|
||||
|
||||
// GetOrderbook returns current aggregated orderbook.
|
||||
func (pf *PriceFeed) GetOrderbook(symbol string) (*trading.AggregatedOrderbook, bool) {
|
||||
pf.mu.RLock()
|
||||
defer pf.mu.RUnlock()
|
||||
book, ok := pf.orderbooks[symbol]
|
||||
return book, ok
|
||||
}
|
||||
|
||||
// GetAllPrices returns all current prices.
|
||||
func (pf *PriceFeed) GetAllPrices() map[string]SymbolPrices {
|
||||
pf.mu.RLock()
|
||||
defer pf.mu.RUnlock()
|
||||
|
||||
result := make(map[string]SymbolPrices, len(pf.prices))
|
||||
for k, v := range pf.prices {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// PriceDisplay formats prices for display.
|
||||
type PriceDisplay struct {
|
||||
feed *PriceFeed
|
||||
lastPrint map[string]decimal.Decimal
|
||||
}
|
||||
|
||||
// NewPriceDisplay creates a price display.
|
||||
func NewPriceDisplay(feed *PriceFeed) *PriceDisplay {
|
||||
return &PriceDisplay{
|
||||
feed: feed,
|
||||
lastPrint: make(map[string]decimal.Decimal),
|
||||
}
|
||||
}
|
||||
|
||||
// Print prints current prices.
|
||||
func (pd *PriceDisplay) Print() {
|
||||
prices := pd.feed.GetAllPrices()
|
||||
|
||||
fmt.Printf("\033[2J\033[H") // Clear screen
|
||||
fmt.Println("=== Real-time Prices ===")
|
||||
fmt.Printf("Updated: %s\n\n", time.Now().Format("15:04:05.000"))
|
||||
|
||||
for _, symbol := range pd.feed.symbols {
|
||||
sp, ok := prices[symbol]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine direction
|
||||
direction := " "
|
||||
if last, ok := pd.lastPrint[symbol]; ok {
|
||||
if sp.MidPrice.GreaterThan(last) {
|
||||
direction = "^" // Up arrow
|
||||
} else if sp.MidPrice.LessThan(last) {
|
||||
direction = "v" // Down arrow
|
||||
}
|
||||
}
|
||||
pd.lastPrint[symbol] = sp.MidPrice
|
||||
|
||||
fmt.Printf("%s %s\n", symbol, direction)
|
||||
fmt.Printf(" Mid: $%s\n", sp.MidPrice.StringFixed(2))
|
||||
fmt.Printf(" Bid: $%s\n", sp.BestBid.StringFixed(2))
|
||||
fmt.Printf(" Ask: $%s\n", sp.BestAsk.StringFixed(2))
|
||||
fmt.Printf(" Spread: %s bps\n", sp.SpreadBps.StringFixed(2))
|
||||
|
||||
// Per-venue prices
|
||||
for venue, vp := range sp.Prices {
|
||||
fmt.Printf(" [%s] bid=%s ask=%s\n",
|
||||
venue,
|
||||
vp.Bid.StringFixed(2),
|
||||
vp.Ask.StringFixed(2))
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Create configuration
|
||||
config := trading.NewConfig()
|
||||
|
||||
// Add LX DEX
|
||||
config.WithNative("lx_dex", trading.NewLxDexConfig(
|
||||
getEnv("LX_DEX_URL", "https://api.dex.lux.network"),
|
||||
))
|
||||
|
||||
// Add CCXT exchanges
|
||||
if key := os.Getenv("BINANCE_KEY"); key != "" {
|
||||
config.WithCcxt("binance", trading.NewCcxtConfig("binance").
|
||||
WithCredentials(key, os.Getenv("BINANCE_SECRET")))
|
||||
}
|
||||
|
||||
if key := os.Getenv("MEXC_KEY"); key != "" {
|
||||
config.WithCcxt("mexc", trading.NewCcxtConfig("mexc").
|
||||
WithCredentials(key, os.Getenv("MEXC_SECRET")))
|
||||
}
|
||||
|
||||
// Create client
|
||||
client := trading.NewClient(config)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := client.Connect(ctx); err != nil {
|
||||
log.Fatalf("Failed to connect: %v", err)
|
||||
}
|
||||
defer client.Disconnect(context.Background())
|
||||
|
||||
// Create price feed
|
||||
symbols := []string{"BTC-USDC", "ETH-USDC", "LUX-USDC"}
|
||||
pollInterval := 500 * time.Millisecond
|
||||
|
||||
feed := NewPriceFeed(client, symbols, pollInterval)
|
||||
|
||||
// Add callback for price updates
|
||||
feed.OnPriceUpdate(func(symbol string, prices SymbolPrices) {
|
||||
// This callback fires on every update
|
||||
// You can use it to trigger trading logic
|
||||
if prices.SpreadBps.GreaterThan(decimal.NewFromInt(50)) {
|
||||
log.Printf("Wide spread on %s: %s bps", symbol, prices.SpreadBps.StringFixed(2))
|
||||
}
|
||||
})
|
||||
|
||||
feed.Start()
|
||||
defer feed.Stop()
|
||||
|
||||
// Display
|
||||
display := NewPriceDisplay(feed)
|
||||
|
||||
// Signal handling
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
// Display ticker
|
||||
displayTicker := time.NewTicker(time.Second)
|
||||
defer displayTicker.Stop()
|
||||
|
||||
log.Println("Price feed started. Press Ctrl+C to exit.")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-sigCh:
|
||||
return
|
||||
case <-displayTicker.C:
|
||||
display.Print()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
@@ -15,272 +15,163 @@
|
||||
* NO SMART CONTRACTS - just coordinated trades through unified SDK.
|
||||
*/
|
||||
|
||||
import { Decimal } from 'decimal.js'
|
||||
import { Decimal } from 'decimal.js';
|
||||
import {
|
||||
LxDex,
|
||||
LxFirstArbitrage,
|
||||
LxFirstConfig,
|
||||
LxFirstOpportunity,
|
||||
LxPrice,
|
||||
VenuePrice,
|
||||
UnifiedArbitrage,
|
||||
UnifiedArbConfig,
|
||||
UnifiedOpportunity,
|
||||
Scanner,
|
||||
ScannerConfig,
|
||||
ArbitrageOpportunity,
|
||||
CrossChainRouter,
|
||||
CrossChainConfig,
|
||||
CrossChainTransport,
|
||||
ChainType,
|
||||
} from '@luxfi/dex-sdk'
|
||||
defaultLxFirstConfig,
|
||||
defaultScannerConfig,
|
||||
defaultCrossChainConfig,
|
||||
} from '../src/arbitrage/index.js';
|
||||
|
||||
// ============================================
|
||||
// Configuration
|
||||
// ============================================
|
||||
|
||||
const LX_DEX_ENDPOINT = process.env.LX_DEX_ENDPOINT || 'wss://dex.lux.network/ws'
|
||||
const LX_API_KEY = process.env.LX_API_KEY
|
||||
const LX_DEX_ENDPOINT = process.env.LX_DEX_ENDPOINT || 'wss://dex.lux.network/ws';
|
||||
|
||||
// ============================================
|
||||
// Arbitrage Bot Class
|
||||
// ============================================
|
||||
|
||||
class ArbitrageBot {
|
||||
private dex: LxDex | null = null
|
||||
private lxFirst: LxFirstArbitrage | null = null
|
||||
private unified: UnifiedArbitrage | null = null
|
||||
private scanner: Scanner | null = null
|
||||
private router: CrossChainRouter | null = null
|
||||
private running = false
|
||||
private totalOpportunities = 0
|
||||
private totalExecutions = 0
|
||||
private totalPnl = new Decimal(0)
|
||||
private lxFirst: LxFirstArbitrage | null = null;
|
||||
private scanner: Scanner | null = null;
|
||||
private router: CrossChainRouter | null = null;
|
||||
private running = false;
|
||||
private totalOpportunities = 0;
|
||||
private totalExecutions = 0;
|
||||
private totalPnl = new Decimal(0);
|
||||
|
||||
async start(): Promise<void> {
|
||||
console.log('='.repeat(60))
|
||||
console.log('LX-FIRST ARBITRAGE BOT')
|
||||
console.log('='.repeat(60))
|
||||
console.log()
|
||||
console.log('='.repeat(60));
|
||||
console.log('LX-FIRST ARBITRAGE BOT');
|
||||
console.log('='.repeat(60));
|
||||
console.log();
|
||||
|
||||
// Initialize DEX client
|
||||
this.dex = new LxDex({
|
||||
endpoint: LX_DEX_ENDPOINT,
|
||||
apiKey: LX_API_KEY,
|
||||
})
|
||||
await this.dex.connect()
|
||||
console.log('[OK] Connected to LX DEX')
|
||||
// Initialize LX-First strategy with default config
|
||||
const lxConfig = defaultLxFirstConfig();
|
||||
this.lxFirst = new LxFirstArbitrage(lxConfig);
|
||||
this.lxFirst.onOpportunity((opp) => this.onLxFirstOpportunity(opp));
|
||||
console.log('[OK] LX-First strategy initialized');
|
||||
|
||||
// Initialize LX-First strategy
|
||||
const lxConfig: LxFirstConfig = {
|
||||
maxStalenessMs: 2000,
|
||||
minDivergenceBps: new Decimal(10),
|
||||
minProfit: new Decimal(5),
|
||||
maxPositionSize: new Decimal(10000),
|
||||
symbols: ['BTC-USDC', 'ETH-USDC', 'LUX-USDC'],
|
||||
venueLatencies: {
|
||||
binance: 50,
|
||||
mexc: 100,
|
||||
okx: 80,
|
||||
uniswap: 12000,
|
||||
pancakeswap: 3000,
|
||||
},
|
||||
}
|
||||
this.lxFirst = new LxFirstArbitrage(lxConfig)
|
||||
this.lxFirst.onOpportunity((opp) => this.onLxFirstOpportunity(opp))
|
||||
console.log('[OK] LX-First strategy initialized')
|
||||
// Initialize Scanner with default config
|
||||
const scannerConfig = defaultScannerConfig();
|
||||
this.scanner = new Scanner(scannerConfig);
|
||||
this.scanner.onOpportunity((opp) => this.onScannerOpportunity(opp));
|
||||
console.log('[OK] Scanner initialized');
|
||||
|
||||
// Initialize Unified Arbitrage
|
||||
const unifiedConfig: UnifiedArbConfig = {
|
||||
minSpreadBps: new Decimal(10),
|
||||
minProfit: new Decimal(5),
|
||||
maxPositionSize: new Decimal(10000),
|
||||
maxTotalExposure: new Decimal(100000),
|
||||
symbols: ['BTC-USDC', 'ETH-USDC', 'LUX-USDC'],
|
||||
venuePriority: ['lx_dex', 'binance', 'mexc', 'lx_amm'],
|
||||
scanIntervalMs: 100,
|
||||
executeTimeoutMs: 5000,
|
||||
maxDailyLoss: new Decimal(1000),
|
||||
maxTradesPerDay: 100,
|
||||
}
|
||||
this.unified = new UnifiedArbitrage(this.dex, unifiedConfig)
|
||||
this.unified.onOpportunity((opp) => this.onUnifiedOpportunity(opp))
|
||||
console.log('[OK] Unified arbitrage initialized')
|
||||
|
||||
// Initialize Scanner
|
||||
const scannerConfig: ScannerConfig = {
|
||||
minSpreadBps: new Decimal(10),
|
||||
minProfitUsd: new Decimal(10),
|
||||
maxPriceAgeMs: 5000,
|
||||
symbols: ['BTC', 'ETH', 'LUX', 'SOL', 'AVAX'],
|
||||
chainIds: ['lux', 'ethereum', 'bsc', 'arbitrum', 'polygon'],
|
||||
scanIntervalMs: 100,
|
||||
maxConcurrency: 50,
|
||||
}
|
||||
this.scanner = new Scanner(scannerConfig)
|
||||
this.scanner.onOpportunity((opp) => this.onScannerOpportunity(opp))
|
||||
console.log('[OK] Scanner initialized')
|
||||
|
||||
// Initialize Cross-Chain Router
|
||||
const routerConfig: CrossChainConfig = {
|
||||
warpEnabled: true,
|
||||
teleportEnabled: true,
|
||||
chains: {
|
||||
lux_mainnet: {
|
||||
chainId: 'lux_mainnet',
|
||||
name: 'Lux Mainnet',
|
||||
chainType: ChainType.LUX_SUBNET,
|
||||
blockTimeMs: 400,
|
||||
finalityMs: 400,
|
||||
warpSupported: true,
|
||||
teleportSupported: true,
|
||||
venues: ['lx_dex', 'lx_amm'],
|
||||
},
|
||||
ethereum: {
|
||||
chainId: '1',
|
||||
name: 'Ethereum',
|
||||
chainType: ChainType.EVM,
|
||||
blockTimeMs: 12000,
|
||||
finalityMs: 900000,
|
||||
warpSupported: false,
|
||||
teleportSupported: true,
|
||||
venues: ['uniswap', 'sushiswap'],
|
||||
},
|
||||
bsc: {
|
||||
chainId: '56',
|
||||
name: 'BNB Smart Chain',
|
||||
chainType: ChainType.EVM,
|
||||
blockTimeMs: 3000,
|
||||
finalityMs: 45000,
|
||||
warpSupported: false,
|
||||
teleportSupported: true,
|
||||
venues: ['pancakeswap'],
|
||||
},
|
||||
binance: {
|
||||
chainId: 'binance',
|
||||
name: 'Binance',
|
||||
chainType: ChainType.CEX,
|
||||
blockTimeMs: 0,
|
||||
finalityMs: 0,
|
||||
warpSupported: false,
|
||||
teleportSupported: false,
|
||||
venues: ['binance'],
|
||||
},
|
||||
},
|
||||
}
|
||||
this.router = new CrossChainRouter(routerConfig)
|
||||
console.log('[OK] Cross-chain router initialized')
|
||||
// Initialize Cross-Chain Router with default config
|
||||
const routerConfig = defaultCrossChainConfig();
|
||||
this.router = new CrossChainRouter(routerConfig);
|
||||
console.log('[OK] Cross-chain router initialized');
|
||||
|
||||
// Start all systems
|
||||
this.lxFirst.start()
|
||||
await this.unified.start()
|
||||
await this.scanner.start()
|
||||
this.running = true
|
||||
this.lxFirst.start();
|
||||
await this.scanner.start();
|
||||
this.running = true;
|
||||
|
||||
console.log()
|
||||
console.log('='.repeat(60))
|
||||
console.log('BOT RUNNING - Press Ctrl+C to stop')
|
||||
console.log('='.repeat(60))
|
||||
console.log()
|
||||
console.log(`Monitoring symbols: ${lxConfig.symbols.join(', ')}`)
|
||||
console.log(`Min divergence: ${lxConfig.minDivergenceBps} bps`)
|
||||
console.log(`Min profit: $${lxConfig.minProfit}`)
|
||||
console.log()
|
||||
console.log();
|
||||
console.log('='.repeat(60));
|
||||
console.log('BOT RUNNING - Press Ctrl+C to stop');
|
||||
console.log('='.repeat(60));
|
||||
console.log();
|
||||
console.log(`Monitoring symbols: ${lxConfig.symbols.join(', ')}`);
|
||||
console.log(`Min divergence: ${lxConfig.minDivergenceBps} bps`);
|
||||
console.log(`Min profit: $${lxConfig.minProfit}`);
|
||||
console.log();
|
||||
|
||||
// Start price feed simulation
|
||||
this.simulatePriceFeeds()
|
||||
this.simulatePriceFeeds();
|
||||
|
||||
// Start stats reporter
|
||||
this.reportStats()
|
||||
this.reportStats();
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
console.log('\nShutting down...')
|
||||
this.running = false
|
||||
console.log('\nShutting down...');
|
||||
this.running = false;
|
||||
|
||||
this.lxFirst?.stop()
|
||||
await this.unified?.stop()
|
||||
await this.scanner?.stop()
|
||||
this.lxFirst?.stop();
|
||||
await this.scanner?.stop();
|
||||
|
||||
this.printFinalStats()
|
||||
this.printFinalStats();
|
||||
}
|
||||
|
||||
private onLxFirstOpportunity(opp: LxFirstOpportunity): void {
|
||||
this.totalOpportunities++
|
||||
this.totalOpportunities++;
|
||||
|
||||
console.log()
|
||||
console.log('='.repeat(50))
|
||||
console.log('LX-FIRST OPPORTUNITY DETECTED')
|
||||
console.log('='.repeat(50))
|
||||
console.log(`Symbol: ${opp.symbol}`)
|
||||
console.log(`LX Price: $${opp.lxPrice.mid}`)
|
||||
console.log(`Stale Venue: ${opp.staleVenue}`)
|
||||
console.log(`Stale Bid/Ask: $${opp.stalePrice.bid} / $${opp.stalePrice.ask}`)
|
||||
console.log(`Staleness: ${opp.staleness}ms`)
|
||||
console.log(`Side: ${opp.side.toUpperCase()}`)
|
||||
console.log(`Divergence: ${opp.divergenceBps} bps`)
|
||||
console.log(`Expected Profit: $${opp.expectedProfit}`)
|
||||
console.log(`Confidence: ${(opp.confidence * 100).toFixed(1)}%`)
|
||||
console.log('='.repeat(50))
|
||||
console.log();
|
||||
console.log('='.repeat(50));
|
||||
console.log('LX-FIRST OPPORTUNITY DETECTED');
|
||||
console.log('='.repeat(50));
|
||||
console.log(`Symbol: ${opp.symbol}`);
|
||||
console.log(`LX Price: $${opp.lxPrice.mid}`);
|
||||
console.log(`Stale Venue: ${opp.staleVenue}`);
|
||||
console.log(`Stale Bid/Ask: $${opp.stalePrice.bid} / $${opp.stalePrice.ask}`);
|
||||
console.log(`Staleness: ${opp.staleness}ms`);
|
||||
console.log(`Side: ${opp.side.toUpperCase()}`);
|
||||
console.log(`Divergence: ${opp.divergenceBps} bps`);
|
||||
console.log(`Expected Profit: $${opp.expectedProfit}`);
|
||||
console.log(`Confidence: ${(opp.confidence * 100).toFixed(1)}%`);
|
||||
console.log('='.repeat(50));
|
||||
|
||||
// Execute if confidence is high enough
|
||||
if (opp.confidence > 0.8) {
|
||||
this.executeLxFirst(opp)
|
||||
this.executeLxFirst(opp);
|
||||
}
|
||||
}
|
||||
|
||||
private onUnifiedOpportunity(opp: UnifiedOpportunity): void {
|
||||
console.log(
|
||||
`[UNIFIED] ${opp.symbol}: Buy ${opp.buyVenue} @ $${opp.buyPrice} -> ` +
|
||||
`Sell ${opp.sellVenue} @ $${opp.sellPrice} | Net: $${opp.netProfit}`
|
||||
)
|
||||
}
|
||||
|
||||
private onScannerOpportunity(opp: ArbitrageOpportunity): void {
|
||||
console.log(
|
||||
`[SCANNER] ${opp.type}: ${opp.buySource.venue} -> ` +
|
||||
`${opp.sellSource.venue} | Spread: ${opp.spreadBps} bps | ` +
|
||||
`Net PnL: $${opp.netPnl}`
|
||||
)
|
||||
`${opp.sellSource.venue} | Spread: ${opp.spreadBps} bps | ` +
|
||||
`Net PnL: $${opp.netPnL}`
|
||||
);
|
||||
}
|
||||
|
||||
private async executeLxFirst(opp: LxFirstOpportunity): Promise<void> {
|
||||
try {
|
||||
console.log(`\n[EXECUTING] ${opp.id}...`)
|
||||
console.log(`\n[EXECUTING] ${opp.id}...`);
|
||||
|
||||
// Determine cross-chain transport
|
||||
const buyChain = this.router!.venueToChain(opp.staleVenue)
|
||||
const sellChain = 'lux_mainnet'
|
||||
const transport = this.router!.determineTransport(buyChain, sellChain)
|
||||
const latency = this.router!.estimateLatency(buyChain, sellChain)
|
||||
const buyChain = this.router!.venueToChain(opp.staleVenue);
|
||||
const sellChain = 'lux_mainnet';
|
||||
const transport = this.router!.determineTransport(buyChain, sellChain);
|
||||
const latency = this.router!.estimateLatency(buyChain, sellChain);
|
||||
|
||||
console.log(` Transport: ${transport}`)
|
||||
console.log(` Est. Latency: ${latency}ms`)
|
||||
console.log(` Transport: ${transport}`);
|
||||
console.log(` Est. Latency: ${latency}ms`);
|
||||
|
||||
if (opp.side === 'buy') {
|
||||
console.log(` Buying on ${opp.staleVenue}...`)
|
||||
console.log(` Buying on ${opp.staleVenue}...`);
|
||||
// In production: place actual order
|
||||
// const order = await cexClient.placeOrder(...)
|
||||
|
||||
console.log(' Hedging on LX DEX...')
|
||||
console.log(' Hedging on LX DEX...');
|
||||
// In production: place hedge order
|
||||
// const hedge = await this.dex.spot.sell(...)
|
||||
} else {
|
||||
console.log(` Selling on ${opp.staleVenue}...`)
|
||||
console.log(` Selling on ${opp.staleVenue}...`);
|
||||
// In production: place actual order
|
||||
|
||||
console.log(' Hedging on LX DEX...')
|
||||
console.log(' Hedging on LX DEX...');
|
||||
// In production: place hedge order
|
||||
}
|
||||
|
||||
// Simulate successful execution
|
||||
this.totalExecutions++
|
||||
const profit = opp.expectedProfit.mul(0.8) // Simulate slippage
|
||||
this.totalPnl = this.totalPnl.add(profit)
|
||||
this.totalExecutions++;
|
||||
const profit = opp.expectedProfit.mul(0.8); // Simulate slippage
|
||||
this.totalPnl = this.totalPnl.add(profit);
|
||||
|
||||
console.log(`[SUCCESS] Executed ${opp.id} | Profit: $${profit.toFixed(2)}`)
|
||||
console.log(`[SUCCESS] Executed ${opp.id} | Profit: $${profit.toFixed(2)}`);
|
||||
} catch (error) {
|
||||
console.log(`[FAILED] ${opp.id}: ${error}`)
|
||||
console.log(`[FAILED] ${opp.id}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,32 +180,36 @@ class ArbitrageBot {
|
||||
'BTC-USDC': new Decimal(50000),
|
||||
'ETH-USDC': new Decimal(3000),
|
||||
'LUX-USDC': new Decimal(25),
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (!this.running) {
|
||||
clearInterval(interval)
|
||||
return
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [symbol, base] of Object.entries(basePrices)) {
|
||||
// Simulate LX DEX price (the oracle)
|
||||
const lxMid = base.mul(1 + (Math.random() - 0.5) * 0.002)
|
||||
this.lxFirst?.updateLxPrice({
|
||||
const lxMid = base.mul(1 + (Math.random() - 0.5) * 0.002);
|
||||
const lxPrice: LxPrice = {
|
||||
symbol,
|
||||
bid: lxMid.mul(0.9999),
|
||||
ask: lxMid.mul(1.0001),
|
||||
mid: lxMid,
|
||||
timestamp: Date.now(),
|
||||
blockNum: Math.floor(Math.random() * 1000000) + 1000000,
|
||||
})
|
||||
blockNum: BigInt(Math.floor(Math.random() * 1000000) + 1000000),
|
||||
};
|
||||
this.lxFirst?.updateLxPrice(lxPrice);
|
||||
|
||||
// Simulate stale CEX prices
|
||||
for (const [venue, latency] of [['binance', 50], ['mexc', 100]] as const) {
|
||||
const divergence = (Math.random() - 0.5) * 0.004
|
||||
const venueMid = base.mul(1 + divergence)
|
||||
for (const [venue, latency] of [
|
||||
['binance', 50],
|
||||
['mexc', 100],
|
||||
] as const) {
|
||||
const divergence = (Math.random() - 0.5) * 0.004;
|
||||
const venueMid = base.mul(1 + divergence);
|
||||
|
||||
this.lxFirst?.updateVenuePrice({
|
||||
const venuePrice: VenuePrice = {
|
||||
venue,
|
||||
symbol,
|
||||
bid: venueMid.mul(0.9998),
|
||||
@@ -322,48 +217,49 @@ class ArbitrageBot {
|
||||
timestamp: Date.now() - latency,
|
||||
latency,
|
||||
stale: false,
|
||||
})
|
||||
};
|
||||
this.lxFirst?.updateVenuePrice(venuePrice);
|
||||
}
|
||||
}
|
||||
}, 100) // 10 updates per second
|
||||
}, 100); // 10 updates per second
|
||||
}
|
||||
|
||||
private reportStats(): void {
|
||||
const interval = setInterval(() => {
|
||||
if (!this.running) {
|
||||
clearInterval(interval)
|
||||
return
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log()
|
||||
console.log('-'.repeat(40))
|
||||
console.log('STATS')
|
||||
console.log(` Opportunities: ${this.totalOpportunities}`)
|
||||
console.log(` Executions: ${this.totalExecutions}`)
|
||||
console.log(` Total PnL: $${this.totalPnl.toFixed(2)}`)
|
||||
console.log();
|
||||
console.log('-'.repeat(40));
|
||||
console.log('STATS');
|
||||
console.log(` Opportunities: ${this.totalOpportunities}`);
|
||||
console.log(` Executions: ${this.totalExecutions}`);
|
||||
console.log(` Total PnL: $${this.totalPnl.toFixed(2)}`);
|
||||
if (this.totalExecutions > 0) {
|
||||
const avgPnl = this.totalPnl.div(this.totalExecutions)
|
||||
console.log(` Avg PnL: $${avgPnl.toFixed(2)}`)
|
||||
const avgPnl = this.totalPnl.div(this.totalExecutions);
|
||||
console.log(` Avg PnL: $${avgPnl.toFixed(2)}`);
|
||||
}
|
||||
console.log('-'.repeat(40))
|
||||
}, 30000) // Every 30 seconds
|
||||
console.log('-'.repeat(40));
|
||||
}, 30000); // Every 30 seconds
|
||||
}
|
||||
|
||||
private printFinalStats(): void {
|
||||
console.log()
|
||||
console.log('='.repeat(50))
|
||||
console.log('FINAL STATISTICS')
|
||||
console.log('='.repeat(50))
|
||||
console.log(`Total Opportunities: ${this.totalOpportunities}`)
|
||||
console.log(`Total Executions: ${this.totalExecutions}`)
|
||||
console.log(`Total PnL: $${this.totalPnl.toFixed(2)}`)
|
||||
console.log();
|
||||
console.log('='.repeat(50));
|
||||
console.log('FINAL STATISTICS');
|
||||
console.log('='.repeat(50));
|
||||
console.log(`Total Opportunities: ${this.totalOpportunities}`);
|
||||
console.log(`Total Executions: ${this.totalExecutions}`);
|
||||
console.log(`Total PnL: $${this.totalPnl.toFixed(2)}`);
|
||||
if (this.totalExecutions > 0) {
|
||||
const winRate = (this.totalExecutions / this.totalOpportunities) * 100
|
||||
const avgPnl = this.totalPnl.div(this.totalExecutions)
|
||||
console.log(`Execution Rate: ${winRate.toFixed(1)}%`)
|
||||
console.log(`Avg PnL per Trade: $${avgPnl.toFixed(2)}`)
|
||||
const winRate = (this.totalExecutions / this.totalOpportunities) * 100;
|
||||
const avgPnl = this.totalPnl.div(this.totalExecutions);
|
||||
console.log(`Execution Rate: ${winRate.toFixed(1)}%`);
|
||||
console.log(`Avg PnL per Trade: $${avgPnl.toFixed(2)}`);
|
||||
}
|
||||
console.log('='.repeat(50))
|
||||
console.log('='.repeat(50));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,31 +268,31 @@ class ArbitrageBot {
|
||||
// ============================================
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const bot = new ArbitrageBot()
|
||||
const bot = new ArbitrageBot();
|
||||
|
||||
// Handle Ctrl+C gracefully
|
||||
process.on('SIGINT', async () => {
|
||||
await bot.stop()
|
||||
process.exit(0)
|
||||
})
|
||||
await bot.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await bot.stop()
|
||||
process.exit(0)
|
||||
})
|
||||
await bot.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
try {
|
||||
await bot.start()
|
||||
await bot.start();
|
||||
|
||||
// Keep running
|
||||
while (true) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fatal error:', error)
|
||||
await bot.stop()
|
||||
process.exit(1)
|
||||
console.error('Fatal error:', error);
|
||||
await bot.stop();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
main();
|
||||
|
||||
Reference in New Issue
Block a user