Skip to main content

polymarket_us/
types.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4// ---------------------------------------------------------------------------
5// String-constant modules (kept for compatibility; prefer the typed enums below)
6// ---------------------------------------------------------------------------
7
8pub mod order_action {
9    pub const BUY: &str = "ORDER_ACTION_BUY";
10    pub const SELL: &str = "ORDER_ACTION_SELL";
11}
12
13pub mod order_type {
14    pub const LIMIT: &str = "ORDER_TYPE_LIMIT";
15}
16
17pub mod tif {
18    pub const GTC: &str = "TIME_IN_FORCE_GOOD_TILL_CANCEL";
19    pub const GTD: &str = "TIME_IN_FORCE_GOOD_TILL_DATE";
20    pub const FAK: &str = "TIME_IN_FORCE_IMMEDIATE_OR_CANCEL";
21    pub const FOK: &str = "TIME_IN_FORCE_FILL_OR_KILL";
22}
23
24pub mod outcome {
25    pub const LONG: &str = "LONG";
26    pub const SHORT: &str = "SHORT";
27}
28
29// ---------------------------------------------------------------------------
30// Typed enums (preferred over the string-constant modules above)
31// ---------------------------------------------------------------------------
32
33/// Whether this order is a buy or a sell.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[non_exhaustive]
36pub enum OrderAction {
37    #[serde(rename = "ORDER_ACTION_BUY")]
38    Buy,
39    #[serde(rename = "ORDER_ACTION_SELL")]
40    Sell,
41}
42
43impl fmt::Display for OrderAction {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            Self::Buy => f.write_str("ORDER_ACTION_BUY"),
47            Self::Sell => f.write_str("ORDER_ACTION_SELL"),
48        }
49    }
50}
51
52/// Outcome side — long (yes) or short (no).
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
54#[non_exhaustive]
55pub enum OrderSide {
56    #[serde(rename = "LONG")]
57    Long,
58    #[serde(rename = "SHORT")]
59    Short,
60}
61
62impl fmt::Display for OrderSide {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        match self {
65            Self::Long => f.write_str("LONG"),
66            Self::Short => f.write_str("SHORT"),
67        }
68    }
69}
70
71/// Order execution type.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
73#[non_exhaustive]
74pub enum OrderType {
75    #[serde(rename = "ORDER_TYPE_LIMIT")]
76    Limit,
77}
78
79impl fmt::Display for OrderType {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        f.write_str("ORDER_TYPE_LIMIT")
82    }
83}
84
85/// Time-in-force policy for an order.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
87#[non_exhaustive]
88pub enum TimeInForce {
89    /// Good-till-cancel — stays open until filled or explicitly cancelled.
90    #[serde(rename = "TIME_IN_FORCE_GOOD_TILL_CANCEL")]
91    GoodTillCancel,
92    /// Good-till-date — expires at a specified timestamp.
93    #[serde(rename = "TIME_IN_FORCE_GOOD_TILL_DATE")]
94    GoodTillDate,
95    /// Immediate-or-cancel (fill-and-kill) — any unfilled portion is cancelled.
96    #[serde(rename = "TIME_IN_FORCE_IMMEDIATE_OR_CANCEL")]
97    ImmediateOrCancel,
98    /// Fill-or-kill — must be filled entirely or cancelled entirely.
99    #[serde(rename = "TIME_IN_FORCE_FILL_OR_KILL")]
100    FillOrKill,
101}
102
103impl fmt::Display for TimeInForce {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        let s = match self {
106            Self::GoodTillCancel => "TIME_IN_FORCE_GOOD_TILL_CANCEL",
107            Self::GoodTillDate => "TIME_IN_FORCE_GOOD_TILL_DATE",
108            Self::ImmediateOrCancel => "TIME_IN_FORCE_IMMEDIATE_OR_CANCEL",
109            Self::FillOrKill => "TIME_IN_FORCE_FILL_OR_KILL",
110        };
111        f.write_str(s)
112    }
113}
114
115/// Known market status values.
116///
117/// [`UsMarket::status`] is kept as a raw `String` so no information is lost when
118/// the API introduces a status this SDK does not model yet. Use
119/// [`UsMarket::parsed_status`] to get this typed view of it.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
121#[serde(rename_all = "lowercase")]
122#[non_exhaustive]
123pub enum MarketStatus {
124    Open,
125    Closed,
126    Resolved,
127    /// Catch-all for any status string not yet modelled here.
128    #[serde(other)]
129    Unknown,
130}
131
132impl MarketStatus {
133    /// Parse a raw status string, case-insensitively.
134    ///
135    /// Anything unrecognised maps to [`MarketStatus::Unknown`] rather than
136    /// failing, so a new server-side status can never break a client.
137    pub fn from_api_str(raw: &str) -> Self {
138        match raw.trim().to_ascii_lowercase().as_str() {
139            "open" => Self::Open,
140            "closed" => Self::Closed,
141            "resolved" => Self::Resolved,
142            _ => Self::Unknown,
143        }
144    }
145}
146
147impl std::str::FromStr for MarketStatus {
148    type Err = std::convert::Infallible;
149
150    fn from_str(s: &str) -> Result<Self, Self::Err> {
151        Ok(Self::from_api_str(s))
152    }
153}
154
155// ---------------------------------------------------------------------------
156// REST response/request types
157// ---------------------------------------------------------------------------
158
159#[derive(Debug, Clone, Deserialize)]
160pub struct HealthResponse {
161    #[serde(default)]
162    pub status: String,
163    #[serde(default)]
164    pub timestamp: String,
165}
166
167#[derive(Debug, Clone, Deserialize)]
168pub struct MarketsResponse {
169    #[serde(default)]
170    pub markets: Vec<UsMarket>,
171}
172
173#[derive(Debug, Clone, Deserialize)]
174pub struct UsMarket {
175    #[serde(default)]
176    pub id: String,
177    #[serde(default)]
178    pub slug: String,
179    #[serde(default)]
180    pub question: String,
181    #[serde(default)]
182    pub status: String,
183    #[serde(default)]
184    pub category: String,
185    #[serde(default, rename = "startDate")]
186    pub start_date: String,
187    #[serde(default, rename = "endDate")]
188    pub end_date: String,
189    #[serde(default)]
190    pub description: String,
191    #[serde(default)]
192    pub active: bool,
193    #[serde(default)]
194    pub closed: bool,
195    #[serde(default, rename = "marketType")]
196    pub market_type: String,
197    #[serde(default, rename = "marketSides")]
198    pub market_sides: Vec<MarketSide>,
199    #[serde(default)]
200    pub instruments: Vec<serde_json::Value>,
201    #[serde(default)]
202    pub outcomes: Vec<serde_json::Value>,
203}
204
205impl UsMarket {
206    /// [`Self::status`] as a typed value. Unrecognised statuses become
207    /// [`MarketStatus::Unknown`]; the raw string remains available on the field.
208    pub fn parsed_status(&self) -> MarketStatus {
209        MarketStatus::from_api_str(&self.status)
210    }
211}
212
213#[derive(Debug, Clone, Deserialize)]
214pub struct MarketSide {
215    #[serde(default)]
216    pub id: String,
217    #[serde(default)]
218    pub identifier: String,
219    #[serde(default)]
220    pub description: String,
221    #[serde(default)]
222    pub price: String,
223    #[serde(default)]
224    pub long: bool,
225    #[serde(default, rename = "marketSideType")]
226    pub market_side_type: String,
227    #[serde(default)]
228    pub team: Option<serde_json::Value>,
229    #[serde(default)]
230    pub player: Option<serde_json::Value>,
231    #[serde(flatten)]
232    pub extra: std::collections::HashMap<String, serde_json::Value>,
233}
234
235#[derive(Debug, Clone, Serialize)]
236pub struct PlaceOrderRequest {
237    pub symbol: String,
238    pub action: OrderAction,
239    #[serde(rename = "outcomeSide")]
240    pub outcome_side: OrderSide,
241    #[serde(rename = "type")]
242    pub order_type: OrderType,
243    pub price: Money,
244    pub quantity: u64,
245    pub tif: TimeInForce,
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub client_order_id: Option<String>,
248    #[serde(skip_serializing_if = "std::ops::Not::not")]
249    pub post_only: bool,
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub expires_at: Option<u64>,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct Money {
256    pub value: String,
257    pub currency: String,
258}
259
260#[derive(Debug, Clone, Deserialize)]
261pub struct PlaceOrderResponse {
262    pub order_id: String,
263    #[serde(default)]
264    pub client_order_id: Option<String>,
265    #[serde(default)]
266    pub status: String,
267    #[serde(default)]
268    pub filled_quantity: u64,
269    #[serde(default)]
270    pub remaining_quantity: u64,
271    #[serde(default)]
272    pub created_at: String,
273}
274
275#[derive(Debug, Clone, Serialize)]
276pub struct BatchedOrderRequest {
277    pub orders: Vec<PlaceOrderRequest>,
278    pub atomic: bool,
279}
280
281#[derive(Debug, Clone, Deserialize)]
282pub struct BatchedOrderResponse {
283    #[serde(default)]
284    pub orders: Vec<PlaceOrderResponse>,
285}
286
287#[derive(Debug, Clone, Deserialize)]
288pub struct CancelOrderResponse {
289    pub order_id: String,
290    #[serde(default)]
291    pub status: String,
292    #[serde(default)]
293    pub cancelled_at: Option<String>,
294}
295
296#[derive(Debug, Clone, Deserialize)]
297pub struct PortfolioPositionsResponse {
298    #[serde(default)]
299    pub positions: std::collections::HashMap<String, UsPosition>,
300    #[serde(default)]
301    pub next_cursor: String,
302    #[serde(default)]
303    pub eof: bool,
304    #[serde(default, rename = "availablePositions")]
305    pub available_positions: Vec<UsPosition>,
306}
307
308#[derive(Debug, Clone, Deserialize)]
309pub struct UsPosition {
310    #[serde(default)]
311    pub symbol: String,
312    #[serde(default)]
313    pub quantity: i64,
314    #[serde(default, rename = "avgEntryPrice")]
315    pub avg_entry_price: String,
316    #[serde(default, rename = "unrealizedPnl")]
317    pub unrealized_pnl: Option<String>,
318}
319
320#[derive(Debug, Clone, Deserialize)]
321pub struct PortfolioActivitiesResponse {
322    #[serde(default)]
323    pub activities: Vec<serde_json::Value>,
324    #[serde(default)]
325    pub next_cursor: Option<String>,
326}
327
328#[derive(Debug, Clone, Deserialize)]
329pub struct AccountBalancesResponse {
330    #[serde(default)]
331    pub balances: Vec<UserBalance>,
332}
333
334#[derive(Debug, Clone, Deserialize)]
335pub struct UserBalance {
336    #[serde(default, rename = "currentBalance")]
337    pub current_balance: f64,
338    #[serde(default)]
339    pub currency: String,
340    #[serde(default, rename = "lastUpdated")]
341    pub last_updated: Option<String>,
342    #[serde(default, rename = "buyingPower")]
343    pub buying_power: f64,
344    #[serde(default, rename = "assetNotional")]
345    pub asset_notional: Option<f64>,
346    #[serde(default, rename = "assetAvailable")]
347    pub asset_available: Option<f64>,
348    #[serde(default, rename = "pendingCredit")]
349    pub pending_credit: Option<f64>,
350    #[serde(default, rename = "openOrders")]
351    pub open_orders: Option<f64>,
352    #[serde(default, rename = "unsettledFunds")]
353    pub unsettled_funds: Option<f64>,
354    #[serde(default, rename = "marginRequirement")]
355    pub margin_requirement: Option<f64>,
356    #[serde(default, rename = "balanceReservation")]
357    pub balance_reservation: Option<f64>,
358}
359
360#[derive(Debug, Clone, Serialize, Default)]
361pub struct CancelOrderParams {
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub quantity: Option<u64>,
364}
365
366#[derive(Debug, Clone, Serialize, Default)]
367pub struct CancelAllOrdersParams {
368    #[serde(skip_serializing_if = "Option::is_none")]
369    pub symbol: Option<String>,
370}
371
372#[derive(Debug, Clone, Deserialize)]
373pub struct CancelAllOrdersResponse {
374    #[serde(default)]
375    pub cancelled: Vec<String>,
376}
377
378#[derive(Debug, Clone, Serialize)]
379pub struct ModifyOrderRequest {
380    pub price: Money,
381    pub quantity: u64,
382}
383
384#[derive(Debug, Clone, Serialize)]
385pub struct PreviewOrderRequest {
386    pub symbol: String,
387    pub action: OrderAction,
388    #[serde(rename = "outcomeSide")]
389    pub outcome_side: OrderSide,
390    #[serde(rename = "type")]
391    pub order_type: OrderType,
392    pub price: Money,
393    pub quantity: u64,
394}
395
396#[derive(Debug, Clone, Deserialize)]
397pub struct PreviewOrderResponse {
398    #[serde(default)]
399    pub estimate: serde_json::Value,
400}
401
402#[derive(Debug, Clone, Serialize)]
403pub struct ClosePositionRequest {
404    pub symbol: String,
405    pub quantity: u64,
406}
407
408#[derive(Debug, Clone, Deserialize)]
409pub struct ClosePositionResponse {
410    #[serde(default)]
411    pub status: String,
412    #[serde(default)]
413    pub order_id: Option<String>,
414}
415
416#[derive(Debug, Clone, Deserialize)]
417pub struct GetOpenOrdersResponse {
418    #[serde(default)]
419    pub orders: Vec<PlaceOrderResponse>,
420}
421
422// Events
423#[derive(Debug, Clone, Deserialize)]
424pub struct EventsResponse {
425    #[serde(default)]
426    pub events: Vec<UsEvent>,
427}
428
429#[derive(Debug, Clone, Deserialize)]
430pub struct UsEvent {
431    #[serde(default)]
432    pub id: String,
433    #[serde(default)]
434    pub slug: String,
435    #[serde(default)]
436    pub title: String,
437    #[serde(default)]
438    pub description: Option<String>,
439    #[serde(default)]
440    pub category: String,
441    #[serde(default)]
442    pub start_date: Option<String>,
443    #[serde(default)]
444    pub end_date: Option<String>,
445    #[serde(flatten)]
446    pub extra: std::collections::HashMap<String, serde_json::Value>,
447}
448
449// Market data helpers
450#[derive(Debug, Clone, Deserialize)]
451pub struct OrderBook {
452    #[serde(default)]
453    pub bids: Vec<PriceLevel>,
454    #[serde(default)]
455    pub asks: Vec<PriceLevel>,
456}
457
458#[derive(Debug, Clone, Deserialize, Serialize)]
459pub struct PriceLevel {
460    pub price: String,
461    pub quantity: String,
462}
463
464#[derive(Debug, Clone, Deserialize)]
465pub struct BestBidOffer {
466    #[serde(default)]
467    pub bid: Option<PriceLevel>,
468    #[serde(default)]
469    pub ask: Option<PriceLevel>,
470}
471
472#[derive(Debug, Clone, Deserialize)]
473pub struct SettlementPrice {
474    #[serde(default)]
475    pub symbol: String,
476    #[serde(default)]
477    pub price: String,
478    #[serde(default)]
479    pub timestamp: String,
480}
481
482// Search
483#[derive(Debug, Clone, Deserialize)]
484pub struct SearchResults {
485    #[serde(default)]
486    pub markets: Vec<UsMarket>,
487    #[serde(default)]
488    pub events: Vec<UsEvent>,
489}
490
491// `League` and `Team` placeholders were removed in 0.4.0. They were never
492// referenced by any request or response type, and publishing unreachable types
493// commits the SDK to a shape the API has not been checked against. They will
494// return alongside the endpoints that populate them.
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499
500    #[test]
501    fn market_status_parses_case_insensitively() {
502        assert_eq!(MarketStatus::from_api_str("open"), MarketStatus::Open);
503        assert_eq!(MarketStatus::from_api_str("OPEN"), MarketStatus::Open);
504        assert_eq!(
505            MarketStatus::from_api_str("  Closed "),
506            MarketStatus::Closed
507        );
508        assert_eq!(
509            MarketStatus::from_api_str("RESOLVED"),
510            MarketStatus::Resolved
511        );
512    }
513
514    #[test]
515    fn market_status_falls_back_to_unknown() {
516        // A status the SDK does not model must not be an error.
517        assert_eq!(MarketStatus::from_api_str("halted"), MarketStatus::Unknown);
518        assert_eq!(MarketStatus::from_api_str(""), MarketStatus::Unknown);
519    }
520
521    #[test]
522    fn parsed_status_reads_the_raw_field() {
523        let json = r#"{"id": "m1", "status": "OPEN"}"#;
524        let market: UsMarket = serde_json::from_str(json).expect("deserialize");
525        // Raw string is preserved, typed view is derived from it.
526        assert_eq!(market.status, "OPEN");
527        assert_eq!(market.parsed_status(), MarketStatus::Open);
528    }
529
530    #[test]
531    fn market_sides_deserialize_into_typed_values() {
532        let json = r#"{
533            "id": "m1",
534            "marketSides": [
535                {"id": "s1", "identifier": "YES", "price": "0.62", "long": true,
536                 "marketSideType": "BINARY", "unmodelledField": 7}
537            ]
538        }"#;
539        let market: UsMarket = serde_json::from_str(json).expect("deserialize");
540        assert_eq!(market.market_sides.len(), 1);
541
542        let side = &market.market_sides[0];
543        assert_eq!(side.identifier, "YES");
544        assert_eq!(side.price, "0.62");
545        assert!(side.long);
546        // Unmodelled keys survive in `extra` rather than being dropped.
547        assert_eq!(
548            side.extra.get("unmodelledField"),
549            Some(&serde_json::json!(7))
550        );
551    }
552
553    #[test]
554    fn market_sides_tolerate_missing_fields() {
555        // Every MarketSide field defaults, so a sparse object must still parse.
556        let json = r#"{"id": "m1", "marketSides": [{}]}"#;
557        let market: UsMarket = serde_json::from_str(json).expect("deserialize");
558        assert_eq!(market.market_sides.len(), 1);
559        assert_eq!(market.market_sides[0].identifier, "");
560    }
561}