polymarket_api/
data.rs

1//! Data API client
2//!
3//! This module provides a client for interacting with Polymarket's Data API,
4//! which allows querying user positions, trade history, and portfolio data.
5
6use {
7    crate::error::Result,
8    serde::{Deserialize, Serialize},
9};
10
11const DATA_API_BASE: &str = "https://data-api.polymarket.com";
12
13/// Trade information from Data API
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct DataTrade {
16    pub proxy_wallet: String,
17    pub side: String,
18    pub asset: String,
19    pub condition_id: String,
20    pub size: f64,
21    pub price: f64,
22    pub timestamp: i64,
23    pub title: String,
24    pub slug: String,
25    pub icon: Option<String>,
26    pub event_slug: String,
27    pub outcome: String,
28    pub outcome_index: i32,
29    pub name: String,
30    pub pseudonym: String,
31    pub bio: Option<String>,
32    pub profile_image: Option<String>,
33    pub profile_image_optimized: Option<String>,
34    pub transaction_hash: String,
35}
36
37/// User position with comprehensive fields
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct Position {
40    #[serde(rename = "proxyWallet", default)]
41    pub proxy_wallet: Option<String>,
42    pub asset: String,
43    #[serde(rename = "conditionId", alias = "condition_id")]
44    pub condition_id: String,
45    #[serde(default)]
46    pub size: Option<f64>,
47    #[serde(rename = "avgPrice", default)]
48    pub avg_price: Option<f64>,
49    #[serde(rename = "initialValue", default)]
50    pub initial_value: Option<f64>,
51    #[serde(rename = "currentValue", default)]
52    pub current_value: Option<f64>,
53    #[serde(rename = "cashPnl", default)]
54    pub cash_pnl: Option<f64>,
55    #[serde(rename = "percentPnl", default)]
56    pub percent_pnl: Option<f64>,
57    #[serde(rename = "totalBought", default)]
58    pub total_bought: Option<f64>,
59    #[serde(rename = "realizedPnl", default)]
60    pub realized_pnl: Option<f64>,
61    #[serde(rename = "percentRealizedPnl", default)]
62    pub percent_realized_pnl: Option<f64>,
63    #[serde(rename = "curPrice", default)]
64    pub cur_price: Option<f64>,
65    #[serde(default)]
66    pub redeemable: Option<bool>,
67    #[serde(default)]
68    pub mergeable: Option<bool>,
69    pub title: String,
70    pub slug: String,
71    #[serde(default)]
72    pub icon: Option<String>,
73    #[serde(rename = "eventSlug", alias = "event_slug")]
74    pub event_slug: String,
75    pub outcome: String,
76    #[serde(rename = "outcomeIndex", alias = "outcome_index")]
77    pub outcome_index: i32,
78    #[serde(rename = "oppositeOutcome", default)]
79    pub opposite_outcome: Option<String>,
80    #[serde(rename = "oppositeAsset", default)]
81    pub opposite_asset: Option<String>,
82    #[serde(rename = "endDate", default)]
83    pub end_date: Option<String>,
84    #[serde(rename = "negativeRisk", default)]
85    pub negative_risk: Option<bool>,
86    // Legacy field for backwards compatibility
87    #[serde(default)]
88    pub quantity: Option<String>,
89    #[serde(default)]
90    pub value: Option<String>,
91}
92
93/// Portfolio summary
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct Portfolio {
96    pub total_value: Option<String>,
97    pub positions: Vec<Position>,
98}
99
100/// Activity type enum
101#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
102#[serde(rename_all = "UPPERCASE")]
103pub enum ActivityType {
104    Trade,
105    Split,
106    Merge,
107    Redeem,
108    Reward,
109    Conversion,
110}
111
112impl ActivityType {
113    fn as_str(&self) -> &'static str {
114        match self {
115            ActivityType::Trade => "TRADE",
116            ActivityType::Split => "SPLIT",
117            ActivityType::Merge => "MERGE",
118            ActivityType::Redeem => "REDEEM",
119            ActivityType::Reward => "REWARD",
120            ActivityType::Conversion => "CONVERSION",
121        }
122    }
123}
124
125/// Sort field for activity queries
126#[derive(Debug, Clone, Copy)]
127pub enum ActivitySortBy {
128    Timestamp,
129    Tokens,
130    Cash,
131}
132
133impl ActivitySortBy {
134    fn as_str(&self) -> &'static str {
135        match self {
136            ActivitySortBy::Timestamp => "TIMESTAMP",
137            ActivitySortBy::Tokens => "TOKENS",
138            ActivitySortBy::Cash => "CASH",
139        }
140    }
141}
142
143/// Sort direction
144#[derive(Debug, Clone, Copy)]
145pub enum SortDirection {
146    Asc,
147    Desc,
148}
149
150impl SortDirection {
151    fn as_str(&self) -> &'static str {
152        match self {
153            SortDirection::Asc => "ASC",
154            SortDirection::Desc => "DESC",
155        }
156    }
157}
158
159/// User activity record
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct Activity {
162    #[serde(rename = "proxyWallet")]
163    pub proxy_wallet: String,
164    pub timestamp: i64,
165    #[serde(rename = "conditionId")]
166    pub condition_id: String,
167    #[serde(rename = "type")]
168    pub activity_type: ActivityType,
169    #[serde(default)]
170    pub size: Option<f64>,
171    #[serde(rename = "usdcSize", default)]
172    pub usdc_size: Option<f64>,
173    #[serde(rename = "transactionHash")]
174    pub transaction_hash: String,
175    #[serde(default)]
176    pub price: Option<f64>,
177    #[serde(default)]
178    pub asset: Option<String>,
179    #[serde(default)]
180    pub side: Option<String>,
181    #[serde(rename = "outcomeIndex", default)]
182    pub outcome_index: Option<i32>,
183    #[serde(default)]
184    pub title: Option<String>,
185    #[serde(default)]
186    pub slug: Option<String>,
187    #[serde(default)]
188    pub icon: Option<String>,
189    #[serde(rename = "eventSlug", default)]
190    pub event_slug: Option<String>,
191    #[serde(default)]
192    pub outcome: Option<String>,
193    #[serde(default)]
194    pub name: Option<String>,
195    #[serde(default)]
196    pub pseudonym: Option<String>,
197    #[serde(default)]
198    pub bio: Option<String>,
199    #[serde(rename = "profileImage", default)]
200    pub profile_image: Option<String>,
201    #[serde(rename = "profileImageOptimized", default)]
202    pub profile_image_optimized: Option<String>,
203}
204
205/// Trade side filter
206#[derive(Debug, Clone, Copy)]
207pub enum TradeSide {
208    Buy,
209    Sell,
210}
211
212impl TradeSide {
213    fn as_str(&self) -> &'static str {
214        match self {
215            TradeSide::Buy => "BUY",
216            TradeSide::Sell => "SELL",
217        }
218    }
219}
220
221/// Data API client
222pub struct DataClient {
223    client: reqwest::Client,
224}
225
226impl DataClient {
227    /// Create a new Data API client
228    pub fn new() -> Self {
229        Self {
230            client: reqwest::Client::new(),
231        }
232    }
233
234    /// Get trades for a specific event
235    pub async fn get_trades_by_event(
236        &self,
237        event_id: u64,
238        limit: Option<usize>,
239        offset: Option<usize>,
240        filter_type: Option<&str>,
241        filter_amount: Option<f64>,
242    ) -> Result<Vec<DataTrade>> {
243        let url = format!("{}/trades", DATA_API_BASE);
244        let mut params = vec![
245            ("eventId", event_id.to_string()),
246            ("limit", limit.unwrap_or(10).to_string()),
247            ("offset", offset.unwrap_or(0).to_string()),
248        ];
249
250        if let Some(filter_type) = filter_type {
251            params.push(("filterType", filter_type.to_string()));
252        }
253        if let Some(filter_amount) = filter_amount {
254            params.push(("filterAmount", filter_amount.to_string()));
255        }
256
257        let trades: Vec<DataTrade> = self
258            .client
259            .get(&url)
260            .query(&params)
261            .send()
262            .await?
263            .json()
264            .await?;
265        Ok(trades)
266    }
267
268    /// Get trades for a specific event slug
269    pub async fn get_trades_by_event_slug(
270        &self,
271        event_slug: &str,
272        limit: Option<usize>,
273        offset: Option<usize>,
274    ) -> Result<Vec<DataTrade>> {
275        let url = format!("{}/trades", DATA_API_BASE);
276        let params = vec![
277            ("eventSlug", event_slug.to_string()),
278            ("limit", limit.unwrap_or(10).to_string()),
279            ("offset", offset.unwrap_or(0).to_string()),
280        ];
281
282        let trades: Vec<DataTrade> = self
283            .client
284            .get(&url)
285            .query(&params)
286            .send()
287            .await?
288            .json()
289            .await?;
290        Ok(trades)
291    }
292
293    /// Get trades for a specific market (condition ID)
294    pub async fn get_trades_by_market(
295        &self,
296        condition_id: &str,
297        limit: Option<usize>,
298        offset: Option<usize>,
299    ) -> Result<Vec<DataTrade>> {
300        let url = format!("{}/trades", DATA_API_BASE);
301        let params = vec![
302            ("conditionId", condition_id.to_string()),
303            ("limit", limit.unwrap_or(10).to_string()),
304            ("offset", offset.unwrap_or(0).to_string()),
305        ];
306
307        let trades: Vec<DataTrade> = self
308            .client
309            .get(&url)
310            .query(&params)
311            .send()
312            .await?
313            .json()
314            .await?;
315        Ok(trades)
316    }
317
318    /// Get user positions (requires authentication)
319    pub async fn get_positions(&self, user_address: &str) -> Result<Vec<Position>> {
320        let url = format!("{}/positions", DATA_API_BASE);
321        let params = [("user", user_address)];
322        let positions: Vec<Position> = self
323            .client
324            .get(&url)
325            .query(&params)
326            .send()
327            .await?
328            .json()
329            .await?;
330        Ok(positions)
331    }
332
333    /// Get portfolio for a user (requires authentication)
334    pub async fn get_portfolio(&self, user_address: &str) -> Result<Portfolio> {
335        let url = format!("{}/portfolio", DATA_API_BASE);
336        let params = [("user", user_address)];
337        let portfolio: Portfolio = self
338            .client
339            .get(&url)
340            .query(&params)
341            .send()
342            .await?
343            .json()
344            .await?;
345        Ok(portfolio)
346    }
347
348    /// Get user activity (trades, splits, merges, redeems, rewards, conversions)
349    ///
350    /// # Arguments
351    /// * `user_address` - The user's wallet address (0x-prefixed)
352    /// * `limit` - Results per page (0-500, default 100)
353    /// * `offset` - Pagination offset (0-10000, default 0)
354    /// * `market` - Optional comma-separated condition IDs
355    /// * `event_id` - Optional event ID (mutually exclusive with market)
356    /// * `activity_types` - Optional filter by activity types
357    /// * `start` - Optional start timestamp
358    /// * `end` - Optional end timestamp
359    /// * `sort_by` - Optional sort field (default: TIMESTAMP)
360    /// * `sort_direction` - Optional sort direction (default: DESC)
361    /// * `side` - Optional trade side filter
362    #[allow(clippy::too_many_arguments)]
363    pub async fn get_activity(
364        &self,
365        user_address: &str,
366        limit: Option<usize>,
367        offset: Option<usize>,
368        market: Option<&str>,
369        event_id: Option<u64>,
370        activity_types: Option<Vec<ActivityType>>,
371        start: Option<i64>,
372        end: Option<i64>,
373        sort_by: Option<ActivitySortBy>,
374        sort_direction: Option<SortDirection>,
375        side: Option<TradeSide>,
376    ) -> Result<Vec<Activity>> {
377        let url = format!("{}/activity", DATA_API_BASE);
378        let mut params = vec![
379            ("user", user_address.to_string()),
380            ("limit", limit.unwrap_or(100).to_string()),
381            ("offset", offset.unwrap_or(0).to_string()),
382        ];
383
384        if let Some(market) = market {
385            params.push(("market", market.to_string()));
386        }
387        if let Some(event_id) = event_id {
388            params.push(("eventId", event_id.to_string()));
389        }
390        if let Some(types) = activity_types {
391            let type_str = types
392                .iter()
393                .map(|t| t.as_str())
394                .collect::<Vec<_>>()
395                .join(",");
396            params.push(("type", type_str));
397        }
398        if let Some(start) = start {
399            params.push(("start", start.to_string()));
400        }
401        if let Some(end) = end {
402            params.push(("end", end.to_string()));
403        }
404        if let Some(sort_by) = sort_by {
405            params.push(("sortBy", sort_by.as_str().to_string()));
406        }
407        if let Some(sort_direction) = sort_direction {
408            params.push(("sortDirection", sort_direction.as_str().to_string()));
409        }
410        if let Some(side) = side {
411            params.push(("side", side.as_str().to_string()));
412        }
413
414        let activities: Vec<Activity> = self
415            .client
416            .get(&url)
417            .query(&params)
418            .send()
419            .await?
420            .json()
421            .await?;
422        Ok(activities)
423    }
424
425    /// Get trades with enhanced filtering options
426    ///
427    /// # Arguments
428    /// * `user_address` - Optional user wallet address
429    /// * `market` - Optional comma-separated condition IDs
430    /// * `event_id` - Optional event ID (mutually exclusive with market)
431    /// * `limit` - Results per page (0-10000, default 100)
432    /// * `offset` - Pagination offset (0-10000, default 0)
433    /// * `taker_only` - Filter to taker-initiated trades (default true)
434    /// * `filter_type` - Optional filter type (CASH or TOKENS)
435    /// * `filter_amount` - Optional filter amount
436    /// * `side` - Optional trade side filter
437    #[allow(clippy::too_many_arguments)]
438    pub async fn get_trades(
439        &self,
440        user_address: Option<&str>,
441        market: Option<&str>,
442        event_id: Option<u64>,
443        limit: Option<usize>,
444        offset: Option<usize>,
445        taker_only: Option<bool>,
446        filter_type: Option<&str>,
447        filter_amount: Option<f64>,
448        side: Option<TradeSide>,
449    ) -> Result<Vec<DataTrade>> {
450        let url = format!("{}/trades", DATA_API_BASE);
451        let mut params = vec![
452            ("limit", limit.unwrap_or(100).to_string()),
453            ("offset", offset.unwrap_or(0).to_string()),
454        ];
455
456        if let Some(user) = user_address {
457            params.push(("user", user.to_string()));
458        }
459        if let Some(market) = market {
460            params.push(("market", market.to_string()));
461        }
462        if let Some(event_id) = event_id {
463            params.push(("eventId", event_id.to_string()));
464        }
465        if let Some(taker_only) = taker_only {
466            params.push(("takerOnly", taker_only.to_string()));
467        }
468        if let Some(filter_type) = filter_type {
469            params.push(("filterType", filter_type.to_string()));
470        }
471        if let Some(filter_amount) = filter_amount {
472            params.push(("filterAmount", filter_amount.to_string()));
473        }
474        if let Some(side) = side {
475            params.push(("side", side.as_str().to_string()));
476        }
477
478        let trades: Vec<DataTrade> = self
479            .client
480            .get(&url)
481            .query(&params)
482            .send()
483            .await?
484            .json()
485            .await?;
486        Ok(trades)
487    }
488
489    /// Get positions with enhanced filtering options
490    ///
491    /// # Arguments
492    /// * `user_address` - User wallet address
493    /// * `market` - Optional comma-separated condition IDs
494    /// * `event_id` - Optional event ID
495    /// * `size_threshold` - Minimum position size (default 1)
496    /// * `redeemable` - Filter redeemable positions
497    /// * `mergeable` - Filter mergeable positions
498    /// * `limit` - Results per page (0-500, default 100)
499    /// * `offset` - Pagination offset (0-10000, default 0)
500    #[allow(clippy::too_many_arguments)]
501    pub async fn get_positions_filtered(
502        &self,
503        user_address: &str,
504        market: Option<&str>,
505        event_id: Option<u64>,
506        size_threshold: Option<f64>,
507        redeemable: Option<bool>,
508        mergeable: Option<bool>,
509        limit: Option<usize>,
510        offset: Option<usize>,
511    ) -> Result<Vec<Position>> {
512        let url = format!("{}/positions", DATA_API_BASE);
513        let mut params = vec![
514            ("user", user_address.to_string()),
515            ("limit", limit.unwrap_or(100).to_string()),
516            ("offset", offset.unwrap_or(0).to_string()),
517        ];
518
519        if let Some(market) = market {
520            params.push(("market", market.to_string()));
521        }
522        if let Some(event_id) = event_id {
523            params.push(("eventId", event_id.to_string()));
524        }
525        if let Some(size_threshold) = size_threshold {
526            params.push(("sizeThreshold", size_threshold.to_string()));
527        }
528        if let Some(redeemable) = redeemable {
529            params.push(("redeemable", redeemable.to_string()));
530        }
531        if let Some(mergeable) = mergeable {
532            params.push(("mergeable", mergeable.to_string()));
533        }
534
535        let positions: Vec<Position> = self
536            .client
537            .get(&url)
538            .query(&params)
539            .send()
540            .await?
541            .json()
542            .await?;
543        Ok(positions)
544    }
545}
546
547impl Default for DataClient {
548    fn default() -> Self {
549        Self::new()
550    }
551}