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, Currency, Money, Price, Quantity},
27};
28use rust_decimal::{Decimal, prelude::ToPrimitive};
29use ustr::Ustr;
30
31use crate::{
32 http::models::{HyperliquidL2Book, HyperliquidLevel},
33 websocket::messages::{WsBookData, WsLevelData},
34};
35
36#[derive(Debug, Clone)]
38pub struct HyperliquidInstrumentInfo {
39 pub instrument_id: InstrumentId,
40 pub price_decimals: u8,
41 pub size_decimals: u8,
42 pub tick_size: Option<Decimal>,
44 pub step_size: Option<Decimal>,
46 pub min_notional: Option<Decimal>,
48}
49
50impl HyperliquidInstrumentInfo {
51 pub fn new(instrument_id: InstrumentId, price_decimals: u8, size_decimals: u8) -> Self {
53 Self {
54 instrument_id,
55 price_decimals,
56 size_decimals,
57 tick_size: None,
58 step_size: None,
59 min_notional: None,
60 }
61 }
62
63 pub fn with_metadata(
65 instrument_id: InstrumentId,
66 price_decimals: u8,
67 size_decimals: u8,
68 tick_size: Decimal,
69 step_size: Decimal,
70 min_notional: Decimal,
71 ) -> Self {
72 Self {
73 instrument_id,
74 price_decimals,
75 size_decimals,
76 tick_size: Some(tick_size),
77 step_size: Some(step_size),
78 min_notional: Some(min_notional),
79 }
80 }
81
82 pub fn with_precision(
84 instrument_id: InstrumentId,
85 price_decimals: u8,
86 size_decimals: u8,
87 ) -> Self {
88 let tick_size = Decimal::new(1, price_decimals as u32);
89 let step_size = Decimal::new(1, size_decimals as u32);
90 Self {
91 instrument_id,
92 price_decimals,
93 size_decimals,
94 tick_size: Some(tick_size),
95 step_size: Some(step_size),
96 min_notional: None,
97 }
98 }
99
100 pub fn default_crypto(instrument_id: InstrumentId) -> Self {
102 Self::with_precision(instrument_id, 2, 5) }
104}
105
106#[derive(Debug, Default)]
108pub struct HyperliquidInstrumentCache {
109 instruments_by_symbol: HashMap<Ustr, HyperliquidInstrumentInfo>,
110}
111
112impl HyperliquidInstrumentCache {
113 pub fn new() -> Self {
115 Self {
116 instruments_by_symbol: HashMap::new(),
117 }
118 }
119
120 pub fn insert(&mut self, symbol: &str, info: HyperliquidInstrumentInfo) {
122 self.instruments_by_symbol.insert(Ustr::from(symbol), info);
123 }
124
125 pub fn get(&self, symbol: &str) -> Option<&HyperliquidInstrumentInfo> {
127 self.instruments_by_symbol.get(&Ustr::from(symbol))
128 }
129
130 pub fn get_all(&self) -> Vec<&HyperliquidInstrumentInfo> {
132 self.instruments_by_symbol.values().collect()
133 }
134
135 pub fn contains(&self, symbol: &str) -> bool {
137 self.instruments_by_symbol.contains_key(&Ustr::from(symbol))
138 }
139
140 pub fn len(&self) -> usize {
142 self.instruments_by_symbol.len()
143 }
144
145 pub fn is_empty(&self) -> bool {
147 self.instruments_by_symbol.is_empty()
148 }
149
150 pub fn clear(&mut self) {
152 self.instruments_by_symbol.clear();
153 }
154}
155
156#[derive(Clone, Debug, PartialEq, Eq, Hash)]
158pub enum HyperliquidTradeKey {
159 Id(String),
161 Seq(u64),
163}
164
165#[derive(Debug)]
167pub struct HyperliquidDataConverter {
168 configs: HashMap<Ustr, HyperliquidInstrumentInfo>,
170}
171
172impl Default for HyperliquidDataConverter {
173 fn default() -> Self {
174 Self::new()
175 }
176}
177
178impl HyperliquidDataConverter {
179 pub fn new() -> Self {
181 Self {
182 configs: HashMap::new(),
183 }
184 }
185
186 pub fn create_latency_model(
191 &self,
192 base_latency_ns: u64,
193 insert_latency_ns: u64,
194 update_latency_ns: u64,
195 delete_latency_ns: u64,
196 ) -> LatencyModel {
197 LatencyModel::new(
198 UnixNanos::from(base_latency_ns),
199 UnixNanos::from(insert_latency_ns),
200 UnixNanos::from(update_latency_ns),
201 UnixNanos::from(delete_latency_ns),
202 )
203 }
204
205 pub fn create_default_latency_model(&self) -> LatencyModel {
207 self.create_latency_model(
209 50_000_000, 10_000_000, 5_000_000, 5_000_000, )
214 }
215
216 pub fn normalize_order_for_symbol(
221 &mut self,
222 symbol: &str,
223 price: Decimal,
224 qty: Decimal,
225 ) -> Result<(Decimal, Decimal), String> {
226 let config = self.get_config(&Ustr::from(symbol));
227
228 let tick_size = config.tick_size.unwrap_or_else(|| Decimal::new(1, 2)); let step_size = config.step_size.unwrap_or_else(|| {
231 match config.size_decimals {
233 0 => Decimal::ONE,
234 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), }
241 });
242 let min_notional = config.min_notional.unwrap_or_else(|| Decimal::from(10)); crate::common::parse::normalize_order(
245 price,
246 qty,
247 tick_size,
248 step_size,
249 min_notional,
250 config.price_decimals,
251 config.size_decimals,
252 )
253 }
254
255 pub fn configure_instrument(&mut self, symbol: &str, config: HyperliquidInstrumentInfo) {
257 self.configs.insert(Ustr::from(symbol), config);
258 }
259
260 fn get_config(&self, symbol: &Ustr) -> HyperliquidInstrumentInfo {
262 self.configs.get(symbol).cloned().unwrap_or_else(|| {
263 let instrument_id = InstrumentId::from(format!("{}.HYPER", symbol).as_str());
265 HyperliquidInstrumentInfo::default_crypto(instrument_id)
266 })
267 }
268
269 pub fn convert_http_snapshot(
271 &self,
272 data: &HyperliquidL2Book,
273 instrument_id: InstrumentId,
274 ts_init: UnixNanos,
275 ) -> Result<OrderBookDeltas, ConversionError> {
276 let config = self.get_config(&data.coin);
277 let mut deltas = Vec::new();
278
279 deltas.push(OrderBookDelta::clear(
281 instrument_id,
282 0, UnixNanos::from(data.time * 1_000_000), ts_init,
285 ));
286
287 let mut order_id = 1u64; for level in &data.levels[0] {
291 let (price, size) = parse_level(level, &config)?;
292 if size.is_positive() {
293 let order = BookOrder::new(OrderSide::Buy, price, size, order_id);
294 deltas.push(OrderBookDelta::new(
295 instrument_id,
296 BookAction::Add,
297 order,
298 RecordFlag::F_LAST as u8, order_id,
300 UnixNanos::from(data.time * 1_000_000),
301 ts_init,
302 ));
303 order_id += 1;
304 }
305 }
306
307 for level in &data.levels[1] {
309 let (price, size) = parse_level(level, &config)?;
310 if size.is_positive() {
311 let order = BookOrder::new(OrderSide::Sell, price, size, order_id);
312 deltas.push(OrderBookDelta::new(
313 instrument_id,
314 BookAction::Add,
315 order,
316 RecordFlag::F_LAST as u8, order_id,
318 UnixNanos::from(data.time * 1_000_000),
319 ts_init,
320 ));
321 order_id += 1;
322 }
323 }
324
325 Ok(OrderBookDeltas::new(instrument_id, deltas))
326 }
327
328 pub fn convert_ws_snapshot(
330 &self,
331 data: &WsBookData,
332 instrument_id: InstrumentId,
333 ts_init: UnixNanos,
334 ) -> Result<OrderBookDeltas, ConversionError> {
335 let config = self.get_config(&data.coin);
336 let mut deltas = Vec::new();
337
338 deltas.push(OrderBookDelta::clear(
340 instrument_id,
341 0, UnixNanos::from(data.time * 1_000_000), ts_init,
344 ));
345
346 let mut order_id = 1u64; for level in &data.levels[0] {
350 let (price, size) = parse_ws_level(level, &config)?;
351 if size.is_positive() {
352 let order = BookOrder::new(OrderSide::Buy, price, size, order_id);
353 deltas.push(OrderBookDelta::new(
354 instrument_id,
355 BookAction::Add,
356 order,
357 RecordFlag::F_LAST as u8,
358 order_id,
359 UnixNanos::from(data.time * 1_000_000),
360 ts_init,
361 ));
362 order_id += 1;
363 }
364 }
365
366 for level in &data.levels[1] {
368 let (price, size) = parse_ws_level(level, &config)?;
369 if size.is_positive() {
370 let order = BookOrder::new(OrderSide::Sell, price, size, order_id);
371 deltas.push(OrderBookDelta::new(
372 instrument_id,
373 BookAction::Add,
374 order,
375 RecordFlag::F_LAST as u8,
376 order_id,
377 UnixNanos::from(data.time * 1_000_000),
378 ts_init,
379 ));
380 order_id += 1;
381 }
382 }
383
384 Ok(OrderBookDeltas::new(instrument_id, deltas))
385 }
386
387 #[allow(clippy::too_many_arguments)]
390 pub fn convert_delta_update(
391 &self,
392 instrument_id: InstrumentId,
393 sequence: u64,
394 ts_event: UnixNanos,
395 ts_init: UnixNanos,
396 bid_updates: &[(String, String)], ask_updates: &[(String, String)], bid_removals: &[String], ask_removals: &[String], ) -> Result<OrderBookDeltas, ConversionError> {
401 let config = self.get_config(&instrument_id.symbol.inner());
402 let mut deltas = Vec::new();
403 let mut order_id = sequence * 1000; for price_str in bid_removals {
407 let price = parse_price(price_str, &config)?;
408 let order = BookOrder::new(OrderSide::Buy, price, Quantity::from("0"), order_id);
409 deltas.push(OrderBookDelta::new(
410 instrument_id,
411 BookAction::Delete,
412 order,
413 0, sequence,
415 ts_event,
416 ts_init,
417 ));
418 order_id += 1;
419 }
420
421 for price_str in ask_removals {
423 let price = parse_price(price_str, &config)?;
424 let order = BookOrder::new(OrderSide::Sell, price, Quantity::from("0"), order_id);
425 deltas.push(OrderBookDelta::new(
426 instrument_id,
427 BookAction::Delete,
428 order,
429 0, sequence,
431 ts_event,
432 ts_init,
433 ));
434 order_id += 1;
435 }
436
437 for (price_str, size_str) in bid_updates {
439 let price = parse_price(price_str, &config)?;
440 let size = parse_size(size_str, &config)?;
441
442 if size.is_positive() {
443 let order = BookOrder::new(OrderSide::Buy, price, size, order_id);
444 deltas.push(OrderBookDelta::new(
445 instrument_id,
446 BookAction::Update, order,
448 0, sequence,
450 ts_event,
451 ts_init,
452 ));
453 } else {
454 let order = BookOrder::new(OrderSide::Buy, price, size, order_id);
456 deltas.push(OrderBookDelta::new(
457 instrument_id,
458 BookAction::Delete,
459 order,
460 0, sequence,
462 ts_event,
463 ts_init,
464 ));
465 }
466 order_id += 1;
467 }
468
469 for (price_str, size_str) in ask_updates {
471 let price = parse_price(price_str, &config)?;
472 let size = parse_size(size_str, &config)?;
473
474 if size.is_positive() {
475 let order = BookOrder::new(OrderSide::Sell, price, size, order_id);
476 deltas.push(OrderBookDelta::new(
477 instrument_id,
478 BookAction::Update, order,
480 0, sequence,
482 ts_event,
483 ts_init,
484 ));
485 } else {
486 let order = BookOrder::new(OrderSide::Sell, price, size, order_id);
488 deltas.push(OrderBookDelta::new(
489 instrument_id,
490 BookAction::Delete,
491 order,
492 0, sequence,
494 ts_event,
495 ts_init,
496 ));
497 }
498 order_id += 1;
499 }
500
501 Ok(OrderBookDeltas::new(instrument_id, deltas))
502 }
503}
504
505fn parse_level(
507 level: &HyperliquidLevel,
508 inst_info: &HyperliquidInstrumentInfo,
509) -> Result<(Price, Quantity), ConversionError> {
510 let price = parse_price(&level.px, inst_info)?;
511 let size = parse_size(&level.sz, inst_info)?;
512 Ok((price, size))
513}
514
515fn parse_ws_level(
517 level: &WsLevelData,
518 config: &HyperliquidInstrumentInfo,
519) -> Result<(Price, Quantity), ConversionError> {
520 let price = parse_price(&level.px, config)?;
521 let size = parse_size(&level.sz, config)?;
522 Ok((price, size))
523}
524
525fn parse_price(
527 price_str: &str,
528 _config: &HyperliquidInstrumentInfo,
529) -> Result<Price, ConversionError> {
530 let _decimal = Decimal::from_str(price_str).map_err(|_| ConversionError::InvalidPrice {
531 value: price_str.to_string(),
532 })?;
533
534 Price::from_str(price_str).map_err(|_| ConversionError::InvalidPrice {
535 value: price_str.to_string(),
536 })
537}
538
539fn parse_size(
541 size_str: &str,
542 _config: &HyperliquidInstrumentInfo,
543) -> Result<Quantity, ConversionError> {
544 let _decimal = Decimal::from_str(size_str).map_err(|_| ConversionError::InvalidSize {
545 value: size_str.to_string(),
546 })?;
547
548 Quantity::from_str(size_str).map_err(|_| ConversionError::InvalidSize {
549 value: size_str.to_string(),
550 })
551}
552
553#[derive(Debug, Clone, PartialEq, Eq)]
555pub enum ConversionError {
556 InvalidPrice { value: String },
558 InvalidSize { value: String },
560 OrderBookDeltasError(String),
562}
563
564impl From<anyhow::Error> for ConversionError {
565 fn from(err: anyhow::Error) -> Self {
566 ConversionError::OrderBookDeltasError(err.to_string())
567 }
568}
569
570impl Display for ConversionError {
571 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
572 match self {
573 ConversionError::InvalidPrice { value } => write!(f, "Invalid price: {}", value),
574 ConversionError::InvalidSize { value } => write!(f, "Invalid size: {}", value),
575 ConversionError::OrderBookDeltasError(msg) => {
576 write!(f, "OrderBookDeltas error: {}", msg)
577 }
578 }
579 }
580}
581
582impl std::error::Error for ConversionError {}
583
584#[derive(Clone, Debug)]
597pub struct HyperliquidPositionData {
598 pub asset: String,
599 pub position: Decimal, pub entry_px: Option<Decimal>,
601 pub unrealized_pnl: Decimal,
602 pub cumulative_funding: Option<Decimal>,
603 pub position_value: Decimal,
604}
605
606impl HyperliquidPositionData {
607 pub fn is_flat(&self) -> bool {
609 self.position.is_zero()
610 }
611
612 pub fn is_long(&self) -> bool {
614 self.position > Decimal::ZERO
615 }
616
617 pub fn is_short(&self) -> bool {
619 self.position < Decimal::ZERO
620 }
621}
622
623#[derive(Clone, Debug)]
631pub struct HyperliquidBalance {
632 pub asset: String,
633 pub total: Decimal,
634 pub available: Decimal,
635 pub sequence: u64,
636 pub ts_event: UnixNanos,
637}
638
639impl HyperliquidBalance {
640 pub fn new(
641 asset: String,
642 total: Decimal,
643 available: Decimal,
644 sequence: u64,
645 ts_event: UnixNanos,
646 ) -> Self {
647 Self {
648 asset,
649 total,
650 available,
651 sequence,
652 ts_event,
653 }
654 }
655
656 pub fn locked(&self) -> Decimal {
658 (self.total - self.available).max(Decimal::ZERO)
659 }
660}
661
662#[derive(Default, Debug)]
670pub struct HyperliquidAccountState {
671 pub balances: HashMap<String, HyperliquidBalance>,
672 pub last_sequence: u64,
673}
674
675impl HyperliquidAccountState {
676 pub fn new() -> Self {
677 Default::default()
678 }
679
680 pub fn get_balance(&self, asset: &str) -> HyperliquidBalance {
682 self.balances.get(asset).cloned().unwrap_or_else(|| {
683 HyperliquidBalance::new(
684 asset.to_string(),
685 Decimal::ZERO,
686 Decimal::ZERO,
687 0,
688 UnixNanos::default(),
689 )
690 })
691 }
692
693 pub fn account_value(&self) -> Decimal {
697 self.balances.values().map(|balance| balance.total).sum()
698 }
699
700 pub fn to_account_state(
715 &self,
716 account_id: AccountId,
717 ts_event: UnixNanos,
718 ts_init: UnixNanos,
719 ) -> anyhow::Result<AccountState> {
720 let balances: Vec<AccountBalance> = self
722 .balances
723 .values()
724 .map(|balance| {
725 let currency = Currency::from(balance.asset.as_str());
727
728 let total = Money::new(balance.total.to_f64().unwrap_or(0.0), currency);
730 let free = Money::new(balance.available.to_f64().unwrap_or(0.0), currency);
731 let locked = total - free; AccountBalance::new(total, locked, free)
734 })
735 .collect();
736
737 let margins = Vec::new();
740
741 let account_type = AccountType::Margin;
743
744 let is_reported = true;
746
747 let event_id = UUID4::new();
749
750 Ok(AccountState::new(
751 account_id,
752 account_type,
753 balances,
754 margins,
755 is_reported,
756 event_id,
757 ts_event,
758 ts_init,
759 None, ))
761 }
762}
763
764#[derive(Debug, Clone)]
774pub enum HyperliquidAccountEvent {
775 BalanceSnapshot {
777 balances: Vec<HyperliquidBalance>,
778 sequence: u64,
779 },
780 BalanceDelta { balance: HyperliquidBalance },
782}
783
784impl HyperliquidAccountState {
785 pub fn apply(&mut self, event: HyperliquidAccountEvent) {
787 match event {
788 HyperliquidAccountEvent::BalanceSnapshot { balances, sequence } => {
789 self.balances.clear();
790
791 for balance in balances {
792 self.balances.insert(balance.asset.clone(), balance);
793 }
794
795 self.last_sequence = sequence;
796 }
797 HyperliquidAccountEvent::BalanceDelta { balance } => {
798 let sequence = balance.sequence;
799 let entry = self
800 .balances
801 .entry(balance.asset.clone())
802 .or_insert_with(|| balance.clone());
803
804 if sequence > entry.sequence {
806 *entry = balance;
807 self.last_sequence = self.last_sequence.max(sequence);
808 }
809 }
810 }
811 }
812}
813
814pub fn parse_position_status_report(
824 position_data: &HyperliquidPositionData,
825 account_id: AccountId,
826 instrument_id: InstrumentId,
827 ts_init: UnixNanos,
828) -> anyhow::Result<PositionStatusReport> {
829 let position_side = if position_data.is_flat() {
831 PositionSide::Flat
832 } else if position_data.is_long() {
833 PositionSide::Long
834 } else {
835 PositionSide::Short
836 };
837
838 let quantity = Quantity::new(position_data.position.abs().to_f64().unwrap_or(0.0), 0);
840
841 let ts_last = ts_init;
843
844 let avg_px_open = position_data.entry_px;
846
847 Ok(PositionStatusReport::new(
848 account_id,
849 instrument_id,
850 position_side.as_specified(),
851 quantity,
852 ts_last,
853 ts_init,
854 None, None, avg_px_open,
857 ))
858}
859
860#[cfg(test)]
866#[allow(dead_code)]
867mod tests {
868 use rstest::rstest;
869
870 use super::*;
871
872 fn load_test_data<T>(filename: &str) -> T
873 where
874 T: serde::de::DeserializeOwned,
875 {
876 let path = format!("test_data/{}", filename);
877 let content = std::fs::read_to_string(path).expect("Failed to read test data");
878 serde_json::from_str(&content).expect("Failed to parse test data")
879 }
880
881 fn test_instrument_id() -> InstrumentId {
882 InstrumentId::from("BTC.HYPER")
883 }
884
885 fn sample_http_book() -> HyperliquidL2Book {
886 load_test_data("http_l2_book_snapshot.json")
887 }
888
889 fn sample_ws_book() -> WsBookData {
890 load_test_data("ws_book_data.json")
891 }
892
893 #[rstest]
894 fn test_http_snapshot_conversion() {
895 let converter = HyperliquidDataConverter::new();
896 let book_data = sample_http_book();
897 let instrument_id = test_instrument_id();
898 let ts_init = UnixNanos::default();
899
900 let deltas = converter
901 .convert_http_snapshot(&book_data, instrument_id, ts_init)
902 .unwrap();
903
904 assert_eq!(deltas.instrument_id, instrument_id);
905 assert_eq!(deltas.deltas.len(), 11); let clear_delta = &deltas.deltas[0];
909 assert_eq!(clear_delta.instrument_id, instrument_id);
910 assert_eq!(clear_delta.action, BookAction::Clear);
911 assert_eq!(clear_delta.order.side, OrderSide::NoOrderSide);
912 assert_eq!(clear_delta.order.price.raw, 0);
913 assert_eq!(clear_delta.order.price.precision, 0);
914 assert_eq!(clear_delta.order.size.raw, 0);
915 assert_eq!(clear_delta.order.size.precision, 0);
916 assert_eq!(clear_delta.order.order_id, 0);
917 assert_eq!(clear_delta.flags, RecordFlag::F_SNAPSHOT as u8);
918 assert_eq!(clear_delta.sequence, 0);
919 assert_eq!(
920 clear_delta.ts_event,
921 UnixNanos::from(book_data.time * 1_000_000)
922 );
923 assert_eq!(clear_delta.ts_init, ts_init);
924
925 let first_bid_delta = &deltas.deltas[1];
927 assert_eq!(first_bid_delta.instrument_id, instrument_id);
928 assert_eq!(first_bid_delta.action, BookAction::Add);
929 assert_eq!(first_bid_delta.order.side, OrderSide::Buy);
930 assert_eq!(first_bid_delta.order.price, Price::from("98450.50"));
931 assert_eq!(first_bid_delta.order.size, Quantity::from("2.5"));
932 assert_eq!(first_bid_delta.order.order_id, 1);
933 assert_eq!(first_bid_delta.flags, RecordFlag::F_LAST as u8);
934 assert_eq!(first_bid_delta.sequence, 1);
935 assert_eq!(
936 first_bid_delta.ts_event,
937 UnixNanos::from(book_data.time * 1_000_000)
938 );
939 assert_eq!(first_bid_delta.ts_init, ts_init);
940
941 for delta in &deltas.deltas[1..] {
943 assert_eq!(delta.action, BookAction::Add);
944 assert!(delta.order.size.is_positive());
945 }
946 }
947
948 #[rstest]
949 fn test_ws_snapshot_conversion() {
950 let converter = HyperliquidDataConverter::new();
951 let book_data = sample_ws_book();
952 let instrument_id = test_instrument_id();
953 let ts_init = UnixNanos::default();
954
955 let deltas = converter
956 .convert_ws_snapshot(&book_data, instrument_id, ts_init)
957 .unwrap();
958
959 assert_eq!(deltas.instrument_id, instrument_id);
960 assert_eq!(deltas.deltas.len(), 11); let clear_delta = &deltas.deltas[0];
964 assert_eq!(clear_delta.instrument_id, instrument_id);
965 assert_eq!(clear_delta.action, BookAction::Clear);
966 assert_eq!(clear_delta.order.side, OrderSide::NoOrderSide);
967 assert_eq!(clear_delta.order.price.raw, 0);
968 assert_eq!(clear_delta.order.price.precision, 0);
969 assert_eq!(clear_delta.order.size.raw, 0);
970 assert_eq!(clear_delta.order.size.precision, 0);
971 assert_eq!(clear_delta.order.order_id, 0);
972 assert_eq!(clear_delta.flags, RecordFlag::F_SNAPSHOT as u8);
973 assert_eq!(clear_delta.sequence, 0);
974 assert_eq!(
975 clear_delta.ts_event,
976 UnixNanos::from(book_data.time * 1_000_000)
977 );
978 assert_eq!(clear_delta.ts_init, ts_init);
979
980 let first_bid_delta = &deltas.deltas[1];
982 assert_eq!(first_bid_delta.instrument_id, instrument_id);
983 assert_eq!(first_bid_delta.action, BookAction::Add);
984 assert_eq!(first_bid_delta.order.side, OrderSide::Buy);
985 assert_eq!(first_bid_delta.order.price, Price::from("98450.50"));
986 assert_eq!(first_bid_delta.order.size, Quantity::from("2.5"));
987 assert_eq!(first_bid_delta.order.order_id, 1);
988 assert_eq!(first_bid_delta.flags, RecordFlag::F_LAST as u8);
989 assert_eq!(first_bid_delta.sequence, 1);
990 assert_eq!(
991 first_bid_delta.ts_event,
992 UnixNanos::from(book_data.time * 1_000_000)
993 );
994 assert_eq!(first_bid_delta.ts_init, ts_init);
995 }
996
997 #[rstest]
998 fn test_delta_update_conversion() {
999 let converter = HyperliquidDataConverter::new();
1000 let instrument_id = test_instrument_id();
1001 let ts_event = UnixNanos::default();
1002 let ts_init = UnixNanos::default();
1003
1004 let bid_updates = vec![("98450.00".to_string(), "1.5".to_string())];
1005 let ask_updates = vec![("98451.00".to_string(), "2.0".to_string())];
1006 let bid_removals = vec!["98449.00".to_string()];
1007 let ask_removals = vec!["98452.00".to_string()];
1008
1009 let deltas = converter
1010 .convert_delta_update(
1011 instrument_id,
1012 123,
1013 ts_event,
1014 ts_init,
1015 &bid_updates,
1016 &ask_updates,
1017 &bid_removals,
1018 &ask_removals,
1019 )
1020 .unwrap();
1021
1022 assert_eq!(deltas.instrument_id, instrument_id);
1023 assert_eq!(deltas.deltas.len(), 4); assert_eq!(deltas.sequence, 123);
1025
1026 let first_delta = &deltas.deltas[0];
1028 assert_eq!(first_delta.instrument_id, instrument_id);
1029 assert_eq!(first_delta.action, BookAction::Delete);
1030 assert_eq!(first_delta.order.side, OrderSide::Buy);
1031 assert_eq!(first_delta.order.price, Price::from("98449.00"));
1032 assert_eq!(first_delta.order.size, Quantity::from("0"));
1033 assert_eq!(first_delta.order.order_id, 123000);
1034 assert_eq!(first_delta.flags, 0);
1035 assert_eq!(first_delta.sequence, 123);
1036 assert_eq!(first_delta.ts_event, ts_event);
1037 assert_eq!(first_delta.ts_init, ts_init);
1038 }
1039
1040 #[rstest]
1041 fn test_price_size_parsing() {
1042 let instrument_id = test_instrument_id();
1043 let config = HyperliquidInstrumentInfo::new(instrument_id, 2, 5);
1044
1045 let price = parse_price("98450.50", &config).unwrap();
1046 assert_eq!(price.to_string(), "98450.50");
1047
1048 let size = parse_size("2.5", &config).unwrap();
1049 assert_eq!(size.to_string(), "2.5");
1050 }
1051
1052 #[rstest]
1053 fn test_hyperliquid_instrument_mini_info() {
1054 let instrument_id = test_instrument_id();
1055
1056 let config = HyperliquidInstrumentInfo::new(instrument_id, 4, 6);
1058 assert_eq!(config.instrument_id, instrument_id);
1059 assert_eq!(config.price_decimals, 4);
1060 assert_eq!(config.size_decimals, 6);
1061
1062 let default_config = HyperliquidInstrumentInfo::default_crypto(instrument_id);
1064 assert_eq!(default_config.instrument_id, instrument_id);
1065 assert_eq!(default_config.price_decimals, 2);
1066 assert_eq!(default_config.size_decimals, 5);
1067 }
1068
1069 #[rstest]
1070 fn test_invalid_price_parsing() {
1071 let instrument_id = test_instrument_id();
1072 let config = HyperliquidInstrumentInfo::new(instrument_id, 2, 5);
1073
1074 let result = parse_price("invalid", &config);
1076 assert!(result.is_err());
1077
1078 match result.unwrap_err() {
1079 ConversionError::InvalidPrice { value } => {
1080 assert_eq!(value, "invalid");
1081 assert!(value.contains("invalid"));
1083 }
1084 _ => panic!("Expected InvalidPrice error"),
1085 }
1086
1087 let size_result = parse_size("not_a_number", &config);
1089 assert!(size_result.is_err());
1090
1091 match size_result.unwrap_err() {
1092 ConversionError::InvalidSize { value } => {
1093 assert_eq!(value, "not_a_number");
1094 assert!(value.contains("not_a_number"));
1096 }
1097 _ => panic!("Expected InvalidSize error"),
1098 }
1099 }
1100
1101 #[rstest]
1102 fn test_configuration() {
1103 let mut converter = HyperliquidDataConverter::new();
1104 let eth_id = InstrumentId::from("ETH.HYPER");
1105 let config = HyperliquidInstrumentInfo::new(eth_id, 4, 8);
1106
1107 let asset = Ustr::from("ETH");
1108
1109 converter.configure_instrument(asset.as_str(), config.clone());
1110
1111 let retrieved_config = converter.get_config(&asset);
1113 assert_eq!(retrieved_config.instrument_id, eth_id);
1114 assert_eq!(retrieved_config.price_decimals, 4);
1115 assert_eq!(retrieved_config.size_decimals, 8);
1116
1117 let default_config = converter.get_config(&Ustr::from("UNKNOWN"));
1119 assert_eq!(
1120 default_config.instrument_id,
1121 InstrumentId::from("UNKNOWN.HYPER")
1122 );
1123 assert_eq!(default_config.price_decimals, 2);
1124 assert_eq!(default_config.size_decimals, 5);
1125
1126 assert_eq!(config.instrument_id, eth_id);
1128 assert_eq!(config.price_decimals, 4);
1129 assert_eq!(config.size_decimals, 8);
1130 }
1131
1132 #[rstest]
1133 fn test_instrument_info_creation() {
1134 let instrument_id = InstrumentId::from("BTC.HYPER");
1135 let info = HyperliquidInstrumentInfo::with_metadata(
1136 instrument_id,
1137 2,
1138 5,
1139 Decimal::from_f64_retain(0.01).unwrap(),
1140 Decimal::from_f64_retain(0.00001).unwrap(),
1141 Decimal::from_f64_retain(10.0).unwrap(),
1142 );
1143
1144 assert_eq!(info.instrument_id, instrument_id);
1145 assert_eq!(info.price_decimals, 2);
1146 assert_eq!(info.size_decimals, 5);
1147 assert_eq!(
1148 info.tick_size,
1149 Some(Decimal::from_f64_retain(0.01).unwrap())
1150 );
1151 assert_eq!(
1152 info.step_size,
1153 Some(Decimal::from_f64_retain(0.00001).unwrap())
1154 );
1155 assert_eq!(
1156 info.min_notional,
1157 Some(Decimal::from_f64_retain(10.0).unwrap())
1158 );
1159 }
1160
1161 #[rstest]
1162 fn test_instrument_info_with_precision() {
1163 let instrument_id = test_instrument_id();
1164 let info = HyperliquidInstrumentInfo::with_precision(instrument_id, 3, 4);
1165 assert_eq!(info.instrument_id, instrument_id);
1166 assert_eq!(info.price_decimals, 3);
1167 assert_eq!(info.size_decimals, 4);
1168 assert_eq!(info.tick_size, Some(Decimal::new(1, 3))); assert_eq!(info.step_size, Some(Decimal::new(1, 4))); }
1171
1172 #[tokio::test]
1173 async fn test_instrument_cache_basic_operations() {
1174 let btc_info = HyperliquidInstrumentInfo::with_metadata(
1175 InstrumentId::from("BTC.HYPER"),
1176 2,
1177 5,
1178 Decimal::from_f64_retain(0.01).unwrap(),
1179 Decimal::from_f64_retain(0.00001).unwrap(),
1180 Decimal::from_f64_retain(10.0).unwrap(),
1181 );
1182
1183 let eth_info = HyperliquidInstrumentInfo::with_metadata(
1184 InstrumentId::from("ETH.HYPER"),
1185 2,
1186 4,
1187 Decimal::from_f64_retain(0.01).unwrap(),
1188 Decimal::from_f64_retain(0.0001).unwrap(),
1189 Decimal::from_f64_retain(10.0).unwrap(),
1190 );
1191
1192 let mut cache = HyperliquidInstrumentCache::new();
1193
1194 cache.insert("BTC", btc_info.clone());
1196 cache.insert("ETH", eth_info.clone());
1197
1198 let retrieved_btc = cache.get("BTC").unwrap();
1200 assert_eq!(retrieved_btc.instrument_id, btc_info.instrument_id);
1201 assert_eq!(retrieved_btc.size_decimals, 5);
1202
1203 let retrieved_eth = cache.get("ETH").unwrap();
1205 assert_eq!(retrieved_eth.instrument_id, eth_info.instrument_id);
1206 assert_eq!(retrieved_eth.size_decimals, 4);
1207
1208 assert_eq!(cache.len(), 2);
1210 assert!(!cache.is_empty());
1211
1212 assert!(cache.contains("BTC"));
1214 assert!(cache.contains("ETH"));
1215 assert!(!cache.contains("UNKNOWN"));
1216
1217 let all_instruments = cache.get_all();
1219 assert_eq!(all_instruments.len(), 2);
1220 }
1221
1222 #[rstest]
1223 fn test_instrument_cache_empty() {
1224 let cache = HyperliquidInstrumentCache::new();
1225 let result = cache.get("UNKNOWN");
1226 assert!(result.is_none());
1227 assert!(cache.is_empty());
1228 assert_eq!(cache.len(), 0);
1229 }
1230
1231 #[rstest]
1232 fn test_latency_model_creation() {
1233 let converter = HyperliquidDataConverter::new();
1234
1235 let latency_model = converter.create_latency_model(
1237 100_000_000, 20_000_000, 10_000_000, 10_000_000, );
1242
1243 assert_eq!(latency_model.base_latency_nanos.as_u64(), 100_000_000);
1244 assert_eq!(latency_model.insert_latency_nanos.as_u64(), 20_000_000);
1245 assert_eq!(latency_model.update_latency_nanos.as_u64(), 10_000_000);
1246 assert_eq!(latency_model.delete_latency_nanos.as_u64(), 10_000_000);
1247
1248 let default_model = converter.create_default_latency_model();
1250 assert_eq!(default_model.base_latency_nanos.as_u64(), 50_000_000);
1251 assert_eq!(default_model.insert_latency_nanos.as_u64(), 10_000_000);
1252 assert_eq!(default_model.update_latency_nanos.as_u64(), 5_000_000);
1253 assert_eq!(default_model.delete_latency_nanos.as_u64(), 5_000_000);
1254
1255 let display_str = format!("{}", default_model);
1257 assert_eq!(display_str, "LatencyModel()");
1258 }
1259
1260 #[rstest]
1261 fn test_normalize_order_for_symbol() {
1262 use rust_decimal_macros::dec;
1263
1264 let mut converter = HyperliquidDataConverter::new();
1265
1266 let btc_info = HyperliquidInstrumentInfo::with_metadata(
1268 InstrumentId::from("BTC.HYPER"),
1269 2,
1270 5,
1271 dec!(0.01), dec!(0.00001), dec!(10.0), );
1275 converter.configure_instrument("BTC", btc_info);
1276
1277 let result = converter.normalize_order_for_symbol(
1279 "BTC",
1280 dec!(50123.456789), dec!(0.123456789), );
1283
1284 assert!(result.is_ok());
1285 let (price, qty) = result.unwrap();
1286 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));
1291 assert!(result_eth.is_ok());
1292
1293 let result_fail = converter.normalize_order_for_symbol(
1295 "BTC",
1296 dec!(1.0), dec!(0.001), );
1299 assert!(result_fail.is_err());
1300 assert!(result_fail.unwrap_err().contains("Notional value"));
1301 }
1302
1303 #[rstest]
1304 fn test_hyperliquid_balance_creation_and_properties() {
1305 use rust_decimal_macros::dec;
1306
1307 let asset = "USD".to_string();
1308 let total = dec!(1000.0);
1309 let available = dec!(750.0);
1310 let sequence = 42;
1311 let ts_event = UnixNanos::default();
1312
1313 let balance = HyperliquidBalance::new(asset.clone(), total, available, sequence, ts_event);
1314
1315 assert_eq!(balance.asset, asset);
1316 assert_eq!(balance.total, total);
1317 assert_eq!(balance.available, available);
1318 assert_eq!(balance.sequence, sequence);
1319 assert_eq!(balance.ts_event, ts_event);
1320 assert_eq!(balance.locked(), dec!(250.0)); let full_balance = HyperliquidBalance::new(
1324 "ETH".to_string(),
1325 dec!(100.0),
1326 dec!(100.0),
1327 1,
1328 UnixNanos::default(),
1329 );
1330 assert_eq!(full_balance.locked(), dec!(0.0));
1331
1332 let weird_balance = HyperliquidBalance::new(
1334 "WEIRD".to_string(),
1335 dec!(50.0),
1336 dec!(60.0),
1337 1,
1338 UnixNanos::default(),
1339 );
1340 assert_eq!(weird_balance.locked(), dec!(0.0));
1341 }
1342
1343 #[rstest]
1344 fn test_hyperliquid_account_state_creation() {
1345 let state = HyperliquidAccountState::new();
1346 assert!(state.balances.is_empty());
1347 assert_eq!(state.last_sequence, 0);
1348
1349 let default_state = HyperliquidAccountState::default();
1350 assert!(default_state.balances.is_empty());
1351 assert_eq!(default_state.last_sequence, 0);
1352 }
1353
1354 #[rstest]
1355 fn test_hyperliquid_account_state_getters() {
1356 use rust_decimal_macros::dec;
1357
1358 let mut state = HyperliquidAccountState::new();
1359
1360 let balance = state.get_balance("USD");
1362 assert_eq!(balance.asset, "USD");
1363 assert_eq!(balance.total, dec!(0.0));
1364 assert_eq!(balance.available, dec!(0.0));
1365
1366 let real_balance = HyperliquidBalance::new(
1368 "USD".to_string(),
1369 dec!(1000.0),
1370 dec!(750.0),
1371 1,
1372 UnixNanos::default(),
1373 );
1374 state.balances.insert("USD".to_string(), real_balance);
1375
1376 let retrieved_balance = state.get_balance("USD");
1378 assert_eq!(retrieved_balance.total, dec!(1000.0));
1379 }
1380
1381 #[rstest]
1382 fn test_hyperliquid_account_state_account_value() {
1383 use rust_decimal_macros::dec;
1384
1385 let mut state = HyperliquidAccountState::new();
1386
1387 state.balances.insert(
1389 "USD".to_string(),
1390 HyperliquidBalance::new(
1391 "USD".to_string(),
1392 dec!(10000.0),
1393 dec!(5000.0),
1394 1,
1395 UnixNanos::default(),
1396 ),
1397 );
1398
1399 let total_value = state.account_value();
1400 assert_eq!(total_value, dec!(10000.0));
1401
1402 state.balances.clear();
1404 let no_balance_value = state.account_value();
1405 assert_eq!(no_balance_value, dec!(0.0));
1406 }
1407
1408 #[rstest]
1409 fn test_hyperliquid_account_event_balance_snapshot() {
1410 use rust_decimal_macros::dec;
1411
1412 let mut state = HyperliquidAccountState::new();
1413
1414 let balance = HyperliquidBalance::new(
1415 "USD".to_string(),
1416 dec!(1000.0),
1417 dec!(750.0),
1418 10,
1419 UnixNanos::default(),
1420 );
1421
1422 let snapshot_event = HyperliquidAccountEvent::BalanceSnapshot {
1423 balances: vec![balance],
1424 sequence: 10,
1425 };
1426
1427 state.apply(snapshot_event);
1428
1429 assert_eq!(state.balances.len(), 1);
1430 assert_eq!(state.last_sequence, 10);
1431 assert_eq!(state.get_balance("USD").total, dec!(1000.0));
1432 }
1433
1434 #[rstest]
1435 fn test_hyperliquid_account_event_balance_delta() {
1436 use rust_decimal_macros::dec;
1437
1438 let mut state = HyperliquidAccountState::new();
1439
1440 let initial_balance = HyperliquidBalance::new(
1442 "USD".to_string(),
1443 dec!(1000.0),
1444 dec!(750.0),
1445 5,
1446 UnixNanos::default(),
1447 );
1448 state.balances.insert("USD".to_string(), initial_balance);
1449 state.last_sequence = 5;
1450
1451 let updated_balance = HyperliquidBalance::new(
1453 "USD".to_string(),
1454 dec!(1200.0),
1455 dec!(900.0),
1456 10,
1457 UnixNanos::default(),
1458 );
1459
1460 let delta_event = HyperliquidAccountEvent::BalanceDelta {
1461 balance: updated_balance,
1462 };
1463
1464 state.apply(delta_event);
1465
1466 let balance = state.get_balance("USD");
1467 assert_eq!(balance.total, dec!(1200.0));
1468 assert_eq!(balance.available, dec!(900.0));
1469 assert_eq!(balance.sequence, 10);
1470 assert_eq!(state.last_sequence, 10);
1471
1472 let old_balance = HyperliquidBalance::new(
1474 "USD".to_string(),
1475 dec!(800.0),
1476 dec!(600.0),
1477 8,
1478 UnixNanos::default(),
1479 );
1480
1481 let old_delta_event = HyperliquidAccountEvent::BalanceDelta {
1482 balance: old_balance,
1483 };
1484
1485 state.apply(old_delta_event);
1486
1487 let balance = state.get_balance("USD");
1489 assert_eq!(balance.total, dec!(1200.0)); assert_eq!(balance.sequence, 10); assert_eq!(state.last_sequence, 10); }
1493}