1use std::{collections::HashMap, fmt::Display, str::FromStr};
17
18use nautilus_core::{UUID4, UnixNanos};
19pub use nautilus_execution::models::latency::LatencyModel;
20use nautilus_model::{
21 data::{delta::OrderBookDelta, deltas::OrderBookDeltas, order::BookOrder},
22 enums::{AccountType, BookAction, OrderSide, PositionSide, RecordFlag},
23 events::AccountState,
24 identifiers::{AccountId, InstrumentId},
25 reports::PositionStatusReport,
26 types::{AccountBalance, Money, Price, Quantity},
27};
28use rust_decimal::{Decimal, prelude::ToPrimitive};
29use ustr::Ustr;
30
31use crate::{
32 http::{
33 models::{HyperliquidL2Book, HyperliquidLevel},
34 parse::get_currency,
35 },
36 websocket::messages::{WsBookData, WsLevelData},
37};
38
39#[derive(Debug, Clone)]
41pub struct HyperliquidInstrumentInfo {
42 pub instrument_id: InstrumentId,
43 pub price_decimals: u8,
44 pub size_decimals: u8,
45 pub tick_size: Option<Decimal>,
47 pub step_size: Option<Decimal>,
49 pub min_notional: Option<Decimal>,
51}
52
53impl HyperliquidInstrumentInfo {
54 pub fn new(instrument_id: InstrumentId, price_decimals: u8, size_decimals: u8) -> Self {
56 Self {
57 instrument_id,
58 price_decimals,
59 size_decimals,
60 tick_size: None,
61 step_size: None,
62 min_notional: None,
63 }
64 }
65
66 pub fn with_metadata(
68 instrument_id: InstrumentId,
69 price_decimals: u8,
70 size_decimals: u8,
71 tick_size: Decimal,
72 step_size: Decimal,
73 min_notional: Decimal,
74 ) -> Self {
75 Self {
76 instrument_id,
77 price_decimals,
78 size_decimals,
79 tick_size: Some(tick_size),
80 step_size: Some(step_size),
81 min_notional: Some(min_notional),
82 }
83 }
84
85 pub fn with_precision(
87 instrument_id: InstrumentId,
88 price_decimals: u8,
89 size_decimals: u8,
90 ) -> Self {
91 let tick_size = Decimal::new(1, price_decimals as u32);
92 let step_size = Decimal::new(1, size_decimals as u32);
93 Self {
94 instrument_id,
95 price_decimals,
96 size_decimals,
97 tick_size: Some(tick_size),
98 step_size: Some(step_size),
99 min_notional: None,
100 }
101 }
102
103 pub fn default_crypto(instrument_id: InstrumentId) -> Self {
105 Self::with_precision(instrument_id, 2, 5) }
107}
108
109#[derive(Debug, Default)]
111pub struct HyperliquidInstrumentCache {
112 instruments_by_symbol: HashMap<Ustr, HyperliquidInstrumentInfo>,
113}
114
115impl HyperliquidInstrumentCache {
116 pub fn new() -> Self {
118 Self {
119 instruments_by_symbol: HashMap::new(),
120 }
121 }
122
123 pub fn insert(&mut self, symbol: &str, info: HyperliquidInstrumentInfo) {
125 self.instruments_by_symbol.insert(Ustr::from(symbol), info);
126 }
127
128 pub fn get(&self, symbol: &str) -> Option<&HyperliquidInstrumentInfo> {
130 self.instruments_by_symbol.get(&Ustr::from(symbol))
131 }
132
133 pub fn get_all(&self) -> Vec<&HyperliquidInstrumentInfo> {
135 self.instruments_by_symbol.values().collect()
136 }
137
138 pub fn contains(&self, symbol: &str) -> bool {
140 self.instruments_by_symbol.contains_key(&Ustr::from(symbol))
141 }
142
143 pub fn len(&self) -> usize {
145 self.instruments_by_symbol.len()
146 }
147
148 pub fn is_empty(&self) -> bool {
150 self.instruments_by_symbol.is_empty()
151 }
152
153 pub fn clear(&mut self) {
155 self.instruments_by_symbol.clear();
156 }
157}
158
159#[derive(Clone, Debug, PartialEq, Eq, Hash)]
161pub enum HyperliquidTradeKey {
162 Id(String),
164 Seq(u64),
166}
167
168#[derive(Debug)]
170pub struct HyperliquidDataConverter {
171 configs: HashMap<Ustr, HyperliquidInstrumentInfo>,
173}
174
175impl Default for HyperliquidDataConverter {
176 fn default() -> Self {
177 Self::new()
178 }
179}
180
181impl HyperliquidDataConverter {
182 pub fn new() -> Self {
184 Self {
185 configs: HashMap::new(),
186 }
187 }
188
189 pub fn create_latency_model(
194 &self,
195 base_latency_ns: u64,
196 insert_latency_ns: u64,
197 update_latency_ns: u64,
198 delete_latency_ns: u64,
199 ) -> LatencyModel {
200 LatencyModel::new(
201 UnixNanos::from(base_latency_ns),
202 UnixNanos::from(insert_latency_ns),
203 UnixNanos::from(update_latency_ns),
204 UnixNanos::from(delete_latency_ns),
205 )
206 }
207
208 pub fn create_default_latency_model(&self) -> LatencyModel {
210 self.create_latency_model(
212 50_000_000, 10_000_000, 5_000_000, 5_000_000, )
217 }
218
219 pub fn normalize_order_for_symbol(
224 &mut self,
225 symbol: &str,
226 price: Decimal,
227 qty: Decimal,
228 ) -> Result<(Decimal, Decimal), String> {
229 let config = self.get_config(&Ustr::from(symbol));
230
231 let tick_size = config.tick_size.unwrap_or_else(|| Decimal::new(1, 2)); let step_size = config.step_size.unwrap_or_else(|| {
234 match config.size_decimals {
236 0 => Decimal::ONE,
237 1 => Decimal::new(1, 1), 2 => Decimal::new(1, 2), 3 => Decimal::new(1, 3), 4 => Decimal::new(1, 4), 5 => Decimal::new(1, 5), _ => Decimal::new(1, 6), }
244 });
245 let min_notional = config.min_notional.unwrap_or_else(|| Decimal::from(10)); crate::common::parse::normalize_order(
248 price,
249 qty,
250 tick_size,
251 step_size,
252 min_notional,
253 config.price_decimals,
254 config.size_decimals,
255 )
256 }
257
258 pub fn configure_instrument(&mut self, symbol: &str, config: HyperliquidInstrumentInfo) {
260 self.configs.insert(Ustr::from(symbol), config);
261 }
262
263 fn get_config(&self, symbol: &Ustr) -> HyperliquidInstrumentInfo {
265 self.configs.get(symbol).cloned().unwrap_or_else(|| {
266 let instrument_id = InstrumentId::from(format!("{symbol}.HYPER").as_str());
268 HyperliquidInstrumentInfo::default_crypto(instrument_id)
269 })
270 }
271
272 pub fn convert_http_snapshot(
274 &self,
275 data: &HyperliquidL2Book,
276 instrument_id: InstrumentId,
277 ts_init: UnixNanos,
278 ) -> Result<OrderBookDeltas, ConversionError> {
279 let config = self.get_config(&data.coin);
280 let mut deltas = Vec::new();
281
282 deltas.push(OrderBookDelta::clear(
284 instrument_id,
285 0, UnixNanos::from(data.time * 1_000_000), ts_init,
288 ));
289
290 let mut order_id = 1u64; for level in &data.levels[0] {
294 let (price, size) = parse_level(level, &config)?;
295 if size.is_positive() {
296 let order = BookOrder::new(OrderSide::Buy, price, size, order_id);
297 deltas.push(OrderBookDelta::new(
298 instrument_id,
299 BookAction::Add,
300 order,
301 RecordFlag::F_LAST as u8, order_id,
303 UnixNanos::from(data.time * 1_000_000),
304 ts_init,
305 ));
306 order_id += 1;
307 }
308 }
309
310 for level in &data.levels[1] {
312 let (price, size) = parse_level(level, &config)?;
313 if size.is_positive() {
314 let order = BookOrder::new(OrderSide::Sell, price, size, order_id);
315 deltas.push(OrderBookDelta::new(
316 instrument_id,
317 BookAction::Add,
318 order,
319 RecordFlag::F_LAST as u8, order_id,
321 UnixNanos::from(data.time * 1_000_000),
322 ts_init,
323 ));
324 order_id += 1;
325 }
326 }
327
328 Ok(OrderBookDeltas::new(instrument_id, deltas))
329 }
330
331 pub fn convert_ws_snapshot(
333 &self,
334 data: &WsBookData,
335 instrument_id: InstrumentId,
336 ts_init: UnixNanos,
337 ) -> Result<OrderBookDeltas, ConversionError> {
338 let config = self.get_config(&data.coin);
339 let mut deltas = Vec::new();
340
341 deltas.push(OrderBookDelta::clear(
343 instrument_id,
344 0, UnixNanos::from(data.time * 1_000_000), ts_init,
347 ));
348
349 let mut order_id = 1u64; for level in &data.levels[0] {
353 let (price, size) = parse_ws_level(level, &config)?;
354 if size.is_positive() {
355 let order = BookOrder::new(OrderSide::Buy, price, size, order_id);
356 deltas.push(OrderBookDelta::new(
357 instrument_id,
358 BookAction::Add,
359 order,
360 RecordFlag::F_LAST as u8,
361 order_id,
362 UnixNanos::from(data.time * 1_000_000),
363 ts_init,
364 ));
365 order_id += 1;
366 }
367 }
368
369 for level in &data.levels[1] {
371 let (price, size) = parse_ws_level(level, &config)?;
372 if size.is_positive() {
373 let order = BookOrder::new(OrderSide::Sell, price, size, order_id);
374 deltas.push(OrderBookDelta::new(
375 instrument_id,
376 BookAction::Add,
377 order,
378 RecordFlag::F_LAST as u8,
379 order_id,
380 UnixNanos::from(data.time * 1_000_000),
381 ts_init,
382 ));
383 order_id += 1;
384 }
385 }
386
387 Ok(OrderBookDeltas::new(instrument_id, deltas))
388 }
389
390 #[allow(clippy::too_many_arguments)]
393 pub fn convert_delta_update(
394 &self,
395 instrument_id: InstrumentId,
396 sequence: u64,
397 ts_event: UnixNanos,
398 ts_init: UnixNanos,
399 bid_updates: &[(String, String)], ask_updates: &[(String, String)], bid_removals: &[String], ask_removals: &[String], ) -> Result<OrderBookDeltas, ConversionError> {
404 let config = self.get_config(&instrument_id.symbol.inner());
405 let mut deltas = Vec::new();
406 let mut order_id = sequence * 1000; for price_str in bid_removals {
410 let price = parse_price(price_str, &config)?;
411 let order = BookOrder::new(OrderSide::Buy, price, Quantity::from("0"), order_id);
412 deltas.push(OrderBookDelta::new(
413 instrument_id,
414 BookAction::Delete,
415 order,
416 0, sequence,
418 ts_event,
419 ts_init,
420 ));
421 order_id += 1;
422 }
423
424 for price_str in ask_removals {
426 let price = parse_price(price_str, &config)?;
427 let order = BookOrder::new(OrderSide::Sell, price, Quantity::from("0"), order_id);
428 deltas.push(OrderBookDelta::new(
429 instrument_id,
430 BookAction::Delete,
431 order,
432 0, sequence,
434 ts_event,
435 ts_init,
436 ));
437 order_id += 1;
438 }
439
440 for (price_str, size_str) in bid_updates {
442 let price = parse_price(price_str, &config)?;
443 let size = parse_size(size_str, &config)?;
444
445 if size.is_positive() {
446 let order = BookOrder::new(OrderSide::Buy, price, size, order_id);
447 deltas.push(OrderBookDelta::new(
448 instrument_id,
449 BookAction::Update, order,
451 0, sequence,
453 ts_event,
454 ts_init,
455 ));
456 } else {
457 let order = BookOrder::new(OrderSide::Buy, price, size, order_id);
459 deltas.push(OrderBookDelta::new(
460 instrument_id,
461 BookAction::Delete,
462 order,
463 0, sequence,
465 ts_event,
466 ts_init,
467 ));
468 }
469 order_id += 1;
470 }
471
472 for (price_str, size_str) in ask_updates {
474 let price = parse_price(price_str, &config)?;
475 let size = parse_size(size_str, &config)?;
476
477 if size.is_positive() {
478 let order = BookOrder::new(OrderSide::Sell, price, size, order_id);
479 deltas.push(OrderBookDelta::new(
480 instrument_id,
481 BookAction::Update, order,
483 0, sequence,
485 ts_event,
486 ts_init,
487 ));
488 } else {
489 let order = BookOrder::new(OrderSide::Sell, price, size, order_id);
491 deltas.push(OrderBookDelta::new(
492 instrument_id,
493 BookAction::Delete,
494 order,
495 0, sequence,
497 ts_event,
498 ts_init,
499 ));
500 }
501 order_id += 1;
502 }
503
504 Ok(OrderBookDeltas::new(instrument_id, deltas))
505 }
506}
507
508fn parse_level(
510 level: &HyperliquidLevel,
511 inst_info: &HyperliquidInstrumentInfo,
512) -> Result<(Price, Quantity), ConversionError> {
513 let price = parse_price(&level.px, inst_info)?;
514 let size = parse_size(&level.sz, inst_info)?;
515 Ok((price, size))
516}
517
518fn parse_ws_level(
520 level: &WsLevelData,
521 config: &HyperliquidInstrumentInfo,
522) -> Result<(Price, Quantity), ConversionError> {
523 let price = parse_price(&level.px, config)?;
524 let size = parse_size(&level.sz, config)?;
525 Ok((price, size))
526}
527
528fn parse_price(
530 price_str: &str,
531 _config: &HyperliquidInstrumentInfo,
532) -> Result<Price, ConversionError> {
533 let _decimal = Decimal::from_str(price_str).map_err(|_| ConversionError::InvalidPrice {
534 value: price_str.to_string(),
535 })?;
536
537 Price::from_str(price_str).map_err(|_| ConversionError::InvalidPrice {
538 value: price_str.to_string(),
539 })
540}
541
542fn parse_size(
544 size_str: &str,
545 _config: &HyperliquidInstrumentInfo,
546) -> Result<Quantity, ConversionError> {
547 let _decimal = Decimal::from_str(size_str).map_err(|_| ConversionError::InvalidSize {
548 value: size_str.to_string(),
549 })?;
550
551 Quantity::from_str(size_str).map_err(|_| ConversionError::InvalidSize {
552 value: size_str.to_string(),
553 })
554}
555
556#[derive(Debug, Clone, PartialEq, Eq)]
558pub enum ConversionError {
559 InvalidPrice { value: String },
561 InvalidSize { value: String },
563 OrderBookDeltasError(String),
565}
566
567impl From<anyhow::Error> for ConversionError {
568 fn from(err: anyhow::Error) -> Self {
569 Self::OrderBookDeltasError(err.to_string())
570 }
571}
572
573impl Display for ConversionError {
574 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
575 match self {
576 Self::InvalidPrice { value } => write!(f, "Invalid price: {value}"),
577 Self::InvalidSize { value } => write!(f, "Invalid size: {value}"),
578 Self::OrderBookDeltasError(msg) => {
579 write!(f, "OrderBookDeltas error: {msg}")
580 }
581 }
582 }
583}
584
585impl std::error::Error for ConversionError {}
586
587#[derive(Clone, Debug)]
600pub struct HyperliquidPositionData {
601 pub asset: String,
602 pub position: Decimal, pub entry_px: Option<Decimal>,
604 pub unrealized_pnl: Decimal,
605 pub cumulative_funding: Option<Decimal>,
606 pub position_value: Decimal,
607}
608
609impl HyperliquidPositionData {
610 pub fn is_flat(&self) -> bool {
612 self.position.is_zero()
613 }
614
615 pub fn is_long(&self) -> bool {
617 self.position > Decimal::ZERO
618 }
619
620 pub fn is_short(&self) -> bool {
622 self.position < Decimal::ZERO
623 }
624}
625
626#[derive(Clone, Debug)]
634pub struct HyperliquidBalance {
635 pub asset: String,
636 pub total: Decimal,
637 pub available: Decimal,
638 pub sequence: u64,
639 pub ts_event: UnixNanos,
640}
641
642impl HyperliquidBalance {
643 pub fn new(
644 asset: String,
645 total: Decimal,
646 available: Decimal,
647 sequence: u64,
648 ts_event: UnixNanos,
649 ) -> Self {
650 Self {
651 asset,
652 total,
653 available,
654 sequence,
655 ts_event,
656 }
657 }
658
659 pub fn locked(&self) -> Decimal {
661 (self.total - self.available).max(Decimal::ZERO)
662 }
663}
664
665#[derive(Default, Debug)]
673pub struct HyperliquidAccountState {
674 pub balances: HashMap<String, HyperliquidBalance>,
675 pub last_sequence: u64,
676}
677
678impl HyperliquidAccountState {
679 pub fn new() -> Self {
680 Default::default()
681 }
682
683 pub fn get_balance(&self, asset: &str) -> HyperliquidBalance {
685 self.balances.get(asset).cloned().unwrap_or_else(|| {
686 HyperliquidBalance::new(
687 asset.to_string(),
688 Decimal::ZERO,
689 Decimal::ZERO,
690 0,
691 UnixNanos::default(),
692 )
693 })
694 }
695
696 pub fn account_value(&self) -> Decimal {
700 self.balances.values().map(|balance| balance.total).sum()
701 }
702
703 pub fn to_account_state(
712 &self,
713 account_id: AccountId,
714 ts_event: UnixNanos,
715 ts_init: UnixNanos,
716 ) -> anyhow::Result<AccountState> {
717 let balances: Vec<AccountBalance> = self
719 .balances
720 .values()
721 .map(|balance| {
722 let currency = get_currency(&balance.asset);
724
725 let total = Money::new(balance.total.to_f64().unwrap_or(0.0), currency);
727 let free = Money::new(balance.available.to_f64().unwrap_or(0.0), currency);
728 let locked = total - free; AccountBalance::new(total, locked, free)
731 })
732 .collect();
733
734 let margins = Vec::new();
737
738 let account_type = AccountType::Margin;
740
741 let is_reported = true;
743
744 let event_id = UUID4::new();
746
747 Ok(AccountState::new(
748 account_id,
749 account_type,
750 balances,
751 margins,
752 is_reported,
753 event_id,
754 ts_event,
755 ts_init,
756 None, ))
758 }
759}
760
761#[derive(Debug, Clone)]
771pub enum HyperliquidAccountEvent {
772 BalanceSnapshot {
774 balances: Vec<HyperliquidBalance>,
775 sequence: u64,
776 },
777 BalanceDelta { balance: HyperliquidBalance },
779}
780
781impl HyperliquidAccountState {
782 pub fn apply(&mut self, event: HyperliquidAccountEvent) {
784 match event {
785 HyperliquidAccountEvent::BalanceSnapshot { balances, sequence } => {
786 self.balances.clear();
787
788 for balance in balances {
789 self.balances.insert(balance.asset.clone(), balance);
790 }
791
792 self.last_sequence = sequence;
793 }
794 HyperliquidAccountEvent::BalanceDelta { balance } => {
795 let sequence = balance.sequence;
796 let entry = self
797 .balances
798 .entry(balance.asset.clone())
799 .or_insert_with(|| balance.clone());
800
801 if sequence > entry.sequence {
803 *entry = balance;
804 self.last_sequence = self.last_sequence.max(sequence);
805 }
806 }
807 }
808 }
809}
810
811pub fn parse_position_status_report(
821 position_data: &HyperliquidPositionData,
822 account_id: AccountId,
823 instrument_id: InstrumentId,
824 ts_init: UnixNanos,
825) -> anyhow::Result<PositionStatusReport> {
826 let position_side = if position_data.is_flat() {
828 PositionSide::Flat
829 } else if position_data.is_long() {
830 PositionSide::Long
831 } else {
832 PositionSide::Short
833 };
834
835 let quantity = Quantity::new(position_data.position.abs().to_f64().unwrap_or(0.0), 0);
837
838 let ts_last = ts_init;
840
841 let avg_px_open = position_data.entry_px;
843
844 Ok(PositionStatusReport::new(
845 account_id,
846 instrument_id,
847 position_side.as_specified(),
848 quantity,
849 ts_last,
850 ts_init,
851 None, None, avg_px_open,
854 ))
855}
856
857#[cfg(test)]
860#[allow(dead_code)]
861mod tests {
862 use rstest::rstest;
863 use rust_decimal_macros::dec;
864
865 use super::*;
866
867 fn load_test_data<T>(filename: &str) -> T
868 where
869 T: serde::de::DeserializeOwned,
870 {
871 let path = format!("test_data/{filename}");
872 let content = std::fs::read_to_string(path).expect("Failed to read test data");
873 serde_json::from_str(&content).expect("Failed to parse test data")
874 }
875
876 fn test_instrument_id() -> InstrumentId {
877 InstrumentId::from("BTC.HYPER")
878 }
879
880 fn sample_http_book() -> HyperliquidL2Book {
881 load_test_data("http_l2_book_snapshot.json")
882 }
883
884 fn sample_ws_book() -> WsBookData {
885 load_test_data("ws_book_data.json")
886 }
887
888 #[rstest]
889 fn test_http_snapshot_conversion() {
890 let converter = HyperliquidDataConverter::new();
891 let book_data = sample_http_book();
892 let instrument_id = test_instrument_id();
893 let ts_init = UnixNanos::default();
894
895 let deltas = converter
896 .convert_http_snapshot(&book_data, instrument_id, ts_init)
897 .unwrap();
898
899 assert_eq!(deltas.instrument_id, instrument_id);
900 assert_eq!(deltas.deltas.len(), 11); let clear_delta = &deltas.deltas[0];
904 assert_eq!(clear_delta.instrument_id, instrument_id);
905 assert_eq!(clear_delta.action, BookAction::Clear);
906 assert_eq!(clear_delta.order.side, OrderSide::NoOrderSide);
907 assert_eq!(clear_delta.order.price.raw, 0);
908 assert_eq!(clear_delta.order.price.precision, 0);
909 assert_eq!(clear_delta.order.size.raw, 0);
910 assert_eq!(clear_delta.order.size.precision, 0);
911 assert_eq!(clear_delta.order.order_id, 0);
912 assert_eq!(clear_delta.flags, RecordFlag::F_SNAPSHOT as u8);
913 assert_eq!(clear_delta.sequence, 0);
914 assert_eq!(
915 clear_delta.ts_event,
916 UnixNanos::from(book_data.time * 1_000_000)
917 );
918 assert_eq!(clear_delta.ts_init, ts_init);
919
920 let first_bid_delta = &deltas.deltas[1];
922 assert_eq!(first_bid_delta.instrument_id, instrument_id);
923 assert_eq!(first_bid_delta.action, BookAction::Add);
924 assert_eq!(first_bid_delta.order.side, OrderSide::Buy);
925 assert_eq!(first_bid_delta.order.price, Price::from("98450.50"));
926 assert_eq!(first_bid_delta.order.size, Quantity::from("2.5"));
927 assert_eq!(first_bid_delta.order.order_id, 1);
928 assert_eq!(first_bid_delta.flags, RecordFlag::F_LAST as u8);
929 assert_eq!(first_bid_delta.sequence, 1);
930 assert_eq!(
931 first_bid_delta.ts_event,
932 UnixNanos::from(book_data.time * 1_000_000)
933 );
934 assert_eq!(first_bid_delta.ts_init, ts_init);
935
936 for delta in &deltas.deltas[1..] {
938 assert_eq!(delta.action, BookAction::Add);
939 assert!(delta.order.size.is_positive());
940 }
941 }
942
943 #[rstest]
944 fn test_ws_snapshot_conversion() {
945 let converter = HyperliquidDataConverter::new();
946 let book_data = sample_ws_book();
947 let instrument_id = test_instrument_id();
948 let ts_init = UnixNanos::default();
949
950 let deltas = converter
951 .convert_ws_snapshot(&book_data, instrument_id, ts_init)
952 .unwrap();
953
954 assert_eq!(deltas.instrument_id, instrument_id);
955 assert_eq!(deltas.deltas.len(), 11); let clear_delta = &deltas.deltas[0];
959 assert_eq!(clear_delta.instrument_id, instrument_id);
960 assert_eq!(clear_delta.action, BookAction::Clear);
961 assert_eq!(clear_delta.order.side, OrderSide::NoOrderSide);
962 assert_eq!(clear_delta.order.price.raw, 0);
963 assert_eq!(clear_delta.order.price.precision, 0);
964 assert_eq!(clear_delta.order.size.raw, 0);
965 assert_eq!(clear_delta.order.size.precision, 0);
966 assert_eq!(clear_delta.order.order_id, 0);
967 assert_eq!(clear_delta.flags, RecordFlag::F_SNAPSHOT as u8);
968 assert_eq!(clear_delta.sequence, 0);
969 assert_eq!(
970 clear_delta.ts_event,
971 UnixNanos::from(book_data.time * 1_000_000)
972 );
973 assert_eq!(clear_delta.ts_init, ts_init);
974
975 let first_bid_delta = &deltas.deltas[1];
977 assert_eq!(first_bid_delta.instrument_id, instrument_id);
978 assert_eq!(first_bid_delta.action, BookAction::Add);
979 assert_eq!(first_bid_delta.order.side, OrderSide::Buy);
980 assert_eq!(first_bid_delta.order.price, Price::from("98450.50"));
981 assert_eq!(first_bid_delta.order.size, Quantity::from("2.5"));
982 assert_eq!(first_bid_delta.order.order_id, 1);
983 assert_eq!(first_bid_delta.flags, RecordFlag::F_LAST as u8);
984 assert_eq!(first_bid_delta.sequence, 1);
985 assert_eq!(
986 first_bid_delta.ts_event,
987 UnixNanos::from(book_data.time * 1_000_000)
988 );
989 assert_eq!(first_bid_delta.ts_init, ts_init);
990 }
991
992 #[rstest]
993 fn test_delta_update_conversion() {
994 let converter = HyperliquidDataConverter::new();
995 let instrument_id = test_instrument_id();
996 let ts_event = UnixNanos::default();
997 let ts_init = UnixNanos::default();
998
999 let bid_updates = vec![("98450.00".to_string(), "1.5".to_string())];
1000 let ask_updates = vec![("98451.00".to_string(), "2.0".to_string())];
1001 let bid_removals = vec!["98449.00".to_string()];
1002 let ask_removals = vec!["98452.00".to_string()];
1003
1004 let deltas = converter
1005 .convert_delta_update(
1006 instrument_id,
1007 123,
1008 ts_event,
1009 ts_init,
1010 &bid_updates,
1011 &ask_updates,
1012 &bid_removals,
1013 &ask_removals,
1014 )
1015 .unwrap();
1016
1017 assert_eq!(deltas.instrument_id, instrument_id);
1018 assert_eq!(deltas.deltas.len(), 4); assert_eq!(deltas.sequence, 123);
1020
1021 let first_delta = &deltas.deltas[0];
1023 assert_eq!(first_delta.instrument_id, instrument_id);
1024 assert_eq!(first_delta.action, BookAction::Delete);
1025 assert_eq!(first_delta.order.side, OrderSide::Buy);
1026 assert_eq!(first_delta.order.price, Price::from("98449.00"));
1027 assert_eq!(first_delta.order.size, Quantity::from("0"));
1028 assert_eq!(first_delta.order.order_id, 123000);
1029 assert_eq!(first_delta.flags, 0);
1030 assert_eq!(first_delta.sequence, 123);
1031 assert_eq!(first_delta.ts_event, ts_event);
1032 assert_eq!(first_delta.ts_init, ts_init);
1033 }
1034
1035 #[rstest]
1036 fn test_price_size_parsing() {
1037 let instrument_id = test_instrument_id();
1038 let config = HyperliquidInstrumentInfo::new(instrument_id, 2, 5);
1039
1040 let price = parse_price("98450.50", &config).unwrap();
1041 assert_eq!(price.to_string(), "98450.50");
1042
1043 let size = parse_size("2.5", &config).unwrap();
1044 assert_eq!(size.to_string(), "2.5");
1045 }
1046
1047 #[rstest]
1048 fn test_hyperliquid_instrument_mini_info() {
1049 let instrument_id = test_instrument_id();
1050
1051 let config = HyperliquidInstrumentInfo::new(instrument_id, 4, 6);
1053 assert_eq!(config.instrument_id, instrument_id);
1054 assert_eq!(config.price_decimals, 4);
1055 assert_eq!(config.size_decimals, 6);
1056
1057 let default_config = HyperliquidInstrumentInfo::default_crypto(instrument_id);
1059 assert_eq!(default_config.instrument_id, instrument_id);
1060 assert_eq!(default_config.price_decimals, 2);
1061 assert_eq!(default_config.size_decimals, 5);
1062 }
1063
1064 #[rstest]
1065 fn test_invalid_price_parsing() {
1066 let instrument_id = test_instrument_id();
1067 let config = HyperliquidInstrumentInfo::new(instrument_id, 2, 5);
1068
1069 let result = parse_price("invalid", &config);
1071 assert!(result.is_err());
1072
1073 match result.unwrap_err() {
1074 ConversionError::InvalidPrice { value } => {
1075 assert_eq!(value, "invalid");
1076 assert!(value.contains("invalid"));
1078 }
1079 _ => panic!("Expected InvalidPrice error"),
1080 }
1081
1082 let size_result = parse_size("not_a_number", &config);
1084 assert!(size_result.is_err());
1085
1086 match size_result.unwrap_err() {
1087 ConversionError::InvalidSize { value } => {
1088 assert_eq!(value, "not_a_number");
1089 assert!(value.contains("not_a_number"));
1091 }
1092 _ => panic!("Expected InvalidSize error"),
1093 }
1094 }
1095
1096 #[rstest]
1097 fn test_configuration() {
1098 let mut converter = HyperliquidDataConverter::new();
1099 let eth_id = InstrumentId::from("ETH.HYPER");
1100 let config = HyperliquidInstrumentInfo::new(eth_id, 4, 8);
1101
1102 let asset = Ustr::from("ETH");
1103
1104 converter.configure_instrument(asset.as_str(), config.clone());
1105
1106 let retrieved_config = converter.get_config(&asset);
1108 assert_eq!(retrieved_config.instrument_id, eth_id);
1109 assert_eq!(retrieved_config.price_decimals, 4);
1110 assert_eq!(retrieved_config.size_decimals, 8);
1111
1112 let default_config = converter.get_config(&Ustr::from("UNKNOWN"));
1114 assert_eq!(
1115 default_config.instrument_id,
1116 InstrumentId::from("UNKNOWN.HYPER")
1117 );
1118 assert_eq!(default_config.price_decimals, 2);
1119 assert_eq!(default_config.size_decimals, 5);
1120
1121 assert_eq!(config.instrument_id, eth_id);
1123 assert_eq!(config.price_decimals, 4);
1124 assert_eq!(config.size_decimals, 8);
1125 }
1126
1127 #[rstest]
1128 fn test_instrument_info_creation() {
1129 let instrument_id = InstrumentId::from("BTC.HYPER");
1130 let info = HyperliquidInstrumentInfo::with_metadata(
1131 instrument_id,
1132 2,
1133 5,
1134 Decimal::from_f64_retain(0.01).unwrap(),
1135 Decimal::from_f64_retain(0.00001).unwrap(),
1136 Decimal::from_f64_retain(10.0).unwrap(),
1137 );
1138
1139 assert_eq!(info.instrument_id, instrument_id);
1140 assert_eq!(info.price_decimals, 2);
1141 assert_eq!(info.size_decimals, 5);
1142 assert_eq!(
1143 info.tick_size,
1144 Some(Decimal::from_f64_retain(0.01).unwrap())
1145 );
1146 assert_eq!(
1147 info.step_size,
1148 Some(Decimal::from_f64_retain(0.00001).unwrap())
1149 );
1150 assert_eq!(
1151 info.min_notional,
1152 Some(Decimal::from_f64_retain(10.0).unwrap())
1153 );
1154 }
1155
1156 #[rstest]
1157 fn test_instrument_info_with_precision() {
1158 let instrument_id = test_instrument_id();
1159 let info = HyperliquidInstrumentInfo::with_precision(instrument_id, 3, 4);
1160 assert_eq!(info.instrument_id, instrument_id);
1161 assert_eq!(info.price_decimals, 3);
1162 assert_eq!(info.size_decimals, 4);
1163 assert_eq!(info.tick_size, Some(dec!(0.001))); assert_eq!(info.step_size, Some(dec!(0.0001))); }
1166
1167 #[tokio::test]
1168 async fn test_instrument_cache_basic_operations() {
1169 let btc_info = HyperliquidInstrumentInfo::with_metadata(
1170 InstrumentId::from("BTC.HYPER"),
1171 2,
1172 5,
1173 Decimal::from_f64_retain(0.01).unwrap(),
1174 Decimal::from_f64_retain(0.00001).unwrap(),
1175 Decimal::from_f64_retain(10.0).unwrap(),
1176 );
1177
1178 let eth_info = HyperliquidInstrumentInfo::with_metadata(
1179 InstrumentId::from("ETH.HYPER"),
1180 2,
1181 4,
1182 Decimal::from_f64_retain(0.01).unwrap(),
1183 Decimal::from_f64_retain(0.0001).unwrap(),
1184 Decimal::from_f64_retain(10.0).unwrap(),
1185 );
1186
1187 let mut cache = HyperliquidInstrumentCache::new();
1188
1189 cache.insert("BTC", btc_info.clone());
1191 cache.insert("ETH", eth_info.clone());
1192
1193 let retrieved_btc = cache.get("BTC").unwrap();
1195 assert_eq!(retrieved_btc.instrument_id, btc_info.instrument_id);
1196 assert_eq!(retrieved_btc.size_decimals, 5);
1197
1198 let retrieved_eth = cache.get("ETH").unwrap();
1200 assert_eq!(retrieved_eth.instrument_id, eth_info.instrument_id);
1201 assert_eq!(retrieved_eth.size_decimals, 4);
1202
1203 assert_eq!(cache.len(), 2);
1205 assert!(!cache.is_empty());
1206
1207 assert!(cache.contains("BTC"));
1209 assert!(cache.contains("ETH"));
1210 assert!(!cache.contains("UNKNOWN"));
1211
1212 let all_instruments = cache.get_all();
1214 assert_eq!(all_instruments.len(), 2);
1215 }
1216
1217 #[rstest]
1218 fn test_instrument_cache_empty() {
1219 let cache = HyperliquidInstrumentCache::new();
1220 let result = cache.get("UNKNOWN");
1221 assert!(result.is_none());
1222 assert!(cache.is_empty());
1223 assert_eq!(cache.len(), 0);
1224 }
1225
1226 #[rstest]
1227 fn test_latency_model_creation() {
1228 let converter = HyperliquidDataConverter::new();
1229
1230 let latency_model = converter.create_latency_model(
1232 100_000_000, 20_000_000, 10_000_000, 10_000_000, );
1237
1238 assert_eq!(latency_model.base_latency_nanos.as_u64(), 100_000_000);
1239 assert_eq!(latency_model.insert_latency_nanos.as_u64(), 20_000_000);
1240 assert_eq!(latency_model.update_latency_nanos.as_u64(), 10_000_000);
1241 assert_eq!(latency_model.delete_latency_nanos.as_u64(), 10_000_000);
1242
1243 let default_model = converter.create_default_latency_model();
1245 assert_eq!(default_model.base_latency_nanos.as_u64(), 50_000_000);
1246 assert_eq!(default_model.insert_latency_nanos.as_u64(), 10_000_000);
1247 assert_eq!(default_model.update_latency_nanos.as_u64(), 5_000_000);
1248 assert_eq!(default_model.delete_latency_nanos.as_u64(), 5_000_000);
1249
1250 let display_str = format!("{default_model}");
1252 assert_eq!(display_str, "LatencyModel()");
1253 }
1254
1255 #[rstest]
1256 fn test_normalize_order_for_symbol() {
1257 use rust_decimal_macros::dec;
1258
1259 let mut converter = HyperliquidDataConverter::new();
1260
1261 let btc_info = HyperliquidInstrumentInfo::with_metadata(
1263 InstrumentId::from("BTC.HYPER"),
1264 2,
1265 5,
1266 dec!(0.01), dec!(0.00001), dec!(10.0), );
1270 converter.configure_instrument("BTC", btc_info);
1271
1272 let result = converter.normalize_order_for_symbol(
1274 "BTC",
1275 dec!(50123.456789), dec!(0.123456789), );
1278
1279 assert!(result.is_ok());
1280 let (price, qty) = result.unwrap();
1281 assert_eq!(price, dec!(50123.45)); assert_eq!(qty, dec!(0.12345)); let result_eth = converter.normalize_order_for_symbol("ETH", dec!(3000.123), dec!(1.23456));
1286 assert!(result_eth.is_ok());
1287
1288 let result_fail = converter.normalize_order_for_symbol(
1290 "BTC",
1291 dec!(1.0), dec!(0.001), );
1294 assert!(result_fail.is_err());
1295 assert!(result_fail.unwrap_err().contains("Notional value"));
1296 }
1297
1298 #[rstest]
1299 fn test_hyperliquid_balance_creation_and_properties() {
1300 use rust_decimal_macros::dec;
1301
1302 let asset = "USD".to_string();
1303 let total = dec!(1000.0);
1304 let available = dec!(750.0);
1305 let sequence = 42;
1306 let ts_event = UnixNanos::default();
1307
1308 let balance = HyperliquidBalance::new(asset.clone(), total, available, sequence, ts_event);
1309
1310 assert_eq!(balance.asset, asset);
1311 assert_eq!(balance.total, total);
1312 assert_eq!(balance.available, available);
1313 assert_eq!(balance.sequence, sequence);
1314 assert_eq!(balance.ts_event, ts_event);
1315 assert_eq!(balance.locked(), dec!(250.0)); let full_balance = HyperliquidBalance::new(
1319 "ETH".to_string(),
1320 dec!(100.0),
1321 dec!(100.0),
1322 1,
1323 UnixNanos::default(),
1324 );
1325 assert_eq!(full_balance.locked(), dec!(0.0));
1326
1327 let weird_balance = HyperliquidBalance::new(
1329 "WEIRD".to_string(),
1330 dec!(50.0),
1331 dec!(60.0),
1332 1,
1333 UnixNanos::default(),
1334 );
1335 assert_eq!(weird_balance.locked(), dec!(0.0));
1336 }
1337
1338 #[rstest]
1339 fn test_hyperliquid_account_state_creation() {
1340 let state = HyperliquidAccountState::new();
1341 assert!(state.balances.is_empty());
1342 assert_eq!(state.last_sequence, 0);
1343
1344 let default_state = HyperliquidAccountState::default();
1345 assert!(default_state.balances.is_empty());
1346 assert_eq!(default_state.last_sequence, 0);
1347 }
1348
1349 #[rstest]
1350 fn test_hyperliquid_account_state_getters() {
1351 use rust_decimal_macros::dec;
1352
1353 let mut state = HyperliquidAccountState::new();
1354
1355 let balance = state.get_balance("USD");
1357 assert_eq!(balance.asset, "USD");
1358 assert_eq!(balance.total, dec!(0.0));
1359 assert_eq!(balance.available, dec!(0.0));
1360
1361 let real_balance = HyperliquidBalance::new(
1363 "USD".to_string(),
1364 dec!(1000.0),
1365 dec!(750.0),
1366 1,
1367 UnixNanos::default(),
1368 );
1369 state.balances.insert("USD".to_string(), real_balance);
1370
1371 let retrieved_balance = state.get_balance("USD");
1373 assert_eq!(retrieved_balance.total, dec!(1000.0));
1374 }
1375
1376 #[rstest]
1377 fn test_hyperliquid_account_state_account_value() {
1378 use rust_decimal_macros::dec;
1379
1380 let mut state = HyperliquidAccountState::new();
1381
1382 state.balances.insert(
1384 "USD".to_string(),
1385 HyperliquidBalance::new(
1386 "USD".to_string(),
1387 dec!(10000.0),
1388 dec!(5000.0),
1389 1,
1390 UnixNanos::default(),
1391 ),
1392 );
1393
1394 let total_value = state.account_value();
1395 assert_eq!(total_value, dec!(10000.0));
1396
1397 state.balances.clear();
1399 let no_balance_value = state.account_value();
1400 assert_eq!(no_balance_value, dec!(0.0));
1401 }
1402
1403 #[rstest]
1404 fn test_hyperliquid_account_event_balance_snapshot() {
1405 use rust_decimal_macros::dec;
1406
1407 let mut state = HyperliquidAccountState::new();
1408
1409 let balance = HyperliquidBalance::new(
1410 "USD".to_string(),
1411 dec!(1000.0),
1412 dec!(750.0),
1413 10,
1414 UnixNanos::default(),
1415 );
1416
1417 let snapshot_event = HyperliquidAccountEvent::BalanceSnapshot {
1418 balances: vec![balance],
1419 sequence: 10,
1420 };
1421
1422 state.apply(snapshot_event);
1423
1424 assert_eq!(state.balances.len(), 1);
1425 assert_eq!(state.last_sequence, 10);
1426 assert_eq!(state.get_balance("USD").total, dec!(1000.0));
1427 }
1428
1429 #[rstest]
1430 fn test_hyperliquid_account_event_balance_delta() {
1431 use rust_decimal_macros::dec;
1432
1433 let mut state = HyperliquidAccountState::new();
1434
1435 let initial_balance = HyperliquidBalance::new(
1437 "USD".to_string(),
1438 dec!(1000.0),
1439 dec!(750.0),
1440 5,
1441 UnixNanos::default(),
1442 );
1443 state.balances.insert("USD".to_string(), initial_balance);
1444 state.last_sequence = 5;
1445
1446 let updated_balance = HyperliquidBalance::new(
1448 "USD".to_string(),
1449 dec!(1200.0),
1450 dec!(900.0),
1451 10,
1452 UnixNanos::default(),
1453 );
1454
1455 let delta_event = HyperliquidAccountEvent::BalanceDelta {
1456 balance: updated_balance,
1457 };
1458
1459 state.apply(delta_event);
1460
1461 let balance = state.get_balance("USD");
1462 assert_eq!(balance.total, dec!(1200.0));
1463 assert_eq!(balance.available, dec!(900.0));
1464 assert_eq!(balance.sequence, 10);
1465 assert_eq!(state.last_sequence, 10);
1466
1467 let old_balance = HyperliquidBalance::new(
1469 "USD".to_string(),
1470 dec!(800.0),
1471 dec!(600.0),
1472 8,
1473 UnixNanos::default(),
1474 );
1475
1476 let old_delta_event = HyperliquidAccountEvent::BalanceDelta {
1477 balance: old_balance,
1478 };
1479
1480 state.apply(old_delta_event);
1481
1482 let balance = state.get_balance("USD");
1484 assert_eq!(balance.total, dec!(1200.0)); assert_eq!(balance.sequence, 10); assert_eq!(state.last_sequence, 10); }
1488}