polynode 0.6.0

Rust SDK for the PolyNode API — real-time Polymarket data
Documentation
use std::collections::HashMap;
use crate::types::orderbook::{OrderbookLevel, BookSnapshot, BookUpdate};

/// Local orderbook state manager. Apply snapshots and deltas to maintain
/// a sorted local copy of the book for any number of assets.
pub struct LocalOrderbook {
    books: HashMap<String, (Vec<OrderbookLevel>, Vec<OrderbookLevel>)>,
}

impl LocalOrderbook {
    pub fn new() -> Self {
        Self {
            books: HashMap::new(),
        }
    }

    /// Replace the full book for an asset.
    pub fn apply_snapshot(&mut self, snap: &BookSnapshot) {
        let mut bids = snap.bids.clone();
        let mut asks = snap.asks.clone();
        // Sort bids descending (best bid first), asks ascending (best ask first)
        bids.sort_by(|a, b| {
            let pa: f64 = b.price.parse().unwrap_or(0.0);
            let pb: f64 = a.price.parse().unwrap_or(0.0);
            pa.partial_cmp(&pb).unwrap_or(std::cmp::Ordering::Equal)
        });
        asks.sort_by(|a, b| {
            let pa: f64 = a.price.parse().unwrap_or(0.0);
            let pb: f64 = b.price.parse().unwrap_or(0.0);
            pa.partial_cmp(&pb).unwrap_or(std::cmp::Ordering::Equal)
        });
        self.books.insert(snap.asset_id.clone(), (bids, asks));
    }

    /// Apply an incremental delta. A level with size "0" means removal.
    pub fn apply_update(&mut self, update: &BookUpdate) {
        let book = self.books.entry(update.asset_id.clone())
            .or_insert_with(|| (Vec::new(), Vec::new()));
        apply_deltas(&mut book.0, &update.bids, true);
        apply_deltas(&mut book.1, &update.asks, false);
    }

    /// Get the full book for an asset.
    pub fn get_book(&self, asset_id: &str) -> Option<(&[OrderbookLevel], &[OrderbookLevel])> {
        self.books.get(asset_id).map(|(b, a)| (b.as_slice(), a.as_slice()))
    }

    /// Best bid (highest price).
    pub fn best_bid(&self, asset_id: &str) -> Option<&OrderbookLevel> {
        self.books.get(asset_id).and_then(|(bids, _)| bids.first())
    }

    /// Best ask (lowest price).
    pub fn best_ask(&self, asset_id: &str) -> Option<&OrderbookLevel> {
        self.books.get(asset_id).and_then(|(_, asks)| asks.first())
    }

    /// Spread between best ask and best bid.
    pub fn spread(&self, asset_id: &str) -> Option<f64> {
        let bid = self.best_bid(asset_id)?;
        let ask = self.best_ask(asset_id)?;
        let bid_price: f64 = bid.price.parse().ok()?;
        let ask_price: f64 = ask.price.parse().ok()?;
        Some(ask_price - bid_price)
    }

    /// Number of tracked assets.
    pub fn len(&self) -> usize {
        self.books.len()
    }

    /// Whether any assets are tracked.
    pub fn is_empty(&self) -> bool {
        self.books.is_empty()
    }

    /// Clear all books.
    pub fn clear(&mut self) {
        self.books.clear();
    }
}

impl Default for LocalOrderbook {
    fn default() -> Self {
        Self::new()
    }
}

fn apply_deltas(levels: &mut Vec<OrderbookLevel>, deltas: &[OrderbookLevel], descending: bool) {
    for delta in deltas {
        // Remove existing level at this price
        levels.retain(|l| l.price != delta.price);
        // Add if size is non-zero
        if delta.size != "0" && delta.size != "0.00" {
            levels.push(delta.clone());
        }
    }
    // Re-sort
    levels.sort_by(|a, b| {
        let pa: f64 = a.price.parse().unwrap_or(0.0);
        let pb: f64 = b.price.parse().unwrap_or(0.0);
        if descending {
            pb.partial_cmp(&pa).unwrap_or(std::cmp::Ordering::Equal)
        } else {
            pa.partial_cmp(&pb).unwrap_or(std::cmp::Ordering::Equal)
        }
    });
}