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!("{}.HYPER", symbol).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)]
863#[allow(dead_code)]
864mod tests {
865 use rstest::rstest;
866 use rust_decimal_macros::dec;
867
868 use super::*;
869
870 fn load_test_data<T>(filename: &str) -> T
871 where
872 T: serde::de::DeserializeOwned,
873 {
874 let path = format!("test_data/{}", filename);
875 let content = std::fs::read_to_string(path).expect("Failed to read test data");
876 serde_json::from_str(&content).expect("Failed to parse test data")
877 }
878
879 fn test_instrument_id() -> InstrumentId {
880 InstrumentId::from("BTC.HYPER")
881 }
882
883 fn sample_http_book() -> HyperliquidL2Book {
884 load_test_data("http_l2_book_snapshot.json")
885 }
886
887 fn sample_ws_book() -> WsBookData {
888 load_test_data("ws_book_data.json")
889 }
890
891 #[rstest]
892 fn test_http_snapshot_conversion() {
893 let converter = HyperliquidDataConverter::new();
894 let book_data = sample_http_book();
895 let instrument_id = test_instrument_id();
896 let ts_init = UnixNanos::default();
897
898 let deltas = converter
899 .convert_http_snapshot(&book_data, instrument_id, ts_init)
900 .unwrap();
901
902 assert_eq!(deltas.instrument_id, instrument_id);
903 assert_eq!(deltas.deltas.len(), 11); let clear_delta = &deltas.deltas[0];
907 assert_eq!(clear_delta.instrument_id, instrument_id);
908 assert_eq!(clear_delta.action, BookAction::Clear);
909 assert_eq!(clear_delta.order.side, OrderSide::NoOrderSide);
910 assert_eq!(clear_delta.order.price.raw, 0);
911 assert_eq!(clear_delta.order.price.precision, 0);
912 assert_eq!(clear_delta.order.size.raw, 0);
913 assert_eq!(clear_delta.order.size.precision, 0);
914 assert_eq!(clear_delta.order.order_id, 0);
915 assert_eq!(clear_delta.flags, RecordFlag::F_SNAPSHOT as u8);
916 assert_eq!(clear_delta.sequence, 0);
917 assert_eq!(
918 clear_delta.ts_event,
919 UnixNanos::from(book_data.time * 1_000_000)
920 );
921 assert_eq!(clear_delta.ts_init, ts_init);
922
923 let first_bid_delta = &deltas.deltas[1];
925 assert_eq!(first_bid_delta.instrument_id, instrument_id);
926 assert_eq!(first_bid_delta.action, BookAction::Add);
927 assert_eq!(first_bid_delta.order.side, OrderSide::Buy);
928 assert_eq!(first_bid_delta.order.price, Price::from("98450.50"));
929 assert_eq!(first_bid_delta.order.size, Quantity::from("2.5"));
930 assert_eq!(first_bid_delta.order.order_id, 1);
931 assert_eq!(first_bid_delta.flags, RecordFlag::F_LAST as u8);
932 assert_eq!(first_bid_delta.sequence, 1);
933 assert_eq!(
934 first_bid_delta.ts_event,
935 UnixNanos::from(book_data.time * 1_000_000)
936 );
937 assert_eq!(first_bid_delta.ts_init, ts_init);
938
939 for delta in &deltas.deltas[1..] {
941 assert_eq!(delta.action, BookAction::Add);
942 assert!(delta.order.size.is_positive());
943 }
944 }
945
946 #[rstest]
947 fn test_ws_snapshot_conversion() {
948 let converter = HyperliquidDataConverter::new();
949 let book_data = sample_ws_book();
950 let instrument_id = test_instrument_id();
951 let ts_init = UnixNanos::default();
952
953 let deltas = converter
954 .convert_ws_snapshot(&book_data, instrument_id, ts_init)
955 .unwrap();
956
957 assert_eq!(deltas.instrument_id, instrument_id);
958 assert_eq!(deltas.deltas.len(), 11); let clear_delta = &deltas.deltas[0];
962 assert_eq!(clear_delta.instrument_id, instrument_id);
963 assert_eq!(clear_delta.action, BookAction::Clear);
964 assert_eq!(clear_delta.order.side, OrderSide::NoOrderSide);
965 assert_eq!(clear_delta.order.price.raw, 0);
966 assert_eq!(clear_delta.order.price.precision, 0);
967 assert_eq!(clear_delta.order.size.raw, 0);
968 assert_eq!(clear_delta.order.size.precision, 0);
969 assert_eq!(clear_delta.order.order_id, 0);
970 assert_eq!(clear_delta.flags, RecordFlag::F_SNAPSHOT as u8);
971 assert_eq!(clear_delta.sequence, 0);
972 assert_eq!(
973 clear_delta.ts_event,
974 UnixNanos::from(book_data.time * 1_000_000)
975 );
976 assert_eq!(clear_delta.ts_init, ts_init);
977
978 let first_bid_delta = &deltas.deltas[1];
980 assert_eq!(first_bid_delta.instrument_id, instrument_id);
981 assert_eq!(first_bid_delta.action, BookAction::Add);
982 assert_eq!(first_bid_delta.order.side, OrderSide::Buy);
983 assert_eq!(first_bid_delta.order.price, Price::from("98450.50"));
984 assert_eq!(first_bid_delta.order.size, Quantity::from("2.5"));
985 assert_eq!(first_bid_delta.order.order_id, 1);
986 assert_eq!(first_bid_delta.flags, RecordFlag::F_LAST as u8);
987 assert_eq!(first_bid_delta.sequence, 1);
988 assert_eq!(
989 first_bid_delta.ts_event,
990 UnixNanos::from(book_data.time * 1_000_000)
991 );
992 assert_eq!(first_bid_delta.ts_init, ts_init);
993 }
994
995 #[rstest]
996 fn test_delta_update_conversion() {
997 let converter = HyperliquidDataConverter::new();
998 let instrument_id = test_instrument_id();
999 let ts_event = UnixNanos::default();
1000 let ts_init = UnixNanos::default();
1001
1002 let bid_updates = vec![("98450.00".to_string(), "1.5".to_string())];
1003 let ask_updates = vec![("98451.00".to_string(), "2.0".to_string())];
1004 let bid_removals = vec!["98449.00".to_string()];
1005 let ask_removals = vec!["98452.00".to_string()];
1006
1007 let deltas = converter
1008 .convert_delta_update(
1009 instrument_id,
1010 123,
1011 ts_event,
1012 ts_init,
1013 &bid_updates,
1014 &ask_updates,
1015 &bid_removals,
1016 &ask_removals,
1017 )
1018 .unwrap();
1019
1020 assert_eq!(deltas.instrument_id, instrument_id);
1021 assert_eq!(deltas.deltas.len(), 4); assert_eq!(deltas.sequence, 123);
1023
1024 let first_delta = &deltas.deltas[0];
1026 assert_eq!(first_delta.instrument_id, instrument_id);
1027 assert_eq!(first_delta.action, BookAction::Delete);
1028 assert_eq!(first_delta.order.side, OrderSide::Buy);
1029 assert_eq!(first_delta.order.price, Price::from("98449.00"));
1030 assert_eq!(first_delta.order.size, Quantity::from("0"));
1031 assert_eq!(first_delta.order.order_id, 123000);
1032 assert_eq!(first_delta.flags, 0);
1033 assert_eq!(first_delta.sequence, 123);
1034 assert_eq!(first_delta.ts_event, ts_event);
1035 assert_eq!(first_delta.ts_init, ts_init);
1036 }
1037
1038 #[rstest]
1039 fn test_price_size_parsing() {
1040 let instrument_id = test_instrument_id();
1041 let config = HyperliquidInstrumentInfo::new(instrument_id, 2, 5);
1042
1043 let price = parse_price("98450.50", &config).unwrap();
1044 assert_eq!(price.to_string(), "98450.50");
1045
1046 let size = parse_size("2.5", &config).unwrap();
1047 assert_eq!(size.to_string(), "2.5");
1048 }
1049
1050 #[rstest]
1051 fn test_hyperliquid_instrument_mini_info() {
1052 let instrument_id = test_instrument_id();
1053
1054 let config = HyperliquidInstrumentInfo::new(instrument_id, 4, 6);
1056 assert_eq!(config.instrument_id, instrument_id);
1057 assert_eq!(config.price_decimals, 4);
1058 assert_eq!(config.size_decimals, 6);
1059
1060 let default_config = HyperliquidInstrumentInfo::default_crypto(instrument_id);
1062 assert_eq!(default_config.instrument_id, instrument_id);
1063 assert_eq!(default_config.price_decimals, 2);
1064 assert_eq!(default_config.size_decimals, 5);
1065 }
1066
1067 #[rstest]
1068 fn test_invalid_price_parsing() {
1069 let instrument_id = test_instrument_id();
1070 let config = HyperliquidInstrumentInfo::new(instrument_id, 2, 5);
1071
1072 let result = parse_price("invalid", &config);
1074 assert!(result.is_err());
1075
1076 match result.unwrap_err() {
1077 ConversionError::InvalidPrice { value } => {
1078 assert_eq!(value, "invalid");
1079 assert!(value.contains("invalid"));
1081 }
1082 _ => panic!("Expected InvalidPrice error"),
1083 }
1084
1085 let size_result = parse_size("not_a_number", &config);
1087 assert!(size_result.is_err());
1088
1089 match size_result.unwrap_err() {
1090 ConversionError::InvalidSize { value } => {
1091 assert_eq!(value, "not_a_number");
1092 assert!(value.contains("not_a_number"));
1094 }
1095 _ => panic!("Expected InvalidSize error"),
1096 }
1097 }
1098
1099 #[rstest]
1100 fn test_configuration() {
1101 let mut converter = HyperliquidDataConverter::new();
1102 let eth_id = InstrumentId::from("ETH.HYPER");
1103 let config = HyperliquidInstrumentInfo::new(eth_id, 4, 8);
1104
1105 let asset = Ustr::from("ETH");
1106
1107 converter.configure_instrument(asset.as_str(), config.clone());
1108
1109 let retrieved_config = converter.get_config(&asset);
1111 assert_eq!(retrieved_config.instrument_id, eth_id);
1112 assert_eq!(retrieved_config.price_decimals, 4);
1113 assert_eq!(retrieved_config.size_decimals, 8);
1114
1115 let default_config = converter.get_config(&Ustr::from("UNKNOWN"));
1117 assert_eq!(
1118 default_config.instrument_id,
1119 InstrumentId::from("UNKNOWN.HYPER")
1120 );
1121 assert_eq!(default_config.price_decimals, 2);
1122 assert_eq!(default_config.size_decimals, 5);
1123
1124 assert_eq!(config.instrument_id, eth_id);
1126 assert_eq!(config.price_decimals, 4);
1127 assert_eq!(config.size_decimals, 8);
1128 }
1129
1130 #[rstest]
1131 fn test_instrument_info_creation() {
1132 let instrument_id = InstrumentId::from("BTC.HYPER");
1133 let info = HyperliquidInstrumentInfo::with_metadata(
1134 instrument_id,
1135 2,
1136 5,
1137 Decimal::from_f64_retain(0.01).unwrap(),
1138 Decimal::from_f64_retain(0.00001).unwrap(),
1139 Decimal::from_f64_retain(10.0).unwrap(),
1140 );
1141
1142 assert_eq!(info.instrument_id, instrument_id);
1143 assert_eq!(info.price_decimals, 2);
1144 assert_eq!(info.size_decimals, 5);
1145 assert_eq!(
1146 info.tick_size,
1147 Some(Decimal::from_f64_retain(0.01).unwrap())
1148 );
1149 assert_eq!(
1150 info.step_size,
1151 Some(Decimal::from_f64_retain(0.00001).unwrap())
1152 );
1153 assert_eq!(
1154 info.min_notional,
1155 Some(Decimal::from_f64_retain(10.0).unwrap())
1156 );
1157 }
1158
1159 #[rstest]
1160 fn test_instrument_info_with_precision() {
1161 let instrument_id = test_instrument_id();
1162 let info = HyperliquidInstrumentInfo::with_precision(instrument_id, 3, 4);
1163 assert_eq!(info.instrument_id, instrument_id);
1164 assert_eq!(info.price_decimals, 3);
1165 assert_eq!(info.size_decimals, 4);
1166 assert_eq!(info.tick_size, Some(dec!(0.001))); assert_eq!(info.step_size, Some(dec!(0.0001))); }
1169
1170 #[tokio::test]
1171 async fn test_instrument_cache_basic_operations() {
1172 let btc_info = HyperliquidInstrumentInfo::with_metadata(
1173 InstrumentId::from("BTC.HYPER"),
1174 2,
1175 5,
1176 Decimal::from_f64_retain(0.01).unwrap(),
1177 Decimal::from_f64_retain(0.00001).unwrap(),
1178 Decimal::from_f64_retain(10.0).unwrap(),
1179 );
1180
1181 let eth_info = HyperliquidInstrumentInfo::with_metadata(
1182 InstrumentId::from("ETH.HYPER"),
1183 2,
1184 4,
1185 Decimal::from_f64_retain(0.01).unwrap(),
1186 Decimal::from_f64_retain(0.0001).unwrap(),
1187 Decimal::from_f64_retain(10.0).unwrap(),
1188 );
1189
1190 let mut cache = HyperliquidInstrumentCache::new();
1191
1192 cache.insert("BTC", btc_info.clone());
1194 cache.insert("ETH", eth_info.clone());
1195
1196 let retrieved_btc = cache.get("BTC").unwrap();
1198 assert_eq!(retrieved_btc.instrument_id, btc_info.instrument_id);
1199 assert_eq!(retrieved_btc.size_decimals, 5);
1200
1201 let retrieved_eth = cache.get("ETH").unwrap();
1203 assert_eq!(retrieved_eth.instrument_id, eth_info.instrument_id);
1204 assert_eq!(retrieved_eth.size_decimals, 4);
1205
1206 assert_eq!(cache.len(), 2);
1208 assert!(!cache.is_empty());
1209
1210 assert!(cache.contains("BTC"));
1212 assert!(cache.contains("ETH"));
1213 assert!(!cache.contains("UNKNOWN"));
1214
1215 let all_instruments = cache.get_all();
1217 assert_eq!(all_instruments.len(), 2);
1218 }
1219
1220 #[rstest]
1221 fn test_instrument_cache_empty() {
1222 let cache = HyperliquidInstrumentCache::new();
1223 let result = cache.get("UNKNOWN");
1224 assert!(result.is_none());
1225 assert!(cache.is_empty());
1226 assert_eq!(cache.len(), 0);
1227 }
1228
1229 #[rstest]
1230 fn test_latency_model_creation() {
1231 let converter = HyperliquidDataConverter::new();
1232
1233 let latency_model = converter.create_latency_model(
1235 100_000_000, 20_000_000, 10_000_000, 10_000_000, );
1240
1241 assert_eq!(latency_model.base_latency_nanos.as_u64(), 100_000_000);
1242 assert_eq!(latency_model.insert_latency_nanos.as_u64(), 20_000_000);
1243 assert_eq!(latency_model.update_latency_nanos.as_u64(), 10_000_000);
1244 assert_eq!(latency_model.delete_latency_nanos.as_u64(), 10_000_000);
1245
1246 let default_model = converter.create_default_latency_model();
1248 assert_eq!(default_model.base_latency_nanos.as_u64(), 50_000_000);
1249 assert_eq!(default_model.insert_latency_nanos.as_u64(), 10_000_000);
1250 assert_eq!(default_model.update_latency_nanos.as_u64(), 5_000_000);
1251 assert_eq!(default_model.delete_latency_nanos.as_u64(), 5_000_000);
1252
1253 let display_str = format!("{}", default_model);
1255 assert_eq!(display_str, "LatencyModel()");
1256 }
1257
1258 #[rstest]
1259 fn test_normalize_order_for_symbol() {
1260 use rust_decimal_macros::dec;
1261
1262 let mut converter = HyperliquidDataConverter::new();
1263
1264 let btc_info = HyperliquidInstrumentInfo::with_metadata(
1266 InstrumentId::from("BTC.HYPER"),
1267 2,
1268 5,
1269 dec!(0.01), dec!(0.00001), dec!(10.0), );
1273 converter.configure_instrument("BTC", btc_info);
1274
1275 let result = converter.normalize_order_for_symbol(
1277 "BTC",
1278 dec!(50123.456789), dec!(0.123456789), );
1281
1282 assert!(result.is_ok());
1283 let (price, qty) = result.unwrap();
1284 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));
1289 assert!(result_eth.is_ok());
1290
1291 let result_fail = converter.normalize_order_for_symbol(
1293 "BTC",
1294 dec!(1.0), dec!(0.001), );
1297 assert!(result_fail.is_err());
1298 assert!(result_fail.unwrap_err().contains("Notional value"));
1299 }
1300
1301 #[rstest]
1302 fn test_hyperliquid_balance_creation_and_properties() {
1303 use rust_decimal_macros::dec;
1304
1305 let asset = "USD".to_string();
1306 let total = dec!(1000.0);
1307 let available = dec!(750.0);
1308 let sequence = 42;
1309 let ts_event = UnixNanos::default();
1310
1311 let balance = HyperliquidBalance::new(asset.clone(), total, available, sequence, ts_event);
1312
1313 assert_eq!(balance.asset, asset);
1314 assert_eq!(balance.total, total);
1315 assert_eq!(balance.available, available);
1316 assert_eq!(balance.sequence, sequence);
1317 assert_eq!(balance.ts_event, ts_event);
1318 assert_eq!(balance.locked(), dec!(250.0)); let full_balance = HyperliquidBalance::new(
1322 "ETH".to_string(),
1323 dec!(100.0),
1324 dec!(100.0),
1325 1,
1326 UnixNanos::default(),
1327 );
1328 assert_eq!(full_balance.locked(), dec!(0.0));
1329
1330 let weird_balance = HyperliquidBalance::new(
1332 "WEIRD".to_string(),
1333 dec!(50.0),
1334 dec!(60.0),
1335 1,
1336 UnixNanos::default(),
1337 );
1338 assert_eq!(weird_balance.locked(), dec!(0.0));
1339 }
1340
1341 #[rstest]
1342 fn test_hyperliquid_account_state_creation() {
1343 let state = HyperliquidAccountState::new();
1344 assert!(state.balances.is_empty());
1345 assert_eq!(state.last_sequence, 0);
1346
1347 let default_state = HyperliquidAccountState::default();
1348 assert!(default_state.balances.is_empty());
1349 assert_eq!(default_state.last_sequence, 0);
1350 }
1351
1352 #[rstest]
1353 fn test_hyperliquid_account_state_getters() {
1354 use rust_decimal_macros::dec;
1355
1356 let mut state = HyperliquidAccountState::new();
1357
1358 let balance = state.get_balance("USD");
1360 assert_eq!(balance.asset, "USD");
1361 assert_eq!(balance.total, dec!(0.0));
1362 assert_eq!(balance.available, dec!(0.0));
1363
1364 let real_balance = HyperliquidBalance::new(
1366 "USD".to_string(),
1367 dec!(1000.0),
1368 dec!(750.0),
1369 1,
1370 UnixNanos::default(),
1371 );
1372 state.balances.insert("USD".to_string(), real_balance);
1373
1374 let retrieved_balance = state.get_balance("USD");
1376 assert_eq!(retrieved_balance.total, dec!(1000.0));
1377 }
1378
1379 #[rstest]
1380 fn test_hyperliquid_account_state_account_value() {
1381 use rust_decimal_macros::dec;
1382
1383 let mut state = HyperliquidAccountState::new();
1384
1385 state.balances.insert(
1387 "USD".to_string(),
1388 HyperliquidBalance::new(
1389 "USD".to_string(),
1390 dec!(10000.0),
1391 dec!(5000.0),
1392 1,
1393 UnixNanos::default(),
1394 ),
1395 );
1396
1397 let total_value = state.account_value();
1398 assert_eq!(total_value, dec!(10000.0));
1399
1400 state.balances.clear();
1402 let no_balance_value = state.account_value();
1403 assert_eq!(no_balance_value, dec!(0.0));
1404 }
1405
1406 #[rstest]
1407 fn test_hyperliquid_account_event_balance_snapshot() {
1408 use rust_decimal_macros::dec;
1409
1410 let mut state = HyperliquidAccountState::new();
1411
1412 let balance = HyperliquidBalance::new(
1413 "USD".to_string(),
1414 dec!(1000.0),
1415 dec!(750.0),
1416 10,
1417 UnixNanos::default(),
1418 );
1419
1420 let snapshot_event = HyperliquidAccountEvent::BalanceSnapshot {
1421 balances: vec![balance],
1422 sequence: 10,
1423 };
1424
1425 state.apply(snapshot_event);
1426
1427 assert_eq!(state.balances.len(), 1);
1428 assert_eq!(state.last_sequence, 10);
1429 assert_eq!(state.get_balance("USD").total, dec!(1000.0));
1430 }
1431
1432 #[rstest]
1433 fn test_hyperliquid_account_event_balance_delta() {
1434 use rust_decimal_macros::dec;
1435
1436 let mut state = HyperliquidAccountState::new();
1437
1438 let initial_balance = HyperliquidBalance::new(
1440 "USD".to_string(),
1441 dec!(1000.0),
1442 dec!(750.0),
1443 5,
1444 UnixNanos::default(),
1445 );
1446 state.balances.insert("USD".to_string(), initial_balance);
1447 state.last_sequence = 5;
1448
1449 let updated_balance = HyperliquidBalance::new(
1451 "USD".to_string(),
1452 dec!(1200.0),
1453 dec!(900.0),
1454 10,
1455 UnixNanos::default(),
1456 );
1457
1458 let delta_event = HyperliquidAccountEvent::BalanceDelta {
1459 balance: updated_balance,
1460 };
1461
1462 state.apply(delta_event);
1463
1464 let balance = state.get_balance("USD");
1465 assert_eq!(balance.total, dec!(1200.0));
1466 assert_eq!(balance.available, dec!(900.0));
1467 assert_eq!(balance.sequence, 10);
1468 assert_eq!(state.last_sequence, 10);
1469
1470 let old_balance = HyperliquidBalance::new(
1472 "USD".to_string(),
1473 dec!(800.0),
1474 dec!(600.0),
1475 8,
1476 UnixNanos::default(),
1477 );
1478
1479 let old_delta_event = HyperliquidAccountEvent::BalanceDelta {
1480 balance: old_balance,
1481 };
1482
1483 state.apply(old_delta_event);
1484
1485 let balance = state.get_balance("USD");
1487 assert_eq!(balance.total, dec!(1200.0)); assert_eq!(balance.sequence, 10); assert_eq!(state.last_sequence, 10); }
1491}