rusty_feeder/exchange/upbit/data/
orderbook.rs

1/*
2 * Upbit orderbook data structures and processing
3 * Optimized for minimal allocations and low latency
4 */
5
6use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8use smallvec::SmallVec;
9use smartstring::alias::String;
10
11/// Maximum number of price levels in the orderbook
12/// Used for SmallVec capacity to avoid heap allocations
13pub const MAX_ORDERBOOK_LEVELS: usize = 15;
14
15/// Raw orderbook message from Upbit WebSocket
16#[derive(Debug, Clone, Deserialize, Serialize)]
17#[repr(align(64))] // Cache line alignment for better performance
18pub struct OrderbookMessage {
19    /// Message type
20    #[serde(rename = "type")]
21    pub message_type: String,
22
23    /// Market code (e.g., "KRW-BTC")
24    pub code: String,
25
26    /// Total ask volume
27    pub total_ask_size: Decimal,
28
29    /// Total bid volume
30    pub total_bid_size: Decimal,
31
32    /// Orderbook units (price levels)
33    pub orderbook_units: SmallVec<[OrderbookUnit; MAX_ORDERBOOK_LEVELS]>,
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 data with additional information
59#[derive(Debug, Clone)]
60#[repr(align(64))] // Cache line alignment for better performance
61pub struct ParsedOrderbookData {
62    /// Market code (e.g., "KRW-BTC")
63    pub code: String,
64
65    /// Ask prices and volumes as tuples [(price, volume), ...]
66    pub asks: SmallVec<[(Decimal, Decimal); MAX_ORDERBOOK_LEVELS]>,
67
68    /// Bid prices and volumes as tuples [(price, volume), ...]
69    pub bids: SmallVec<[(Decimal, Decimal); MAX_ORDERBOOK_LEVELS]>,
70
71    /// Sequential ID or timestamp for ordering
72    pub sequence: u64,
73
74    /// Original message timestamp in nanoseconds
75    pub timestamp_ns: u64,
76}
77
78impl ParsedOrderbookData {
79    /// Create new parsed orderbook data from raw message
80    #[inline(always)]
81    #[must_use]
82    pub fn from_message(msg: &OrderbookMessage) -> Self {
83        let mut asks = SmallVec::with_capacity(MAX_ORDERBOOK_LEVELS);
84        let mut bids = SmallVec::with_capacity(MAX_ORDERBOOK_LEVELS);
85
86        // Process orderbook units and insert in sorted order
87        for (i, unit) in msg.orderbook_units.iter().enumerate() {
88            // Limit to MAX_ORDERBOOK_LEVELS to ensure no allocations
89            if i >= MAX_ORDERBOOK_LEVELS {
90                break;
91            }
92
93            // Process ask price/size and insert in sorted order (ascending)
94            let ask_price = unit.ask_price;
95            let ask_size = unit.ask_size;
96
97            // Find the correct insertion position for ask using binary search
98            // For asks, we want ascending order (lower prices first)
99            let ask_pos = match asks
100                .binary_search_by(|existing: &(Decimal, Decimal)| existing.0.cmp(&ask_price))
101            {
102                Ok(pos) => pos,  // Exact match (shouldn't happen with unique prices)
103                Err(pos) => pos, // This is where we should insert
104            };
105            asks.insert(ask_pos, (ask_price, ask_size));
106
107            // Process bid price/size and insert in sorted order (descending)
108            let bid_price = unit.bid_price;
109            let bid_size = unit.bid_size;
110
111            // Find the correct insertion position for bid using binary search
112            // For bids, we want descending order (higher prices first)
113            let bid_pos = match bids.binary_search_by(|existing: &(Decimal, Decimal)| {
114                // Reverse the comparison for descending order
115                match existing.0.cmp(&bid_price) {
116                    std::cmp::Ordering::Less => std::cmp::Ordering::Greater,
117                    std::cmp::Ordering::Greater => std::cmp::Ordering::Less,
118                    std::cmp::Ordering::Equal => std::cmp::Ordering::Equal,
119                }
120            }) {
121                Ok(pos) => pos,  // Exact match (shouldn't happen with unique prices)
122                Err(pos) => pos, // This is where we should insert
123            };
124            bids.insert(bid_pos, (bid_price, bid_size));
125        }
126
127        Self {
128            code: msg.code.clone(),
129            asks,
130            bids,
131            sequence: msg.timestamp, // Use timestamp as sequence
132            timestamp_ns: msg.timestamp * 1_000_000, // Convert to nanoseconds
133        }
134    }
135
136    /// Calculate the best ask price
137    #[inline(always)]
138    pub fn best_ask(&self) -> Option<Decimal> {
139        self.asks.first().map(|ask| ask.0)
140    }
141
142    /// Calculate the best bid price
143    #[inline(always)]
144    pub fn best_bid(&self) -> Option<Decimal> {
145        self.bids.first().map(|bid| bid.0)
146    }
147
148    /// Calculate the spread (best ask - best bid)
149    #[inline(always)]
150    pub fn spread(&self) -> Option<Decimal> {
151        match (self.best_ask(), self.best_bid()) {
152            (Some(ask), Some(bid)) => Some(ask - bid),
153            _ => None,
154        }
155    }
156
157    /// Calculate the mid price ((best ask + best bid) / 2)
158    #[inline(always)]
159    pub fn mid_price(&self) -> Option<Decimal> {
160        match (self.best_ask(), self.best_bid()) {
161            (Some(ask), Some(bid)) => Some((ask + bid) / Decimal::from(2)),
162            _ => None,
163        }
164    }
165}