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:
2025-12-12 05:17:23 -08:00
parent b70d1cf53f
commit 84190aaa45
38 changed files with 4927 additions and 727 deletions
+4 -12
View File
@@ -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
+4 -10
View File
@@ -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
+40 -50
View File
@@ -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
+7 -4
View File
@@ -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
})
+86 -111
View File
@@ -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
+7 -5
View File
@@ -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)
```
---
+6
View File
@@ -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"
+33 -128
View File
@@ -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(())
}
+18 -18
View File
@@ -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(),
+23 -31
View File
@@ -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,
+15 -15
View File
@@ -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,
+1 -1
View File
@@ -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,
+3 -3
View File
@@ -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(),
});
}
+2 -7
View File
@@ -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::*;
+4 -4
View File
@@ -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}"))
}
}
+12 -10
View File
@@ -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() {
+2 -2
View File
@@ -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
+7 -11
View File
@@ -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;
}
}
}
}
});
+26 -30
View File
@@ -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;
+98
View File
@@ -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
+90
View File
@@ -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
+288
View File
@@ -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)
}
})
}
+114
View File
@@ -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
View File
@@ -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)
}
+340
View File
@@ -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
}
+140 -244
View File
@@ -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();