1use digdigdig3::core::types::{StreamEvent, Ticker};
9use serde::{Deserialize, Serialize};
10
11use crate::series::DataPoint;
12
13#[inline]
16fn opt_f64(v: Option<f64>) -> f64 {
17 v.unwrap_or(f64::NAN)
18}
19
20#[inline]
21fn opt_i64_enc(v: Option<i64>) -> u64 {
22 match v {
23 Some(x) => x as u64,
24 None => i64::MIN as u64,
25 }
26}
27
28#[inline]
29fn opt_i64_dec(raw: u64) -> Option<i64> {
30 let v = raw as i64;
31 if v == i64::MIN { None } else { Some(v) }
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct TickerIndicatorsPoint {
54 pub ts_ms: i64,
55 pub last: f64,
56 pub bid: f64,
57 pub ask: f64,
58 pub bid_qty: f64,
59 pub ask_qty: f64,
60 pub high_24h: f64,
61 pub low_24h: f64,
62 pub open_price: f64,
63 pub prev_close_price: f64,
64 pub vol_24h: f64,
65 pub quote_vol_24h: f64,
66 pub change_pct_24h: f64,
67 pub weighted_avg_price: f64,
68 pub last_qty: f64,
69 pub mark_price: f64,
70 pub index_price: f64,
71 pub open_interest: f64,
72 pub funding_rate: f64,
73 pub count: Option<i64>,
75}
76
77const INDICATORS_SIZE: usize = 160;
78
79impl TickerIndicatorsPoint {
80 pub fn from_ticker(t: &Ticker) -> Self {
81 Self {
82 ts_ms: t.timestamp,
83 last: t.last_price,
84 bid: opt_f64(t.bid_price),
85 ask: opt_f64(t.ask_price),
86 bid_qty: opt_f64(t.bid_qty),
87 ask_qty: opt_f64(t.ask_qty),
88 high_24h: opt_f64(t.high_24h),
89 low_24h: opt_f64(t.low_24h),
90 open_price: opt_f64(t.open_price),
91 prev_close_price: opt_f64(t.prev_close_price),
92 vol_24h: opt_f64(t.volume_24h),
93 quote_vol_24h: opt_f64(t.quote_volume_24h),
94 change_pct_24h: opt_f64(t.price_change_percent_24h),
95 weighted_avg_price: opt_f64(t.weighted_avg_price),
96 last_qty: opt_f64(t.last_qty),
97 mark_price: opt_f64(t.mark_price),
98 index_price: opt_f64(t.index_price),
99 open_interest: opt_f64(t.open_interest),
100 funding_rate: opt_f64(t.funding_rate),
101 count: t.count,
102 }
103 }
104}
105
106impl DataPoint for TickerIndicatorsPoint {
107 const RECORD_SIZE: usize = INDICATORS_SIZE;
108
109 fn encode(&self, out: &mut [u8]) {
110 out[0..8].copy_from_slice(&(self.ts_ms as u64).to_le_bytes());
111 out[8..16].copy_from_slice(&self.last.to_le_bytes());
112 out[16..24].copy_from_slice(&self.bid.to_le_bytes());
113 out[24..32].copy_from_slice(&self.ask.to_le_bytes());
114 out[32..40].copy_from_slice(&self.bid_qty.to_le_bytes());
115 out[40..48].copy_from_slice(&self.ask_qty.to_le_bytes());
116 out[48..56].copy_from_slice(&self.high_24h.to_le_bytes());
117 out[56..64].copy_from_slice(&self.low_24h.to_le_bytes());
118 out[64..72].copy_from_slice(&self.open_price.to_le_bytes());
119 out[72..80].copy_from_slice(&self.prev_close_price.to_le_bytes());
120 out[80..88].copy_from_slice(&self.vol_24h.to_le_bytes());
121 out[88..96].copy_from_slice(&self.quote_vol_24h.to_le_bytes());
122 out[96..104].copy_from_slice(&self.change_pct_24h.to_le_bytes());
123 out[104..112].copy_from_slice(&self.weighted_avg_price.to_le_bytes());
124 out[112..120].copy_from_slice(&self.last_qty.to_le_bytes());
125 out[120..128].copy_from_slice(&self.mark_price.to_le_bytes());
126 out[128..136].copy_from_slice(&self.index_price.to_le_bytes());
127 out[136..144].copy_from_slice(&self.open_interest.to_le_bytes());
128 out[144..152].copy_from_slice(&self.funding_rate.to_le_bytes());
129 out[152..160].copy_from_slice(&opt_i64_enc(self.count).to_le_bytes());
130 }
131
132 fn decode(bytes: &[u8]) -> Option<Self> {
133 if bytes.len() != INDICATORS_SIZE {
134 return None;
135 }
136 Some(Self {
137 ts_ms: u64::from_le_bytes(bytes[0..8].try_into().ok()?) as i64,
138 last: f64::from_le_bytes(bytes[8..16].try_into().ok()?),
139 bid: f64::from_le_bytes(bytes[16..24].try_into().ok()?),
140 ask: f64::from_le_bytes(bytes[24..32].try_into().ok()?),
141 bid_qty: f64::from_le_bytes(bytes[32..40].try_into().ok()?),
142 ask_qty: f64::from_le_bytes(bytes[40..48].try_into().ok()?),
143 high_24h: f64::from_le_bytes(bytes[48..56].try_into().ok()?),
144 low_24h: f64::from_le_bytes(bytes[56..64].try_into().ok()?),
145 open_price: f64::from_le_bytes(bytes[64..72].try_into().ok()?),
146 prev_close_price: f64::from_le_bytes(bytes[72..80].try_into().ok()?),
147 vol_24h: f64::from_le_bytes(bytes[80..88].try_into().ok()?),
148 quote_vol_24h: f64::from_le_bytes(bytes[88..96].try_into().ok()?),
149 change_pct_24h: f64::from_le_bytes(bytes[96..104].try_into().ok()?),
150 weighted_avg_price: f64::from_le_bytes(bytes[104..112].try_into().ok()?),
151 last_qty: f64::from_le_bytes(bytes[112..120].try_into().ok()?),
152 mark_price: f64::from_le_bytes(bytes[120..128].try_into().ok()?),
153 index_price: f64::from_le_bytes(bytes[128..136].try_into().ok()?),
154 open_interest: f64::from_le_bytes(bytes[136..144].try_into().ok()?),
155 funding_rate: f64::from_le_bytes(bytes[144..152].try_into().ok()?),
156 count: opt_i64_dec(u64::from_le_bytes(bytes[152..160].try_into().ok()?)),
157 })
158 }
159
160 fn timestamp_ms(&self) -> i64 { self.ts_ms }
161
162 fn from_stream_event(ev: &StreamEvent) -> Option<Self> {
163 if let StreamEvent::Ticker { ticker, .. } = ev {
164 Some(Self::from_ticker(ticker))
165 } else {
166 None
167 }
168 }
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct TickerFullPoint {
207 pub ts_ms: i64,
208 pub last: f64,
209 pub bid: f64,
210 pub ask: f64,
211 pub bid_qty: f64,
212 pub ask_qty: f64,
213 pub high_24h: f64,
214 pub low_24h: f64,
215 pub vol_24h: f64,
216 pub quote_vol_24h: f64,
217 pub price_change_24h: f64,
218 pub price_change_pct_24h: f64,
219 pub open_price: f64,
220 pub prev_close_price: f64,
221 pub prev_price_24h: f64,
222 pub prev_price_1h: f64,
223 pub weighted_avg_price: f64,
224 pub open_utc: f64,
225 pub turnover_24h: f64,
226 pub first_id: Option<i64>,
227 pub last_id: Option<i64>,
228 pub count: Option<i64>,
229 pub mark_price: f64,
230 pub index_price: f64,
231 pub open_interest: f64,
232 pub open_interest_value: f64,
233 pub single_open_interest: f64,
234 pub funding_rate: f64,
235 pub next_funding_time: Option<i64>,
236 pub funding_interval_hour: f64,
237 pub funding_cap: f64,
238 pub basis: f64,
239 pub basis_rate: f64,
240 pub predicted_delivery_price: f64,
241 pub delivery_time: Option<i64>,
242 pub settlement_price: f64,
243 pub funding_8h: f64,
244 pub min_price: f64,
245 pub max_price: f64,
246 pub volume_notional: f64,
247 pub last_qty: f64,
248 pub interest_value: f64,
249 pub last_trade_time: Option<i64>,
250 pub open_time: Option<i64>,
251 pub update_id: Option<i64>,
252 pub state: Option<String>,
254}
255
256const FULL_BLOB_OFFSET: usize = 364;
259const FULL_SIZE: usize = 376;
260
261fn encode_ticker_full_numeric(p: &TickerFullPoint, out: &mut [u8]) {
262 let mut o = 0usize;
263 macro_rules! w8 {
264 ($v:expr) => { out[o..o+8].copy_from_slice(&$v.to_le_bytes()); o += 8; };
265 }
266 w8!((p.ts_ms as u64));
267 w8!(p.last);
268 w8!(p.bid);
269 w8!(p.ask);
270 w8!(p.bid_qty);
271 w8!(p.ask_qty);
272 w8!(p.high_24h);
273 w8!(p.low_24h);
274 w8!(p.vol_24h);
275 w8!(p.quote_vol_24h);
276 w8!(p.price_change_24h);
277 w8!(p.price_change_pct_24h);
278 w8!(p.open_price);
279 w8!(p.prev_close_price);
280 w8!(p.prev_price_24h);
281 w8!(p.prev_price_1h);
282 w8!(p.weighted_avg_price);
283 w8!(p.open_utc);
284 w8!(p.turnover_24h);
285 w8!(opt_i64_enc(p.first_id));
286 w8!(opt_i64_enc(p.last_id));
287 w8!(opt_i64_enc(p.count));
288 w8!(p.mark_price);
289 w8!(p.index_price);
290 w8!(p.open_interest);
291 w8!(p.open_interest_value);
292 w8!(p.single_open_interest);
293 w8!(p.funding_rate);
294 w8!(opt_i64_enc(p.next_funding_time));
295 w8!(p.funding_interval_hour);
296 w8!(p.funding_cap);
297 w8!(p.basis);
298 w8!(p.basis_rate);
299 w8!(p.predicted_delivery_price);
300 w8!(opt_i64_enc(p.delivery_time));
301 w8!(p.settlement_price);
302 w8!(p.funding_8h);
303 w8!(p.min_price);
304 w8!(p.max_price);
305 w8!(p.volume_notional);
306 w8!(p.last_qty);
307 w8!(p.interest_value);
308 w8!(opt_i64_enc(p.last_trade_time));
309 w8!(opt_i64_enc(p.open_time));
310 w8!(opt_i64_enc(p.update_id));
311 debug_assert_eq!(o, FULL_BLOB_OFFSET);
312}
313
314fn decode_ticker_full_numeric(bytes: &[u8]) -> Option<TickerFullPoint> {
315 if bytes.len() < FULL_BLOB_OFFSET {
316 return None;
317 }
318 let mut o = 0usize;
319 macro_rules! rf64 {
320 () => {{ let v = f64::from_le_bytes(bytes[o..o+8].try_into().ok()?); o += 8; v }};
321 }
322 macro_rules! ru64 {
323 () => {{ let v = u64::from_le_bytes(bytes[o..o+8].try_into().ok()?); o += 8; v }};
324 }
325 let result = Some(TickerFullPoint {
326 ts_ms: ru64!() as i64,
327 last: rf64!(),
328 bid: rf64!(),
329 ask: rf64!(),
330 bid_qty: rf64!(),
331 ask_qty: rf64!(),
332 high_24h: rf64!(),
333 low_24h: rf64!(),
334 vol_24h: rf64!(),
335 quote_vol_24h: rf64!(),
336 price_change_24h: rf64!(),
337 price_change_pct_24h: rf64!(),
338 open_price: rf64!(),
339 prev_close_price: rf64!(),
340 prev_price_24h: rf64!(),
341 prev_price_1h: rf64!(),
342 weighted_avg_price: rf64!(),
343 open_utc: rf64!(),
344 turnover_24h: rf64!(),
345 first_id: opt_i64_dec(ru64!()),
346 last_id: opt_i64_dec(ru64!()),
347 count: opt_i64_dec(ru64!()),
348 mark_price: rf64!(),
349 index_price: rf64!(),
350 open_interest: rf64!(),
351 open_interest_value: rf64!(),
352 single_open_interest: rf64!(),
353 funding_rate: rf64!(),
354 next_funding_time: opt_i64_dec(ru64!()),
355 funding_interval_hour: rf64!(),
356 funding_cap: rf64!(),
357 basis: rf64!(),
358 basis_rate: rf64!(),
359 predicted_delivery_price: rf64!(),
360 delivery_time: opt_i64_dec(ru64!()),
361 settlement_price: rf64!(),
362 funding_8h: rf64!(),
363 min_price: rf64!(),
364 max_price: rf64!(),
365 volume_notional: rf64!(),
366 last_qty: rf64!(),
367 interest_value: rf64!(),
368 last_trade_time: opt_i64_dec(ru64!()),
369 open_time: opt_i64_dec(ru64!()),
370 update_id: opt_i64_dec(ru64!()),
371 state: None, });
373 debug_assert_eq!(o, FULL_BLOB_OFFSET, "ticker full decode offset mismatch");
374 result
375}
376
377impl TickerFullPoint {
378 pub fn from_ticker(t: &Ticker) -> Self {
379 Self {
380 ts_ms: t.timestamp,
381 last: t.last_price,
382 bid: opt_f64(t.bid_price),
383 ask: opt_f64(t.ask_price),
384 bid_qty: opt_f64(t.bid_qty),
385 ask_qty: opt_f64(t.ask_qty),
386 high_24h: opt_f64(t.high_24h),
387 low_24h: opt_f64(t.low_24h),
388 vol_24h: opt_f64(t.volume_24h),
389 quote_vol_24h: opt_f64(t.quote_volume_24h),
390 price_change_24h: opt_f64(t.price_change_24h),
391 price_change_pct_24h: opt_f64(t.price_change_percent_24h),
392 open_price: opt_f64(t.open_price),
393 prev_close_price: opt_f64(t.prev_close_price),
394 prev_price_24h: opt_f64(t.prev_price_24h),
395 prev_price_1h: opt_f64(t.prev_price_1h),
396 weighted_avg_price: opt_f64(t.weighted_avg_price),
397 open_utc: opt_f64(t.open_utc),
398 turnover_24h: opt_f64(t.turnover_24h),
399 first_id: t.first_id,
400 last_id: t.last_id,
401 count: t.count,
402 mark_price: opt_f64(t.mark_price),
403 index_price: opt_f64(t.index_price),
404 open_interest: opt_f64(t.open_interest),
405 open_interest_value: opt_f64(t.open_interest_value),
406 single_open_interest: opt_f64(t.single_open_interest),
407 funding_rate: opt_f64(t.funding_rate),
408 next_funding_time: t.next_funding_time,
409 funding_interval_hour: opt_f64(t.funding_interval_hour),
410 funding_cap: opt_f64(t.funding_cap),
411 basis: opt_f64(t.basis),
412 basis_rate: opt_f64(t.basis_rate),
413 predicted_delivery_price: opt_f64(t.predicted_delivery_price),
414 delivery_time: t.delivery_time,
415 settlement_price: opt_f64(t.settlement_price),
416 funding_8h: opt_f64(t.funding_8h),
417 min_price: opt_f64(t.min_price),
418 max_price: opt_f64(t.max_price),
419 volume_notional: opt_f64(t.volume_notional),
420 last_qty: opt_f64(t.last_qty),
421 interest_value: opt_f64(t.interest_value),
422 last_trade_time: t.last_trade_time,
423 open_time: t.open_time,
424 update_id: t.update_id,
425 state: t.state.clone(),
426 }
427 }
428}
429
430impl DataPoint for TickerFullPoint {
431 const RECORD_SIZE: usize = FULL_SIZE;
432
433 fn encode(&self, out: &mut [u8]) {
434 encode_ticker_full_numeric(self, out);
435 }
437
438 fn decode(bytes: &[u8]) -> Option<Self> {
439 if bytes.len() != FULL_SIZE {
440 return None;
441 }
442 decode_ticker_full_numeric(bytes)
443 }
444
445 fn timestamp_ms(&self) -> i64 { self.ts_ms }
446
447 fn from_stream_event(ev: &StreamEvent) -> Option<Self> {
448 if let StreamEvent::Ticker { ticker, .. } = ev {
449 Some(Self::from_ticker(ticker))
450 } else {
451 None
452 }
453 }
454
455 fn encode_blob(&self) -> Option<Vec<u8>> {
456 let s = self.state.as_deref().unwrap_or("");
457 let bytes = s.as_bytes();
458 let len = bytes.len().min(u16::MAX as usize);
459 let mut blob = Vec::with_capacity(2 + len);
460 blob.extend_from_slice(&(len as u16).to_le_bytes());
461 blob.extend_from_slice(&bytes[..len]);
462 Some(blob)
463 }
464
465 fn decode_blob(header: &[u8], blob: &[u8]) -> Option<Self> {
466 let mut p = decode_ticker_full_numeric(header)?;
467 if blob.len() >= 2 {
468 let slen = u16::from_le_bytes(blob[0..2].try_into().ok()?) as usize;
469 if blob.len() >= 2 + slen {
470 let s = std::str::from_utf8(&blob[2..2 + slen]).ok()?;
471 if !s.is_empty() {
472 p.state = Some(s.to_owned());
473 }
474 }
475 }
476 Some(p)
477 }
478
479 fn blob_pointer_offset() -> Option<usize> { Some(FULL_BLOB_OFFSET) }
480}