rusty_feeder/exchange/bithumb/data/
orderbook.rs

1/*
2 * Bithumb orderbook data structures and processing
3 * Optimized for minimal allocations and low latency
4 */
5
6use rust_decimal::Decimal;
7use rusty_model::data::orderbook::PriceLevel;
8use serde::{Deserialize, Serialize};
9use smallvec::SmallVec;
10use smartstring::alias::String;
11
12/// Raw orderbook message from Bithumb WebSocket
13#[derive(Debug, Clone, Deserialize, Serialize)]
14#[repr(align(64))] // Cache line alignment for better performance
15pub struct OrderbookMessage {
16    /// Message type
17    #[serde(rename = "type")]
18    pub message_type: String,
19
20    /// Market code (e.g., "KRW-BTC")
21    pub code: String,
22
23    /// Total ask volume
24    pub total_ask_size: Decimal,
25
26    /// Total bid volume
27    pub total_bid_size: Decimal,
28
29    /// Orderbook units (price levels)
30    pub orderbook_units: SmallVec<[OrderbookUnit; 32]>,
31
32    /// Price level aggregation
33    pub level: f64,
34
35    /// Message timestamp in milliseconds
36    pub timestamp: u64,
37
38    /// Stream type (SNAPSHOT or REALTIME)
39    pub stream_type: String,
40}
41
42/// Individual price level in the orderbook
43#[derive(Debug, Clone, Deserialize, Serialize)]
44pub struct OrderbookUnit {
45    /// Ask price
46    pub ask_price: Decimal,
47
48    /// Bid price
49    pub bid_price: Decimal,
50
51    /// Ask size/volume
52    pub ask_size: Decimal,
53
54    /// Bid size/volume
55    pub bid_size: Decimal,
56}
57
58/// Processed orderbook with additional information
59#[derive(Debug, Clone)]
60#[repr(align(64))] // Cache line alignment for better performance
61pub struct Orderbook {
62    /// Market code (e.g., "KRW-BTC")
63    pub code: String,
64
65    /// Timestamp in nanoseconds
66    pub timestamp_ns: u64,
67
68    /// Local timestamp when processed (nanoseconds)
69    pub local_time_ns: u64,
70
71    /// Best ask price
72    pub best_ask_price: Decimal,
73
74    /// Best bid price
75    pub best_bid_price: Decimal,
76
77    /// Ask levels (using SmallVec to avoid heap allocations)
78    pub asks: SmallVec<[PriceLevel; 20]>,
79
80    /// Bid levels (using SmallVec to avoid heap allocations)
81    pub bids: SmallVec<[PriceLevel; 20]>,
82}
83
84impl Orderbook {
85    /// Create a new orderbook from a raw message
86    #[inline(always)]
87    #[must_use]
88    pub fn from_orderbook_message(msg: &OrderbookMessage, local_time_ns: u64) -> Self {
89        // Convert millisecond timestamp to nanoseconds for HFT precision
90        let timestamp_ns = msg.timestamp * 1_000_000;
91
92        // Pre-allocate SmallVec with capacity for all entries
93        let capacity = msg.orderbook_units.len();
94        let mut asks = SmallVec::with_capacity(capacity);
95        let mut bids = SmallVec::with_capacity(capacity);
96
97        // Extract ask and bid levels and insert in sorted order
98        for unit in &msg.orderbook_units {
99            // Create ask entry
100            let ask_entry = PriceLevel {
101                price: unit.ask_price,
102                quantity: unit.ask_size,
103            };
104
105            // Find the correct insertion position for ask using binary search
106            // For asks, we want ascending order (lower prices first)
107            let ask_pos = match asks
108                .binary_search_by(|existing: &PriceLevel| existing.price.cmp(&ask_entry.price))
109            {
110                Ok(pos) => pos,  // Exact match (shouldn't happen with unique prices)
111                Err(pos) => pos, // This is where we should insert
112            };
113            asks.insert(ask_pos, ask_entry);
114
115            // Create bid entry
116            let bid_entry = PriceLevel {
117                price: unit.bid_price,
118                quantity: unit.bid_size,
119            };
120
121            // Find the correct insertion position for bid using binary search
122            // For bids, we want descending order (higher prices first)
123            let bid_pos = match bids.binary_search_by(|existing: &PriceLevel| {
124                existing.price.cmp(&bid_entry.price).reverse()
125            }) {
126                Ok(pos) => pos,  // Exact match (shouldn't happen with unique prices)
127                Err(pos) => pos, // This is where we should insert
128            };
129            bids.insert(bid_pos, bid_entry);
130        }
131
132        // Get best prices (may be None if the orderbook is empty)
133        let best_ask_price = asks.first().map_or(Decimal::ZERO, |entry| entry.price);
134        let best_bid_price = bids.first().map_or(Decimal::ZERO, |entry| entry.price);
135
136        Self {
137            code: msg.code.clone(),
138            timestamp_ns,
139            local_time_ns,
140            best_ask_price,
141            best_bid_price,
142            asks,
143            bids,
144        }
145    }
146
147    /// Calculate spread (difference between best ask and best bid)
148    #[inline(always)]
149    pub fn spread(&self) -> Decimal {
150        if self.best_ask_price.is_zero() || self.best_bid_price.is_zero() {
151            return Decimal::ZERO;
152        }
153        self.best_ask_price - self.best_bid_price
154    }
155
156    /// Calculate mid price (average of best ask and best bid)
157    #[inline(always)]
158    pub fn mid_price(&self) -> Decimal {
159        if self.best_ask_price.is_zero() || self.best_bid_price.is_zero() {
160            return Decimal::ZERO;
161        }
162        (self.best_ask_price + self.best_bid_price) / Decimal::TWO
163    }
164}