Skip to main content

digdigdig3_station/data/
order_update.rs

1use digdigdig3::core::types::StreamEvent;
2use serde::{Deserialize, Serialize};
3
4use crate::series::DataPoint;
5
6/// On-disk record (96 bytes LE) for an order lifecycle event:
7///   u64  ts_ms              (8)  — event timestamp
8///   u64  order_id_hash      (8)  — fnv1a-64 of order_id string
9///   u64  client_id_hash     (8)  — fnv1a-64 of client_order_id, or 0
10///   u8   status             (1)  — OrderStatus enum value (see STATUS_* consts)
11///   u8   side               (1)  — 0=Buy 1=Sell
12///   u8   order_type         (1)  — 0=Market 1=Limit 2=Other
13///   u8   _pad               (1)
14///   f64  price              (8)  — limit price (0.0 for market)
15///   f64  qty                (8)  — total order quantity
16///   f64  filled_qty         (8)  — cumulative filled quantity
17///   f64  avg_price          (8)  — average fill price (0.0 if none)
18///   f64  last_fill_price    (8)  — last fill price (0.0 if none)
19///   f64  last_fill_qty      (8)  — last fill quantity (0.0 if none)
20///   f64  last_fill_comm     (8)  — last fill commission (0.0 if none)
21///   32 bytes reserved (zero-padded)
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct OrderUpdatePoint {
24    pub ts_ms: i64,
25    pub order_id_hash: u64,
26    pub client_id_hash: u64,
27    /// OrderStatus encoded: 0=New/Open, 1=PartiallyFilled, 2=Filled,
28    /// 3=Canceled, 4=Rejected/Expired
29    pub status: u8,
30    /// 0=Buy, 1=Sell
31    pub side: u8,
32    /// 0=Market, 1=Limit, 2=Other
33    pub order_type: u8,
34    pub price: f64,
35    pub qty: f64,
36    pub filled_qty: f64,
37    pub avg_price: f64,
38    pub last_fill_price: f64,
39    pub last_fill_qty: f64,
40    pub last_fill_commission: f64,
41}
42
43const SIZE: usize = 96;
44
45impl OrderUpdatePoint {
46    pub fn status_label(&self) -> &'static str {
47        match self.status {
48            0 => "New",
49            1 => "PartiallyFilled",
50            2 => "Filled",
51            3 => "Canceled",
52            4 => "Rejected/Expired",
53            _ => "Unknown",
54        }
55    }
56
57    pub fn side_label(&self) -> &'static str {
58        match self.side {
59            0 => "Buy",
60            1 => "Sell",
61            _ => "?",
62        }
63    }
64
65    pub fn order_type_label(&self) -> &'static str {
66        match self.order_type {
67            0 => "Market",
68            1 => "Limit",
69            _ => "Other",
70        }
71    }
72}
73
74impl DataPoint for OrderUpdatePoint {
75    const RECORD_SIZE: usize = SIZE;
76
77    fn encode(&self, out: &mut [u8]) {
78        out[0..8].copy_from_slice(&(self.ts_ms as u64).to_le_bytes());
79        out[8..16].copy_from_slice(&self.order_id_hash.to_le_bytes());
80        out[16..24].copy_from_slice(&self.client_id_hash.to_le_bytes());
81        out[24] = self.status;
82        out[25] = self.side;
83        out[26] = self.order_type;
84        // out[27] = pad
85        out[28..36].copy_from_slice(&self.price.to_le_bytes());
86        out[36..44].copy_from_slice(&self.qty.to_le_bytes());
87        out[44..52].copy_from_slice(&self.filled_qty.to_le_bytes());
88        out[52..60].copy_from_slice(&self.avg_price.to_le_bytes());
89        out[60..68].copy_from_slice(&self.last_fill_price.to_le_bytes());
90        out[68..76].copy_from_slice(&self.last_fill_qty.to_le_bytes());
91        out[76..84].copy_from_slice(&self.last_fill_commission.to_le_bytes());
92        // bytes 84..96 reserved
93    }
94
95    fn decode(bytes: &[u8]) -> Option<Self> {
96        if bytes.len() != SIZE {
97            return None;
98        }
99        let ts_ms = u64::from_le_bytes(bytes[0..8].try_into().ok()?) as i64;
100        let order_id_hash = u64::from_le_bytes(bytes[8..16].try_into().ok()?);
101        let client_id_hash = u64::from_le_bytes(bytes[16..24].try_into().ok()?);
102        let status = bytes[24];
103        let side = bytes[25];
104        let order_type = bytes[26];
105        let price = f64::from_le_bytes(bytes[28..36].try_into().ok()?);
106        let qty = f64::from_le_bytes(bytes[36..44].try_into().ok()?);
107        let filled_qty = f64::from_le_bytes(bytes[44..52].try_into().ok()?);
108        let avg_price = f64::from_le_bytes(bytes[52..60].try_into().ok()?);
109        let last_fill_price = f64::from_le_bytes(bytes[60..68].try_into().ok()?);
110        let last_fill_qty = f64::from_le_bytes(bytes[68..76].try_into().ok()?);
111        let last_fill_commission = f64::from_le_bytes(bytes[76..84].try_into().ok()?);
112        Some(Self {
113            ts_ms,
114            order_id_hash,
115            client_id_hash,
116            status,
117            side,
118            order_type,
119            price,
120            qty,
121            filled_qty,
122            avg_price,
123            last_fill_price,
124            last_fill_qty,
125            last_fill_commission,
126        })
127    }
128
129    fn timestamp_ms(&self) -> i64 {
130        self.ts_ms
131    }
132
133    fn from_stream_event(ev: &StreamEvent) -> Option<Self> {
134        if let StreamEvent::OrderUpdate { event, .. } = ev {
135            use digdigdig3::core::types::{OrderSide, OrderStatus, OrderType};
136            let status = match event.status {
137                OrderStatus::New => 0,
138                OrderStatus::Open => 0,
139                OrderStatus::PartiallyFilled => 1,
140                OrderStatus::Filled => 2,
141                OrderStatus::Canceled => 3,
142                OrderStatus::Rejected => 4,
143                OrderStatus::Expired => 4,
144            };
145            let side = match event.side {
146                OrderSide::Buy => 0,
147                OrderSide::Sell => 1,
148            };
149            let order_type = match event.order_type {
150                OrderType::Market => 0,
151                OrderType::Limit { .. } => 1,
152                _ => 2,
153            };
154            Some(Self {
155                ts_ms: event.timestamp,
156                order_id_hash: fnv1a_64(event.order_id.as_bytes()),
157                client_id_hash: event
158                    .client_order_id
159                    .as_deref()
160                    .map(|s| fnv1a_64(s.as_bytes()))
161                    .unwrap_or(0),
162                status,
163                side,
164                order_type,
165                price: event.price.unwrap_or(0.0),
166                qty: event.quantity,
167                filled_qty: event.filled_quantity,
168                avg_price: event.average_price.unwrap_or(0.0),
169                last_fill_price: event.last_fill_price.unwrap_or(0.0),
170                last_fill_qty: event.last_fill_quantity.unwrap_or(0.0),
171                last_fill_commission: event.last_fill_commission.unwrap_or(0.0),
172            })
173        } else {
174            None
175        }
176    }
177}
178
179fn fnv1a_64(bytes: &[u8]) -> u64 {
180    let mut h: u64 = 0xcbf29ce484222325;
181    for b in bytes {
182        h ^= *b as u64;
183        h = h.wrapping_mul(0x100000001b3);
184    }
185    h
186}