Skip to main content

nautilus_polymarket/common/
enums.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Venue-specific enums for the Polymarket CLOB API.
17
18use std::fmt::{Debug, Display};
19
20use nautilus_model::enums::{AggressorSide, OrderSide, OrderStatus, TimeInForce};
21use serde::{Deserialize, Serialize};
22use serde_repr::{Deserialize_repr, Serialize_repr};
23use strum::{Display as StrumDisplay, EnumString};
24use ustr::Ustr;
25
26/// Role of the private key used to sign orders, independent of the wallet signature format.
27#[cfg_attr(
28    feature = "python",
29    pyo3::pyclass(
30        frozen,
31        eq,
32        eq_int,
33        module = "nautilus_trader.adapters.polymarket",
34        from_py_object
35    )
36)]
37#[cfg_attr(
38    feature = "python",
39    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.polymarket")
40)]
41#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
42pub enum PolymarketSignerType {
43    #[default]
44    Owner,
45    Session,
46}
47
48#[cfg(feature = "python")]
49#[pyo3::pymethods]
50#[pyo3_stub_gen::derive::gen_stub_pymethods]
51impl PolymarketSignerType {
52    const fn __hash__(&self) -> isize {
53        *self as isize
54    }
55}
56
57/// EIP-712 signature type for order signing.
58///
59/// Serialized as a numeric value (0/1/2/3) on the wire.
60#[cfg_attr(
61    feature = "python",
62    pyo3::pyclass(
63        frozen,
64        eq,
65        eq_int,
66        hash,
67        module = "nautilus_trader.adapters.polymarket",
68        from_py_object,
69    )
70)]
71#[cfg_attr(
72    feature = "python",
73    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.polymarket")
74)]
75#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize_repr, Deserialize_repr)]
76#[repr(u8)]
77pub enum PolymarketSignatureType {
78    Eoa = 0,
79    PolyProxy = 1,
80    PolyGnosisSafe = 2,
81    Poly1271 = 3,
82}
83
84/// Outcome label for a Polymarket market token.
85///
86/// Free-form string from the API (e.g. "Yes", "No", "Up", "Down").
87/// Every Polymarket market has exactly two outcome tokens; this holds
88/// whichever label the API assigns to one of them.
89#[repr(C)]
90#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
91pub struct PolymarketOutcome(Ustr);
92
93impl PolymarketOutcome {
94    #[must_use]
95    pub fn yes() -> Self {
96        Self(Ustr::from("Yes"))
97    }
98
99    #[must_use]
100    pub fn no() -> Self {
101        Self(Ustr::from("No"))
102    }
103
104    #[must_use]
105    pub fn up() -> Self {
106        Self(Ustr::from("Up"))
107    }
108
109    #[must_use]
110    pub fn down() -> Self {
111        Self(Ustr::from("Down"))
112    }
113
114    #[must_use]
115    pub const fn inner(&self) -> Ustr {
116        self.0
117    }
118
119    #[must_use]
120    pub fn as_str(&self) -> &str {
121        self.0.as_str()
122    }
123}
124
125impl Debug for PolymarketOutcome {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        write!(f, "\"{}\"", self.0)
128    }
129}
130
131impl Display for PolymarketOutcome {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        write!(f, "{}", self.0)
134    }
135}
136
137impl From<&str> for PolymarketOutcome {
138    fn from(value: &str) -> Self {
139        Self(Ustr::from(value))
140    }
141}
142
143impl From<Ustr> for PolymarketOutcome {
144    fn from(value: Ustr) -> Self {
145        Self(value)
146    }
147}
148
149/// Order side on the Polymarket CLOB.
150#[derive(
151    Clone, Copy, Debug, PartialEq, Eq, Hash, StrumDisplay, EnumString, Serialize, Deserialize,
152)]
153#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
154#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
155pub enum PolymarketOrderSide {
156    Buy,
157    Sell,
158}
159
160/// Liquidity side for fills.
161#[derive(
162    Clone, Copy, Debug, PartialEq, Eq, Hash, StrumDisplay, EnumString, Serialize, Deserialize,
163)]
164#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
165#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
166pub enum PolymarketLiquiditySide {
167    Maker,
168    Taker,
169}
170
171/// Order type (time-in-force variant) on the Polymarket CLOB.
172#[derive(
173    Clone, Copy, Debug, PartialEq, Eq, Hash, StrumDisplay, EnumString, Serialize, Deserialize,
174)]
175pub enum PolymarketOrderType {
176    FOK,
177    /// Immediate or cancel.
178    FAK,
179    GTC,
180    GTD,
181}
182
183/// WebSocket event type for user channel messages.
184#[derive(
185    Clone, Copy, Debug, PartialEq, Eq, Hash, StrumDisplay, EnumString, Serialize, Deserialize,
186)]
187#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
188#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
189pub enum PolymarketEventType {
190    Placement,
191    /// Emitted for a match.
192    Update,
193    Cancellation,
194    Trade,
195}
196
197/// Order status on the Polymarket CLOB.
198#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, StrumDisplay, EnumString, Serialize)]
199#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
200#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
201pub enum PolymarketOrderStatus {
202    Invalid,
203    Live,
204    /// Marketable but subject to matching delay.
205    Delayed,
206    Matched,
207    /// Marketable but failure delaying, placement not successful.
208    Unmatched,
209    Canceled,
210    CanceledMarketResolved,
211}
212
213impl PolymarketOrderStatus {
214    pub(crate) fn parse_wire(value: &str) -> Option<(Self, Option<&str>)> {
215        // Match the longest known variant first so `CANCELED_MARKET_RESOLVED`
216        // is not truncated to `CANCELED` when a reason suffix is present.
217        const VARIANTS: &[(&str, PolymarketOrderStatus)] = &[
218            (
219                "CANCELED_MARKET_RESOLVED",
220                PolymarketOrderStatus::CanceledMarketResolved,
221            ),
222            ("INVALID", PolymarketOrderStatus::Invalid),
223            ("LIVE", PolymarketOrderStatus::Live),
224            ("DELAYED", PolymarketOrderStatus::Delayed),
225            ("MATCHED", PolymarketOrderStatus::Matched),
226            ("UNMATCHED", PolymarketOrderStatus::Unmatched),
227            ("CANCELED", PolymarketOrderStatus::Canceled),
228        ];
229
230        let value = value.strip_prefix("ORDER_STATUS_").unwrap_or(value);
231
232        if let Ok(status) = <Self as std::str::FromStr>::from_str(value) {
233            return Some((status, None));
234        }
235
236        VARIANTS.iter().find_map(|(prefix, status)| {
237            value
238                .strip_prefix(prefix)
239                .and_then(|suffix| suffix.strip_prefix('_'))
240                .map(|reason| (*status, Some(reason)))
241        })
242    }
243}
244
245impl<'de> Deserialize<'de> for PolymarketOrderStatus {
246    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
247    where
248        D: serde::Deserializer<'de>,
249    {
250        let s = String::deserialize(deserializer)?;
251
252        Self::parse_wire(&s)
253            .map(|(status, _)| status)
254            .ok_or_else(|| serde::de::Error::custom(format!("Unknown PolymarketOrderStatus: {s}")))
255    }
256}
257
258/// Trade settlement status on the Polymarket exchange.
259#[derive(
260    Clone, Copy, Debug, PartialEq, Eq, Hash, StrumDisplay, EnumString, Serialize, Deserialize,
261)]
262#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
263#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
264pub enum PolymarketTradeStatus {
265    /// Sent to the executor service for on-chain submission.
266    #[serde(alias = "TRADE_STATUS_MATCHED")]
267    Matched,
268    /// Mined on-chain, no finality threshold yet.
269    #[serde(alias = "TRADE_STATUS_MINED")]
270    Mined,
271    /// Strong probabilistic finality achieved.
272    #[serde(alias = "TRADE_STATUS_CONFIRMED")]
273    Confirmed,
274    /// Transaction failed, being retried by the operator.
275    #[serde(alias = "TRADE_STATUS_RETRYING")]
276    Retrying,
277    /// Permanently failed, no more retries.
278    #[serde(alias = "TRADE_STATUS_FAILED")]
279    Failed,
280}
281
282impl PolymarketTradeStatus {
283    /// Returns `true` if this status represents a finalized trade.
284    #[must_use]
285    pub const fn is_finalized(&self) -> bool {
286        matches!(self, Self::Confirmed)
287    }
288
289    /// Returns `true` while settlement can still succeed or fail.
290    #[must_use]
291    pub const fn is_pending_settlement(&self) -> bool {
292        matches!(self, Self::Matched | Self::Mined | Self::Retrying)
293    }
294}
295
296impl From<PolymarketOrderSide> for OrderSide {
297    fn from(value: PolymarketOrderSide) -> Self {
298        match value {
299            PolymarketOrderSide::Buy => Self::Buy,
300            PolymarketOrderSide::Sell => Self::Sell,
301        }
302    }
303}
304
305impl From<OrderSide> for PolymarketOrderSide {
306    fn from(value: OrderSide) -> Self {
307        match value {
308            OrderSide::Buy => Self::Buy,
309            OrderSide::Sell => Self::Sell,
310        }
311    }
312}
313
314impl From<PolymarketOrderSide> for AggressorSide {
315    fn from(value: PolymarketOrderSide) -> Self {
316        match value {
317            PolymarketOrderSide::Buy => Self::Buy,
318            PolymarketOrderSide::Sell => Self::Sell,
319        }
320    }
321}
322
323impl From<PolymarketOrderType> for TimeInForce {
324    fn from(value: PolymarketOrderType) -> Self {
325        match value {
326            PolymarketOrderType::GTC => Self::Gtc,
327            PolymarketOrderType::GTD => Self::Gtd,
328            PolymarketOrderType::FOK => Self::Fok,
329            // Fill-And-Kill is equivalent to Immediate-Or-Cancel
330            PolymarketOrderType::FAK => Self::Ioc,
331        }
332    }
333}
334
335impl TryFrom<TimeInForce> for PolymarketOrderType {
336    type Error = anyhow::Error;
337
338    fn try_from(value: TimeInForce) -> anyhow::Result<Self> {
339        match value {
340            TimeInForce::Gtc => Ok(Self::GTC),
341            TimeInForce::Gtd => Ok(Self::GTD),
342            TimeInForce::Fok => Ok(Self::FOK),
343            TimeInForce::Ioc => Ok(Self::FAK),
344            _ => anyhow::bail!("Unsupported `TimeInForce` for Polymarket: {value:?}"),
345        }
346    }
347}
348
349impl PolymarketOrderType {
350    pub(crate) fn from_market_time_in_force(value: TimeInForce) -> anyhow::Result<Self> {
351        match value {
352            TimeInForce::Fok => Ok(Self::FOK),
353            TimeInForce::Ioc => Ok(Self::FAK),
354            _ => anyhow::bail!("Unsupported `TimeInForce` for Polymarket market order: {value:?}"),
355        }
356    }
357}
358
359impl From<PolymarketOrderStatus> for OrderStatus {
360    fn from(value: PolymarketOrderStatus) -> Self {
361        match value {
362            PolymarketOrderStatus::Invalid => Self::Rejected,
363            PolymarketOrderStatus::Live => Self::Accepted,
364            PolymarketOrderStatus::Delayed => Self::Accepted,
365            PolymarketOrderStatus::Matched => Self::Filled,
366            // Placement failure (never became live), treat as rejected
367            PolymarketOrderStatus::Unmatched => Self::Rejected,
368            PolymarketOrderStatus::Canceled => Self::Canceled,
369            // Market resolved = order expired due to market settlement
370            PolymarketOrderStatus::CanceledMarketResolved => Self::Expired,
371        }
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use rstest::rstest;
378
379    use super::*;
380
381    #[rstest]
382    fn test_signature_type_serializes_as_u8() {
383        assert_eq!(
384            serde_json::to_string(&PolymarketSignatureType::Eoa).unwrap(),
385            "0"
386        );
387        assert_eq!(
388            serde_json::to_string(&PolymarketSignatureType::PolyProxy).unwrap(),
389            "1"
390        );
391        assert_eq!(
392            serde_json::to_string(&PolymarketSignatureType::PolyGnosisSafe).unwrap(),
393            "2"
394        );
395        assert_eq!(
396            serde_json::to_string(&PolymarketSignatureType::Poly1271).unwrap(),
397            "3"
398        );
399    }
400
401    #[rstest]
402    fn test_signature_type_deserializes_from_u8() {
403        assert_eq!(
404            serde_json::from_str::<PolymarketSignatureType>("0").unwrap(),
405            PolymarketSignatureType::Eoa
406        );
407        assert_eq!(
408            serde_json::from_str::<PolymarketSignatureType>("1").unwrap(),
409            PolymarketSignatureType::PolyProxy
410        );
411        assert_eq!(
412            serde_json::from_str::<PolymarketSignatureType>("2").unwrap(),
413            PolymarketSignatureType::PolyGnosisSafe
414        );
415        assert_eq!(
416            serde_json::from_str::<PolymarketSignatureType>("3").unwrap(),
417            PolymarketSignatureType::Poly1271
418        );
419    }
420
421    #[rstest]
422    fn test_order_side_serde_screaming_snake() {
423        assert_eq!(
424            serde_json::to_string(&PolymarketOrderSide::Buy).unwrap(),
425            "\"BUY\""
426        );
427        assert_eq!(
428            serde_json::from_str::<PolymarketOrderSide>("\"SELL\"").unwrap(),
429            PolymarketOrderSide::Sell
430        );
431    }
432
433    #[rstest]
434    fn test_event_type_serde_screaming_snake() {
435        assert_eq!(
436            serde_json::to_string(&PolymarketEventType::Placement).unwrap(),
437            "\"PLACEMENT\""
438        );
439        assert_eq!(
440            serde_json::from_str::<PolymarketEventType>("\"TRADE\"").unwrap(),
441            PolymarketEventType::Trade
442        );
443    }
444
445    #[rstest]
446    fn test_order_status_serde_screaming_snake() {
447        assert_eq!(
448            serde_json::to_string(&PolymarketOrderStatus::Live).unwrap(),
449            "\"LIVE\""
450        );
451        assert_eq!(
452            serde_json::from_str::<PolymarketOrderStatus>("\"CANCELED_MARKET_RESOLVED\"").unwrap(),
453            PolymarketOrderStatus::CanceledMarketResolved
454        );
455    }
456
457    #[rstest]
458    fn test_order_status_deserializes_openapi_prefix() {
459        assert_eq!(
460            serde_json::from_str::<PolymarketOrderStatus>("\"ORDER_STATUS_LIVE\"").unwrap(),
461            PolymarketOrderStatus::Live
462        );
463        assert_eq!(
464            serde_json::from_str::<PolymarketOrderStatus>("\"ORDER_STATUS_CANCELED_reason\"")
465                .unwrap(),
466            PolymarketOrderStatus::Canceled
467        );
468    }
469
470    #[rstest]
471    #[case(
472        "\"CANCELED_order couldn't be fully filled. FOK orders are fully filled or killed.\"",
473        PolymarketOrderStatus::Canceled
474    )]
475    #[case("\"CANCELED_some other reason\"", PolymarketOrderStatus::Canceled)]
476    #[case(
477        "\"CANCELED_MARKET_RESOLVED_resolved at block 12345\"",
478        PolymarketOrderStatus::CanceledMarketResolved
479    )]
480    #[case(
481        "\"UNMATCHED_insufficient liquidity\"",
482        PolymarketOrderStatus::Unmatched
483    )]
484    fn test_order_status_strips_reason_suffix(
485        #[case] raw: &str,
486        #[case] expected: PolymarketOrderStatus,
487    ) {
488        assert_eq!(
489            serde_json::from_str::<PolymarketOrderStatus>(raw).unwrap(),
490            expected,
491        );
492    }
493
494    #[rstest]
495    fn test_order_status_rejects_unknown() {
496        assert!(serde_json::from_str::<PolymarketOrderStatus>("\"UNKNOWN_STATUS\"").is_err());
497        assert!(serde_json::from_str::<PolymarketOrderStatus>("\"\"").is_err());
498    }
499
500    #[rstest]
501    fn test_trade_status_serde_screaming_snake() {
502        assert_eq!(
503            serde_json::to_string(&PolymarketTradeStatus::Confirmed).unwrap(),
504            "\"CONFIRMED\""
505        );
506        assert_eq!(
507            serde_json::from_str::<PolymarketTradeStatus>("\"RETRYING\"").unwrap(),
508            PolymarketTradeStatus::Retrying
509        );
510    }
511
512    #[rstest]
513    fn test_trade_status_deserializes_openapi_prefix() {
514        assert_eq!(
515            serde_json::from_str::<PolymarketTradeStatus>("\"TRADE_STATUS_CONFIRMED\"").unwrap(),
516            PolymarketTradeStatus::Confirmed
517        );
518    }
519
520    #[rstest]
521    #[case(PolymarketOrderSide::Buy, OrderSide::Buy)]
522    #[case(PolymarketOrderSide::Sell, OrderSide::Sell)]
523    fn test_order_side_to_nautilus(#[case] poly: PolymarketOrderSide, #[case] expected: OrderSide) {
524        assert_eq!(OrderSide::from(poly), expected);
525    }
526
527    #[rstest]
528    #[case(OrderSide::Buy, PolymarketOrderSide::Buy)]
529    #[case(OrderSide::Sell, PolymarketOrderSide::Sell)]
530    fn test_nautilus_order_side_to_poly(
531        #[case] nautilus: OrderSide,
532        #[case] expected: PolymarketOrderSide,
533    ) {
534        assert_eq!(PolymarketOrderSide::from(nautilus), expected);
535    }
536
537    #[rstest]
538    #[case(PolymarketOrderSide::Buy, AggressorSide::Buy)]
539    #[case(PolymarketOrderSide::Sell, AggressorSide::Sell)]
540    fn test_order_side_to_aggressor(
541        #[case] poly: PolymarketOrderSide,
542        #[case] expected: AggressorSide,
543    ) {
544        assert_eq!(AggressorSide::from(poly), expected);
545    }
546
547    #[rstest]
548    #[case(PolymarketOrderType::GTC, TimeInForce::Gtc)]
549    #[case(PolymarketOrderType::GTD, TimeInForce::Gtd)]
550    #[case(PolymarketOrderType::FOK, TimeInForce::Fok)]
551    #[case(PolymarketOrderType::FAK, TimeInForce::Ioc)]
552    fn test_order_type_to_time_in_force(
553        #[case] poly: PolymarketOrderType,
554        #[case] expected: TimeInForce,
555    ) {
556        assert_eq!(TimeInForce::from(poly), expected);
557    }
558
559    #[rstest]
560    #[case(TimeInForce::Gtc, PolymarketOrderType::GTC)]
561    #[case(TimeInForce::Gtd, PolymarketOrderType::GTD)]
562    #[case(TimeInForce::Fok, PolymarketOrderType::FOK)]
563    #[case(TimeInForce::Ioc, PolymarketOrderType::FAK)]
564    fn test_time_in_force_to_order_type(
565        #[case] tif: TimeInForce,
566        #[case] expected: PolymarketOrderType,
567    ) {
568        assert_eq!(PolymarketOrderType::try_from(tif).unwrap(), expected);
569    }
570
571    #[rstest]
572    #[case(TimeInForce::Ioc, PolymarketOrderType::FAK)]
573    #[case(TimeInForce::Fok, PolymarketOrderType::FOK)]
574    fn test_market_time_in_force_to_order_type(
575        #[case] tif: TimeInForce,
576        #[case] expected: PolymarketOrderType,
577    ) {
578        assert_eq!(
579            PolymarketOrderType::from_market_time_in_force(tif).unwrap(),
580            expected,
581        );
582    }
583
584    #[rstest]
585    #[case(TimeInForce::Gtc)]
586    #[case(TimeInForce::Gtd)]
587    fn test_market_time_in_force_to_order_type_rejects_non_market_tif(#[case] tif: TimeInForce) {
588        assert!(PolymarketOrderType::from_market_time_in_force(tif).is_err());
589    }
590
591    #[rstest]
592    #[case(PolymarketOrderStatus::Invalid, OrderStatus::Rejected)]
593    #[case(PolymarketOrderStatus::Live, OrderStatus::Accepted)]
594    #[case(PolymarketOrderStatus::Delayed, OrderStatus::Accepted)]
595    #[case(PolymarketOrderStatus::Matched, OrderStatus::Filled)]
596    #[case(PolymarketOrderStatus::Unmatched, OrderStatus::Rejected)]
597    #[case(PolymarketOrderStatus::Canceled, OrderStatus::Canceled)]
598    #[case(PolymarketOrderStatus::CanceledMarketResolved, OrderStatus::Expired)]
599    fn test_order_status_to_nautilus(
600        #[case] poly: PolymarketOrderStatus,
601        #[case] expected: OrderStatus,
602    ) {
603        assert_eq!(OrderStatus::from(poly), expected);
604    }
605
606    #[rstest]
607    fn test_trade_status_is_finalized() {
608        assert!(!PolymarketTradeStatus::Mined.is_finalized());
609        assert!(PolymarketTradeStatus::Confirmed.is_finalized());
610        assert!(!PolymarketTradeStatus::Matched.is_finalized());
611        assert!(!PolymarketTradeStatus::Retrying.is_finalized());
612        assert!(!PolymarketTradeStatus::Failed.is_finalized());
613    }
614
615    #[rstest]
616    fn test_trade_status_is_pending_settlement() {
617        assert!(PolymarketTradeStatus::Matched.is_pending_settlement());
618        assert!(PolymarketTradeStatus::Mined.is_pending_settlement());
619        assert!(PolymarketTradeStatus::Retrying.is_pending_settlement());
620        assert!(!PolymarketTradeStatus::Confirmed.is_pending_settlement());
621        assert!(!PolymarketTradeStatus::Failed.is_pending_settlement());
622    }
623}