164 lines
5.0 KiB
Rust
164 lines
5.0 KiB
Rust
use anyhow::Result;
|
|
use dashmap::DashMap;
|
|
use futures::Stream;
|
|
use futures::StreamExt;
|
|
use polymarket_client_sdk::clob::ws::{Client as WsClient, types::response::BookUpdate};
|
|
use polymarket_client_sdk::types::{B256, U256};
|
|
use std::collections::HashMap;
|
|
use std::pin::Pin;
|
|
use tracing::{debug, info};
|
|
|
|
use crate::market::MarketInfo;
|
|
|
|
/// Shorten B256 for logs: 0x + first 8 hex, e.g. 0xb91126b7..
|
|
#[inline]
|
|
fn short_b256(b: &B256) -> String {
|
|
let s = format!("{b}");
|
|
if s.len() > 12 { format!("{}..", &s[..10]) } else { s }
|
|
}
|
|
|
|
/// Shorten U256 for logs: last 8 digits, e.g. ..67033653
|
|
#[inline]
|
|
fn short_u256(u: &U256) -> String {
|
|
let s = format!("{u}");
|
|
if s.len() > 12 {
|
|
format!("..{}", &s[s.len().saturating_sub(8)..])
|
|
} else {
|
|
s
|
|
}
|
|
}
|
|
|
|
pub struct OrderBookMonitor {
|
|
ws_client: WsClient,
|
|
books: DashMap<U256, BookUpdate>,
|
|
market_map: HashMap<B256, (U256, U256)>, // market_id -> (yes_token_id, no_token_id)
|
|
}
|
|
|
|
pub struct OrderBookPair {
|
|
pub yes_book: BookUpdate,
|
|
pub no_book: BookUpdate,
|
|
pub market_id: B256,
|
|
}
|
|
|
|
impl OrderBookMonitor {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
// Use unauthenticated client: orderbook is public, no auth needed
|
|
// Only user data (orders, trades) requires auth
|
|
ws_client: WsClient::default(),
|
|
books: DashMap::new(),
|
|
market_map: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Subscribe to new market
|
|
pub fn subscribe_market(&mut self, market: &MarketInfo) -> Result<()> {
|
|
// Record market mapping
|
|
self.market_map.insert(
|
|
market.market_id,
|
|
(market.yes_token_id, market.no_token_id),
|
|
);
|
|
|
|
info!(
|
|
market_id = short_b256(&market.market_id),
|
|
yes = short_u256(&market.yes_token_id),
|
|
no = short_u256(&market.no_token_id),
|
|
"Subscribe to market orderbook"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Create orderbook subscription stream
|
|
///
|
|
/// Note: Orderbook uses unauthenticated WebSocket; orderbook data is public.
|
|
/// Only user data (order status, trade history) needs auth.
|
|
pub fn create_orderbook_stream(
|
|
&self,
|
|
) -> Result<Pin<Box<dyn Stream<Item = Result<BookUpdate>> + Send + '_>>> {
|
|
// Collect all token_ids to subscribe
|
|
let token_ids: Vec<U256> = self
|
|
.market_map
|
|
.values()
|
|
.flat_map(|(yes, no)| [*yes, *no])
|
|
.collect();
|
|
|
|
if token_ids.is_empty() {
|
|
return Err(anyhow::anyhow!("No markets to subscribe"));
|
|
}
|
|
|
|
info!(token_count = token_ids.len(), "Creating orderbook stream (unauthenticated)");
|
|
|
|
// subscribe_orderbook does not need auth
|
|
let stream = self.ws_client.subscribe_orderbook(token_ids)?;
|
|
// Convert SDK Error to anyhow::Error
|
|
let stream = stream.map(|result| result.map_err(|e| anyhow::anyhow!("{}", e)));
|
|
Ok(Box::pin(stream))
|
|
}
|
|
|
|
/// Handle orderbook update
|
|
pub fn handle_book_update(&self, book: BookUpdate) -> Option<OrderBookPair> {
|
|
|
|
// Print top 5 bid/ask (debug)
|
|
if !book.bids.is_empty() {
|
|
let top_bids: Vec<String> = book.bids.iter()
|
|
.take(5)
|
|
.map(|b| format!("{}@{}", b.size, b.price))
|
|
.collect();
|
|
debug!(
|
|
asset_id = %book.asset_id,
|
|
"Top 5 bids: {}",
|
|
top_bids.join(", ")
|
|
);
|
|
}
|
|
if !book.asks.is_empty() {
|
|
let top_asks: Vec<String> = book.asks.iter()
|
|
.take(5)
|
|
.map(|a| format!("{}@{}", a.size, a.price))
|
|
.collect();
|
|
debug!(
|
|
asset_id = short_u256(&book.asset_id),
|
|
"Top 5 asks: {}",
|
|
top_asks.join(", ")
|
|
);
|
|
}
|
|
|
|
// Update orderbook cache
|
|
self.books.insert(book.asset_id, book.clone());
|
|
|
|
// Find which market this token belongs to; either side update returns OrderBookPair for arbitrage
|
|
for (market_id, (yes_token, no_token)) in &self.market_map {
|
|
if book.asset_id == *yes_token {
|
|
if let Some(no_book) = self.books.get(no_token) {
|
|
return Some(OrderBookPair {
|
|
yes_book: book.clone(),
|
|
no_book: no_book.clone(),
|
|
market_id: *market_id,
|
|
});
|
|
}
|
|
} else if book.asset_id == *no_token {
|
|
if let Some(yes_book) = self.books.get(yes_token) {
|
|
return Some(OrderBookPair {
|
|
yes_book: yes_book.clone(),
|
|
no_book: book.clone(),
|
|
market_id: *market_id,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Get orderbook if present
|
|
pub fn get_book(&self, token_id: U256) -> Option<BookUpdate> {
|
|
self.books.get(&token_id).map(|b| b.clone())
|
|
}
|
|
|
|
/// Clear all subscriptions
|
|
pub fn clear(&mut self) {
|
|
self.books.clear();
|
|
self.market_map.clear();
|
|
}
|
|
}
|