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)]
331#[serde(rename_all = "camelCase")]
332#[non_exhaustive]
333pub struct HlFrontendOpenOrder {
334 pub oid: u64,
336 pub coin: String,
338 pub side: crate::market::TradeSide,
340 pub limit_px: Decimal,
342 pub sz: Decimal,
344 pub orig_sz: Decimal,
346 pub timestamp: u64,
348 pub order_type: String,
350 pub tif: Option<String>,
352 pub reduce_only: bool,
354 pub is_trigger: bool,
356 pub is_position_tpsl: bool,
358 pub trigger_condition: String,
360 pub trigger_px: Decimal,
362 pub cloid: Option<String>,
364 pub children: Vec<HlFrontendOpenOrder>,
366}
367
368impl HlFrontendOpenOrder {
369 #[allow(clippy::too_many_arguments)]
371 pub fn new(
372 oid: u64,
373 coin: String,
374 side: crate::market::TradeSide,
375 limit_px: Decimal,
376 sz: Decimal,
377 orig_sz: Decimal,
378 timestamp: u64,
379 order_type: String,
380 tif: Option<String>,
381 reduce_only: bool,
382 is_trigger: bool,
383 is_position_tpsl: bool,
384 trigger_condition: String,
385 trigger_px: Decimal,
386 cloid: Option<String>,
387 children: Vec<HlFrontendOpenOrder>,
388 ) -> Self {
389 Self {
390 oid,
391 coin,
392 side,
393 limit_px,
394 sz,
395 orig_sz,
396 timestamp,
397 order_type,
398 tif,
399 reduce_only,
400 is_trigger,
401 is_position_tpsl,
402 trigger_condition,
403 trigger_px,
404 cloid,
405 children,
406 }
407 }
408}
409
410#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
412#[serde(rename_all = "camelCase")]
413#[non_exhaustive]
414pub struct HlOrderDetail {
415 pub oid: u64,
417 pub coin: String,
419 pub side: crate::market::TradeSide,
421 pub limit_px: Decimal,
423 pub sz: Decimal,
425 pub timestamp: u64,
427 pub order_type: String,
429 pub cloid: Option<String>,
431 pub status: String,
433}
434
435impl HlOrderDetail {
436 #[allow(clippy::too_many_arguments)]
438 pub fn new(
439 oid: u64,
440 coin: String,
441 side: crate::market::TradeSide,
442 limit_px: Decimal,
443 sz: Decimal,
444 timestamp: u64,
445 order_type: String,
446 cloid: Option<String>,
447 status: String,
448 ) -> Self {
449 Self {
450 oid,
451 coin,
452 side,
453 limit_px,
454 sz,
455 timestamp,
456 order_type,
457 cloid,
458 status,
459 }
460 }
461}
462
463#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
465#[serde(rename_all = "camelCase")]
466#[non_exhaustive]
467pub struct HlFundingEntry {
468 pub coin: String,
470 pub funding_rate: Decimal,
472 pub premium: Decimal,
474 pub time: u64,
476}
477
478impl HlFundingEntry {
479 pub fn new(coin: String, funding_rate: Decimal, premium: Decimal, time: u64) -> Self {
481 Self {
482 coin,
483 funding_rate,
484 premium,
485 time,
486 }
487 }
488}
489
490#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
492#[serde(rename_all = "camelCase")]
493#[non_exhaustive]
494pub struct HlUserFundingEntry {
495 pub coin: String,
497 pub usdc: Decimal,
499 pub szi: Decimal,
501 pub funding_rate: Decimal,
503 pub time: u64,
505}
506
507impl HlUserFundingEntry {
508 pub fn new(
510 coin: String,
511 usdc: Decimal,
512 szi: Decimal,
513 funding_rate: Decimal,
514 time: u64,
515 ) -> Self {
516 Self {
517 coin,
518 usdc,
519 szi,
520 funding_rate,
521 time,
522 }
523 }
524}
525
526#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
528#[serde(rename_all = "camelCase")]
529#[non_exhaustive]
530pub struct HlHistoricalOrder {
531 pub oid: u64,
533 pub coin: String,
535 pub side: crate::market::TradeSide,
537 pub limit_px: Decimal,
539 pub sz: Decimal,
541 pub timestamp: u64,
543 pub order_type: String,
545 pub cloid: Option<String>,
547 pub status: String,
549}
550
551impl HlHistoricalOrder {
552 #[allow(clippy::too_many_arguments)]
554 pub fn new(
555 oid: u64,
556 coin: String,
557 side: crate::market::TradeSide,
558 limit_px: Decimal,
559 sz: Decimal,
560 timestamp: u64,
561 order_type: String,
562 cloid: Option<String>,
563 status: String,
564 ) -> Self {
565 Self {
566 oid,
567 coin,
568 side,
569 limit_px,
570 sz,
571 timestamp,
572 order_type,
573 cloid,
574 status,
575 }
576 }
577}
578
579#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
581#[serde(rename_all = "camelCase")]
582#[non_exhaustive]
583pub struct HlReferralState {
584 pub referrer: Option<String>,
586 pub referral_code: Option<String>,
588 pub cum_vlm: Decimal,
590 pub rewards: Decimal,
592}
593
594impl HlReferralState {
595 pub fn new(
597 referrer: Option<String>,
598 referral_code: Option<String>,
599 cum_vlm: Decimal,
600 rewards: Decimal,
601 ) -> Self {
602 Self {
603 referrer,
604 referral_code,
605 cum_vlm,
606 rewards,
607 }
608 }
609}
610
611#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
613#[serde(rename_all = "camelCase")]
614#[non_exhaustive]
615pub struct HlActiveAssetData {
616 pub coin: String,
618 pub leverage: Decimal,
620 pub max_trade_szs: Vec<Decimal>,
622 pub available_to_trade: Vec<Decimal>,
624 pub mark_px: Decimal,
626}
627
628impl HlActiveAssetData {
629 pub fn new(
631 coin: String,
632 leverage: Decimal,
633 max_trade_szs: Vec<Decimal>,
634 available_to_trade: Vec<Decimal>,
635 mark_px: Decimal,
636 ) -> Self {
637 Self {
638 coin,
639 leverage,
640 max_trade_szs,
641 available_to_trade,
642 mark_px,
643 }
644 }
645}
646
647#[cfg(test)]
648mod tests {
649 use super::*;
650 use crate::market::TradeSide;
651 use std::str::FromStr;
652
653 #[test]
654 fn position_serde_roundtrip() {
655 let pos = HlPosition {
656 coin: "BTC".into(),
657 size: Decimal::from_str("0.5").unwrap(),
658 entry_px: Decimal::from_str("60000.0").unwrap(),
659 unrealized_pnl: Decimal::from_str("150.0").unwrap(),
660 leverage: Decimal::from_str("10.0").unwrap(),
661 liquidation_px: Some(Decimal::from_str("54000.0").unwrap()),
662 };
663 let json = serde_json::to_string(&pos).unwrap();
664 let parsed: HlPosition = serde_json::from_str(&json).unwrap();
665 assert_eq!(parsed.coin, "BTC");
666 assert_eq!(parsed.size, Decimal::from_str("0.5").unwrap());
667 assert_eq!(parsed.entry_px, Decimal::from_str("60000.0").unwrap());
668 assert_eq!(parsed.unrealized_pnl, Decimal::from_str("150.0").unwrap());
669 assert_eq!(parsed.leverage, Decimal::from_str("10.0").unwrap());
670 assert_eq!(
671 parsed.liquidation_px,
672 Some(Decimal::from_str("54000.0").unwrap())
673 );
674 }
675
676 #[test]
677 fn position_no_liquidation_px_roundtrip() {
678 let pos = HlPosition {
679 coin: "ETH".into(),
680 size: Decimal::from_str("-2.0").unwrap(),
681 entry_px: Decimal::from_str("3000.0").unwrap(),
682 unrealized_pnl: Decimal::from_str("-50.0").unwrap(),
683 leverage: Decimal::from_str("5.0").unwrap(),
684 liquidation_px: None,
685 };
686 let json = serde_json::to_string(&pos).unwrap();
687 let parsed: HlPosition = serde_json::from_str(&json).unwrap();
688 assert!(parsed.liquidation_px.is_none());
689 assert!(parsed.size < Decimal::ZERO);
690 }
691
692 #[test]
693 fn position_camel_case_keys() {
694 let pos = HlPosition {
695 coin: "X".into(),
696 size: Decimal::ONE,
697 entry_px: Decimal::ONE,
698 unrealized_pnl: Decimal::ZERO,
699 leverage: Decimal::ONE,
700 liquidation_px: None,
701 };
702 let json = serde_json::to_string(&pos).unwrap();
703 assert!(json.contains("entryPx"));
704 assert!(json.contains("unrealizedPnl"));
705 assert!(json.contains("liquidationPx"));
706 }
707
708 #[test]
709 fn fill_serde_roundtrip() {
710 let fill = HlFill {
711 coin: "ETH".into(),
712 px: Decimal::from_str("3000.0").unwrap(),
713 sz: Decimal::from_str("1.5").unwrap(),
714 is_buy: true,
715 timestamp: 1700000000000,
716 fee: Decimal::from_str("0.75").unwrap(),
717 closed_pnl: Decimal::ZERO,
718 };
719 let json = serde_json::to_string(&fill).unwrap();
720 let parsed: HlFill = serde_json::from_str(&json).unwrap();
721 assert_eq!(parsed.coin, "ETH");
722 assert_eq!(parsed.px, Decimal::from_str("3000.0").unwrap());
723 assert_eq!(parsed.sz, Decimal::from_str("1.5").unwrap());
724 assert!(parsed.is_buy);
725 assert_eq!(parsed.timestamp, 1700000000000);
726 assert_eq!(parsed.fee, Decimal::from_str("0.75").unwrap());
727 assert_eq!(parsed.closed_pnl, Decimal::ZERO);
728 }
729
730 #[test]
731 fn fill_camel_case_keys() {
732 let fill = HlFill {
733 coin: "X".into(),
734 px: Decimal::ONE,
735 sz: Decimal::ONE,
736 is_buy: false,
737 timestamp: 0,
738 fee: Decimal::ZERO,
739 closed_pnl: Decimal::from_str("100.0").unwrap(),
740 };
741 let json = serde_json::to_string(&fill).unwrap();
742 assert!(json.contains("isBuy"));
743 assert!(json.contains("closedPnl"));
744 }
745
746 #[test]
747 fn account_state_serde_roundtrip() {
748 let state = HlAccountState {
749 equity: Decimal::from_str("100000.0").unwrap(),
750 margin_available: Decimal::from_str("50000.0").unwrap(),
751 positions: vec![HlPosition {
752 coin: "BTC".into(),
753 size: Decimal::from_str("0.1").unwrap(),
754 entry_px: Decimal::from_str("60000.0").unwrap(),
755 unrealized_pnl: Decimal::ZERO,
756 leverage: Decimal::from_str("10.0").unwrap(),
757 liquidation_px: None,
758 }],
759 };
760 let json = serde_json::to_string(&state).unwrap();
761 let parsed: HlAccountState = serde_json::from_str(&json).unwrap();
762 assert_eq!(parsed.equity, Decimal::from_str("100000.0").unwrap());
763 assert_eq!(
764 parsed.margin_available,
765 Decimal::from_str("50000.0").unwrap()
766 );
767 assert_eq!(parsed.positions.len(), 1);
768 assert_eq!(parsed.positions[0].coin, "BTC");
769 }
770
771 #[test]
772 fn account_state_empty_positions_roundtrip() {
773 let state = HlAccountState {
774 equity: Decimal::ZERO,
775 margin_available: Decimal::ZERO,
776 positions: vec![],
777 };
778 let json = serde_json::to_string(&state).unwrap();
779 let parsed: HlAccountState = serde_json::from_str(&json).unwrap();
780 assert!(parsed.positions.is_empty());
781 }
782
783 #[test]
784 fn account_state_camel_case_keys() {
785 let state = HlAccountState {
786 equity: Decimal::ONE,
787 margin_available: Decimal::ONE,
788 positions: vec![],
789 };
790 let json = serde_json::to_string(&state).unwrap();
791 assert!(json.contains("marginAvailable"));
792 }
793
794 #[test]
795 fn vault_summary_serde_roundtrip() {
796 let json = serde_json::json!({
797 "vaultAddress": "0xabc123",
798 "name": "My Vault",
799 "leaderEquity": "10000.0",
800 "followerEquity": "50000.0",
801 "allTimePnl": "2500.0"
802 });
803 let parsed: HlVaultSummary = serde_json::from_value(json).unwrap();
804 assert_eq!(parsed.vault_address, "0xabc123");
805 assert_eq!(parsed.name, "My Vault");
806 assert_eq!(
807 parsed.leader_equity,
808 Some(Decimal::from_str("10000.0").unwrap())
809 );
810 assert_eq!(
811 parsed.follower_equity,
812 Some(Decimal::from_str("50000.0").unwrap())
813 );
814 assert_eq!(
815 parsed.all_time_pnl,
816 Some(Decimal::from_str("2500.0").unwrap())
817 );
818 }
819
820 #[test]
821 fn vault_summary_minimal_fields() {
822 let json = serde_json::json!({
823 "vaultAddress": "0xdef456",
824 "name": "Minimal Vault"
825 });
826 let parsed: HlVaultSummary = serde_json::from_value(json).unwrap();
827 assert_eq!(parsed.vault_address, "0xdef456");
828 assert_eq!(parsed.name, "Minimal Vault");
829 assert!(parsed.leader_equity.is_none());
830 assert!(parsed.follower_equity.is_none());
831 assert!(parsed.all_time_pnl.is_none());
832 }
833
834 #[test]
835 fn vault_summary_extra_fields_captured() {
836 let json = serde_json::json!({
837 "vaultAddress": "0x111",
838 "name": "V",
839 "someNewField": 42
840 });
841 let parsed: HlVaultSummary = serde_json::from_value(json).unwrap();
842 assert_eq!(
843 parsed.extra.get("someNewField").unwrap(),
844 &serde_json::json!(42)
845 );
846 }
847
848 #[test]
849 fn vault_summary_camel_case_keys() {
850 let summary = HlVaultSummary {
851 vault_address: "0x1".into(),
852 name: "V".into(),
853 leader_equity: Some(Decimal::ONE),
854 follower_equity: None,
855 all_time_pnl: None,
856 extra: HashMap::new(),
857 };
858 let json = serde_json::to_string(&summary).unwrap();
859 assert!(json.contains("vaultAddress"));
860 assert!(json.contains("leaderEquity"));
861 }
862
863 #[test]
864 fn vault_details_serde_roundtrip() {
865 let json = serde_json::json!({
866 "name": "Alpha Vault",
867 "vaultAddress": "0xvault",
868 "leader": "0xleader",
869 "portfolio": {"equity": "100000"},
870 "followerCount": 25
871 });
872 let parsed: HlVaultDetails = serde_json::from_value(json).unwrap();
873 assert_eq!(parsed.name, "Alpha Vault");
874 assert_eq!(parsed.vault_address, "0xvault");
875 assert_eq!(parsed.leader.as_deref(), Some("0xleader"));
876 assert!(parsed.portfolio.is_some());
877 assert_eq!(parsed.follower_count, Some(25));
878 }
879
880 #[test]
881 fn vault_details_minimal_fields() {
882 let json = serde_json::json!({
883 "name": "Min",
884 "vaultAddress": "0xmin"
885 });
886 let parsed: HlVaultDetails = serde_json::from_value(json).unwrap();
887 assert_eq!(parsed.name, "Min");
888 assert!(parsed.leader.is_none());
889 assert!(parsed.portfolio.is_none());
890 assert!(parsed.follower_count.is_none());
891 }
892
893 #[test]
894 fn vault_details_extra_fields_captured() {
895 let json = serde_json::json!({
896 "name": "V",
897 "vaultAddress": "0x1",
898 "customMetric": "hello"
899 });
900 let parsed: HlVaultDetails = serde_json::from_value(json).unwrap();
901 assert_eq!(
902 parsed.extra.get("customMetric").unwrap(),
903 &serde_json::json!("hello")
904 );
905 }
906
907 #[test]
908 fn extra_agent_serde_roundtrip() {
909 let json = serde_json::json!({
910 "address": "0xagent1",
911 "name": "Trading Bot"
912 });
913 let parsed: HlExtraAgent = serde_json::from_value(json).unwrap();
914 assert_eq!(parsed.address, "0xagent1");
915 assert_eq!(parsed.name.as_deref(), Some("Trading Bot"));
916 }
917
918 #[test]
919 fn extra_agent_minimal_fields() {
920 let json = serde_json::json!({
921 "address": "0xagent2"
922 });
923 let parsed: HlExtraAgent = serde_json::from_value(json).unwrap();
924 assert_eq!(parsed.address, "0xagent2");
925 assert!(parsed.name.is_none());
926 }
927
928 #[test]
929 fn extra_agent_extra_fields_captured() {
930 let json = serde_json::json!({
931 "address": "0xagent3",
932 "permissions": ["trade", "withdraw"]
933 });
934 let parsed: HlExtraAgent = serde_json::from_value(json).unwrap();
935 assert!(parsed.extra.contains_key("permissions"));
936 }
937
938 #[test]
939 fn staking_delegation_serde_roundtrip() {
940 let delegation = HlStakingDelegation {
941 validator: "0xval1".into(),
942 amount: Decimal::from_str("1000.0").unwrap(),
943 rewards: Decimal::from_str("5.25").unwrap(),
944 };
945 let json = serde_json::to_string(&delegation).unwrap();
946 let parsed: HlStakingDelegation = serde_json::from_str(&json).unwrap();
947 assert_eq!(parsed.validator, "0xval1");
948 assert_eq!(parsed.amount, Decimal::from_str("1000.0").unwrap());
949 assert_eq!(parsed.rewards, Decimal::from_str("5.25").unwrap());
950 }
951
952 #[test]
953 fn staking_delegation_from_json() {
954 let json = serde_json::json!({
955 "validator": "0xabc",
956 "amount": "500.0",
957 "rewards": "2.5"
958 });
959 let parsed: HlStakingDelegation = serde_json::from_value(json).unwrap();
960 assert_eq!(parsed.validator, "0xabc");
961 assert_eq!(parsed.amount, Decimal::from_str("500.0").unwrap());
962 assert_eq!(parsed.rewards, Decimal::from_str("2.5").unwrap());
963 }
964
965 #[test]
966 fn borrow_lend_state_serde_roundtrip() {
967 let state = HlBorrowLendState {
968 coin: "USDC".into(),
969 supply: Decimal::from_str("10000.0").unwrap(),
970 borrow: Decimal::from_str("5000.0").unwrap(),
971 apy: Decimal::from_str("0.05").unwrap(),
972 };
973 let json = serde_json::to_string(&state).unwrap();
974 let parsed: HlBorrowLendState = serde_json::from_str(&json).unwrap();
975 assert_eq!(parsed.coin, "USDC");
976 assert_eq!(parsed.supply, Decimal::from_str("10000.0").unwrap());
977 assert_eq!(parsed.borrow, Decimal::from_str("5000.0").unwrap());
978 assert_eq!(parsed.apy, Decimal::from_str("0.05").unwrap());
979 }
980
981 #[test]
982 fn borrow_lend_state_from_json() {
983 let json = serde_json::json!({
984 "coin": "ETH",
985 "supply": "100.0",
986 "borrow": "0.0",
987 "apy": "0.03"
988 });
989 let parsed: HlBorrowLendState = serde_json::from_value(json).unwrap();
990 assert_eq!(parsed.coin, "ETH");
991 assert_eq!(parsed.supply, Decimal::from_str("100.0").unwrap());
992 assert_eq!(parsed.borrow, Decimal::ZERO);
993 assert_eq!(parsed.apy, Decimal::from_str("0.03").unwrap());
994 }
995
996 #[test]
997 fn borrow_lend_state_camel_case_keys() {
998 let state = HlBorrowLendState {
999 coin: "X".into(),
1000 supply: Decimal::ONE,
1001 borrow: Decimal::ZERO,
1002 apy: Decimal::ZERO,
1003 };
1004 let json = serde_json::to_string(&state).unwrap();
1005 assert!(json.contains("supply"));
1007 assert!(json.contains("borrow"));
1008 assert!(json.contains("apy"));
1009 }
1010
1011 #[test]
1012 fn extra_agent_camel_case_keys() {
1013 let agent = HlExtraAgent {
1014 address: "0x1".into(),
1015 name: Some("Bot".into()),
1016 extra: HashMap::new(),
1017 };
1018 let json = serde_json::to_string(&agent).unwrap();
1019 assert!(json.contains("address"));
1020 assert!(json.contains("name"));
1021 }
1022
1023 #[test]
1024 fn user_fees_serde_roundtrip() {
1025 let fees = HlUserFees {
1026 fee_tier: "VIP2".into(),
1027 maker_rate: Decimal::from_str("0.0001").unwrap(),
1028 taker_rate: Decimal::from_str("0.0003").unwrap(),
1029 };
1030 let json = serde_json::to_string(&fees).unwrap();
1031 let parsed: HlUserFees = serde_json::from_str(&json).unwrap();
1032 assert_eq!(parsed.fee_tier, "VIP2");
1033 assert_eq!(parsed.maker_rate, Decimal::from_str("0.0001").unwrap());
1034 assert_eq!(parsed.taker_rate, Decimal::from_str("0.0003").unwrap());
1035 }
1036
1037 #[test]
1038 fn user_fees_camel_case_keys() {
1039 let fees = HlUserFees {
1040 fee_tier: "T1".into(),
1041 maker_rate: Decimal::ZERO,
1042 taker_rate: Decimal::ONE,
1043 };
1044 let json = serde_json::to_string(&fees).unwrap();
1045 assert!(json.contains("feeTier"));
1046 assert!(json.contains("makerRate"));
1047 assert!(json.contains("takerRate"));
1048 }
1049
1050 #[test]
1051 fn user_fees_constructor() {
1052 let fees = HlUserFees::new(
1053 "VIP1".into(),
1054 Decimal::from_str("0.0002").unwrap(),
1055 Decimal::from_str("0.0005").unwrap(),
1056 );
1057 assert_eq!(fees.fee_tier, "VIP1");
1058 assert_eq!(fees.maker_rate, Decimal::from_str("0.0002").unwrap());
1059 assert_eq!(fees.taker_rate, Decimal::from_str("0.0005").unwrap());
1060 }
1061
1062 #[test]
1063 fn rate_limit_status_serde_roundtrip() {
1064 let status = HlRateLimitStatus {
1065 used: 42,
1066 limit: 1200,
1067 window_ms: 60000,
1068 };
1069 let json = serde_json::to_string(&status).unwrap();
1070 let parsed: HlRateLimitStatus = serde_json::from_str(&json).unwrap();
1071 assert_eq!(parsed.used, 42);
1072 assert_eq!(parsed.limit, 1200);
1073 assert_eq!(parsed.window_ms, 60000);
1074 }
1075
1076 #[test]
1077 fn rate_limit_status_camel_case_keys() {
1078 let status = HlRateLimitStatus {
1079 used: 0,
1080 limit: 100,
1081 window_ms: 30000,
1082 };
1083 let json = serde_json::to_string(&status).unwrap();
1084 assert!(json.contains("windowMs"));
1085 }
1086
1087 #[test]
1088 fn rate_limit_status_constructor() {
1089 let status = HlRateLimitStatus::new(10, 500, 60000);
1090 assert_eq!(status.used, 10);
1091 assert_eq!(status.limit, 500);
1092 assert_eq!(status.window_ms, 60000);
1093 }
1094
1095 #[test]
1096 fn open_order_serde_roundtrip() {
1097 let order = HlOpenOrder {
1098 oid: 12345,
1099 coin: "BTC".into(),
1100 side: TradeSide::Buy,
1101 limit_px: Decimal::from_str("60000.0").unwrap(),
1102 sz: Decimal::from_str("0.5").unwrap(),
1103 timestamp: 1700000000000,
1104 order_type: "Limit".into(),
1105 cloid: Some("my-order-1".into()),
1106 };
1107 let json = serde_json::to_string(&order).unwrap();
1108 let parsed: HlOpenOrder = serde_json::from_str(&json).unwrap();
1109 assert_eq!(parsed.oid, 12345);
1110 assert_eq!(parsed.coin, "BTC");
1111 assert_eq!(parsed.side, TradeSide::Buy);
1112 assert_eq!(parsed.limit_px, Decimal::from_str("60000.0").unwrap());
1113 assert_eq!(parsed.sz, Decimal::from_str("0.5").unwrap());
1114 assert_eq!(parsed.timestamp, 1700000000000);
1115 assert_eq!(parsed.order_type, "Limit");
1116 assert_eq!(parsed.cloid.as_deref(), Some("my-order-1"));
1117 }
1118
1119 #[test]
1120 fn open_order_no_cloid_roundtrip() {
1121 let order = HlOpenOrder {
1122 oid: 99,
1123 coin: "ETH".into(),
1124 side: TradeSide::Sell,
1125 limit_px: Decimal::from_str("3000.0").unwrap(),
1126 sz: Decimal::ONE,
1127 timestamp: 0,
1128 order_type: "Limit".into(),
1129 cloid: None,
1130 };
1131 let json = serde_json::to_string(&order).unwrap();
1132 let parsed: HlOpenOrder = serde_json::from_str(&json).unwrap();
1133 assert!(parsed.cloid.is_none());
1134 }
1135
1136 #[test]
1137 fn open_order_camel_case_keys() {
1138 let order = HlOpenOrder {
1139 oid: 1,
1140 coin: "X".into(),
1141 side: TradeSide::Buy,
1142 limit_px: Decimal::ONE,
1143 sz: Decimal::ONE,
1144 timestamp: 0,
1145 order_type: "Limit".into(),
1146 cloid: None,
1147 };
1148 let json = serde_json::to_string(&order).unwrap();
1149 assert!(json.contains("limitPx"));
1150 assert!(json.contains("orderType"));
1151 }
1152
1153 #[test]
1154 fn order_detail_serde_roundtrip() {
1155 let detail = HlOrderDetail {
1156 oid: 555,
1157 coin: "SOL".into(),
1158 side: TradeSide::Buy,
1159 limit_px: Decimal::from_str("150.0").unwrap(),
1160 sz: Decimal::from_str("10.0").unwrap(),
1161 timestamp: 1700000000000,
1162 order_type: "Limit".into(),
1163 cloid: None,
1164 status: "filled".into(),
1165 };
1166 let json = serde_json::to_string(&detail).unwrap();
1167 let parsed: HlOrderDetail = serde_json::from_str(&json).unwrap();
1168 assert_eq!(parsed.oid, 555);
1169 assert_eq!(parsed.status, "filled");
1170 assert_eq!(parsed.coin, "SOL");
1171 }
1172
1173 #[test]
1174 fn order_detail_camel_case_keys() {
1175 let detail = HlOrderDetail {
1176 oid: 1,
1177 coin: "X".into(),
1178 side: TradeSide::Buy,
1179 limit_px: Decimal::ONE,
1180 sz: Decimal::ONE,
1181 timestamp: 0,
1182 order_type: "Limit".into(),
1183 cloid: Some("c1".into()),
1184 status: "open".into(),
1185 };
1186 let json = serde_json::to_string(&detail).unwrap();
1187 assert!(json.contains("limitPx"));
1188 assert!(json.contains("orderType"));
1189 }
1190
1191 #[test]
1192 fn funding_entry_serde_roundtrip() {
1193 let entry = HlFundingEntry {
1194 coin: "BTC".into(),
1195 funding_rate: Decimal::from_str("0.0001").unwrap(),
1196 premium: Decimal::from_str("0.00005").unwrap(),
1197 time: 1700000000000,
1198 };
1199 let json = serde_json::to_string(&entry).unwrap();
1200 let parsed: HlFundingEntry = serde_json::from_str(&json).unwrap();
1201 assert_eq!(parsed.coin, "BTC");
1202 assert_eq!(parsed.funding_rate, Decimal::from_str("0.0001").unwrap());
1203 assert_eq!(parsed.premium, Decimal::from_str("0.00005").unwrap());
1204 assert_eq!(parsed.time, 1700000000000);
1205 }
1206
1207 #[test]
1208 fn funding_entry_camel_case_keys() {
1209 let entry = HlFundingEntry {
1210 coin: "X".into(),
1211 funding_rate: Decimal::ONE,
1212 premium: Decimal::ZERO,
1213 time: 0,
1214 };
1215 let json = serde_json::to_string(&entry).unwrap();
1216 assert!(json.contains("fundingRate"));
1217 }
1218
1219 #[test]
1220 fn user_funding_entry_serde_roundtrip() {
1221 let entry = HlUserFundingEntry {
1222 coin: "ETH".into(),
1223 usdc: Decimal::from_str("-1.5").unwrap(),
1224 szi: Decimal::from_str("2.0").unwrap(),
1225 funding_rate: Decimal::from_str("0.0002").unwrap(),
1226 time: 1700000000000,
1227 };
1228 let json = serde_json::to_string(&entry).unwrap();
1229 let parsed: HlUserFundingEntry = serde_json::from_str(&json).unwrap();
1230 assert_eq!(parsed.coin, "ETH");
1231 assert_eq!(parsed.usdc, Decimal::from_str("-1.5").unwrap());
1232 assert_eq!(parsed.szi, Decimal::from_str("2.0").unwrap());
1233 assert_eq!(parsed.funding_rate, Decimal::from_str("0.0002").unwrap());
1234 assert_eq!(parsed.time, 1700000000000);
1235 }
1236
1237 #[test]
1238 fn user_funding_entry_camel_case_keys() {
1239 let entry = HlUserFundingEntry {
1240 coin: "X".into(),
1241 usdc: Decimal::ONE,
1242 szi: Decimal::ONE,
1243 funding_rate: Decimal::ZERO,
1244 time: 0,
1245 };
1246 let json = serde_json::to_string(&entry).unwrap();
1247 assert!(json.contains("fundingRate"));
1248 }
1249
1250 #[test]
1251 fn historical_order_serde_roundtrip() {
1252 let order = HlHistoricalOrder {
1253 oid: 777,
1254 coin: "BTC".into(),
1255 side: TradeSide::Sell,
1256 limit_px: Decimal::from_str("65000.0").unwrap(),
1257 sz: Decimal::from_str("0.1").unwrap(),
1258 timestamp: 1700000000000,
1259 order_type: "Limit".into(),
1260 cloid: Some("hist-1".into()),
1261 status: "filled".into(),
1262 };
1263 let json = serde_json::to_string(&order).unwrap();
1264 let parsed: HlHistoricalOrder = serde_json::from_str(&json).unwrap();
1265 assert_eq!(parsed.oid, 777);
1266 assert_eq!(parsed.status, "filled");
1267 assert_eq!(parsed.coin, "BTC");
1268 assert_eq!(parsed.cloid.as_deref(), Some("hist-1"));
1269 }
1270
1271 #[test]
1272 fn historical_order_camel_case_keys() {
1273 let order = HlHistoricalOrder {
1274 oid: 1,
1275 coin: "X".into(),
1276 side: TradeSide::Buy,
1277 limit_px: Decimal::ONE,
1278 sz: Decimal::ONE,
1279 timestamp: 0,
1280 order_type: "Limit".into(),
1281 cloid: None,
1282 status: "canceled".into(),
1283 };
1284 let json = serde_json::to_string(&order).unwrap();
1285 assert!(json.contains("limitPx"));
1286 assert!(json.contains("orderType"));
1287 }
1288
1289 #[test]
1290 fn referral_state_serde_roundtrip() {
1291 let state = HlReferralState::new(
1292 Some("0xabc".into()),
1293 Some("CODE123".into()),
1294 Decimal::from_str("50000.0").unwrap(),
1295 Decimal::from_str("100.5").unwrap(),
1296 );
1297 let json = serde_json::to_string(&state).unwrap();
1298 let parsed: HlReferralState = serde_json::from_str(&json).unwrap();
1299 assert_eq!(parsed.referrer.as_deref(), Some("0xabc"));
1300 assert_eq!(parsed.referral_code.as_deref(), Some("CODE123"));
1301 assert_eq!(parsed.cum_vlm, Decimal::from_str("50000.0").unwrap());
1302 assert_eq!(parsed.rewards, Decimal::from_str("100.5").unwrap());
1303 }
1304
1305 #[test]
1306 fn referral_state_camel_case_keys() {
1307 let state = HlReferralState::new(None, None, Decimal::ZERO, Decimal::ZERO);
1308 let json = serde_json::to_string(&state).unwrap();
1309 assert!(json.contains("cumVlm"));
1310 assert!(json.contains("referralCode"));
1311 }
1312}