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}
167
168impl std::fmt::Display for TradeSide {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 match self {
171 Self::Buy => write!(f, "BUY"),
172 Self::Sell => write!(f, "SELL"),
173 }
174 }
175}
176
177#[cfg_attr(feature = "specta", derive(specta::Type))]
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
180#[serde(rename_all = "UPPERCASE")]
181pub enum TradeFilterType {
182 Cash,
184 Tokens,
186}
187
188impl std::fmt::Display for TradeFilterType {
189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190 match self {
191 Self::Cash => write!(f, "CASH"),
192 Self::Tokens => write!(f, "TOKENS"),
193 }
194 }
195}
196
197#[cfg_attr(feature = "specta", derive(specta::Type))]
199#[derive(Debug, Clone, Serialize, Deserialize)]
200#[serde(rename_all = "camelCase")]
201pub struct Trade {
202 pub proxy_wallet: String,
204 pub side: TradeSide,
206 pub asset: String,
208 pub condition_id: String,
210 pub size: f64,
212 pub price: f64,
214 #[cfg_attr(feature = "specta", specta(type = f64))]
216 pub timestamp: i64,
217 pub title: String,
219 pub slug: String,
221 pub icon: Option<String>,
223 pub event_slug: Option<String>,
225 pub outcome: String,
227 pub outcome_index: u32,
229 pub name: Option<String>,
231 pub pseudonym: Option<String>,
233 pub bio: Option<String>,
235 pub profile_image: Option<String>,
237 pub profile_image_optimized: Option<String>,
239 pub transaction_hash: Option<String>,
241}
242
243#[cfg_attr(feature = "specta", derive(specta::Type))]
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
246#[serde(rename_all = "UPPERCASE")]
247pub enum ActivityType {
248 Trade,
250 Split,
252 Merge,
254 Redeem,
256 Reward,
258 Conversion,
260 #[serde(rename = "MAKER_REBATE")]
262 MakerRebate,
263 #[serde(rename = "REFERRAL_REWARD")]
265 ReferralReward,
266}
267
268impl std::fmt::Display for ActivityType {
269 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270 match self {
271 Self::Trade => write!(f, "TRADE"),
272 Self::Split => write!(f, "SPLIT"),
273 Self::Merge => write!(f, "MERGE"),
274 Self::Redeem => write!(f, "REDEEM"),
275 Self::Reward => write!(f, "REWARD"),
276 Self::Conversion => write!(f, "CONVERSION"),
277 Self::MakerRebate => write!(f, "MAKER_REBATE"),
278 Self::ReferralReward => write!(f, "REFERRAL_REWARD"),
279 }
280 }
281}
282
283#[cfg_attr(feature = "specta", derive(specta::Type))]
285#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
286#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
287pub enum ActivitySortBy {
288 #[default]
290 Timestamp,
291 Tokens,
293 Cash,
295}
296
297impl std::fmt::Display for ActivitySortBy {
298 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299 match self {
300 Self::Timestamp => write!(f, "TIMESTAMP"),
301 Self::Tokens => write!(f, "TOKENS"),
302 Self::Cash => write!(f, "CASH"),
303 }
304 }
305}
306
307#[cfg_attr(feature = "specta", derive(specta::Type))]
309#[derive(Debug, Clone, Serialize, Deserialize)]
310#[serde(rename_all = "camelCase")]
311pub struct Activity {
312 pub proxy_wallet: String,
314 #[cfg_attr(feature = "specta", specta(type = f64))]
316 pub timestamp: i64,
317 pub condition_id: String,
319 #[serde(rename = "type")]
321 pub activity_type: ActivityType,
322 pub size: f64,
324 pub usdc_size: f64,
326 pub transaction_hash: Option<String>,
328 pub price: Option<f64>,
330 pub asset: Option<String>,
332 pub side: Option<String>,
335 pub outcome_index: Option<u32>,
337 pub title: Option<String>,
339 pub slug: Option<String>,
341 pub icon: Option<String>,
343 pub outcome: Option<String>,
345 pub name: Option<String>,
347 pub pseudonym: Option<String>,
349 pub bio: Option<String>,
351 pub profile_image: Option<String>,
353 pub profile_image_optimized: Option<String>,
355}
356
357#[cfg_attr(feature = "specta", derive(specta::Type))]
359#[derive(Debug, Clone, Serialize, Deserialize)]
360#[serde(rename_all = "camelCase")]
361pub struct Position {
362 pub proxy_wallet: String,
364 pub asset: String,
366 pub condition_id: String,
368 pub size: f64,
370 pub avg_price: f64,
372 pub initial_value: f64,
374 pub current_value: f64,
376 pub cash_pnl: f64,
378 pub percent_pnl: f64,
380 pub total_bought: f64,
382 pub realized_pnl: f64,
384 pub percent_realized_pnl: f64,
386 pub cur_price: f64,
388 pub redeemable: bool,
390 pub mergeable: bool,
392 pub title: String,
394 pub slug: String,
396 pub icon: Option<String>,
398 pub event_slug: Option<String>,
400 pub outcome: String,
402 pub outcome_index: u32,
404 pub opposite_outcome: String,
406 pub opposite_asset: String,
408 pub end_date: Option<String>,
410 pub negative_risk: bool,
412}
413
414#[cfg_attr(feature = "specta", derive(specta::Type))]
419#[derive(Debug, Clone, Serialize, Deserialize)]
420#[serde(rename_all = "camelCase")]
421pub struct MarketPositionV1 {
422 pub proxy_wallet: String,
424 pub name: String,
426 pub profile_image: Option<String>,
428 pub verified: bool,
430 pub asset: String,
432 pub condition_id: String,
434 pub avg_price: f64,
436 pub size: f64,
438 #[serde(rename = "currPrice")]
440 pub curr_price: f64,
441 pub current_value: f64,
443 pub cash_pnl: f64,
445 pub total_bought: f64,
447 pub realized_pnl: f64,
449 pub total_pnl: f64,
451 pub outcome: String,
453 pub outcome_index: u32,
455}
456
457#[cfg_attr(feature = "specta", derive(specta::Type))]
459#[derive(Debug, Clone, Serialize, Deserialize)]
460pub struct MetaMarketPositionV1 {
461 pub token: String,
463 pub positions: Vec<MarketPositionV1>,
465}
466
467#[cfg_attr(feature = "specta", derive(specta::Type))]
469#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
470#[serde(rename_all = "UPPERCASE")]
471pub enum MarketPositionStatus {
472 Open,
474 Closed,
476 #[default]
478 All,
479}
480
481impl std::fmt::Display for MarketPositionStatus {
482 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483 match self {
484 Self::Open => write!(f, "OPEN"),
485 Self::Closed => write!(f, "CLOSED"),
486 Self::All => write!(f, "ALL"),
487 }
488 }
489}
490
491#[cfg_attr(feature = "specta", derive(specta::Type))]
493#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
494#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
495pub enum MarketPositionSortBy {
496 Tokens,
498 CashPnl,
500 RealizedPnl,
502 #[default]
504 TotalPnl,
505}
506
507impl std::fmt::Display for MarketPositionSortBy {
508 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
509 match self {
510 Self::Tokens => write!(f, "TOKENS"),
511 Self::CashPnl => write!(f, "CASH_PNL"),
512 Self::RealizedPnl => write!(f, "REALIZED_PNL"),
513 Self::TotalPnl => write!(f, "TOTAL_PNL"),
514 }
515 }
516}
517
518#[cfg_attr(feature = "specta", derive(specta::Type))]
520#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
521#[serde(rename_all = "UPPERCASE")]
522pub enum TimePeriod {
523 #[default]
525 Day,
526 Week,
528 Month,
530 All,
532}
533
534impl std::fmt::Display for TimePeriod {
535 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
536 match self {
537 Self::Day => write!(f, "DAY"),
538 Self::Week => write!(f, "WEEK"),
539 Self::Month => write!(f, "MONTH"),
540 Self::All => write!(f, "ALL"),
541 }
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548
549 #[test]
551 fn position_sort_by_display_matches_serde() {
552 let variants = [
553 PositionSortBy::Current,
554 PositionSortBy::Initial,
555 PositionSortBy::Tokens,
556 PositionSortBy::CashPnl,
557 PositionSortBy::PercentPnl,
558 PositionSortBy::Title,
559 PositionSortBy::Resolving,
560 PositionSortBy::Price,
561 PositionSortBy::AvgPrice,
562 ];
563 for variant in variants {
564 let serialized = serde_json::to_value(variant).unwrap();
565 let display = variant.to_string();
566 assert_eq!(
567 format!("\"{}\"", display),
568 serialized.to_string(),
569 "Display mismatch for {:?}",
570 variant
571 );
572 }
573 }
574
575 #[test]
577 fn closed_position_sort_by_display_matches_serde() {
578 let variants = [
579 ClosedPositionSortBy::RealizedPnl,
580 ClosedPositionSortBy::Title,
581 ClosedPositionSortBy::Price,
582 ClosedPositionSortBy::AvgPrice,
583 ClosedPositionSortBy::Timestamp,
584 ];
585 for variant in variants {
586 let serialized = serde_json::to_value(variant).unwrap();
587 let display = variant.to_string();
588 assert_eq!(
589 format!("\"{}\"", display),
590 serialized.to_string(),
591 "Display mismatch for {:?}",
592 variant
593 );
594 }
595 }
596
597 #[test]
598 fn activity_sort_by_display_matches_serde() {
599 let variants = [
600 ActivitySortBy::Timestamp,
601 ActivitySortBy::Tokens,
602 ActivitySortBy::Cash,
603 ];
604 for variant in variants {
605 let serialized = serde_json::to_value(variant).unwrap();
606 let display = variant.to_string();
607 assert_eq!(
608 format!("\"{}\"", display),
609 serialized.to_string(),
610 "Display mismatch for {:?}",
611 variant
612 );
613 }
614 }
615
616 #[test]
617 fn sort_direction_display_matches_serde() {
618 let variants = [SortDirection::Asc, SortDirection::Desc];
619 for variant in variants {
620 let serialized = serde_json::to_value(variant).unwrap();
621 let display = variant.to_string();
622 assert_eq!(
623 format!("\"{}\"", display),
624 serialized.to_string(),
625 "Display mismatch for {:?}",
626 variant
627 );
628 }
629 }
630
631 #[test]
632 fn trade_side_display_matches_serde() {
633 let variants = [TradeSide::Buy, TradeSide::Sell];
634 for variant in variants {
635 let serialized = serde_json::to_value(variant).unwrap();
636 let display = variant.to_string();
637 assert_eq!(
638 format!("\"{}\"", display),
639 serialized.to_string(),
640 "Display mismatch for {:?}",
641 variant
642 );
643 }
644 }
645
646 #[test]
647 fn trade_filter_type_display_matches_serde() {
648 let variants = [TradeFilterType::Cash, TradeFilterType::Tokens];
649 for variant in variants {
650 let serialized = serde_json::to_value(variant).unwrap();
651 let display = variant.to_string();
652 assert_eq!(
653 format!("\"{}\"", display),
654 serialized.to_string(),
655 "Display mismatch for {:?}",
656 variant
657 );
658 }
659 }
660
661 #[test]
662 fn activity_type_display_matches_serde() {
663 let variants = [
664 ActivityType::Trade,
665 ActivityType::Split,
666 ActivityType::Merge,
667 ActivityType::Redeem,
668 ActivityType::Reward,
669 ActivityType::Conversion,
670 ActivityType::MakerRebate,
671 ActivityType::ReferralReward,
672 ];
673 for variant in variants {
674 let serialized = serde_json::to_value(variant).unwrap();
675 let display = variant.to_string();
676 assert_eq!(
677 format!("\"{}\"", display),
678 serialized.to_string(),
679 "Display mismatch for {:?}",
680 variant
681 );
682 }
683 }
684
685 #[test]
686 fn activity_type_roundtrip_serde() {
687 for variant in [
688 ActivityType::Trade,
689 ActivityType::Split,
690 ActivityType::Merge,
691 ActivityType::Redeem,
692 ActivityType::Reward,
693 ActivityType::Conversion,
694 ActivityType::MakerRebate,
695 ActivityType::ReferralReward,
696 ] {
697 let json = serde_json::to_string(&variant).unwrap();
698 let deserialized: ActivityType = serde_json::from_str(&json).unwrap();
699 assert_eq!(variant, deserialized);
700 }
701 }
702
703 #[test]
704 fn activity_type_rejects_unknown_variant() {
705 let result = serde_json::from_str::<ActivityType>("\"UNKNOWN\"");
706 assert!(result.is_err(), "should reject unknown activity type");
707 }
708
709 #[test]
710 fn activity_type_rejects_lowercase() {
711 let result = serde_json::from_str::<ActivityType>("\"trade\"");
712 assert!(result.is_err(), "should reject lowercase activity type");
713 }
714
715 #[test]
716 fn sort_direction_default_is_desc() {
717 assert_eq!(SortDirection::default(), SortDirection::Desc);
718 }
719
720 #[test]
721 fn closed_position_sort_by_default_is_realized_pnl() {
722 assert_eq!(
723 ClosedPositionSortBy::default(),
724 ClosedPositionSortBy::RealizedPnl
725 );
726 }
727
728 #[test]
729 fn activity_sort_by_default_is_timestamp() {
730 assert_eq!(ActivitySortBy::default(), ActivitySortBy::Timestamp);
731 }
732
733 #[test]
734 fn position_sort_by_serde_roundtrip() {
735 for variant in [
736 PositionSortBy::Current,
737 PositionSortBy::Initial,
738 PositionSortBy::Tokens,
739 PositionSortBy::CashPnl,
740 PositionSortBy::PercentPnl,
741 PositionSortBy::Title,
742 PositionSortBy::Resolving,
743 PositionSortBy::Price,
744 PositionSortBy::AvgPrice,
745 ] {
746 let json = serde_json::to_string(&variant).unwrap();
747 let deserialized: PositionSortBy = serde_json::from_str(&json).unwrap();
748 assert_eq!(variant, deserialized);
749 }
750 }
751
752 #[test]
753 fn deserialize_position_from_json() {
754 let json = r#"{
755 "proxyWallet": "0xabc123",
756 "asset": "token123",
757 "conditionId": "cond456",
758 "size": 100.5,
759 "avgPrice": 0.65,
760 "initialValue": 65.0,
761 "currentValue": 70.0,
762 "cashPnl": 5.0,
763 "percentPnl": 7.69,
764 "totalBought": 100.5,
765 "realizedPnl": 2.0,
766 "percentRealizedPnl": 3.08,
767 "curPrice": 0.70,
768 "redeemable": false,
769 "mergeable": true,
770 "title": "Will X happen?",
771 "slug": "will-x-happen",
772 "icon": "https://example.com/icon.png",
773 "eventSlug": "x-event",
774 "outcome": "Yes",
775 "outcomeIndex": 0,
776 "oppositeOutcome": "No",
777 "oppositeAsset": "token789",
778 "endDate": "2025-12-31",
779 "negativeRisk": false
780 }"#;
781
782 let pos: Position = serde_json::from_str(json).unwrap();
783 assert_eq!(pos.proxy_wallet, "0xabc123");
784 assert_eq!(pos.asset, "token123");
785 assert_eq!(pos.condition_id, "cond456");
786 assert!((pos.size - 100.5).abs() < f64::EPSILON);
787 assert!((pos.avg_price - 0.65).abs() < f64::EPSILON);
788 assert!((pos.initial_value - 65.0).abs() < f64::EPSILON);
789 assert!((pos.current_value - 70.0).abs() < f64::EPSILON);
790 assert!((pos.cash_pnl - 5.0).abs() < f64::EPSILON);
791 assert!(!pos.redeemable);
792 assert!(pos.mergeable);
793 assert_eq!(pos.title, "Will X happen?");
794 assert_eq!(pos.outcome, "Yes");
795 assert_eq!(pos.outcome_index, 0);
796 assert_eq!(pos.opposite_outcome, "No");
797 assert!(!pos.negative_risk);
798 assert_eq!(pos.icon, Some("https://example.com/icon.png".to_string()));
799 assert_eq!(pos.event_slug, Some("x-event".to_string()));
800 }
801
802 #[test]
803 fn deserialize_position_with_null_optionals() {
804 let json = r#"{
805 "proxyWallet": "0xabc123",
806 "asset": "token123",
807 "conditionId": "cond456",
808 "size": 0.0,
809 "avgPrice": 0.0,
810 "initialValue": 0.0,
811 "currentValue": 0.0,
812 "cashPnl": 0.0,
813 "percentPnl": 0.0,
814 "totalBought": 0.0,
815 "realizedPnl": 0.0,
816 "percentRealizedPnl": 0.0,
817 "curPrice": 0.0,
818 "redeemable": false,
819 "mergeable": false,
820 "title": "Test",
821 "slug": "test",
822 "icon": null,
823 "eventSlug": null,
824 "outcome": "No",
825 "outcomeIndex": 1,
826 "oppositeOutcome": "Yes",
827 "oppositeAsset": "token000",
828 "endDate": null,
829 "negativeRisk": true
830 }"#;
831
832 let pos: Position = serde_json::from_str(json).unwrap();
833 assert!(pos.icon.is_none());
834 assert!(pos.event_slug.is_none());
835 assert!(pos.end_date.is_none());
836 assert!(pos.negative_risk);
837 }
838
839 #[test]
840 fn deserialize_closed_position_from_json() {
841 let json = r#"{
842 "proxyWallet": "0xdef456",
843 "asset": "token_closed",
844 "conditionId": "cond_closed",
845 "avgPrice": 0.45,
846 "totalBought": 200.0,
847 "realizedPnl": -10.0,
848 "curPrice": 0.35,
849 "timestamp": 1700000000,
850 "title": "Closed market?",
851 "slug": "closed-market",
852 "icon": null,
853 "eventSlug": "closed-event",
854 "outcome": "No",
855 "outcomeIndex": 1,
856 "oppositeOutcome": "Yes",
857 "oppositeAsset": "token_opp",
858 "endDate": "2024-06-30"
859 }"#;
860
861 let closed: ClosedPosition = serde_json::from_str(json).unwrap();
862 assert_eq!(closed.proxy_wallet, "0xdef456");
863 assert!((closed.avg_price - 0.45).abs() < f64::EPSILON);
864 assert!((closed.realized_pnl - (-10.0)).abs() < f64::EPSILON);
865 assert_eq!(closed.timestamp, 1700000000);
866 assert_eq!(closed.outcome, "No");
867 assert_eq!(closed.outcome_index, 1);
868 assert!(closed.icon.is_none());
869 assert_eq!(closed.event_slug, Some("closed-event".to_string()));
870 }
871
872 #[test]
873 fn deserialize_trade_from_json() {
874 let json = r#"{
875 "proxyWallet": "0x1234",
876 "side": "BUY",
877 "asset": "token_buy",
878 "conditionId": "cond_trade",
879 "size": 50.0,
880 "price": 0.72,
881 "timestamp": 1700001000,
882 "title": "Trade market?",
883 "slug": "trade-market",
884 "icon": "https://example.com/trade.png",
885 "eventSlug": null,
886 "outcome": "Yes",
887 "outcomeIndex": 0,
888 "name": "TraderOne",
889 "pseudonym": "t1",
890 "bio": "A trader",
891 "profileImage": null,
892 "profileImageOptimized": null,
893 "transactionHash": "0xhash123"
894 }"#;
895
896 let trade: Trade = serde_json::from_str(json).unwrap();
897 assert_eq!(trade.proxy_wallet, "0x1234");
898 assert_eq!(trade.side, TradeSide::Buy);
899 assert!((trade.size - 50.0).abs() < f64::EPSILON);
900 assert!((trade.price - 0.72).abs() < f64::EPSILON);
901 assert_eq!(trade.timestamp, 1700001000);
902 assert_eq!(trade.name, Some("TraderOne".to_string()));
903 assert_eq!(trade.transaction_hash, Some("0xhash123".to_string()));
904 assert!(trade.profile_image.is_none());
905 }
906
907 #[test]
908 fn deserialize_trade_sell_side() {
909 let json = r#"{
910 "proxyWallet": "0x5678",
911 "side": "SELL",
912 "asset": "token_sell",
913 "conditionId": "cond_sell",
914 "size": 25.0,
915 "price": 0.30,
916 "timestamp": 1700002000,
917 "title": "Sell test",
918 "slug": "sell-test",
919 "icon": null,
920 "eventSlug": null,
921 "outcome": "No",
922 "outcomeIndex": 1,
923 "name": null,
924 "pseudonym": null,
925 "bio": null,
926 "profileImage": null,
927 "profileImageOptimized": null,
928 "transactionHash": null
929 }"#;
930
931 let trade: Trade = serde_json::from_str(json).unwrap();
932 assert_eq!(trade.side, TradeSide::Sell);
933 assert!(trade.name.is_none());
934 assert!(trade.transaction_hash.is_none());
935 }
936
937 #[test]
938 fn deserialize_activity_from_json() {
939 let json = r#"{
940 "proxyWallet": "0xact123",
941 "timestamp": 1700003000,
942 "conditionId": "cond_act",
943 "type": "TRADE",
944 "size": 10.0,
945 "usdcSize": 7.50,
946 "transactionHash": "0xacthash",
947 "price": 0.75,
948 "asset": "token_act",
949 "side": "BUY",
950 "outcomeIndex": 0,
951 "title": "Activity market",
952 "slug": "activity-market",
953 "icon": null,
954 "outcome": "Yes",
955 "name": null,
956 "pseudonym": null,
957 "bio": null,
958 "profileImage": null,
959 "profileImageOptimized": null
960 }"#;
961
962 let activity: Activity = serde_json::from_str(json).unwrap();
963 assert_eq!(activity.proxy_wallet, "0xact123");
964 assert_eq!(activity.activity_type, ActivityType::Trade);
965 assert!((activity.size - 10.0).abs() < f64::EPSILON);
966 assert!((activity.usdc_size - 7.50).abs() < f64::EPSILON);
967 assert_eq!(activity.side, Some("BUY".to_string()));
968 assert_eq!(activity.outcome_index, Some(0));
969 }
970
971 #[test]
972 fn deserialize_activity_merge_type() {
973 let json = r#"{
974 "proxyWallet": "0xmerge",
975 "timestamp": 1700004000,
976 "conditionId": "cond_merge",
977 "type": "MERGE",
978 "size": 5.0,
979 "usdcSize": 3.0,
980 "transactionHash": null,
981 "price": null,
982 "asset": null,
983 "side": "",
984 "outcomeIndex": null,
985 "title": null,
986 "slug": null,
987 "icon": null,
988 "outcome": null,
989 "name": null,
990 "pseudonym": null,
991 "bio": null,
992 "profileImage": null,
993 "profileImageOptimized": null
994 }"#;
995
996 let activity: Activity = serde_json::from_str(json).unwrap();
997 assert_eq!(activity.activity_type, ActivityType::Merge);
998 assert_eq!(activity.side, Some("".to_string()));
1000 assert!(activity.price.is_none());
1001 assert!(activity.asset.is_none());
1002 assert!(activity.title.is_none());
1003 }
1004
1005 #[test]
1006 fn deserialize_user_value() {
1007 let json = r#"{"user": "0xuser", "value": 1234.56}"#;
1008 let uv: UserValue = serde_json::from_str(json).unwrap();
1009 assert_eq!(uv.user, "0xuser");
1010 assert!((uv.value - 1234.56).abs() < f64::EPSILON);
1011 }
1012
1013 #[test]
1014 fn deserialize_open_interest() {
1015 let json = r#"{"market": "0xcond", "value": 50000.0}"#;
1016 let oi: OpenInterest = serde_json::from_str(json).unwrap();
1017 assert_eq!(oi.market, "0xcond");
1018 assert!((oi.value - 50000.0).abs() < f64::EPSILON);
1019 }
1020
1021 #[test]
1022 fn market_position_status_display_matches_serde() {
1023 for variant in [
1024 MarketPositionStatus::Open,
1025 MarketPositionStatus::Closed,
1026 MarketPositionStatus::All,
1027 ] {
1028 let serialized = serde_json::to_value(variant).unwrap();
1029 assert_eq!(format!("\"{}\"", variant), serialized.to_string());
1030 }
1031 }
1032
1033 #[test]
1034 fn market_position_status_default_is_all() {
1035 assert_eq!(MarketPositionStatus::default(), MarketPositionStatus::All);
1036 }
1037
1038 #[test]
1039 fn market_position_sort_by_display_matches_serde() {
1040 for variant in [
1041 MarketPositionSortBy::Tokens,
1042 MarketPositionSortBy::CashPnl,
1043 MarketPositionSortBy::RealizedPnl,
1044 MarketPositionSortBy::TotalPnl,
1045 ] {
1046 let serialized = serde_json::to_value(variant).unwrap();
1047 assert_eq!(format!("\"{}\"", variant), serialized.to_string());
1048 }
1049 }
1050
1051 #[test]
1052 fn market_position_sort_by_default_is_total_pnl() {
1053 assert_eq!(
1054 MarketPositionSortBy::default(),
1055 MarketPositionSortBy::TotalPnl
1056 );
1057 }
1058
1059 #[test]
1060 fn deserialize_market_position_v1() {
1061 let json = r#"{
1063 "proxyWallet": "0xabc",
1064 "name": "Alice",
1065 "profileImage": "https://example.com/a.png",
1066 "verified": true,
1067 "asset": "token_a",
1068 "conditionId": "cond_mp",
1069 "avgPrice": 0.42,
1070 "size": 1234.5,
1071 "currPrice": 0.51,
1072 "currentValue": 629.60,
1073 "cashPnl": 110.0,
1074 "totalBought": 520.0,
1075 "realizedPnl": 15.5,
1076 "totalPnl": 125.5,
1077 "outcome": "Yes",
1078 "outcomeIndex": 0
1079 }"#;
1080
1081 let pos: MarketPositionV1 = serde_json::from_str(json).unwrap();
1082 assert_eq!(pos.proxy_wallet, "0xabc");
1083 assert_eq!(pos.name, "Alice");
1084 assert_eq!(
1085 pos.profile_image.as_deref(),
1086 Some("https://example.com/a.png")
1087 );
1088 assert!(pos.verified);
1089 assert_eq!(pos.asset, "token_a");
1090 assert_eq!(pos.condition_id, "cond_mp");
1091 assert!((pos.avg_price - 0.42).abs() < f64::EPSILON);
1092 assert!((pos.size - 1234.5).abs() < f64::EPSILON);
1093 assert!((pos.curr_price - 0.51).abs() < f64::EPSILON);
1094 assert!((pos.current_value - 629.60).abs() < f64::EPSILON);
1095 assert!((pos.cash_pnl - 110.0).abs() < f64::EPSILON);
1096 assert!((pos.total_bought - 520.0).abs() < f64::EPSILON);
1097 assert!((pos.realized_pnl - 15.5).abs() < f64::EPSILON);
1098 assert!((pos.total_pnl - 125.5).abs() < f64::EPSILON);
1099 assert_eq!(pos.outcome, "Yes");
1100 assert_eq!(pos.outcome_index, 0);
1101 }
1102
1103 #[test]
1104 fn market_position_v1_roundtrip() {
1105 let original = MarketPositionV1 {
1106 proxy_wallet: "0xabc".into(),
1107 name: "Alice".into(),
1108 profile_image: None,
1109 verified: false,
1110 asset: "token_a".into(),
1111 condition_id: "cond_mp".into(),
1112 avg_price: 0.5,
1113 size: 10.0,
1114 curr_price: 0.6,
1115 current_value: 6.0,
1116 cash_pnl: 1.0,
1117 total_bought: 5.0,
1118 realized_pnl: 0.0,
1119 total_pnl: 1.0,
1120 outcome: "No".into(),
1121 outcome_index: 1,
1122 };
1123 let json = serde_json::to_string(&original).unwrap();
1124 assert!(json.contains("\"currPrice\""));
1126 let back: MarketPositionV1 = serde_json::from_str(&json).unwrap();
1127 assert_eq!(back.proxy_wallet, original.proxy_wallet);
1128 assert_eq!(back.outcome_index, original.outcome_index);
1129 assert!((back.curr_price - original.curr_price).abs() < f64::EPSILON);
1130 }
1131
1132 #[test]
1133 fn deserialize_meta_market_position_v1() {
1134 let json = r#"{
1135 "token": "token_a",
1136 "positions": [
1137 {
1138 "proxyWallet": "0xabc",
1139 "name": "Alice",
1140 "profileImage": null,
1141 "verified": false,
1142 "asset": "token_a",
1143 "conditionId": "cond_mp",
1144 "avgPrice": 0.42,
1145 "size": 100.0,
1146 "currPrice": 0.51,
1147 "currentValue": 51.0,
1148 "cashPnl": 9.0,
1149 "totalBought": 42.0,
1150 "realizedPnl": 0.0,
1151 "totalPnl": 9.0,
1152 "outcome": "Yes",
1153 "outcomeIndex": 0
1154 }
1155 ]
1156 }"#;
1157
1158 let meta: MetaMarketPositionV1 = serde_json::from_str(json).unwrap();
1159 assert_eq!(meta.token, "token_a");
1160 assert_eq!(meta.positions.len(), 1);
1161 assert_eq!(meta.positions[0].name, "Alice");
1162 assert!(meta.positions[0].profile_image.is_none());
1163 }
1164}