Skip to main content

polymarket_us/
types.rs

1use serde::{Deserialize, Deserializer, 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    /// Outcome names, e.g. `["Titans", "Chargers"]`.
202    ///
203    /// The gateway sends this as a JSON-encoded *string* rather than an array,
204    /// so it is decoded on the way in; a plain array is accepted too.
205    #[serde(default, deserialize_with = "json_encoded_array")]
206    pub outcomes: Vec<String>,
207    /// Prices matching [`Self::outcomes`] positionally, as decimal strings.
208    ///
209    /// Encoded the same way as `outcomes`.
210    #[serde(
211        default,
212        rename = "outcomePrices",
213        deserialize_with = "json_encoded_array"
214    )]
215    pub outcome_prices: Vec<String>,
216}
217
218impl UsMarket {
219    /// [`Self::status`] as a typed value. Unrecognised statuses become
220    /// [`MarketStatus::Unknown`]; the raw string remains available on the field.
221    pub fn parsed_status(&self) -> MarketStatus {
222        MarketStatus::from_api_str(&self.status)
223    }
224}
225
226/// Deserialize a field the gateway double-encodes: an array delivered as a
227/// string whose *contents* are JSON.
228///
229/// `outcomes` arrives as `"[\"Titans\",\"Chargers\"]"` — note the outer
230/// quotes — not as `["Titans", "Chargers"]`. Declaring the field as a sequence
231/// therefore fails the whole response with `invalid type: string ..., expected
232/// a sequence`, which is what broke `markets().list()` against live data.
233///
234/// A plain array is accepted too, so this keeps working if the gateway stops
235/// double-encoding. Non-string elements are rendered rather than rejected,
236/// since a price is equally plausible as `"0.55"` or `0.55` and neither is
237/// worth failing an entire market listing over.
238fn json_encoded_array<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
239where
240    D: Deserializer<'de>,
241{
242    #[derive(Deserialize)]
243    #[serde(untagged)]
244    enum Encoding {
245        Array(Vec<serde_json::Value>),
246        Encoded(String),
247    }
248
249    let items = match Option::<Encoding>::deserialize(deserializer)? {
250        None => return Ok(Vec::new()),
251        Some(Encoding::Array(items)) => items,
252        Some(Encoding::Encoded(raw)) => {
253            let trimmed = raw.trim();
254            if trimmed.is_empty() {
255                return Ok(Vec::new());
256            }
257            serde_json::from_str(trimmed).map_err(serde::de::Error::custom)?
258        }
259    };
260
261    Ok(items.into_iter().map(render_scalar).collect())
262}
263
264/// A string element as itself, anything else as its JSON rendering — so `1`
265/// becomes `"1"` rather than `"\"1\""`.
266fn render_scalar(value: serde_json::Value) -> String {
267    match value {
268        serde_json::Value::String(text) => text,
269        other => other.to_string(),
270    }
271}
272
273#[derive(Debug, Clone, Deserialize)]
274pub struct MarketSide {
275    #[serde(default)]
276    pub id: String,
277    #[serde(default)]
278    pub identifier: String,
279    #[serde(default)]
280    pub description: String,
281    #[serde(default)]
282    pub price: String,
283    #[serde(default)]
284    pub long: bool,
285    #[serde(default, rename = "marketSideType")]
286    pub market_side_type: String,
287    #[serde(default)]
288    pub team: Option<serde_json::Value>,
289    #[serde(default)]
290    pub player: Option<serde_json::Value>,
291    #[serde(flatten)]
292    pub extra: std::collections::HashMap<String, serde_json::Value>,
293}
294
295#[derive(Debug, Clone, Serialize)]
296pub struct PlaceOrderRequest {
297    pub symbol: String,
298    pub action: OrderAction,
299    #[serde(rename = "outcomeSide")]
300    pub outcome_side: OrderSide,
301    #[serde(rename = "type")]
302    pub order_type: OrderType,
303    pub price: Money,
304    pub quantity: u64,
305    pub tif: TimeInForce,
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub client_order_id: Option<String>,
308    #[serde(skip_serializing_if = "std::ops::Not::not")]
309    pub post_only: bool,
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub expires_at: Option<u64>,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct Money {
316    pub value: String,
317    pub currency: String,
318}
319
320#[derive(Debug, Clone, Deserialize)]
321pub struct PlaceOrderResponse {
322    pub order_id: String,
323    #[serde(default)]
324    pub client_order_id: Option<String>,
325    #[serde(default)]
326    pub status: String,
327    #[serde(default)]
328    pub filled_quantity: u64,
329    #[serde(default)]
330    pub remaining_quantity: u64,
331    #[serde(default)]
332    pub created_at: String,
333}
334
335#[derive(Debug, Clone, Serialize)]
336pub struct BatchedOrderRequest {
337    pub orders: Vec<PlaceOrderRequest>,
338    pub atomic: bool,
339}
340
341#[derive(Debug, Clone, Deserialize)]
342pub struct BatchedOrderResponse {
343    #[serde(default)]
344    pub orders: Vec<PlaceOrderResponse>,
345}
346
347#[derive(Debug, Clone, Deserialize)]
348pub struct CancelOrderResponse {
349    pub order_id: String,
350    #[serde(default)]
351    pub status: String,
352    #[serde(default)]
353    pub cancelled_at: Option<String>,
354}
355
356#[derive(Debug, Clone, Deserialize)]
357pub struct PortfolioPositionsResponse {
358    #[serde(default)]
359    pub positions: std::collections::HashMap<String, UsPosition>,
360    #[serde(default)]
361    pub next_cursor: String,
362    #[serde(default)]
363    pub eof: bool,
364    #[serde(default, rename = "availablePositions")]
365    pub available_positions: Vec<UsPosition>,
366}
367
368#[derive(Debug, Clone, Deserialize)]
369pub struct UsPosition {
370    #[serde(default)]
371    pub symbol: String,
372    #[serde(default)]
373    pub quantity: i64,
374    #[serde(default, rename = "avgEntryPrice")]
375    pub avg_entry_price: String,
376    #[serde(default, rename = "unrealizedPnl")]
377    pub unrealized_pnl: Option<String>,
378}
379
380#[derive(Debug, Clone, Deserialize)]
381pub struct PortfolioActivitiesResponse {
382    #[serde(default)]
383    pub activities: Vec<serde_json::Value>,
384    #[serde(default)]
385    pub next_cursor: Option<String>,
386}
387
388#[derive(Debug, Clone, Deserialize)]
389pub struct AccountBalancesResponse {
390    #[serde(default)]
391    pub balances: Vec<UserBalance>,
392}
393
394#[derive(Debug, Clone, Deserialize)]
395pub struct UserBalance {
396    #[serde(default, rename = "currentBalance")]
397    pub current_balance: f64,
398    #[serde(default)]
399    pub currency: String,
400    #[serde(default, rename = "lastUpdated")]
401    pub last_updated: Option<String>,
402    #[serde(default, rename = "buyingPower")]
403    pub buying_power: f64,
404    #[serde(default, rename = "assetNotional")]
405    pub asset_notional: Option<f64>,
406    #[serde(default, rename = "assetAvailable")]
407    pub asset_available: Option<f64>,
408    #[serde(default, rename = "pendingCredit")]
409    pub pending_credit: Option<f64>,
410    #[serde(default, rename = "openOrders")]
411    pub open_orders: Option<f64>,
412    #[serde(default, rename = "unsettledFunds")]
413    pub unsettled_funds: Option<f64>,
414    #[serde(default, rename = "marginRequirement")]
415    pub margin_requirement: Option<f64>,
416    #[serde(default, rename = "balanceReservation")]
417    pub balance_reservation: Option<f64>,
418}
419
420#[derive(Debug, Clone, Serialize, Default)]
421pub struct CancelOrderParams {
422    #[serde(skip_serializing_if = "Option::is_none")]
423    pub quantity: Option<u64>,
424}
425
426#[derive(Debug, Clone, Serialize, Default)]
427pub struct CancelAllOrdersParams {
428    #[serde(skip_serializing_if = "Option::is_none")]
429    pub symbol: Option<String>,
430}
431
432#[derive(Debug, Clone, Deserialize)]
433pub struct CancelAllOrdersResponse {
434    #[serde(default)]
435    pub cancelled: Vec<String>,
436}
437
438#[derive(Debug, Clone, Serialize)]
439pub struct ModifyOrderRequest {
440    pub price: Money,
441    pub quantity: u64,
442}
443
444#[derive(Debug, Clone, Serialize)]
445pub struct PreviewOrderRequest {
446    pub symbol: String,
447    pub action: OrderAction,
448    #[serde(rename = "outcomeSide")]
449    pub outcome_side: OrderSide,
450    #[serde(rename = "type")]
451    pub order_type: OrderType,
452    pub price: Money,
453    pub quantity: u64,
454}
455
456#[derive(Debug, Clone, Deserialize)]
457pub struct PreviewOrderResponse {
458    #[serde(default)]
459    pub estimate: serde_json::Value,
460}
461
462#[derive(Debug, Clone, Serialize)]
463pub struct ClosePositionRequest {
464    pub symbol: String,
465    pub quantity: u64,
466}
467
468#[derive(Debug, Clone, Deserialize)]
469pub struct ClosePositionResponse {
470    #[serde(default)]
471    pub status: String,
472    #[serde(default)]
473    pub order_id: Option<String>,
474}
475
476#[derive(Debug, Clone, Deserialize)]
477pub struct GetOpenOrdersResponse {
478    #[serde(default)]
479    pub orders: Vec<PlaceOrderResponse>,
480}
481
482// Events
483#[derive(Debug, Clone, Deserialize)]
484pub struct EventsResponse {
485    #[serde(default)]
486    pub events: Vec<UsEvent>,
487}
488
489#[derive(Debug, Clone, Deserialize)]
490pub struct UsEvent {
491    #[serde(default)]
492    pub id: String,
493    #[serde(default)]
494    pub slug: String,
495    #[serde(default)]
496    pub title: String,
497    #[serde(default)]
498    pub description: Option<String>,
499    #[serde(default)]
500    pub category: String,
501    #[serde(default)]
502    pub start_date: Option<String>,
503    #[serde(default)]
504    pub end_date: Option<String>,
505    #[serde(flatten)]
506    pub extra: std::collections::HashMap<String, serde_json::Value>,
507}
508
509// Market data helpers
510#[derive(Debug, Clone, Deserialize)]
511pub struct OrderBook {
512    #[serde(default)]
513    pub bids: Vec<PriceLevel>,
514    #[serde(default)]
515    pub asks: Vec<PriceLevel>,
516}
517
518#[derive(Debug, Clone, Deserialize, Serialize)]
519pub struct PriceLevel {
520    pub price: String,
521    pub quantity: String,
522}
523
524#[derive(Debug, Clone, Deserialize)]
525pub struct BestBidOffer {
526    #[serde(default)]
527    pub bid: Option<PriceLevel>,
528    #[serde(default)]
529    pub ask: Option<PriceLevel>,
530}
531
532#[derive(Debug, Clone, Deserialize)]
533pub struct SettlementPrice {
534    #[serde(default)]
535    pub symbol: String,
536    #[serde(default)]
537    pub price: String,
538    #[serde(default)]
539    pub timestamp: String,
540}
541
542// Search
543#[derive(Debug, Clone, Deserialize)]
544pub struct SearchResults {
545    #[serde(default)]
546    pub markets: Vec<UsMarket>,
547    #[serde(default)]
548    pub events: Vec<UsEvent>,
549}
550
551// `League` and `Team` placeholders were removed in 0.4.0. They were never
552// referenced by any request or response type, and publishing unreachable types
553// commits the SDK to a shape the API has not been checked against. They will
554// return alongside the endpoints that populate them.
555
556#[cfg(test)]
557mod tests {
558    use super::*;
559
560    #[test]
561    fn outcomes_parse_from_the_gateway_double_encoded_form() {
562        // The exact shape that broke `markets().list()` in 0.5.1.
563        let market: UsMarket = serde_json::from_str(
564            r#"{"slug":"x","outcomes":"[\"Titans\",\"Chargers\"]","outcomePrices":"[\"1\",\"0\"]"}"#,
565        )
566        .expect("should deserialize");
567        assert_eq!(market.outcomes, ["Titans", "Chargers"]);
568        assert_eq!(market.outcome_prices, ["1", "0"]);
569    }
570
571    #[test]
572    fn outcomes_parse_from_a_plain_array() {
573        // Accepted so the SDK survives the gateway dropping the encoding.
574        let market: UsMarket =
575            serde_json::from_str(r#"{"outcomes":["Yes","No"],"outcomePrices":["0.6","0.4"]}"#)
576                .expect("should deserialize");
577        assert_eq!(market.outcomes, ["Yes", "No"]);
578        assert_eq!(market.outcome_prices, ["0.6", "0.4"]);
579    }
580
581    #[test]
582    fn numeric_elements_are_rendered_rather_than_rejected() {
583        let market: UsMarket =
584            serde_json::from_str(r#"{"outcomePrices":"[1,0.55]"}"#).expect("should deserialize");
585        assert_eq!(market.outcome_prices, ["1", "0.55"]);
586    }
587
588    #[test]
589    fn absent_null_and_empty_outcomes_all_become_an_empty_vec() {
590        for body in [r#"{}"#, r#"{"outcomes":null}"#, r#"{"outcomes":""}"#] {
591            let market: UsMarket =
592                serde_json::from_str(body).unwrap_or_else(|err| panic!("{body}: {err}"));
593            assert!(market.outcomes.is_empty(), "{body}");
594        }
595    }
596
597    #[test]
598    fn a_malformed_encoding_is_an_error_not_a_silent_empty() {
599        let err = serde_json::from_str::<UsMarket>(r#"{"outcomes":"[\"unterminated"}"#)
600            .expect_err("should fail");
601        assert!(
602            err.to_string().contains("EOF") || err.to_string().contains("control"),
603            "unexpected error: {err}"
604        );
605    }
606
607    #[test]
608    fn market_status_parses_case_insensitively() {
609        assert_eq!(MarketStatus::from_api_str("open"), MarketStatus::Open);
610        assert_eq!(MarketStatus::from_api_str("OPEN"), MarketStatus::Open);
611        assert_eq!(
612            MarketStatus::from_api_str("  Closed "),
613            MarketStatus::Closed
614        );
615        assert_eq!(
616            MarketStatus::from_api_str("RESOLVED"),
617            MarketStatus::Resolved
618        );
619    }
620
621    #[test]
622    fn market_status_falls_back_to_unknown() {
623        // A status the SDK does not model must not be an error.
624        assert_eq!(MarketStatus::from_api_str("halted"), MarketStatus::Unknown);
625        assert_eq!(MarketStatus::from_api_str(""), MarketStatus::Unknown);
626    }
627
628    #[test]
629    fn parsed_status_reads_the_raw_field() {
630        let json = r#"{"id": "m1", "status": "OPEN"}"#;
631        let market: UsMarket = serde_json::from_str(json).expect("deserialize");
632        // Raw string is preserved, typed view is derived from it.
633        assert_eq!(market.status, "OPEN");
634        assert_eq!(market.parsed_status(), MarketStatus::Open);
635    }
636
637    #[test]
638    fn market_sides_deserialize_into_typed_values() {
639        let json = r#"{
640            "id": "m1",
641            "marketSides": [
642                {"id": "s1", "identifier": "YES", "price": "0.62", "long": true,
643                 "marketSideType": "BINARY", "unmodelledField": 7}
644            ]
645        }"#;
646        let market: UsMarket = serde_json::from_str(json).expect("deserialize");
647        assert_eq!(market.market_sides.len(), 1);
648
649        let side = &market.market_sides[0];
650        assert_eq!(side.identifier, "YES");
651        assert_eq!(side.price, "0.62");
652        assert!(side.long);
653        // Unmodelled keys survive in `extra` rather than being dropped.
654        assert_eq!(
655            side.extra.get("unmodelledField"),
656            Some(&serde_json::json!(7))
657        );
658    }
659
660    #[test]
661    fn market_sides_tolerate_missing_fields() {
662        // Every MarketSide field defaults, so a sparse object must still parse.
663        let json = r#"{"id": "m1", "marketSides": [{}]}"#;
664        let market: UsMarket = serde_json::from_str(json).expect("deserialize");
665        assert_eq!(market.market_sides.len(), 1);
666        assert_eq!(market.market_sides[0].identifier, "");
667    }
668}