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