1use std::collections::HashMap;
2
3use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "camelCase")]
9#[non_exhaustive]
10pub struct HlPosition {
11 pub coin: String,
13 pub size: Decimal,
15 pub entry_px: Decimal,
17 pub unrealized_pnl: Decimal,
19 pub leverage: Decimal,
21 pub liquidation_px: Option<Decimal>,
23}
24
25impl HlPosition {
26 pub fn new(
28 coin: String,
29 size: Decimal,
30 entry_px: Decimal,
31 unrealized_pnl: Decimal,
32 leverage: Decimal,
33 liquidation_px: Option<Decimal>,
34 ) -> Self {
35 Self {
36 coin,
37 size,
38 entry_px,
39 unrealized_pnl,
40 leverage,
41 liquidation_px,
42 }
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase")]
49#[non_exhaustive]
50pub struct HlFill {
51 pub coin: String,
53 pub px: Decimal,
55 pub sz: Decimal,
57 pub is_buy: bool,
59 pub timestamp: u64,
61 pub fee: Decimal,
63 pub closed_pnl: Decimal,
65}
66
67impl HlFill {
68 pub fn new(
70 coin: String,
71 px: Decimal,
72 sz: Decimal,
73 is_buy: bool,
74 timestamp: u64,
75 fee: Decimal,
76 closed_pnl: Decimal,
77 ) -> Self {
78 Self {
79 coin,
80 px,
81 sz,
82 is_buy,
83 timestamp,
84 fee,
85 closed_pnl,
86 }
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93#[non_exhaustive]
94pub struct HlAccountState {
95 pub equity: Decimal,
97 pub margin_available: Decimal,
99 pub positions: Vec<HlPosition>,
101}
102
103impl HlAccountState {
104 pub fn new(equity: Decimal, margin_available: Decimal, positions: Vec<HlPosition>) -> Self {
106 Self {
107 equity,
108 margin_available,
109 positions,
110 }
111 }
112}
113
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct HlVaultSummary {
121 pub vault_address: String,
123 pub name: String,
125 #[serde(default)]
127 pub leader_equity: Option<Decimal>,
128 #[serde(default)]
130 pub follower_equity: Option<Decimal>,
131 #[serde(default)]
133 pub all_time_pnl: Option<Decimal>,
134 #[serde(flatten)]
136 pub extra: HashMap<String, serde_json::Value>,
137}
138
139#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct HlVaultDetails {
145 pub name: String,
147 pub vault_address: String,
149 #[serde(default)]
151 pub leader: Option<String>,
152 #[serde(default)]
154 pub portfolio: Option<serde_json::Value>,
155 #[serde(default)]
157 pub follower_count: Option<u64>,
158 #[serde(flatten)]
160 pub extra: HashMap<String, serde_json::Value>,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165#[serde(rename_all = "camelCase")]
166#[non_exhaustive]
167pub struct HlUserFees {
168 pub fee_tier: String,
170 pub maker_rate: Decimal,
172 pub taker_rate: Decimal,
174}
175
176impl HlUserFees {
177 pub fn new(fee_tier: String, maker_rate: Decimal, taker_rate: Decimal) -> Self {
179 Self {
180 fee_tier,
181 maker_rate,
182 taker_rate,
183 }
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190#[non_exhaustive]
191pub struct HlRateLimitStatus {
192 pub used: u64,
194 pub limit: u64,
196 pub window_ms: u64,
198}
199
200impl HlRateLimitStatus {
201 pub fn new(used: u64, limit: u64, window_ms: u64) -> Self {
203 Self {
204 used,
205 limit,
206 window_ms,
207 }
208 }
209}
210
211#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
215#[serde(rename_all = "camelCase")]
216pub struct HlExtraAgent {
217 pub address: String,
219 #[serde(default)]
221 pub name: Option<String>,
222 #[serde(flatten)]
224 pub extra: HashMap<String, serde_json::Value>,
225}
226
227#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
229#[serde(rename_all = "camelCase")]
230#[non_exhaustive]
231pub struct HlStakingDelegation {
232 pub validator: String,
234 pub amount: Decimal,
236 pub rewards: Decimal,
238}
239
240impl HlStakingDelegation {
241 pub fn new(validator: String, amount: Decimal, rewards: Decimal) -> Self {
243 Self {
244 validator,
245 amount,
246 rewards,
247 }
248 }
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
253#[serde(rename_all = "camelCase")]
254#[non_exhaustive]
255pub struct HlBorrowLendState {
256 pub coin: String,
258 pub supply: Decimal,
260 pub borrow: Decimal,
262 pub apy: Decimal,
264}
265
266impl HlBorrowLendState {
267 pub fn new(coin: String, supply: Decimal, borrow: Decimal, apy: Decimal) -> Self {
269 Self {
270 coin,
271 supply,
272 borrow,
273 apy,
274 }
275 }
276}
277
278#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
280#[serde(rename_all = "camelCase")]
281#[non_exhaustive]
282pub struct HlOpenOrder {
283 pub oid: u64,
285 pub coin: String,
287 pub side: crate::market::TradeSide,
289 pub limit_px: Decimal,
291 pub sz: Decimal,
293 pub timestamp: u64,
295 pub order_type: String,
297 pub cloid: Option<String>,
299}
300
301impl HlOpenOrder {
302 #[allow(clippy::too_many_arguments)]
304 pub fn new(
305 oid: u64,
306 coin: String,
307 side: crate::market::TradeSide,
308 limit_px: Decimal,
309 sz: Decimal,
310 timestamp: u64,
311 order_type: String,
312 cloid: Option<String>,
313 ) -> Self {
314 Self {
315 oid,
316 coin,
317 side,
318 limit_px,
319 sz,
320 timestamp,
321 order_type,
322 cloid,
323 }
324 }
325}
326
327#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
329#[serde(rename_all = "camelCase")]
330#[non_exhaustive]
331pub struct HlOrderDetail {
332 pub oid: u64,
334 pub coin: String,
336 pub side: crate::market::TradeSide,
338 pub limit_px: Decimal,
340 pub sz: Decimal,
342 pub timestamp: u64,
344 pub order_type: String,
346 pub cloid: Option<String>,
348 pub status: String,
350}
351
352impl HlOrderDetail {
353 #[allow(clippy::too_many_arguments)]
355 pub fn new(
356 oid: u64,
357 coin: String,
358 side: crate::market::TradeSide,
359 limit_px: Decimal,
360 sz: Decimal,
361 timestamp: u64,
362 order_type: String,
363 cloid: Option<String>,
364 status: String,
365 ) -> Self {
366 Self {
367 oid,
368 coin,
369 side,
370 limit_px,
371 sz,
372 timestamp,
373 order_type,
374 cloid,
375 status,
376 }
377 }
378}
379
380#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
382#[serde(rename_all = "camelCase")]
383#[non_exhaustive]
384pub struct HlFundingEntry {
385 pub coin: String,
387 pub funding_rate: Decimal,
389 pub premium: Decimal,
391 pub time: u64,
393}
394
395impl HlFundingEntry {
396 pub fn new(coin: String, funding_rate: Decimal, premium: Decimal, time: u64) -> Self {
398 Self {
399 coin,
400 funding_rate,
401 premium,
402 time,
403 }
404 }
405}
406
407#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
409#[serde(rename_all = "camelCase")]
410#[non_exhaustive]
411pub struct HlUserFundingEntry {
412 pub coin: String,
414 pub usdc: Decimal,
416 pub szi: Decimal,
418 pub funding_rate: Decimal,
420 pub time: u64,
422}
423
424impl HlUserFundingEntry {
425 pub fn new(
427 coin: String,
428 usdc: Decimal,
429 szi: Decimal,
430 funding_rate: Decimal,
431 time: u64,
432 ) -> Self {
433 Self {
434 coin,
435 usdc,
436 szi,
437 funding_rate,
438 time,
439 }
440 }
441}
442
443#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
445#[serde(rename_all = "camelCase")]
446#[non_exhaustive]
447pub struct HlHistoricalOrder {
448 pub oid: u64,
450 pub coin: String,
452 pub side: crate::market::TradeSide,
454 pub limit_px: Decimal,
456 pub sz: Decimal,
458 pub timestamp: u64,
460 pub order_type: String,
462 pub cloid: Option<String>,
464 pub status: String,
466}
467
468impl HlHistoricalOrder {
469 #[allow(clippy::too_many_arguments)]
471 pub fn new(
472 oid: u64,
473 coin: String,
474 side: crate::market::TradeSide,
475 limit_px: Decimal,
476 sz: Decimal,
477 timestamp: u64,
478 order_type: String,
479 cloid: Option<String>,
480 status: String,
481 ) -> Self {
482 Self {
483 oid,
484 coin,
485 side,
486 limit_px,
487 sz,
488 timestamp,
489 order_type,
490 cloid,
491 status,
492 }
493 }
494}
495
496#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
498#[serde(rename_all = "camelCase")]
499#[non_exhaustive]
500pub struct HlReferralState {
501 pub referrer: Option<String>,
503 pub referral_code: Option<String>,
505 pub cum_vlm: Decimal,
507 pub rewards: Decimal,
509}
510
511impl HlReferralState {
512 pub fn new(
514 referrer: Option<String>,
515 referral_code: Option<String>,
516 cum_vlm: Decimal,
517 rewards: Decimal,
518 ) -> Self {
519 Self {
520 referrer,
521 referral_code,
522 cum_vlm,
523 rewards,
524 }
525 }
526}
527
528#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
530#[serde(rename_all = "camelCase")]
531#[non_exhaustive]
532pub struct HlActiveAssetData {
533 pub coin: String,
535 pub leverage: Decimal,
537 pub max_trade_szs: Vec<Decimal>,
539 pub available_to_trade: Vec<Decimal>,
541 pub mark_px: Decimal,
543}
544
545impl HlActiveAssetData {
546 pub fn new(
548 coin: String,
549 leverage: Decimal,
550 max_trade_szs: Vec<Decimal>,
551 available_to_trade: Vec<Decimal>,
552 mark_px: Decimal,
553 ) -> Self {
554 Self {
555 coin,
556 leverage,
557 max_trade_szs,
558 available_to_trade,
559 mark_px,
560 }
561 }
562}
563
564#[cfg(test)]
565mod tests {
566 use super::*;
567 use crate::market::TradeSide;
568 use std::str::FromStr;
569
570 #[test]
571 fn position_serde_roundtrip() {
572 let pos = HlPosition {
573 coin: "BTC".into(),
574 size: Decimal::from_str("0.5").unwrap(),
575 entry_px: Decimal::from_str("60000.0").unwrap(),
576 unrealized_pnl: Decimal::from_str("150.0").unwrap(),
577 leverage: Decimal::from_str("10.0").unwrap(),
578 liquidation_px: Some(Decimal::from_str("54000.0").unwrap()),
579 };
580 let json = serde_json::to_string(&pos).unwrap();
581 let parsed: HlPosition = serde_json::from_str(&json).unwrap();
582 assert_eq!(parsed.coin, "BTC");
583 assert_eq!(parsed.size, Decimal::from_str("0.5").unwrap());
584 assert_eq!(parsed.entry_px, Decimal::from_str("60000.0").unwrap());
585 assert_eq!(parsed.unrealized_pnl, Decimal::from_str("150.0").unwrap());
586 assert_eq!(parsed.leverage, Decimal::from_str("10.0").unwrap());
587 assert_eq!(
588 parsed.liquidation_px,
589 Some(Decimal::from_str("54000.0").unwrap())
590 );
591 }
592
593 #[test]
594 fn position_no_liquidation_px_roundtrip() {
595 let pos = HlPosition {
596 coin: "ETH".into(),
597 size: Decimal::from_str("-2.0").unwrap(),
598 entry_px: Decimal::from_str("3000.0").unwrap(),
599 unrealized_pnl: Decimal::from_str("-50.0").unwrap(),
600 leverage: Decimal::from_str("5.0").unwrap(),
601 liquidation_px: None,
602 };
603 let json = serde_json::to_string(&pos).unwrap();
604 let parsed: HlPosition = serde_json::from_str(&json).unwrap();
605 assert!(parsed.liquidation_px.is_none());
606 assert!(parsed.size < Decimal::ZERO);
607 }
608
609 #[test]
610 fn position_camel_case_keys() {
611 let pos = HlPosition {
612 coin: "X".into(),
613 size: Decimal::ONE,
614 entry_px: Decimal::ONE,
615 unrealized_pnl: Decimal::ZERO,
616 leverage: Decimal::ONE,
617 liquidation_px: None,
618 };
619 let json = serde_json::to_string(&pos).unwrap();
620 assert!(json.contains("entryPx"));
621 assert!(json.contains("unrealizedPnl"));
622 assert!(json.contains("liquidationPx"));
623 }
624
625 #[test]
626 fn fill_serde_roundtrip() {
627 let fill = HlFill {
628 coin: "ETH".into(),
629 px: Decimal::from_str("3000.0").unwrap(),
630 sz: Decimal::from_str("1.5").unwrap(),
631 is_buy: true,
632 timestamp: 1700000000000,
633 fee: Decimal::from_str("0.75").unwrap(),
634 closed_pnl: Decimal::ZERO,
635 };
636 let json = serde_json::to_string(&fill).unwrap();
637 let parsed: HlFill = serde_json::from_str(&json).unwrap();
638 assert_eq!(parsed.coin, "ETH");
639 assert_eq!(parsed.px, Decimal::from_str("3000.0").unwrap());
640 assert_eq!(parsed.sz, Decimal::from_str("1.5").unwrap());
641 assert!(parsed.is_buy);
642 assert_eq!(parsed.timestamp, 1700000000000);
643 assert_eq!(parsed.fee, Decimal::from_str("0.75").unwrap());
644 assert_eq!(parsed.closed_pnl, Decimal::ZERO);
645 }
646
647 #[test]
648 fn fill_camel_case_keys() {
649 let fill = HlFill {
650 coin: "X".into(),
651 px: Decimal::ONE,
652 sz: Decimal::ONE,
653 is_buy: false,
654 timestamp: 0,
655 fee: Decimal::ZERO,
656 closed_pnl: Decimal::from_str("100.0").unwrap(),
657 };
658 let json = serde_json::to_string(&fill).unwrap();
659 assert!(json.contains("isBuy"));
660 assert!(json.contains("closedPnl"));
661 }
662
663 #[test]
664 fn account_state_serde_roundtrip() {
665 let state = HlAccountState {
666 equity: Decimal::from_str("100000.0").unwrap(),
667 margin_available: Decimal::from_str("50000.0").unwrap(),
668 positions: vec![HlPosition {
669 coin: "BTC".into(),
670 size: Decimal::from_str("0.1").unwrap(),
671 entry_px: Decimal::from_str("60000.0").unwrap(),
672 unrealized_pnl: Decimal::ZERO,
673 leverage: Decimal::from_str("10.0").unwrap(),
674 liquidation_px: None,
675 }],
676 };
677 let json = serde_json::to_string(&state).unwrap();
678 let parsed: HlAccountState = serde_json::from_str(&json).unwrap();
679 assert_eq!(parsed.equity, Decimal::from_str("100000.0").unwrap());
680 assert_eq!(
681 parsed.margin_available,
682 Decimal::from_str("50000.0").unwrap()
683 );
684 assert_eq!(parsed.positions.len(), 1);
685 assert_eq!(parsed.positions[0].coin, "BTC");
686 }
687
688 #[test]
689 fn account_state_empty_positions_roundtrip() {
690 let state = HlAccountState {
691 equity: Decimal::ZERO,
692 margin_available: Decimal::ZERO,
693 positions: vec![],
694 };
695 let json = serde_json::to_string(&state).unwrap();
696 let parsed: HlAccountState = serde_json::from_str(&json).unwrap();
697 assert!(parsed.positions.is_empty());
698 }
699
700 #[test]
701 fn account_state_camel_case_keys() {
702 let state = HlAccountState {
703 equity: Decimal::ONE,
704 margin_available: Decimal::ONE,
705 positions: vec![],
706 };
707 let json = serde_json::to_string(&state).unwrap();
708 assert!(json.contains("marginAvailable"));
709 }
710
711 #[test]
712 fn vault_summary_serde_roundtrip() {
713 let json = serde_json::json!({
714 "vaultAddress": "0xabc123",
715 "name": "My Vault",
716 "leaderEquity": "10000.0",
717 "followerEquity": "50000.0",
718 "allTimePnl": "2500.0"
719 });
720 let parsed: HlVaultSummary = serde_json::from_value(json).unwrap();
721 assert_eq!(parsed.vault_address, "0xabc123");
722 assert_eq!(parsed.name, "My Vault");
723 assert_eq!(
724 parsed.leader_equity,
725 Some(Decimal::from_str("10000.0").unwrap())
726 );
727 assert_eq!(
728 parsed.follower_equity,
729 Some(Decimal::from_str("50000.0").unwrap())
730 );
731 assert_eq!(
732 parsed.all_time_pnl,
733 Some(Decimal::from_str("2500.0").unwrap())
734 );
735 }
736
737 #[test]
738 fn vault_summary_minimal_fields() {
739 let json = serde_json::json!({
740 "vaultAddress": "0xdef456",
741 "name": "Minimal Vault"
742 });
743 let parsed: HlVaultSummary = serde_json::from_value(json).unwrap();
744 assert_eq!(parsed.vault_address, "0xdef456");
745 assert_eq!(parsed.name, "Minimal Vault");
746 assert!(parsed.leader_equity.is_none());
747 assert!(parsed.follower_equity.is_none());
748 assert!(parsed.all_time_pnl.is_none());
749 }
750
751 #[test]
752 fn vault_summary_extra_fields_captured() {
753 let json = serde_json::json!({
754 "vaultAddress": "0x111",
755 "name": "V",
756 "someNewField": 42
757 });
758 let parsed: HlVaultSummary = serde_json::from_value(json).unwrap();
759 assert_eq!(
760 parsed.extra.get("someNewField").unwrap(),
761 &serde_json::json!(42)
762 );
763 }
764
765 #[test]
766 fn vault_summary_camel_case_keys() {
767 let summary = HlVaultSummary {
768 vault_address: "0x1".into(),
769 name: "V".into(),
770 leader_equity: Some(Decimal::ONE),
771 follower_equity: None,
772 all_time_pnl: None,
773 extra: HashMap::new(),
774 };
775 let json = serde_json::to_string(&summary).unwrap();
776 assert!(json.contains("vaultAddress"));
777 assert!(json.contains("leaderEquity"));
778 }
779
780 #[test]
781 fn vault_details_serde_roundtrip() {
782 let json = serde_json::json!({
783 "name": "Alpha Vault",
784 "vaultAddress": "0xvault",
785 "leader": "0xleader",
786 "portfolio": {"equity": "100000"},
787 "followerCount": 25
788 });
789 let parsed: HlVaultDetails = serde_json::from_value(json).unwrap();
790 assert_eq!(parsed.name, "Alpha Vault");
791 assert_eq!(parsed.vault_address, "0xvault");
792 assert_eq!(parsed.leader.as_deref(), Some("0xleader"));
793 assert!(parsed.portfolio.is_some());
794 assert_eq!(parsed.follower_count, Some(25));
795 }
796
797 #[test]
798 fn vault_details_minimal_fields() {
799 let json = serde_json::json!({
800 "name": "Min",
801 "vaultAddress": "0xmin"
802 });
803 let parsed: HlVaultDetails = serde_json::from_value(json).unwrap();
804 assert_eq!(parsed.name, "Min");
805 assert!(parsed.leader.is_none());
806 assert!(parsed.portfolio.is_none());
807 assert!(parsed.follower_count.is_none());
808 }
809
810 #[test]
811 fn vault_details_extra_fields_captured() {
812 let json = serde_json::json!({
813 "name": "V",
814 "vaultAddress": "0x1",
815 "customMetric": "hello"
816 });
817 let parsed: HlVaultDetails = serde_json::from_value(json).unwrap();
818 assert_eq!(
819 parsed.extra.get("customMetric").unwrap(),
820 &serde_json::json!("hello")
821 );
822 }
823
824 #[test]
825 fn extra_agent_serde_roundtrip() {
826 let json = serde_json::json!({
827 "address": "0xagent1",
828 "name": "Trading Bot"
829 });
830 let parsed: HlExtraAgent = serde_json::from_value(json).unwrap();
831 assert_eq!(parsed.address, "0xagent1");
832 assert_eq!(parsed.name.as_deref(), Some("Trading Bot"));
833 }
834
835 #[test]
836 fn extra_agent_minimal_fields() {
837 let json = serde_json::json!({
838 "address": "0xagent2"
839 });
840 let parsed: HlExtraAgent = serde_json::from_value(json).unwrap();
841 assert_eq!(parsed.address, "0xagent2");
842 assert!(parsed.name.is_none());
843 }
844
845 #[test]
846 fn extra_agent_extra_fields_captured() {
847 let json = serde_json::json!({
848 "address": "0xagent3",
849 "permissions": ["trade", "withdraw"]
850 });
851 let parsed: HlExtraAgent = serde_json::from_value(json).unwrap();
852 assert!(parsed.extra.contains_key("permissions"));
853 }
854
855 #[test]
856 fn staking_delegation_serde_roundtrip() {
857 let delegation = HlStakingDelegation {
858 validator: "0xval1".into(),
859 amount: Decimal::from_str("1000.0").unwrap(),
860 rewards: Decimal::from_str("5.25").unwrap(),
861 };
862 let json = serde_json::to_string(&delegation).unwrap();
863 let parsed: HlStakingDelegation = serde_json::from_str(&json).unwrap();
864 assert_eq!(parsed.validator, "0xval1");
865 assert_eq!(parsed.amount, Decimal::from_str("1000.0").unwrap());
866 assert_eq!(parsed.rewards, Decimal::from_str("5.25").unwrap());
867 }
868
869 #[test]
870 fn staking_delegation_from_json() {
871 let json = serde_json::json!({
872 "validator": "0xabc",
873 "amount": "500.0",
874 "rewards": "2.5"
875 });
876 let parsed: HlStakingDelegation = serde_json::from_value(json).unwrap();
877 assert_eq!(parsed.validator, "0xabc");
878 assert_eq!(parsed.amount, Decimal::from_str("500.0").unwrap());
879 assert_eq!(parsed.rewards, Decimal::from_str("2.5").unwrap());
880 }
881
882 #[test]
883 fn borrow_lend_state_serde_roundtrip() {
884 let state = HlBorrowLendState {
885 coin: "USDC".into(),
886 supply: Decimal::from_str("10000.0").unwrap(),
887 borrow: Decimal::from_str("5000.0").unwrap(),
888 apy: Decimal::from_str("0.05").unwrap(),
889 };
890 let json = serde_json::to_string(&state).unwrap();
891 let parsed: HlBorrowLendState = serde_json::from_str(&json).unwrap();
892 assert_eq!(parsed.coin, "USDC");
893 assert_eq!(parsed.supply, Decimal::from_str("10000.0").unwrap());
894 assert_eq!(parsed.borrow, Decimal::from_str("5000.0").unwrap());
895 assert_eq!(parsed.apy, Decimal::from_str("0.05").unwrap());
896 }
897
898 #[test]
899 fn borrow_lend_state_from_json() {
900 let json = serde_json::json!({
901 "coin": "ETH",
902 "supply": "100.0",
903 "borrow": "0.0",
904 "apy": "0.03"
905 });
906 let parsed: HlBorrowLendState = serde_json::from_value(json).unwrap();
907 assert_eq!(parsed.coin, "ETH");
908 assert_eq!(parsed.supply, Decimal::from_str("100.0").unwrap());
909 assert_eq!(parsed.borrow, Decimal::ZERO);
910 assert_eq!(parsed.apy, Decimal::from_str("0.03").unwrap());
911 }
912
913 #[test]
914 fn borrow_lend_state_camel_case_keys() {
915 let state = HlBorrowLendState {
916 coin: "X".into(),
917 supply: Decimal::ONE,
918 borrow: Decimal::ZERO,
919 apy: Decimal::ZERO,
920 };
921 let json = serde_json::to_string(&state).unwrap();
922 assert!(json.contains("supply"));
924 assert!(json.contains("borrow"));
925 assert!(json.contains("apy"));
926 }
927
928 #[test]
929 fn extra_agent_camel_case_keys() {
930 let agent = HlExtraAgent {
931 address: "0x1".into(),
932 name: Some("Bot".into()),
933 extra: HashMap::new(),
934 };
935 let json = serde_json::to_string(&agent).unwrap();
936 assert!(json.contains("address"));
937 assert!(json.contains("name"));
938 }
939
940 #[test]
941 fn user_fees_serde_roundtrip() {
942 let fees = HlUserFees {
943 fee_tier: "VIP2".into(),
944 maker_rate: Decimal::from_str("0.0001").unwrap(),
945 taker_rate: Decimal::from_str("0.0003").unwrap(),
946 };
947 let json = serde_json::to_string(&fees).unwrap();
948 let parsed: HlUserFees = serde_json::from_str(&json).unwrap();
949 assert_eq!(parsed.fee_tier, "VIP2");
950 assert_eq!(parsed.maker_rate, Decimal::from_str("0.0001").unwrap());
951 assert_eq!(parsed.taker_rate, Decimal::from_str("0.0003").unwrap());
952 }
953
954 #[test]
955 fn user_fees_camel_case_keys() {
956 let fees = HlUserFees {
957 fee_tier: "T1".into(),
958 maker_rate: Decimal::ZERO,
959 taker_rate: Decimal::ONE,
960 };
961 let json = serde_json::to_string(&fees).unwrap();
962 assert!(json.contains("feeTier"));
963 assert!(json.contains("makerRate"));
964 assert!(json.contains("takerRate"));
965 }
966
967 #[test]
968 fn user_fees_constructor() {
969 let fees = HlUserFees::new(
970 "VIP1".into(),
971 Decimal::from_str("0.0002").unwrap(),
972 Decimal::from_str("0.0005").unwrap(),
973 );
974 assert_eq!(fees.fee_tier, "VIP1");
975 assert_eq!(fees.maker_rate, Decimal::from_str("0.0002").unwrap());
976 assert_eq!(fees.taker_rate, Decimal::from_str("0.0005").unwrap());
977 }
978
979 #[test]
980 fn rate_limit_status_serde_roundtrip() {
981 let status = HlRateLimitStatus {
982 used: 42,
983 limit: 1200,
984 window_ms: 60000,
985 };
986 let json = serde_json::to_string(&status).unwrap();
987 let parsed: HlRateLimitStatus = serde_json::from_str(&json).unwrap();
988 assert_eq!(parsed.used, 42);
989 assert_eq!(parsed.limit, 1200);
990 assert_eq!(parsed.window_ms, 60000);
991 }
992
993 #[test]
994 fn rate_limit_status_camel_case_keys() {
995 let status = HlRateLimitStatus {
996 used: 0,
997 limit: 100,
998 window_ms: 30000,
999 };
1000 let json = serde_json::to_string(&status).unwrap();
1001 assert!(json.contains("windowMs"));
1002 }
1003
1004 #[test]
1005 fn rate_limit_status_constructor() {
1006 let status = HlRateLimitStatus::new(10, 500, 60000);
1007 assert_eq!(status.used, 10);
1008 assert_eq!(status.limit, 500);
1009 assert_eq!(status.window_ms, 60000);
1010 }
1011
1012 #[test]
1013 fn open_order_serde_roundtrip() {
1014 let order = HlOpenOrder {
1015 oid: 12345,
1016 coin: "BTC".into(),
1017 side: TradeSide::Buy,
1018 limit_px: Decimal::from_str("60000.0").unwrap(),
1019 sz: Decimal::from_str("0.5").unwrap(),
1020 timestamp: 1700000000000,
1021 order_type: "Limit".into(),
1022 cloid: Some("my-order-1".into()),
1023 };
1024 let json = serde_json::to_string(&order).unwrap();
1025 let parsed: HlOpenOrder = serde_json::from_str(&json).unwrap();
1026 assert_eq!(parsed.oid, 12345);
1027 assert_eq!(parsed.coin, "BTC");
1028 assert_eq!(parsed.side, TradeSide::Buy);
1029 assert_eq!(parsed.limit_px, Decimal::from_str("60000.0").unwrap());
1030 assert_eq!(parsed.sz, Decimal::from_str("0.5").unwrap());
1031 assert_eq!(parsed.timestamp, 1700000000000);
1032 assert_eq!(parsed.order_type, "Limit");
1033 assert_eq!(parsed.cloid.as_deref(), Some("my-order-1"));
1034 }
1035
1036 #[test]
1037 fn open_order_no_cloid_roundtrip() {
1038 let order = HlOpenOrder {
1039 oid: 99,
1040 coin: "ETH".into(),
1041 side: TradeSide::Sell,
1042 limit_px: Decimal::from_str("3000.0").unwrap(),
1043 sz: Decimal::ONE,
1044 timestamp: 0,
1045 order_type: "Limit".into(),
1046 cloid: None,
1047 };
1048 let json = serde_json::to_string(&order).unwrap();
1049 let parsed: HlOpenOrder = serde_json::from_str(&json).unwrap();
1050 assert!(parsed.cloid.is_none());
1051 }
1052
1053 #[test]
1054 fn open_order_camel_case_keys() {
1055 let order = HlOpenOrder {
1056 oid: 1,
1057 coin: "X".into(),
1058 side: TradeSide::Buy,
1059 limit_px: Decimal::ONE,
1060 sz: Decimal::ONE,
1061 timestamp: 0,
1062 order_type: "Limit".into(),
1063 cloid: None,
1064 };
1065 let json = serde_json::to_string(&order).unwrap();
1066 assert!(json.contains("limitPx"));
1067 assert!(json.contains("orderType"));
1068 }
1069
1070 #[test]
1071 fn order_detail_serde_roundtrip() {
1072 let detail = HlOrderDetail {
1073 oid: 555,
1074 coin: "SOL".into(),
1075 side: TradeSide::Buy,
1076 limit_px: Decimal::from_str("150.0").unwrap(),
1077 sz: Decimal::from_str("10.0").unwrap(),
1078 timestamp: 1700000000000,
1079 order_type: "Limit".into(),
1080 cloid: None,
1081 status: "filled".into(),
1082 };
1083 let json = serde_json::to_string(&detail).unwrap();
1084 let parsed: HlOrderDetail = serde_json::from_str(&json).unwrap();
1085 assert_eq!(parsed.oid, 555);
1086 assert_eq!(parsed.status, "filled");
1087 assert_eq!(parsed.coin, "SOL");
1088 }
1089
1090 #[test]
1091 fn order_detail_camel_case_keys() {
1092 let detail = HlOrderDetail {
1093 oid: 1,
1094 coin: "X".into(),
1095 side: TradeSide::Buy,
1096 limit_px: Decimal::ONE,
1097 sz: Decimal::ONE,
1098 timestamp: 0,
1099 order_type: "Limit".into(),
1100 cloid: Some("c1".into()),
1101 status: "open".into(),
1102 };
1103 let json = serde_json::to_string(&detail).unwrap();
1104 assert!(json.contains("limitPx"));
1105 assert!(json.contains("orderType"));
1106 }
1107
1108 #[test]
1109 fn funding_entry_serde_roundtrip() {
1110 let entry = HlFundingEntry {
1111 coin: "BTC".into(),
1112 funding_rate: Decimal::from_str("0.0001").unwrap(),
1113 premium: Decimal::from_str("0.00005").unwrap(),
1114 time: 1700000000000,
1115 };
1116 let json = serde_json::to_string(&entry).unwrap();
1117 let parsed: HlFundingEntry = serde_json::from_str(&json).unwrap();
1118 assert_eq!(parsed.coin, "BTC");
1119 assert_eq!(parsed.funding_rate, Decimal::from_str("0.0001").unwrap());
1120 assert_eq!(parsed.premium, Decimal::from_str("0.00005").unwrap());
1121 assert_eq!(parsed.time, 1700000000000);
1122 }
1123
1124 #[test]
1125 fn funding_entry_camel_case_keys() {
1126 let entry = HlFundingEntry {
1127 coin: "X".into(),
1128 funding_rate: Decimal::ONE,
1129 premium: Decimal::ZERO,
1130 time: 0,
1131 };
1132 let json = serde_json::to_string(&entry).unwrap();
1133 assert!(json.contains("fundingRate"));
1134 }
1135
1136 #[test]
1137 fn user_funding_entry_serde_roundtrip() {
1138 let entry = HlUserFundingEntry {
1139 coin: "ETH".into(),
1140 usdc: Decimal::from_str("-1.5").unwrap(),
1141 szi: Decimal::from_str("2.0").unwrap(),
1142 funding_rate: Decimal::from_str("0.0002").unwrap(),
1143 time: 1700000000000,
1144 };
1145 let json = serde_json::to_string(&entry).unwrap();
1146 let parsed: HlUserFundingEntry = serde_json::from_str(&json).unwrap();
1147 assert_eq!(parsed.coin, "ETH");
1148 assert_eq!(parsed.usdc, Decimal::from_str("-1.5").unwrap());
1149 assert_eq!(parsed.szi, Decimal::from_str("2.0").unwrap());
1150 assert_eq!(parsed.funding_rate, Decimal::from_str("0.0002").unwrap());
1151 assert_eq!(parsed.time, 1700000000000);
1152 }
1153
1154 #[test]
1155 fn user_funding_entry_camel_case_keys() {
1156 let entry = HlUserFundingEntry {
1157 coin: "X".into(),
1158 usdc: Decimal::ONE,
1159 szi: Decimal::ONE,
1160 funding_rate: Decimal::ZERO,
1161 time: 0,
1162 };
1163 let json = serde_json::to_string(&entry).unwrap();
1164 assert!(json.contains("fundingRate"));
1165 }
1166
1167 #[test]
1168 fn historical_order_serde_roundtrip() {
1169 let order = HlHistoricalOrder {
1170 oid: 777,
1171 coin: "BTC".into(),
1172 side: TradeSide::Sell,
1173 limit_px: Decimal::from_str("65000.0").unwrap(),
1174 sz: Decimal::from_str("0.1").unwrap(),
1175 timestamp: 1700000000000,
1176 order_type: "Limit".into(),
1177 cloid: Some("hist-1".into()),
1178 status: "filled".into(),
1179 };
1180 let json = serde_json::to_string(&order).unwrap();
1181 let parsed: HlHistoricalOrder = serde_json::from_str(&json).unwrap();
1182 assert_eq!(parsed.oid, 777);
1183 assert_eq!(parsed.status, "filled");
1184 assert_eq!(parsed.coin, "BTC");
1185 assert_eq!(parsed.cloid.as_deref(), Some("hist-1"));
1186 }
1187
1188 #[test]
1189 fn historical_order_camel_case_keys() {
1190 let order = HlHistoricalOrder {
1191 oid: 1,
1192 coin: "X".into(),
1193 side: TradeSide::Buy,
1194 limit_px: Decimal::ONE,
1195 sz: Decimal::ONE,
1196 timestamp: 0,
1197 order_type: "Limit".into(),
1198 cloid: None,
1199 status: "canceled".into(),
1200 };
1201 let json = serde_json::to_string(&order).unwrap();
1202 assert!(json.contains("limitPx"));
1203 assert!(json.contains("orderType"));
1204 }
1205
1206 #[test]
1207 fn referral_state_serde_roundtrip() {
1208 let state = HlReferralState::new(
1209 Some("0xabc".into()),
1210 Some("CODE123".into()),
1211 Decimal::from_str("50000.0").unwrap(),
1212 Decimal::from_str("100.5").unwrap(),
1213 );
1214 let json = serde_json::to_string(&state).unwrap();
1215 let parsed: HlReferralState = serde_json::from_str(&json).unwrap();
1216 assert_eq!(parsed.referrer.as_deref(), Some("0xabc"));
1217 assert_eq!(parsed.referral_code.as_deref(), Some("CODE123"));
1218 assert_eq!(parsed.cum_vlm, Decimal::from_str("50000.0").unwrap());
1219 assert_eq!(parsed.rewards, Decimal::from_str("100.5").unwrap());
1220 }
1221
1222 #[test]
1223 fn referral_state_camel_case_keys() {
1224 let state = HlReferralState::new(None, None, Decimal::ZERO, Decimal::ZERO);
1225 let json = serde_json::to_string(&state).unwrap();
1226 assert!(json.contains("cumVlm"));
1227 assert!(json.contains("referralCode"));
1228 }
1229}