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.core.nautilus_pyo3.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<'de> Deserialize<'de> for PolymarketOrderStatus {
183    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
184    where
185        D: serde::Deserializer<'de>,
186    {
187        // Slow-path table for `<VARIANT>_<reason>` inputs. Polymarket sometimes
188        // appends `_<reason>` to a status (e.g. `CANCELED_order couldn't be
189        // fully filled...`). Match the longest known variant first so
190        // `CANCELED_MARKET_RESOLVED` is not truncated to `CANCELED`.
191        const VARIANTS: &[(&str, PolymarketOrderStatus)] = &[
192            (
193                "CANCELED_MARKET_RESOLVED",
194                PolymarketOrderStatus::CanceledMarketResolved,
195            ),
196            ("INVALID", PolymarketOrderStatus::Invalid),
197            ("LIVE", PolymarketOrderStatus::Live),
198            ("DELAYED", PolymarketOrderStatus::Delayed),
199            ("MATCHED", PolymarketOrderStatus::Matched),
200            ("UNMATCHED", PolymarketOrderStatus::Unmatched),
201            ("CANCELED", PolymarketOrderStatus::Canceled),
202        ];
203
204        let s = String::deserialize(deserializer)?;
205
206        // Fast path: exact match through the strum-derived `FromStr`.
207        if let Ok(status) = <Self as std::str::FromStr>::from_str(&s) {
208            return Ok(status);
209        }
210
211        for (prefix, status) in VARIANTS {
212            if s.len() > prefix.len()
213                && s.is_char_boundary(prefix.len())
214                && &s[..prefix.len()] == *prefix
215                && s.as_bytes()[prefix.len()] == b'_'
216            {
217                return Ok(*status);
218            }
219        }
220        Err(serde::de::Error::custom(format!(
221            "Unknown PolymarketOrderStatus: {s}"
222        )))
223    }
224}
225
226/// Trade settlement status on the Polymarket exchange.
227#[derive(
228    Clone, Copy, Debug, PartialEq, Eq, Hash, StrumDisplay, EnumString, Serialize, Deserialize,
229)]
230#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
231#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
232pub enum PolymarketTradeStatus {
233    /// Sent to the executor service for on-chain submission.
234    Matched,
235    /// Mined on-chain, no finality threshold yet.
236    Mined,
237    /// Strong probabilistic finality achieved.
238    Confirmed,
239    /// Transaction failed, being retried by the operator.
240    Retrying,
241    /// Permanently failed, no more retries.
242    Failed,
243}
244
245impl PolymarketTradeStatus {
246    /// Returns `true` if this status represents a finalized trade.
247    #[must_use]
248    pub const fn is_finalized(&self) -> bool {
249        matches!(self, Self::Mined | Self::Confirmed)
250    }
251}
252
253impl From<PolymarketOrderSide> for OrderSide {
254    fn from(value: PolymarketOrderSide) -> Self {
255        match value {
256            PolymarketOrderSide::Buy => Self::Buy,
257            PolymarketOrderSide::Sell => Self::Sell,
258        }
259    }
260}
261
262impl TryFrom<OrderSide> for PolymarketOrderSide {
263    type Error = anyhow::Error;
264
265    fn try_from(value: OrderSide) -> anyhow::Result<Self> {
266        match value {
267            OrderSide::Buy => Ok(Self::Buy),
268            OrderSide::Sell => Ok(Self::Sell),
269            _ => anyhow::bail!("Invalid `OrderSide` for Polymarket: {value:?}"),
270        }
271    }
272}
273
274impl From<PolymarketOrderSide> for AggressorSide {
275    fn from(value: PolymarketOrderSide) -> Self {
276        match value {
277            PolymarketOrderSide::Buy => Self::Buyer,
278            PolymarketOrderSide::Sell => Self::Seller,
279        }
280    }
281}
282
283impl From<PolymarketOrderType> for TimeInForce {
284    fn from(value: PolymarketOrderType) -> Self {
285        match value {
286            PolymarketOrderType::GTC => Self::Gtc,
287            PolymarketOrderType::GTD => Self::Gtd,
288            PolymarketOrderType::FOK => Self::Fok,
289            // Fill-And-Kill is equivalent to Immediate-Or-Cancel
290            PolymarketOrderType::FAK => Self::Ioc,
291        }
292    }
293}
294
295impl TryFrom<TimeInForce> for PolymarketOrderType {
296    type Error = anyhow::Error;
297
298    fn try_from(value: TimeInForce) -> anyhow::Result<Self> {
299        match value {
300            TimeInForce::Gtc => Ok(Self::GTC),
301            TimeInForce::Gtd => Ok(Self::GTD),
302            TimeInForce::Fok => Ok(Self::FOK),
303            TimeInForce::Ioc => Ok(Self::FAK),
304            _ => anyhow::bail!("Unsupported `TimeInForce` for Polymarket: {value:?}"),
305        }
306    }
307}
308
309impl PolymarketOrderType {
310    pub(crate) fn from_market_time_in_force(value: TimeInForce) -> anyhow::Result<Self> {
311        match value {
312            TimeInForce::Fok => Ok(Self::FOK),
313            TimeInForce::Ioc => Ok(Self::FAK),
314            _ => anyhow::bail!("Unsupported `TimeInForce` for Polymarket market order: {value:?}"),
315        }
316    }
317}
318
319impl From<PolymarketOrderStatus> for OrderStatus {
320    fn from(value: PolymarketOrderStatus) -> Self {
321        match value {
322            PolymarketOrderStatus::Invalid => Self::Rejected,
323            PolymarketOrderStatus::Live => Self::Accepted,
324            PolymarketOrderStatus::Delayed => Self::Accepted,
325            PolymarketOrderStatus::Matched => Self::Filled,
326            // Placement failure (never became live), treat as rejected
327            PolymarketOrderStatus::Unmatched => Self::Rejected,
328            PolymarketOrderStatus::Canceled => Self::Canceled,
329            // Market resolved = order expired due to market settlement
330            PolymarketOrderStatus::CanceledMarketResolved => Self::Expired,
331        }
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use rstest::rstest;
338
339    use super::*;
340
341    #[rstest]
342    fn test_signature_type_serializes_as_u8() {
343        assert_eq!(serde_json::to_string(&SignatureType::Eoa).unwrap(), "0");
344        assert_eq!(
345            serde_json::to_string(&SignatureType::PolyProxy).unwrap(),
346            "1"
347        );
348        assert_eq!(
349            serde_json::to_string(&SignatureType::PolyGnosisSafe).unwrap(),
350            "2"
351        );
352        assert_eq!(
353            serde_json::to_string(&SignatureType::Poly1271).unwrap(),
354            "3"
355        );
356    }
357
358    #[rstest]
359    fn test_signature_type_deserializes_from_u8() {
360        assert_eq!(
361            serde_json::from_str::<SignatureType>("0").unwrap(),
362            SignatureType::Eoa
363        );
364        assert_eq!(
365            serde_json::from_str::<SignatureType>("1").unwrap(),
366            SignatureType::PolyProxy
367        );
368        assert_eq!(
369            serde_json::from_str::<SignatureType>("2").unwrap(),
370            SignatureType::PolyGnosisSafe
371        );
372        assert_eq!(
373            serde_json::from_str::<SignatureType>("3").unwrap(),
374            SignatureType::Poly1271
375        );
376    }
377
378    #[rstest]
379    fn test_order_side_serde_screaming_snake() {
380        assert_eq!(
381            serde_json::to_string(&PolymarketOrderSide::Buy).unwrap(),
382            "\"BUY\""
383        );
384        assert_eq!(
385            serde_json::from_str::<PolymarketOrderSide>("\"SELL\"").unwrap(),
386            PolymarketOrderSide::Sell
387        );
388    }
389
390    #[rstest]
391    fn test_event_type_serde_screaming_snake() {
392        assert_eq!(
393            serde_json::to_string(&PolymarketEventType::Placement).unwrap(),
394            "\"PLACEMENT\""
395        );
396        assert_eq!(
397            serde_json::from_str::<PolymarketEventType>("\"TRADE\"").unwrap(),
398            PolymarketEventType::Trade
399        );
400    }
401
402    #[rstest]
403    fn test_order_status_serde_screaming_snake() {
404        assert_eq!(
405            serde_json::to_string(&PolymarketOrderStatus::Live).unwrap(),
406            "\"LIVE\""
407        );
408        assert_eq!(
409            serde_json::from_str::<PolymarketOrderStatus>("\"CANCELED_MARKET_RESOLVED\"").unwrap(),
410            PolymarketOrderStatus::CanceledMarketResolved
411        );
412    }
413
414    #[rstest]
415    #[case(
416        "\"CANCELED_order couldn't be fully filled. FOK orders are fully filled or killed.\"",
417        PolymarketOrderStatus::Canceled
418    )]
419    #[case("\"CANCELED_some other reason\"", PolymarketOrderStatus::Canceled)]
420    #[case(
421        "\"CANCELED_MARKET_RESOLVED_resolved at block 12345\"",
422        PolymarketOrderStatus::CanceledMarketResolved
423    )]
424    #[case(
425        "\"UNMATCHED_insufficient liquidity\"",
426        PolymarketOrderStatus::Unmatched
427    )]
428    fn test_order_status_strips_reason_suffix(
429        #[case] raw: &str,
430        #[case] expected: PolymarketOrderStatus,
431    ) {
432        assert_eq!(
433            serde_json::from_str::<PolymarketOrderStatus>(raw).unwrap(),
434            expected,
435        );
436    }
437
438    #[rstest]
439    fn test_order_status_rejects_unknown() {
440        assert!(serde_json::from_str::<PolymarketOrderStatus>("\"UNKNOWN_STATUS\"").is_err());
441        assert!(serde_json::from_str::<PolymarketOrderStatus>("\"\"").is_err());
442    }
443
444    #[rstest]
445    fn test_trade_status_serde_screaming_snake() {
446        assert_eq!(
447            serde_json::to_string(&PolymarketTradeStatus::Confirmed).unwrap(),
448            "\"CONFIRMED\""
449        );
450        assert_eq!(
451            serde_json::from_str::<PolymarketTradeStatus>("\"RETRYING\"").unwrap(),
452            PolymarketTradeStatus::Retrying
453        );
454    }
455
456    #[rstest]
457    #[case(PolymarketOrderSide::Buy, OrderSide::Buy)]
458    #[case(PolymarketOrderSide::Sell, OrderSide::Sell)]
459    fn test_order_side_to_nautilus(#[case] poly: PolymarketOrderSide, #[case] expected: OrderSide) {
460        assert_eq!(OrderSide::from(poly), expected);
461    }
462
463    #[rstest]
464    #[case(OrderSide::Buy, PolymarketOrderSide::Buy)]
465    #[case(OrderSide::Sell, PolymarketOrderSide::Sell)]
466    fn test_nautilus_order_side_to_poly(
467        #[case] nautilus: OrderSide,
468        #[case] expected: PolymarketOrderSide,
469    ) {
470        assert_eq!(PolymarketOrderSide::try_from(nautilus).unwrap(), expected);
471    }
472
473    #[rstest]
474    #[case(PolymarketOrderSide::Buy, AggressorSide::Buyer)]
475    #[case(PolymarketOrderSide::Sell, AggressorSide::Seller)]
476    fn test_order_side_to_aggressor(
477        #[case] poly: PolymarketOrderSide,
478        #[case] expected: AggressorSide,
479    ) {
480        assert_eq!(AggressorSide::from(poly), expected);
481    }
482
483    #[rstest]
484    #[case(PolymarketOrderType::GTC, TimeInForce::Gtc)]
485    #[case(PolymarketOrderType::GTD, TimeInForce::Gtd)]
486    #[case(PolymarketOrderType::FOK, TimeInForce::Fok)]
487    #[case(PolymarketOrderType::FAK, TimeInForce::Ioc)]
488    fn test_order_type_to_time_in_force(
489        #[case] poly: PolymarketOrderType,
490        #[case] expected: TimeInForce,
491    ) {
492        assert_eq!(TimeInForce::from(poly), expected);
493    }
494
495    #[rstest]
496    #[case(TimeInForce::Gtc, PolymarketOrderType::GTC)]
497    #[case(TimeInForce::Gtd, PolymarketOrderType::GTD)]
498    #[case(TimeInForce::Fok, PolymarketOrderType::FOK)]
499    #[case(TimeInForce::Ioc, PolymarketOrderType::FAK)]
500    fn test_time_in_force_to_order_type(
501        #[case] tif: TimeInForce,
502        #[case] expected: PolymarketOrderType,
503    ) {
504        assert_eq!(PolymarketOrderType::try_from(tif).unwrap(), expected);
505    }
506
507    #[rstest]
508    #[case(TimeInForce::Ioc, PolymarketOrderType::FAK)]
509    #[case(TimeInForce::Fok, PolymarketOrderType::FOK)]
510    fn test_market_time_in_force_to_order_type(
511        #[case] tif: TimeInForce,
512        #[case] expected: PolymarketOrderType,
513    ) {
514        assert_eq!(
515            PolymarketOrderType::from_market_time_in_force(tif).unwrap(),
516            expected,
517        );
518    }
519
520    #[rstest]
521    #[case(TimeInForce::Gtc)]
522    #[case(TimeInForce::Gtd)]
523    fn test_market_time_in_force_to_order_type_rejects_non_market_tif(#[case] tif: TimeInForce) {
524        assert!(PolymarketOrderType::from_market_time_in_force(tif).is_err());
525    }
526
527    #[rstest]
528    #[case(PolymarketOrderStatus::Invalid, OrderStatus::Rejected)]
529    #[case(PolymarketOrderStatus::Live, OrderStatus::Accepted)]
530    #[case(PolymarketOrderStatus::Delayed, OrderStatus::Accepted)]
531    #[case(PolymarketOrderStatus::Matched, OrderStatus::Filled)]
532    #[case(PolymarketOrderStatus::Unmatched, OrderStatus::Rejected)]
533    #[case(PolymarketOrderStatus::Canceled, OrderStatus::Canceled)]
534    #[case(PolymarketOrderStatus::CanceledMarketResolved, OrderStatus::Expired)]
535    fn test_order_status_to_nautilus(
536        #[case] poly: PolymarketOrderStatus,
537        #[case] expected: OrderStatus,
538    ) {
539        assert_eq!(OrderStatus::from(poly), expected);
540    }
541
542    #[rstest]
543    fn test_trade_status_is_finalized() {
544        assert!(PolymarketTradeStatus::Mined.is_finalized());
545        assert!(PolymarketTradeStatus::Confirmed.is_finalized());
546        assert!(!PolymarketTradeStatus::Matched.is_finalized());
547        assert!(!PolymarketTradeStatus::Retrying.is_finalized());
548        assert!(!PolymarketTradeStatus::Failed.is_finalized());
549    }
550}