1use serde::{Deserialize, Serialize};
2
3#[cfg_attr(feature = "specta", derive(specta::Type))]
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct UserValue {
7 pub user: String,
9 pub value: f64,
11}
12
13#[cfg_attr(feature = "specta", derive(specta::Type))]
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct OpenInterest {
17 pub market: String,
19 pub value: f64,
21}
22
23#[cfg_attr(feature = "specta", derive(specta::Type))]
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
27pub enum PositionSortBy {
28 Current,
30 Initial,
32 Tokens,
34 CashPnl,
36 PercentPnl,
38 Title,
40 Resolving,
42 Price,
44 AvgPrice,
46}
47
48impl std::fmt::Display for PositionSortBy {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 Self::Current => write!(f, "CURRENT"),
52 Self::Initial => write!(f, "INITIAL"),
53 Self::Tokens => write!(f, "TOKENS"),
54 Self::CashPnl => write!(f, "CASH_PNL"),
55 Self::PercentPnl => write!(f, "PERCENT_PNL"),
56 Self::Title => write!(f, "TITLE"),
57 Self::Resolving => write!(f, "RESOLVING"),
58 Self::Price => write!(f, "PRICE"),
59 Self::AvgPrice => write!(f, "AVG_PRICE"),
60 }
61 }
62}
63
64#[cfg_attr(feature = "specta", derive(specta::Type))]
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
67#[serde(rename_all = "UPPERCASE")]
68pub enum SortDirection {
69 Asc,
71 #[default]
73 Desc,
74}
75
76impl std::fmt::Display for SortDirection {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 match self {
79 Self::Asc => write!(f, "ASC"),
80 Self::Desc => write!(f, "DESC"),
81 }
82 }
83}
84
85#[cfg_attr(feature = "specta", derive(specta::Type))]
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
88#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
89pub enum ClosedPositionSortBy {
90 #[default]
92 RealizedPnl,
93 Title,
95 Price,
97 AvgPrice,
99 Timestamp,
101}
102
103impl std::fmt::Display for ClosedPositionSortBy {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 match self {
106 Self::RealizedPnl => write!(f, "REALIZED_PNL"),
107 Self::Title => write!(f, "TITLE"),
108 Self::Price => write!(f, "PRICE"),
109 Self::AvgPrice => write!(f, "AVG_PRICE"),
110 Self::Timestamp => write!(f, "TIMESTAMP"),
111 }
112 }
113}
114
115#[cfg_attr(feature = "specta", derive(specta::Type))]
117#[derive(Debug, Clone, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct ClosedPosition {
120 pub proxy_wallet: String,
122 pub asset: String,
124 pub condition_id: String,
126 pub avg_price: f64,
128 pub total_bought: f64,
130 pub realized_pnl: f64,
132 pub cur_price: f64,
134 #[cfg_attr(feature = "specta", specta(type = f64))]
136 pub timestamp: i64,
137 pub title: String,
139 pub slug: String,
141 pub icon: Option<String>,
143 pub event_slug: Option<String>,
145 pub outcome: String,
147 pub outcome_index: u32,
149 pub opposite_outcome: String,
151 pub opposite_asset: String,
153 pub end_date: Option<String>,
155}
156
157#[cfg_attr(feature = "specta", derive(specta::Type))]
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(rename_all = "UPPERCASE")]
161pub enum TradeSide {
162 Buy,
164 Sell,
166 #[serde(other)]
170 Unknown,
171}
172
173impl std::fmt::Display for TradeSide {
174 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175 match self {
176 Self::Buy => write!(f, "BUY"),
177 Self::Sell => write!(f, "SELL"),
178 Self::Unknown => write!(f, "UNKNOWN"),
179 }
180 }
181}
182
183#[cfg_attr(feature = "specta", derive(specta::Type))]
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
186#[serde(rename_all = "UPPERCASE")]
187pub enum TradeFilterType {
188 Cash,
190 Tokens,
192}
193
194impl std::fmt::Display for TradeFilterType {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 match self {
197 Self::Cash => write!(f, "CASH"),
198 Self::Tokens => write!(f, "TOKENS"),
199 }
200 }
201}
202
203#[cfg_attr(feature = "specta", derive(specta::Type))]
205#[derive(Debug, Clone, Serialize, Deserialize)]
206#[serde(rename_all = "camelCase")]
207pub struct Trade {
208 pub proxy_wallet: String,
210 pub side: TradeSide,
212 pub asset: String,
214 pub condition_id: String,
216 pub size: f64,
218 pub price: f64,
220 #[cfg_attr(feature = "specta", specta(type = f64))]
222 pub timestamp: i64,
223 pub title: String,
225 pub slug: String,
227 pub icon: Option<String>,
229 pub event_slug: Option<String>,
231 pub outcome: String,
233 pub outcome_index: u32,
235 pub name: Option<String>,
237 pub pseudonym: Option<String>,
239 pub bio: Option<String>,
241 pub profile_image: Option<String>,
243 pub profile_image_optimized: Option<String>,
245 pub transaction_hash: Option<String>,
247}
248
249#[cfg_attr(feature = "specta", derive(specta::Type))]
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
252#[serde(rename_all = "UPPERCASE")]
253pub enum ActivityType {
254 Trade,
256 Split,
258 Merge,
260 Redeem,
262 Reward,
264 Conversion,
266 #[serde(rename = "MAKER_REBATE")]
268 MakerRebate,
269 #[serde(rename = "REFERRAL_REWARD")]
271 ReferralReward,
272 #[serde(rename = "TAKER_REBATE")]
274 TakerRebate,
275 #[serde(other)]
279 Unknown,
280}
281
282impl std::fmt::Display for ActivityType {
283 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284 match self {
285 Self::Trade => write!(f, "TRADE"),
286 Self::Split => write!(f, "SPLIT"),
287 Self::Merge => write!(f, "MERGE"),
288 Self::Redeem => write!(f, "REDEEM"),
289 Self::Reward => write!(f, "REWARD"),
290 Self::Conversion => write!(f, "CONVERSION"),
291 Self::MakerRebate => write!(f, "MAKER_REBATE"),
292 Self::ReferralReward => write!(f, "REFERRAL_REWARD"),
293 Self::TakerRebate => write!(f, "TAKER_REBATE"),
294 Self::Unknown => write!(f, "UNKNOWN"),
295 }
296 }
297}
298
299#[cfg_attr(feature = "specta", derive(specta::Type))]
301#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
302#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
303pub enum ActivitySortBy {
304 #[default]
306 Timestamp,
307 Tokens,
309 Cash,
311}
312
313impl std::fmt::Display for ActivitySortBy {
314 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315 match self {
316 Self::Timestamp => write!(f, "TIMESTAMP"),
317 Self::Tokens => write!(f, "TOKENS"),
318 Self::Cash => write!(f, "CASH"),
319 }
320 }
321}
322
323#[cfg_attr(feature = "specta", derive(specta::Type))]
325#[derive(Debug, Clone, Serialize, Deserialize)]
326#[serde(rename_all = "camelCase")]
327pub struct Activity {
328 pub proxy_wallet: String,
330 #[cfg_attr(feature = "specta", specta(type = f64))]
332 pub timestamp: i64,
333 pub condition_id: String,
335 #[serde(rename = "type")]
337 pub activity_type: ActivityType,
338 pub size: f64,
340 pub usdc_size: f64,
342 pub transaction_hash: Option<String>,
344 pub price: Option<f64>,
346 pub asset: Option<String>,
348 pub side: Option<String>,
351 pub outcome_index: Option<u32>,
353 pub title: Option<String>,
355 pub slug: Option<String>,
357 pub icon: Option<String>,
359 pub outcome: Option<String>,
361 pub name: Option<String>,
363 pub pseudonym: Option<String>,
365 pub bio: Option<String>,
367 pub profile_image: Option<String>,
369 pub profile_image_optimized: Option<String>,
371}
372
373#[cfg_attr(feature = "specta", derive(specta::Type))]
375#[derive(Debug, Clone, Serialize, Deserialize)]
376#[serde(rename_all = "camelCase")]
377pub struct Position {
378 pub proxy_wallet: String,
380 pub asset: String,
382 pub condition_id: String,
384 pub size: f64,
386 pub avg_price: f64,
388 pub initial_value: f64,
390 pub current_value: f64,
392 pub cash_pnl: f64,
394 pub percent_pnl: f64,
396 pub total_bought: f64,
398 pub realized_pnl: f64,
400 pub percent_realized_pnl: f64,
402 pub cur_price: f64,
404 pub redeemable: bool,
406 pub mergeable: bool,
408 pub title: String,
410 pub slug: String,
412 pub icon: Option<String>,
414 pub event_slug: Option<String>,
416 pub outcome: String,
418 pub outcome_index: u32,
420 pub opposite_outcome: String,
422 pub opposite_asset: String,
424 pub end_date: Option<String>,
426 pub negative_risk: bool,
428}
429
430#[cfg_attr(feature = "specta", derive(specta::Type))]
435#[derive(Debug, Clone, Serialize, Deserialize)]
436#[serde(rename_all = "camelCase")]
437pub struct MarketPositionV1 {
438 pub proxy_wallet: String,
440 pub name: String,
442 pub profile_image: Option<String>,
444 pub verified: bool,
446 pub asset: String,
448 pub condition_id: String,
450 pub avg_price: f64,
452 pub size: f64,
454 #[serde(rename = "currPrice")]
456 pub curr_price: f64,
457 pub current_value: f64,
459 pub cash_pnl: f64,
461 pub total_bought: f64,
463 pub realized_pnl: f64,
465 pub total_pnl: f64,
467 pub outcome: String,
469 pub outcome_index: u32,
471}
472
473#[cfg_attr(feature = "specta", derive(specta::Type))]
475#[derive(Debug, Clone, Serialize, Deserialize)]
476pub struct MetaMarketPositionV1 {
477 pub token: String,
479 pub positions: Vec<MarketPositionV1>,
481}
482
483#[cfg_attr(feature = "specta", derive(specta::Type))]
485#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
486#[serde(rename_all = "UPPERCASE")]
487pub enum MarketPositionStatus {
488 Open,
490 Closed,
492 #[default]
494 All,
495}
496
497impl std::fmt::Display for MarketPositionStatus {
498 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
499 match self {
500 Self::Open => write!(f, "OPEN"),
501 Self::Closed => write!(f, "CLOSED"),
502 Self::All => write!(f, "ALL"),
503 }
504 }
505}
506
507#[cfg_attr(feature = "specta", derive(specta::Type))]
509#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
510#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
511pub enum MarketPositionSortBy {
512 Tokens,
514 CashPnl,
516 RealizedPnl,
518 #[default]
520 TotalPnl,
521}
522
523impl std::fmt::Display for MarketPositionSortBy {
524 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
525 match self {
526 Self::Tokens => write!(f, "TOKENS"),
527 Self::CashPnl => write!(f, "CASH_PNL"),
528 Self::RealizedPnl => write!(f, "REALIZED_PNL"),
529 Self::TotalPnl => write!(f, "TOTAL_PNL"),
530 }
531 }
532}
533
534#[cfg_attr(feature = "specta", derive(specta::Type))]
536#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
537#[serde(rename_all = "UPPERCASE")]
538pub enum TimePeriod {
539 #[default]
541 Day,
542 Week,
544 Month,
546 All,
548}
549
550impl std::fmt::Display for TimePeriod {
551 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
552 match self {
553 Self::Day => write!(f, "DAY"),
554 Self::Week => write!(f, "WEEK"),
555 Self::Month => write!(f, "MONTH"),
556 Self::All => write!(f, "ALL"),
557 }
558 }
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564
565 #[test]
567 fn position_sort_by_display_matches_serde() {
568 let variants = [
569 PositionSortBy::Current,
570 PositionSortBy::Initial,
571 PositionSortBy::Tokens,
572 PositionSortBy::CashPnl,
573 PositionSortBy::PercentPnl,
574 PositionSortBy::Title,
575 PositionSortBy::Resolving,
576 PositionSortBy::Price,
577 PositionSortBy::AvgPrice,
578 ];
579 for variant in variants {
580 let serialized = serde_json::to_value(variant).unwrap();
581 let display = variant.to_string();
582 assert_eq!(
583 format!("\"{}\"", display),
584 serialized.to_string(),
585 "Display mismatch for {:?}",
586 variant
587 );
588 }
589 }
590
591 #[test]
593 fn closed_position_sort_by_display_matches_serde() {
594 let variants = [
595 ClosedPositionSortBy::RealizedPnl,
596 ClosedPositionSortBy::Title,
597 ClosedPositionSortBy::Price,
598 ClosedPositionSortBy::AvgPrice,
599 ClosedPositionSortBy::Timestamp,
600 ];
601 for variant in variants {
602 let serialized = serde_json::to_value(variant).unwrap();
603 let display = variant.to_string();
604 assert_eq!(
605 format!("\"{}\"", display),
606 serialized.to_string(),
607 "Display mismatch for {:?}",
608 variant
609 );
610 }
611 }
612
613 #[test]
614 fn activity_sort_by_display_matches_serde() {
615 let variants = [
616 ActivitySortBy::Timestamp,
617 ActivitySortBy::Tokens,
618 ActivitySortBy::Cash,
619 ];
620 for variant in variants {
621 let serialized = serde_json::to_value(variant).unwrap();
622 let display = variant.to_string();
623 assert_eq!(
624 format!("\"{}\"", display),
625 serialized.to_string(),
626 "Display mismatch for {:?}",
627 variant
628 );
629 }
630 }
631
632 #[test]
633 fn sort_direction_display_matches_serde() {
634 let variants = [SortDirection::Asc, SortDirection::Desc];
635 for variant in variants {
636 let serialized = serde_json::to_value(variant).unwrap();
637 let display = variant.to_string();
638 assert_eq!(
639 format!("\"{}\"", display),
640 serialized.to_string(),
641 "Display mismatch for {:?}",
642 variant
643 );
644 }
645 }
646
647 #[test]
648 fn trade_side_display_matches_serde() {
649 let variants = [TradeSide::Buy, TradeSide::Sell, TradeSide::Unknown];
650 for variant in variants {
651 let serialized = serde_json::to_value(variant).unwrap();
652 let display = variant.to_string();
653 assert_eq!(
654 format!("\"{}\"", display),
655 serialized.to_string(),
656 "Display mismatch for {:?}",
657 variant
658 );
659 }
660 }
661
662 #[test]
663 fn trade_side_falls_back_to_unknown_for_future_values() {
664 let result: TradeSide = serde_json::from_str("\"SOME_FUTURE_SIDE\"").unwrap();
665 assert_eq!(result, TradeSide::Unknown);
666 }
667
668 #[test]
669 fn trade_filter_type_display_matches_serde() {
670 let variants = [TradeFilterType::Cash, TradeFilterType::Tokens];
671 for variant in variants {
672 let serialized = serde_json::to_value(variant).unwrap();
673 let display = variant.to_string();
674 assert_eq!(
675 format!("\"{}\"", display),
676 serialized.to_string(),
677 "Display mismatch for {:?}",
678 variant
679 );
680 }
681 }
682
683 #[test]
684 fn activity_type_display_matches_serde() {
685 let variants = [
686 ActivityType::Trade,
687 ActivityType::Split,
688 ActivityType::Merge,
689 ActivityType::Redeem,
690 ActivityType::Reward,
691 ActivityType::Conversion,
692 ActivityType::MakerRebate,
693 ActivityType::ReferralReward,
694 ActivityType::TakerRebate,
695 ActivityType::Unknown,
696 ];
697 for variant in variants {
698 let serialized = serde_json::to_value(variant).unwrap();
699 let display = variant.to_string();
700 assert_eq!(
701 format!("\"{}\"", display),
702 serialized.to_string(),
703 "Display mismatch for {:?}",
704 variant
705 );
706 }
707 }
708
709 #[test]
710 fn activity_type_roundtrip_serde() {
711 for variant in [
712 ActivityType::Trade,
713 ActivityType::Split,
714 ActivityType::Merge,
715 ActivityType::Redeem,
716 ActivityType::Reward,
717 ActivityType::Conversion,
718 ActivityType::MakerRebate,
719 ActivityType::ReferralReward,
720 ActivityType::TakerRebate,
721 ] {
722 let json = serde_json::to_string(&variant).unwrap();
723 let deserialized: ActivityType = serde_json::from_str(&json).unwrap();
724 assert_eq!(variant, deserialized);
725 }
726 }
727
728 #[test]
729 fn activity_type_falls_back_to_unknown_for_future_types() {
730 let result: ActivityType = serde_json::from_str("\"SOME_FUTURE_TYPE\"").unwrap();
732 assert_eq!(result, ActivityType::Unknown);
733
734 let result: ActivityType = serde_json::from_str("\"trade\"").unwrap();
736 assert_eq!(result, ActivityType::Unknown);
737 }
738
739 #[test]
740 fn sort_direction_default_is_desc() {
741 assert_eq!(SortDirection::default(), SortDirection::Desc);
742 }
743
744 #[test]
745 fn closed_position_sort_by_default_is_realized_pnl() {
746 assert_eq!(
747 ClosedPositionSortBy::default(),
748 ClosedPositionSortBy::RealizedPnl
749 );
750 }
751
752 #[test]
753 fn activity_sort_by_default_is_timestamp() {
754 assert_eq!(ActivitySortBy::default(), ActivitySortBy::Timestamp);
755 }
756
757 #[test]
758 fn position_sort_by_serde_roundtrip() {
759 for variant in [
760 PositionSortBy::Current,
761 PositionSortBy::Initial,
762 PositionSortBy::Tokens,
763 PositionSortBy::CashPnl,
764 PositionSortBy::PercentPnl,
765 PositionSortBy::Title,
766 PositionSortBy::Resolving,
767 PositionSortBy::Price,
768 PositionSortBy::AvgPrice,
769 ] {
770 let json = serde_json::to_string(&variant).unwrap();
771 let deserialized: PositionSortBy = serde_json::from_str(&json).unwrap();
772 assert_eq!(variant, deserialized);
773 }
774 }
775
776 #[test]
777 fn deserialize_position_from_json() {
778 let json = r#"{
779 "proxyWallet": "0xabc123",
780 "asset": "token123",
781 "conditionId": "cond456",
782 "size": 100.5,
783 "avgPrice": 0.65,
784 "initialValue": 65.0,
785 "currentValue": 70.0,
786 "cashPnl": 5.0,
787 "percentPnl": 7.69,
788 "totalBought": 100.5,
789 "realizedPnl": 2.0,
790 "percentRealizedPnl": 3.08,
791 "curPrice": 0.70,
792 "redeemable": false,
793 "mergeable": true,
794 "title": "Will X happen?",
795 "slug": "will-x-happen",
796 "icon": "https://example.com/icon.png",
797 "eventSlug": "x-event",
798 "outcome": "Yes",
799 "outcomeIndex": 0,
800 "oppositeOutcome": "No",
801 "oppositeAsset": "token789",
802 "endDate": "2025-12-31",
803 "negativeRisk": false
804 }"#;
805
806 let pos: Position = serde_json::from_str(json).unwrap();
807 assert_eq!(pos.proxy_wallet, "0xabc123");
808 assert_eq!(pos.asset, "token123");
809 assert_eq!(pos.condition_id, "cond456");
810 assert!((pos.size - 100.5).abs() < f64::EPSILON);
811 assert!((pos.avg_price - 0.65).abs() < f64::EPSILON);
812 assert!((pos.initial_value - 65.0).abs() < f64::EPSILON);
813 assert!((pos.current_value - 70.0).abs() < f64::EPSILON);
814 assert!((pos.cash_pnl - 5.0).abs() < f64::EPSILON);
815 assert!(!pos.redeemable);
816 assert!(pos.mergeable);
817 assert_eq!(pos.title, "Will X happen?");
818 assert_eq!(pos.outcome, "Yes");
819 assert_eq!(pos.outcome_index, 0);
820 assert_eq!(pos.opposite_outcome, "No");
821 assert!(!pos.negative_risk);
822 assert_eq!(pos.icon, Some("https://example.com/icon.png".to_string()));
823 assert_eq!(pos.event_slug, Some("x-event".to_string()));
824 }
825
826 #[test]
827 fn deserialize_position_with_null_optionals() {
828 let json = r#"{
829 "proxyWallet": "0xabc123",
830 "asset": "token123",
831 "conditionId": "cond456",
832 "size": 0.0,
833 "avgPrice": 0.0,
834 "initialValue": 0.0,
835 "currentValue": 0.0,
836 "cashPnl": 0.0,
837 "percentPnl": 0.0,
838 "totalBought": 0.0,
839 "realizedPnl": 0.0,
840 "percentRealizedPnl": 0.0,
841 "curPrice": 0.0,
842 "redeemable": false,
843 "mergeable": false,
844 "title": "Test",
845 "slug": "test",
846 "icon": null,
847 "eventSlug": null,
848 "outcome": "No",
849 "outcomeIndex": 1,
850 "oppositeOutcome": "Yes",
851 "oppositeAsset": "token000",
852 "endDate": null,
853 "negativeRisk": true
854 }"#;
855
856 let pos: Position = serde_json::from_str(json).unwrap();
857 assert!(pos.icon.is_none());
858 assert!(pos.event_slug.is_none());
859 assert!(pos.end_date.is_none());
860 assert!(pos.negative_risk);
861 }
862
863 #[test]
864 fn deserialize_closed_position_from_json() {
865 let json = r#"{
866 "proxyWallet": "0xdef456",
867 "asset": "token_closed",
868 "conditionId": "cond_closed",
869 "avgPrice": 0.45,
870 "totalBought": 200.0,
871 "realizedPnl": -10.0,
872 "curPrice": 0.35,
873 "timestamp": 1700000000,
874 "title": "Closed market?",
875 "slug": "closed-market",
876 "icon": null,
877 "eventSlug": "closed-event",
878 "outcome": "No",
879 "outcomeIndex": 1,
880 "oppositeOutcome": "Yes",
881 "oppositeAsset": "token_opp",
882 "endDate": "2024-06-30"
883 }"#;
884
885 let closed: ClosedPosition = serde_json::from_str(json).unwrap();
886 assert_eq!(closed.proxy_wallet, "0xdef456");
887 assert!((closed.avg_price - 0.45).abs() < f64::EPSILON);
888 assert!((closed.realized_pnl - (-10.0)).abs() < f64::EPSILON);
889 assert_eq!(closed.timestamp, 1700000000);
890 assert_eq!(closed.outcome, "No");
891 assert_eq!(closed.outcome_index, 1);
892 assert!(closed.icon.is_none());
893 assert_eq!(closed.event_slug, Some("closed-event".to_string()));
894 }
895
896 #[test]
897 fn deserialize_trade_from_json() {
898 let json = r#"{
899 "proxyWallet": "0x1234",
900 "side": "BUY",
901 "asset": "token_buy",
902 "conditionId": "cond_trade",
903 "size": 50.0,
904 "price": 0.72,
905 "timestamp": 1700001000,
906 "title": "Trade market?",
907 "slug": "trade-market",
908 "icon": "https://example.com/trade.png",
909 "eventSlug": null,
910 "outcome": "Yes",
911 "outcomeIndex": 0,
912 "name": "TraderOne",
913 "pseudonym": "t1",
914 "bio": "A trader",
915 "profileImage": null,
916 "profileImageOptimized": null,
917 "transactionHash": "0xhash123"
918 }"#;
919
920 let trade: Trade = serde_json::from_str(json).unwrap();
921 assert_eq!(trade.proxy_wallet, "0x1234");
922 assert_eq!(trade.side, TradeSide::Buy);
923 assert!((trade.size - 50.0).abs() < f64::EPSILON);
924 assert!((trade.price - 0.72).abs() < f64::EPSILON);
925 assert_eq!(trade.timestamp, 1700001000);
926 assert_eq!(trade.name, Some("TraderOne".to_string()));
927 assert_eq!(trade.transaction_hash, Some("0xhash123".to_string()));
928 assert!(trade.profile_image.is_none());
929 }
930
931 #[test]
932 fn deserialize_trade_sell_side() {
933 let json = r#"{
934 "proxyWallet": "0x5678",
935 "side": "SELL",
936 "asset": "token_sell",
937 "conditionId": "cond_sell",
938 "size": 25.0,
939 "price": 0.30,
940 "timestamp": 1700002000,
941 "title": "Sell test",
942 "slug": "sell-test",
943 "icon": null,
944 "eventSlug": null,
945 "outcome": "No",
946 "outcomeIndex": 1,
947 "name": null,
948 "pseudonym": null,
949 "bio": null,
950 "profileImage": null,
951 "profileImageOptimized": null,
952 "transactionHash": null
953 }"#;
954
955 let trade: Trade = serde_json::from_str(json).unwrap();
956 assert_eq!(trade.side, TradeSide::Sell);
957 assert!(trade.name.is_none());
958 assert!(trade.transaction_hash.is_none());
959 }
960
961 #[test]
962 fn deserialize_activity_from_json() {
963 let json = r#"{
964 "proxyWallet": "0xact123",
965 "timestamp": 1700003000,
966 "conditionId": "cond_act",
967 "type": "TRADE",
968 "size": 10.0,
969 "usdcSize": 7.50,
970 "transactionHash": "0xacthash",
971 "price": 0.75,
972 "asset": "token_act",
973 "side": "BUY",
974 "outcomeIndex": 0,
975 "title": "Activity market",
976 "slug": "activity-market",
977 "icon": null,
978 "outcome": "Yes",
979 "name": null,
980 "pseudonym": null,
981 "bio": null,
982 "profileImage": null,
983 "profileImageOptimized": null
984 }"#;
985
986 let activity: Activity = serde_json::from_str(json).unwrap();
987 assert_eq!(activity.proxy_wallet, "0xact123");
988 assert_eq!(activity.activity_type, ActivityType::Trade);
989 assert!((activity.size - 10.0).abs() < f64::EPSILON);
990 assert!((activity.usdc_size - 7.50).abs() < f64::EPSILON);
991 assert_eq!(activity.side, Some("BUY".to_string()));
992 assert_eq!(activity.outcome_index, Some(0));
993 }
994
995 #[test]
996 fn deserialize_activity_merge_type() {
997 let json = r#"{
998 "proxyWallet": "0xmerge",
999 "timestamp": 1700004000,
1000 "conditionId": "cond_merge",
1001 "type": "MERGE",
1002 "size": 5.0,
1003 "usdcSize": 3.0,
1004 "transactionHash": null,
1005 "price": null,
1006 "asset": null,
1007 "side": "",
1008 "outcomeIndex": null,
1009 "title": null,
1010 "slug": null,
1011 "icon": null,
1012 "outcome": null,
1013 "name": null,
1014 "pseudonym": null,
1015 "bio": null,
1016 "profileImage": null,
1017 "profileImageOptimized": null
1018 }"#;
1019
1020 let activity: Activity = serde_json::from_str(json).unwrap();
1021 assert_eq!(activity.activity_type, ActivityType::Merge);
1022 assert_eq!(activity.side, Some("".to_string()));
1024 assert!(activity.price.is_none());
1025 assert!(activity.asset.is_none());
1026 assert!(activity.title.is_none());
1027 }
1028
1029 #[test]
1030 fn deserialize_user_value() {
1031 let json = r#"{"user": "0xuser", "value": 1234.56}"#;
1032 let uv: UserValue = serde_json::from_str(json).unwrap();
1033 assert_eq!(uv.user, "0xuser");
1034 assert!((uv.value - 1234.56).abs() < f64::EPSILON);
1035 }
1036
1037 #[test]
1038 fn deserialize_open_interest() {
1039 let json = r#"{"market": "0xcond", "value": 50000.0}"#;
1040 let oi: OpenInterest = serde_json::from_str(json).unwrap();
1041 assert_eq!(oi.market, "0xcond");
1042 assert!((oi.value - 50000.0).abs() < f64::EPSILON);
1043 }
1044
1045 #[test]
1046 fn market_position_status_display_matches_serde() {
1047 for variant in [
1048 MarketPositionStatus::Open,
1049 MarketPositionStatus::Closed,
1050 MarketPositionStatus::All,
1051 ] {
1052 let serialized = serde_json::to_value(variant).unwrap();
1053 assert_eq!(format!("\"{}\"", variant), serialized.to_string());
1054 }
1055 }
1056
1057 #[test]
1058 fn market_position_status_default_is_all() {
1059 assert_eq!(MarketPositionStatus::default(), MarketPositionStatus::All);
1060 }
1061
1062 #[test]
1063 fn market_position_sort_by_display_matches_serde() {
1064 for variant in [
1065 MarketPositionSortBy::Tokens,
1066 MarketPositionSortBy::CashPnl,
1067 MarketPositionSortBy::RealizedPnl,
1068 MarketPositionSortBy::TotalPnl,
1069 ] {
1070 let serialized = serde_json::to_value(variant).unwrap();
1071 assert_eq!(format!("\"{}\"", variant), serialized.to_string());
1072 }
1073 }
1074
1075 #[test]
1076 fn market_position_sort_by_default_is_total_pnl() {
1077 assert_eq!(
1078 MarketPositionSortBy::default(),
1079 MarketPositionSortBy::TotalPnl
1080 );
1081 }
1082
1083 #[test]
1084 fn deserialize_market_position_v1() {
1085 let json = r#"{
1087 "proxyWallet": "0xabc",
1088 "name": "Alice",
1089 "profileImage": "https://example.com/a.png",
1090 "verified": true,
1091 "asset": "token_a",
1092 "conditionId": "cond_mp",
1093 "avgPrice": 0.42,
1094 "size": 1234.5,
1095 "currPrice": 0.51,
1096 "currentValue": 629.60,
1097 "cashPnl": 110.0,
1098 "totalBought": 520.0,
1099 "realizedPnl": 15.5,
1100 "totalPnl": 125.5,
1101 "outcome": "Yes",
1102 "outcomeIndex": 0
1103 }"#;
1104
1105 let pos: MarketPositionV1 = serde_json::from_str(json).unwrap();
1106 assert_eq!(pos.proxy_wallet, "0xabc");
1107 assert_eq!(pos.name, "Alice");
1108 assert_eq!(
1109 pos.profile_image.as_deref(),
1110 Some("https://example.com/a.png")
1111 );
1112 assert!(pos.verified);
1113 assert_eq!(pos.asset, "token_a");
1114 assert_eq!(pos.condition_id, "cond_mp");
1115 assert!((pos.avg_price - 0.42).abs() < f64::EPSILON);
1116 assert!((pos.size - 1234.5).abs() < f64::EPSILON);
1117 assert!((pos.curr_price - 0.51).abs() < f64::EPSILON);
1118 assert!((pos.current_value - 629.60).abs() < f64::EPSILON);
1119 assert!((pos.cash_pnl - 110.0).abs() < f64::EPSILON);
1120 assert!((pos.total_bought - 520.0).abs() < f64::EPSILON);
1121 assert!((pos.realized_pnl - 15.5).abs() < f64::EPSILON);
1122 assert!((pos.total_pnl - 125.5).abs() < f64::EPSILON);
1123 assert_eq!(pos.outcome, "Yes");
1124 assert_eq!(pos.outcome_index, 0);
1125 }
1126
1127 #[test]
1128 fn market_position_v1_roundtrip() {
1129 let original = MarketPositionV1 {
1130 proxy_wallet: "0xabc".into(),
1131 name: "Alice".into(),
1132 profile_image: None,
1133 verified: false,
1134 asset: "token_a".into(),
1135 condition_id: "cond_mp".into(),
1136 avg_price: 0.5,
1137 size: 10.0,
1138 curr_price: 0.6,
1139 current_value: 6.0,
1140 cash_pnl: 1.0,
1141 total_bought: 5.0,
1142 realized_pnl: 0.0,
1143 total_pnl: 1.0,
1144 outcome: "No".into(),
1145 outcome_index: 1,
1146 };
1147 let json = serde_json::to_string(&original).unwrap();
1148 assert!(json.contains("\"currPrice\""));
1150 let back: MarketPositionV1 = serde_json::from_str(&json).unwrap();
1151 assert_eq!(back.proxy_wallet, original.proxy_wallet);
1152 assert_eq!(back.outcome_index, original.outcome_index);
1153 assert!((back.curr_price - original.curr_price).abs() < f64::EPSILON);
1154 }
1155
1156 #[test]
1157 fn deserialize_meta_market_position_v1() {
1158 let json = r#"{
1159 "token": "token_a",
1160 "positions": [
1161 {
1162 "proxyWallet": "0xabc",
1163 "name": "Alice",
1164 "profileImage": null,
1165 "verified": false,
1166 "asset": "token_a",
1167 "conditionId": "cond_mp",
1168 "avgPrice": 0.42,
1169 "size": 100.0,
1170 "currPrice": 0.51,
1171 "currentValue": 51.0,
1172 "cashPnl": 9.0,
1173 "totalBought": 42.0,
1174 "realizedPnl": 0.0,
1175 "totalPnl": 9.0,
1176 "outcome": "Yes",
1177 "outcomeIndex": 0
1178 }
1179 ]
1180 }"#;
1181
1182 let meta: MetaMarketPositionV1 = serde_json::from_str(json).unwrap();
1183 assert_eq!(meta.token, "token_a");
1184 assert_eq!(meta.positions.len(), 1);
1185 assert_eq!(meta.positions[0].name, "Alice");
1186 assert!(meta.positions[0].profile_image.is_none());
1187 }
1188}