mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
With formatting fixed, the Rust SDK job advanced to `cargo clippy --all-targets
--all-features -- -D warnings` and failed there with 9 lib errors. Clearing those
exposed 21 more in examples/ that the lib failure had masked.
Library (9): unnecessary_sort_by x4 -> sort_by_key / Reverse; derivable_impls x2
-> #[derive(Default)] + #[default]; a hidden elided lifetime -> Timer<'_>;
field_reassign_with_default; and `VenueCapabilities::orderBook` -> `order_book`.
The `"orderBook"` at adapters/mod.rs:30 is a VENUE-NAME STRING LITERAL, not the
method, and is deliberately left alone.
Examples (21): 15 applied by rustfix (unused imports/variables, unnecessary f64
casts, clone-on-Copy). The 6 remaining were dead code, and one of them was a real
defect:
ArbitrageBot::handle_opportunity was never called. It holds the bot's entire
execution and stats path, so `router` was also never read and the example could
only ever print a final report of zeros. LxFirstArbitrage::on_opportunity is the
intended wire; start() now registers it. The callback is a sync
Box<dyn Fn + Send + Sync>, so it spawns.
The other five were vestigial fields carrying nothing: MakerState's two
Option<Order> (the loop builds OrderRequest and simulates fills directly, so they
were only ever None) and OrderResult::symbol (every result is for the one
request's symbol). Plus a manual checked division.
Verified in the order CI runs them:
cargo fmt --check CLEAN
cargo clippy --all-targets --all-features -D CLEAN
cargo test --all-features 106 passed, 0 failed
cargo build --release CLEAN
Co-authored-by: Hanzo Dev <[email protected]>
399 lines
13 KiB
Rust
399 lines
13 KiB
Rust
//! LX-First Arbitrage Bot Example
|
|
//!
|
|
//! This bot uses the LX-First strategy where LX DEX prices are treated
|
|
//! as the "truth" (fastest venue with nanosecond updates, 200ms blocks).
|
|
//! Other venues are always stale by comparison.
|
|
//!
|
|
//! Arbitrage = exploiting stale venues before they catch up to LX prices.
|
|
//!
|
|
//! Cross-chain transport options:
|
|
//! - Warp: For Lux subnet communication (<500ms)
|
|
//! - Teleport: For EVM chain bridging (~30s)
|
|
//! - CEX API: Direct trading (instant)
|
|
//!
|
|
//! NO SMART CONTRACTS - just coordinated trades through unified SDK.
|
|
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use anyhow::Result;
|
|
use rust_decimal::Decimal;
|
|
use rust_decimal_macros::dec;
|
|
use tokio::sync::RwLock;
|
|
use tokio::time::interval;
|
|
|
|
use lx_trading::arbitrage::{
|
|
ChainType, CrossChainConfig, CrossChainInfo, CrossChainRouter, LxFirstArbitrage, LxFirstConfig,
|
|
LxFirstOpportunity, LxPrice, Scanner, ScannerConfig, VenuePrice,
|
|
};
|
|
|
|
// ============================================
|
|
// Arbitrage Bot
|
|
// ============================================
|
|
|
|
struct ArbitrageBot {
|
|
lx_first: Arc<LxFirstArbitrage>,
|
|
scanner: Arc<RwLock<Scanner>>,
|
|
router: Arc<CrossChainRouter>,
|
|
running: Arc<RwLock<bool>>,
|
|
stats: Arc<RwLock<Stats>>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct Stats {
|
|
total_opportunities: u64,
|
|
total_executions: u64,
|
|
total_pnl: Decimal,
|
|
}
|
|
|
|
impl ArbitrageBot {
|
|
async fn new() -> Result<Self> {
|
|
println!("{}", "=".repeat(60));
|
|
println!("LX-FIRST ARBITRAGE BOT");
|
|
println!("{}", "=".repeat(60));
|
|
println!();
|
|
|
|
// Initialize LX-First strategy
|
|
let lx_config = LxFirstConfig {
|
|
max_staleness_ms: 2000,
|
|
min_divergence_bps: dec!(10),
|
|
min_profit: dec!(5),
|
|
max_position_size: dec!(10000),
|
|
symbols: vec!["BTC-USDC".into(), "ETH-USDC".into(), "LUX-USDC".into()],
|
|
venue_latencies: [
|
|
("binance".into(), 50),
|
|
("mexc".into(), 100),
|
|
("okx".into(), 80),
|
|
("uniswap".into(), 12000),
|
|
("pancakeswap".into(), 3000),
|
|
]
|
|
.into_iter()
|
|
.collect(),
|
|
};
|
|
let lx_first = Arc::new(LxFirstArbitrage::new(lx_config));
|
|
println!("[OK] LX-First strategy initialized");
|
|
|
|
// Initialize Scanner
|
|
let scanner_config = ScannerConfig {
|
|
min_spread_bps: dec!(10),
|
|
min_profit_usd: dec!(10),
|
|
max_price_age_ms: 5000,
|
|
symbols: vec![
|
|
"BTC".into(),
|
|
"ETH".into(),
|
|
"LUX".into(),
|
|
"SOL".into(),
|
|
"AVAX".into(),
|
|
],
|
|
chain_ids: vec![
|
|
"lux".into(),
|
|
"ethereum".into(),
|
|
"bsc".into(),
|
|
"arbitrum".into(),
|
|
"polygon".into(),
|
|
],
|
|
scan_interval_ms: 100,
|
|
max_concurrency: 50,
|
|
};
|
|
let scanner = Arc::new(RwLock::new(Scanner::new(scanner_config)));
|
|
println!("[OK] Scanner initialized");
|
|
|
|
// Initialize Cross-Chain Router
|
|
let mut router_config = CrossChainConfig {
|
|
warp_enabled: true,
|
|
teleport_enabled: true,
|
|
..Default::default()
|
|
};
|
|
router_config.chains.insert(
|
|
"lux_mainnet".into(),
|
|
CrossChainInfo {
|
|
chain_id: "lux_mainnet".into(),
|
|
name: "Lux Mainnet".into(),
|
|
chain_type: ChainType::LuxSubnet,
|
|
block_time_ms: 400,
|
|
finality_ms: 400,
|
|
warp_supported: true,
|
|
teleport_supported: true,
|
|
venues: vec!["lx_dex".into(), "lx_amm".into()],
|
|
},
|
|
);
|
|
router_config.chains.insert(
|
|
"ethereum".into(),
|
|
CrossChainInfo {
|
|
chain_id: "1".into(),
|
|
name: "Ethereum".into(),
|
|
chain_type: ChainType::Evm,
|
|
block_time_ms: 12000,
|
|
finality_ms: 900000,
|
|
warp_supported: false,
|
|
teleport_supported: true,
|
|
venues: vec!["uniswap".into(), "sushiswap".into()],
|
|
},
|
|
);
|
|
router_config.chains.insert(
|
|
"binance".into(),
|
|
CrossChainInfo {
|
|
chain_id: "binance".into(),
|
|
name: "Binance".into(),
|
|
chain_type: ChainType::Cex,
|
|
block_time_ms: 0,
|
|
finality_ms: 0,
|
|
warp_supported: false,
|
|
teleport_supported: false,
|
|
venues: vec!["binance".into()],
|
|
},
|
|
);
|
|
let router = Arc::new(CrossChainRouter::new(router_config));
|
|
println!("[OK] Cross-chain router initialized");
|
|
|
|
Ok(Self {
|
|
lx_first,
|
|
scanner,
|
|
router,
|
|
running: Arc::new(RwLock::new(false)),
|
|
stats: Arc::new(RwLock::new(Stats::default())),
|
|
})
|
|
}
|
|
|
|
async fn start(&self) -> Result<()> {
|
|
// Start systems
|
|
self.lx_first.start().await;
|
|
{
|
|
let mut scanner = self.scanner.write().await;
|
|
scanner.start().await;
|
|
}
|
|
|
|
*self.running.write().await = true;
|
|
|
|
println!();
|
|
println!("{}", "=".repeat(60));
|
|
println!("BOT RUNNING - Press Ctrl+C to stop");
|
|
println!("{}", "=".repeat(60));
|
|
println!();
|
|
println!("Monitoring symbols: BTC-USDC, ETH-USDC, LUX-USDC");
|
|
println!("Min divergence: 10 bps");
|
|
println!("Min profit: $5");
|
|
println!();
|
|
|
|
// Route detected opportunities into handle_opportunity. Without this the
|
|
// bot never executes and its final report is all zeros -- the callback is
|
|
// the only path from detection to execution.
|
|
let router = self.router.clone();
|
|
let stats = self.stats.clone();
|
|
self.lx_first
|
|
.on_opportunity(Box::new(move |opp| {
|
|
let (router, stats) = (router.clone(), stats.clone());
|
|
tokio::spawn(async move { Self::handle_opportunity(&opp, &router, &stats).await });
|
|
}))
|
|
.await;
|
|
|
|
// Spawn price feed simulator
|
|
let lx_first = self.lx_first.clone();
|
|
let running = self.running.clone();
|
|
tokio::spawn(async move {
|
|
Self::simulate_price_feeds(lx_first, running).await;
|
|
});
|
|
|
|
// Spawn stats reporter
|
|
let stats = self.stats.clone();
|
|
let running = self.running.clone();
|
|
tokio::spawn(async move {
|
|
Self::report_stats(stats, running).await;
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn stop(&self) {
|
|
println!("\nShutting down...");
|
|
*self.running.write().await = false;
|
|
|
|
self.lx_first.stop().await;
|
|
{
|
|
let mut scanner = self.scanner.write().await;
|
|
scanner.stop().await;
|
|
}
|
|
|
|
self.print_final_stats().await;
|
|
}
|
|
|
|
async fn simulate_price_feeds(lx_first: Arc<LxFirstArbitrage>, running: Arc<RwLock<bool>>) {
|
|
use rand::{rngs::StdRng, Rng, SeedableRng};
|
|
|
|
let base_prices = [
|
|
("BTC-USDC", dec!(50000)),
|
|
("ETH-USDC", dec!(3000)),
|
|
("LUX-USDC", dec!(25)),
|
|
];
|
|
|
|
let mut interval = interval(Duration::from_millis(100));
|
|
let mut rng = StdRng::from_entropy();
|
|
|
|
loop {
|
|
interval.tick().await;
|
|
|
|
if !*running.read().await {
|
|
break;
|
|
}
|
|
|
|
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();
|
|
|
|
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();
|
|
|
|
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_opportunity(
|
|
opp: &LxFirstOpportunity,
|
|
router: &CrossChainRouter,
|
|
stats: &Arc<RwLock<Stats>>,
|
|
) {
|
|
println!();
|
|
println!("{}", "=".repeat(50));
|
|
println!("LX-FIRST OPPORTUNITY DETECTED");
|
|
println!("{}", "=".repeat(50));
|
|
println!("Symbol: {}", opp.symbol);
|
|
println!("LX Price: ${}", opp.lx_price.mid);
|
|
println!("Stale Venue: {}", opp.stale_venue);
|
|
println!(
|
|
"Stale Bid/Ask: ${} / ${}",
|
|
opp.stale_price.bid, opp.stale_price.ask
|
|
);
|
|
println!("Staleness: {}ms", opp.staleness);
|
|
println!("Side: {}", opp.side.to_uppercase());
|
|
println!("Divergence: {} bps", opp.divergence_bps);
|
|
println!("Expected Profit: ${}", opp.expected_profit);
|
|
println!("Confidence: {:.1}%", opp.confidence * 100.0);
|
|
println!("{}", "=".repeat(50));
|
|
|
|
if opp.confidence > 0.8 {
|
|
println!("\n[EXECUTING] {}...", opp.id);
|
|
|
|
// Determine cross-chain transport
|
|
let buy_chain = router.venue_to_chain(&opp.stale_venue);
|
|
let sell_chain = "lux_mainnet";
|
|
let transport = router.determine_transport(&buy_chain, sell_chain);
|
|
let latency = router.estimate_latency(&buy_chain, sell_chain);
|
|
|
|
println!(" Transport: {transport:?}");
|
|
println!(" Est. Latency: {latency}ms");
|
|
|
|
if opp.side == "buy" {
|
|
println!(" Buying on {}...", opp.stale_venue);
|
|
println!(" Hedging on LX DEX...");
|
|
} else {
|
|
println!(" Selling on {}...", opp.stale_venue);
|
|
println!(" Hedging on LX DEX...");
|
|
}
|
|
|
|
// Simulate successful execution
|
|
let profit = opp.expected_profit * dec!(0.8);
|
|
|
|
{
|
|
let mut s = stats.write().await;
|
|
s.total_executions += 1;
|
|
s.total_pnl += profit;
|
|
}
|
|
|
|
println!("[SUCCESS] Executed {} | Profit: ${:.2}", opp.id, profit);
|
|
}
|
|
|
|
{
|
|
let mut s = stats.write().await;
|
|
s.total_opportunities += 1;
|
|
}
|
|
}
|
|
|
|
async fn report_stats(stats: Arc<RwLock<Stats>>, running: Arc<RwLock<bool>>) {
|
|
let mut interval = interval(Duration::from_secs(30));
|
|
|
|
loop {
|
|
interval.tick().await;
|
|
|
|
if !*running.read().await {
|
|
break;
|
|
}
|
|
|
|
let s = stats.read().await;
|
|
println!();
|
|
println!("{}", "-".repeat(40));
|
|
println!("STATS");
|
|
println!(" Opportunities: {}", s.total_opportunities);
|
|
println!(" Executions: {}", s.total_executions);
|
|
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: ${avg_pnl:.2}");
|
|
}
|
|
println!("{}", "-".repeat(40));
|
|
}
|
|
}
|
|
|
|
async fn print_final_stats(&self) {
|
|
let s = self.stats.read().await;
|
|
println!();
|
|
println!("{}", "=".repeat(50));
|
|
println!("FINAL STATISTICS");
|
|
println!("{}", "=".repeat(50));
|
|
println!("Total Opportunities: {}", s.total_opportunities);
|
|
println!("Total Executions: {}", s.total_executions);
|
|
println!("Total PnL: ${:.2}", s.total_pnl);
|
|
if s.total_executions > 0 && s.total_opportunities > 0 {
|
|
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: {win_rate:.1}%");
|
|
println!("Avg PnL per Trade: ${avg_pnl:.2}");
|
|
}
|
|
println!("{}", "=".repeat(50));
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// Main Entry Point
|
|
// ============================================
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
// Initialize logging
|
|
tracing_subscriber::fmt::init();
|
|
|
|
let bot = ArbitrageBot::new().await?;
|
|
bot.start().await?;
|
|
|
|
// Wait for Ctrl+C
|
|
tokio::signal::ctrl_c().await?;
|
|
bot.stop().await;
|
|
|
|
Ok(())
|
|
}
|