|
use anyhow::{Context, Result}; |
|
use log::info; |
|
use reqwest::Client; |
|
use serde::{Deserialize, Serialize}; |
|
use std::collections::HashMap; |
|
use tokio::time::{sleep, Duration}; |
|
|
|
// Structs cho API responses (giữ nguyên) |
|
#[derive(Deserialize, Debug)] |
|
struct Bid { |
|
price: f64, |
|
} |
|
|
|
#[derive(Deserialize, Debug)] |
|
struct Ask { |
|
price: f64, |
|
} |
|
|
|
#[derive(Deserialize, Debug)] |
|
struct Orderbook { |
|
bids: Vec<Bid>, |
|
asks: Vec<Ask>, |
|
} |
|
|
|
#[derive(Deserialize, Debug)] |
|
struct Position { |
|
size: f64, |
|
} |
|
|
|
#[derive(Deserialize, Debug)] |
|
struct Account { |
|
positions: HashMap<String, Position>, |
|
} |
|
|
|
#[derive(Serialize, Debug)] |
|
struct OrderPayload { |
|
side: String, |
|
symbol: String, |
|
#[serde(rename = "type")] |
|
r#type: String, |
|
quantity: f64, |
|
price: Option<f64>, |
|
reduce_only: Option<bool>, |
|
} |
|
|
|
#[derive(Deserialize, Debug)] |
|
struct OrderResponse { |
|
id: String, |
|
} |
|
|
|
#[derive(Deserialize, Debug)] |
|
struct PnlResponse { |
|
pnl: f64, |
|
} |
|
|
|
// Client wrapper (giữ nguyên) |
|
struct LighterClient { |
|
client: Client, |
|
base_url: String, |
|
} |
|
|
|
impl LighterClient { |
|
fn new(base_url: &str) -> Self { |
|
let client = Client::builder().build().expect("Failed to build HTTP client"); |
|
Self { |
|
client, |
|
base_url: base_url.to_string(), |
|
} |
|
} |
|
|
|
async fn get_account(&self, index: &str) -> Result<Account> { |
|
let url = format!("{}/api/v1/account", self.base_url); |
|
let resp: Account = self |
|
.client |
|
.get(&url) |
|
.query(&[("index", index)]) |
|
.send() |
|
.await? |
|
.json() |
|
.await |
|
.context("Failed to get account")?; |
|
Ok(resp) |
|
} |
|
|
|
async fn get_orderbook(&self, symbol: &str) -> Result<Orderbook> { |
|
let url = format!("{}/api/v1/orderbook", self.base_url); |
|
let resp: Orderbook = self |
|
.client |
|
.get(&url) |
|
.query(&[("symbol", symbol)]) |
|
.send() |
|
.await? |
|
.json() |
|
.await |
|
.context("Failed to get orderbook")?; |
|
Ok(resp) |
|
} |
|
|
|
async fn cancel_all_orders(&self, symbol: &str) -> Result<()> { |
|
let url = format!("{}/api/v1/orders/cancelAll", self.base_url); |
|
self.client |
|
.post(&url) |
|
.query(&[("symbol", symbol)]) |
|
.send() |
|
.await? |
|
.error_for_status()?; |
|
Ok(()) |
|
} |
|
|
|
async fn create_order(&self, payload: OrderPayload) -> Result<OrderResponse> { |
|
let url = format!("{}/api/v1/order", self.base_url); |
|
let resp: OrderResponse = self |
|
.client |
|
.post(&url) |
|
.json(&payload) |
|
.send() |
|
.await? |
|
.json() |
|
.await |
|
.context("Failed to create order")?; |
|
Ok(resp) |
|
} |
|
|
|
async fn get_pnl(&self, index: &str) -> Result<f64> { |
|
let url = format!("{}/api/v1/account/pnl", self.base_url); |
|
let resp: PnlResponse = self |
|
.client |
|
.get(&url) |
|
.query(&[("index", index)]) |
|
.send() |
|
.await? |
|
.json() |
|
.await |
|
.context("Failed to get PNL")?; |
|
Ok(resp.pnl) |
|
} |
|
} |
|
|
|
// Hàm A-S (giữ nguyên logic) |
|
fn avellaneda_stoikov( |
|
mid_price: f64, |
|
inventory: f64, |
|
gamma: f64, |
|
sigma: f64, |
|
tau: f64, |
|
k: f64, |
|
) -> (f64, f64) { |
|
let r = mid_price - inventory * gamma * sigma.powi(2) * tau; |
|
let half_spread = (1.0 / gamma) * (1.0 + gamma / k).ln() + 0.5 * gamma * sigma.powi(2) * tau; |
|
let bid = r - half_spread; |
|
let ask = r + half_spread; |
|
(bid, ask) |
|
} |
|
|
|
// Hàm đặt layered orders (mới: để tăng volume) |
|
async fn place_layered_orders(client: &LighterClient, symbol: &str, base_bid: f64, base_ask: f64, order_size: f64) -> Result<()> { |
|
let levels = 2; // 2 levels: tight + lax (spread * 1.2) |
|
for level in 0..levels { |
|
let multiplier = if level == 0 { 1.0 } else { 1.2 }; |
|
let bid_price = base_bid * multiplier; |
|
let ask_price = base_ask / multiplier; // Inverse cho ask |
|
|
|
// Buy layer |
|
let buy_payload = OrderPayload { |
|
side: "buy".to_string(), |
|
symbol: symbol.to_string(), |
|
r#type: "limit".to_string(), |
|
quantity: order_size, |
|
price: Some(bid_price), |
|
reduce_only: None, |
|
}; |
|
client.create_order(buy_payload).await?; |
|
|
|
// Sell layer |
|
let sell_payload = OrderPayload { |
|
side: "sell".to_string(), |
|
symbol: symbol.to_string(), |
|
r#type: "limit".to_string(), |
|
quantity: order_size, |
|
price: Some(ask_price), |
|
reduce_only: None, |
|
}; |
|
client.create_order(sell_payload).await?; |
|
} |
|
Ok(()) |
|
} |
|
|
|
#[tokio::main] |
|
async fn main() -> Result<()> { |
|
env_logger::init(); |
|
|
|
let base_url = "https://mainnet.zklighter.elliot.ai".to_string(); // Zero fee mainnet |
|
let mut client = LighterClient::new(&base_url); |
|
|
|
// Params A-S CHỈNH: Ưu tiên volume lớn (spread hẹp, size lớn, hedge chặt) |
|
let gamma = 0.05; // Giảm: Spread hẹp hơn → fills nhiều hơn |
|
let sigma = 0.05; // Giữ (tune theo ETH vol ~5%/ngày) |
|
let tau = 1800.0; // Giảm: Short horizon → adjust nhanh |
|
let k = 0.2; // Tăng: Intensity cao → spread hẹp |
|
let order_size = 0.1; // Tăng: Volume per trade lớn hơn |
|
let inventory_max = 0.02; // Giảm: Hedge thường → volume từ hedge |
|
let symbol = "ETH-USD".to_string(); |
|
let index = "1".to_string(); |
|
|
|
loop { |
|
// 1. Lấy account/positions |
|
let account = client.get_account(&index).await?; |
|
let inventory = account.positions.get(&symbol).map_or(0.0, |p| p.size); |
|
info!("Current inventory: {}", inventory); |
|
|
|
// 2. Lấy orderbook và mid-price |
|
let orderbook = client.get_orderbook(&symbol).await?; |
|
if orderbook.bids.is_empty() || orderbook.asks.is_empty() { |
|
info!("No orderbook data, skipping..."); |
|
sleep(Duration::from_secs(5)).await; // Tăng tần suất: 5s |
|
continue; |
|
} |
|
|
|
let best_bid = orderbook.bids.iter().map(|b| b.price).fold(f64::MIN, f64::max); |
|
let best_ask = orderbook.asks.iter().map(|a| a.price).fold(f64::MAX, f64::min); |
|
let mid_price = (best_bid + best_ask) / 2.0; |
|
info!("Mid price: {}", mid_price); |
|
|
|
// 3. Tính bid/ask theo A-S |
|
let (bid_price, ask_price) = avellaneda_stoikov(mid_price, inventory, gamma, sigma, tau, k); |
|
info!("A-S Quotes: Bid {}, Ask {}", bid_price, ask_price); |
|
|
|
// 4. Hủy orders cũ |
|
let _ = client.cancel_all_orders(&symbol).await; // Ignore error để tránh delay |
|
|
|
// 5. Đặt layered limit orders (mới: tăng volume qua multi-levels) |
|
place_layered_orders(&client, &symbol, bid_price, ask_price, order_size).await?; |
|
|
|
// 6. Hedge nếu inventory lệch |
|
if inventory.abs() > inventory_max { |
|
let hedge_side = if inventory > 0.0 { "sell" } else { "buy" }; |
|
let hedge_size = inventory.abs(); |
|
let hedge_payload = OrderPayload { |
|
side: hedge_side.to_string(), |
|
symbol: symbol.clone(), |
|
r#type: "market".to_string(), |
|
quantity: hedge_size, |
|
price: None, |
|
reduce_only: Some(true), |
|
}; |
|
if let Ok(order) = client.create_order(hedge_payload).await { |
|
info!("Hedged {} {} with market order: {}", hedge_size, hedge_side, order.id); |
|
} |
|
} |
|
|
|
// 7. Lấy PNL |
|
if let Ok(pnl) = client.get_pnl(&index).await { |
|
info!("Current PNL: {}", pnl); |
|
} |
|
|
|
sleep(Duration::from_secs(5)).await; // Update nhanh hơn cho volume cao |
|
} |
|
} |